zoukankan      html  css  js  c++  java
  • quick-cocos2d-x的热更新机制实现

    quick-cocos2d-x的热更新机制实现

    0 依赖

    这里说的热更新,指的是客户端的更新。

    大致的流程是,客户端在启动后访问更新api,根据更新api的反馈,下载更新资源,然后使用新的资源启动客户端,或者直接使用新资源不重启客户端。

    这种方式可以跳过AppStore的审核,避免了用户频繁下载、安装、覆盖产品包。

    我们一般使用这种方式快速修复产品BUG和增加新功能。

    本文基于 quick-cocos2d-x zrong 修改版 。

    1 前言

    1.1 他山之石

    在实现这个机制之前,我研究了这几篇文章:

    另外,我也查看了 AssetsManager 的源码和 sample 。

    不幸的是,这几个方案我都不能直接拿来用。因此思考再三,还是自己写了一套方案。

    ==重要提醒==

    这篇文章很长,但我不愿意将其分成多个部分。这本来就是一件事,分开的话有种开房时洗完澡妹子却说两个小时后才能来。这中间干点啥呢?

    所以,如果你不能坚持两个小时(能么?不能?),或者你的持久度不能坚持到把这篇文章看完(大概要10~30分钟吧),那还是不要往下看的比较好。

    当然,你也可能坚挺了30分钟之后才发现妹子是凤姐,不要怪我这30分钟里面没开灯哦……

    1.2 为什么要重复造轮子

    上面的几个方案侧重于尽量简化用户(使用此方案的程序员)的操作,而简化带来的副作用就是会损失一些灵活性。

    正如 Roberto Ierusalimschy 在 Lua程序设计(第2版) 第15章开头所说:

    通常,Lua不会设置规则(policy)。相反,Lua会提供许多强有力的机制来使开发者有能力实现出最适合的规则。

    我认为更新模块也不应该设置规则,而是尽可能提供一些机制来满足程序员的需要。这些机制并不是我发明的,而是Lua和quick本来就提供的。让程序员自己实现自己的升级系统,一定比我这个无证野路子的方法更好.

    因此,本文中讲述的并非是一套通用的机制,而是我根据上面说到的这些机制实现的一套适合自己的方法。当然你可以直接拿去用,但要记住:

    • 用的好,请告诉你的朋友。
    • 出了问题,请告诉别找我。

    1.3 需求的复杂性

    热更新有许多的必要条件,每个产品的需求可能都不太相同。

    例如,每个产品的版本号设计都不太相同,有的有大版本、小版本;有的则有主版本、次版本、编译版本。我以前的习惯,是在主版本变化的时候需要整包更新,而次版本变化代表逻辑更新,编译版本代表资源更新等等。这些需要自己来定义升级规则。

    再例如,有的产品希望逐个下载升级包,有的产品希望把所有资源打包成一个升级包;有的产品直接使用文件名作为资源名在游戏中调用,而有的产品会把资源名改为指纹码(例如MD5)形式来实现升级的多版本共存和实时回滚,还有的产品甚至要求能在用户玩游戏的过程中完成自动更新。

    AssetsManager 那套机制就太死板,在真实的产品中不修改很难使用。

    而我也不建议使用 CCUserDefault 这种东西——在Lua的世界里,为什么要用XML做配置文件?

    如果抽象出我的需求,其实只有1点:

    能更新一切

    这个说的有点大了,准确的说,应该是 能更新一切Lua代码与资源 。

    如果你的整个游戏都是Lua写的(对于quick的项目来说应该是这样),其实也就是更新一切。

    1.4 版本号设计

    关于上面 需求的复杂性 中提到的版本号的问题,可以参考一下这篇文章:语义化版本2.0.0 。

    我基于语义化版本设计了一套规则在团队内部使用:项目版本描述规则 。

    在这里,我尽量详细地阐述我的思路和做法,抛砖引玉吧。

    2 特色

    基本的热更新功能就不说了大家都有。我这套机制还有如下几个特色:

    2.1 可以更新 frameworks_precompiled.zip 模块

    为了行文方便,后面会把 frameworks_precompiled.zip 简称为 framework 。

    frameworks 模块是 quick 的核心模块,在quick 生成的项目中,它直接在 AppDelegate.cpp 中载入 main.lua 之前进行载入。如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    bool AppDelegate::applicationDidFinishLaunching()
    {
    // initialize director
    CCDirector *pDirector = CCDirector::sharedDirector();
    pDirector->setOpenGLView(CCEGLView::sharedOpenGLView());
    pDirector->setProjection(kCCDirectorProjection2D);
     
    // set FPS. the default value is 1.0/60 if you don't call this
    pDirector->setAnimationInterval(1.0 / 60);
     
    // register lua engine
    CCLuaEngine *pEngine = CCLuaEngine::defaultEngine();
    CCScriptEngineManager::sharedManager()->setScriptEngine(pEngine);
     
    CCLuaStack *pStack = pEngine->getLuaStack();
     
    #if (CC_TARGET_PLATFORM == CC_PLATFORM_IOS || CC_TARGET_PLATFORM == CC_PLATFORM_ANDROID)
    // load framework
    pStack->loadChunksFromZIP("res/framework_precompiled.zip");
     
    // set script path
    string path = CCFileUtils::sharedFileUtils()->fullPathForFilename("scripts/main.lua");
    ......

    这可以说明这个核心模块对quick的重要性。正因为它重要,所以必须要能更新它。

    2.2 可以更新 update 模块自身

    更新功能是客户端启动后载入的第一个lua模块,它负责载入更新资源,以及启动主项目。一般情况下,这个模块是不需要改动的。对它进行改动,既不科学,也不安全(安全啊……)。

    但是万一呢?大家知道策划和运营同学都是二班的,或许哪天就有二班同学找你说:改改怕什么?又不会怀孕…… 所以这个必须有。

    2.3 纯lua实现

    把这个拿出来说纯粹是撑数的。不凑个三大特色怎么好意思?

    上面SunLightJuly和Henry同学的方案当然也是纯lua的。用quick你不搞纯lua好意思出来混?小心廖大一眼瞪死你。

    当然,我这个不是纯lua的,我基于 AssetsManager(C++) 的代码实现了一个 Updater 模块。

    而且,我还改了 AppDelegate 中的启动代码。

    所以,你看,我不仅是撑数,还是忽悠。

    3 Updater(C++)

    AssetsManager 中提供了下载资源,访问更新列表,解压zip包,删除临时文件,设置搜索路径等等一系列的功能。但它的使用方式相当死板,我只能传递一个获取版本号的地址,一个zip包的地址,一个临时文件夹路径,然后就干等着。期间啥也干不了。

    当然,我可以通过 quick 为其增加的 registerScriptHandler 方法让lua得知下载进度和网络状态等等。但下载进度的数字居然以事件名的方式通过字符串传过来的!这个就太匪夷所思了点。

    于是,我对这个 AssetsManager 进行了修改。因为修改的东西实在太多,改完后就不好意思再叫这个名字了(其实主要是现在的名字比较短 XD)。我们只需要记住这个 Updater 是使用 AssetsManager 修改的即可。

    在上面SunLightJuly和Henry同学的方法中,使用的是 CCHTTPRequest 来获取网络资源的。CCHTTPRequest 封装了cURL 操作。而在 Updater 中,是直接封装的 cURL 操作。

    在我的设计中,逻辑应该尽量放在lua中,C++部分只提供功能供lua调用。因为lua可以进行热更新,而C++部分则只能整包更新。

    Updater 主要实现的内容如下:

    3.1 删除了不需要的方法

    get和set相关的一堆方法都被删除了。new对象的时候也不必传递参数了。

    3.2 增加 getUpdateInfo 方法

    这个方法通过HTTP协议获取升级列表数据,获取到的数据直接返回,C++并不做处理。

    3.3 修改 update 方法

    这个方法通过HTTP协议下载升级包,需要提供四个参数:

    1. zip文件的url;
    2. zip文件的保存位置;
    3. zip 文件的解压临时目录;
    4. 解压之前是否需要清空临时目录。

    3.4 修改事件类型

    我把把传递给lua的事件分成了四种类型:

    3.4.1 UPDATER_MESSAGE_UPDATE_SUCCEED

    事件名为 success,代表更新成功,zip文件下载并解压完毕;

    3.4.2 UPDATER_MESSAGE_STATE

    事件名为 state,更新过程中的状态(下载开始、结束,解压开始、结束)也传递给了lua。这个方法是这样实现的:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    void Updater::Helper::handlerState(Message *msg)
    {
    StateMessage* stateMsg = (StateMessage*)msg->obj;
    if(stateMsg->manager->_delegate)
    {
    stateMsg->manager->_delegate->onState(stateMsg->code);
    }
    if (stateMsg->manager->_scriptHandler)
    {
    std::string stateMessage;
    switch ((StateCode)stateMsg->code)
    {
    case kDownStart:
    stateMessage = "downloadStart";
    break;
     
    case kDownDone:
    stateMessage = "downloadDone";
    break;
     
    case kUncompressStart:
    stateMessage = "uncompressStart";
    break;
    case kUncompressDone:
    stateMessage = "uncompressDone";
    break;
     
    default:
    stateMessage = "stateUnknown";
    }
     
    CCScriptEngineManager::sharedManager()
    ->getScriptEngine()
    ->executeEvent(
    stateMsg->manager->_scriptHandler,
    "state",
    CCString::create(stateMessage.c_str()),
    "CCString");
    }
     
    delete ((StateMessage*)msg->obj);
    }

    3.4.3 UPDATER_MESSAGE_PROGRESS

    事件名为 progress,传递的对象为一个 CCInteger ,代表进度。详细的实现可以看 源码

    3.4.4 UPDATER_MESSAGE_ERROR

    事件名为 error,传递的对象是一个 CCString,值有这样几个:

    • errorCreateFile
    • errorNetwork
    • errorNoNewVersion
    • errorUncompress
    • errorUnknown

    方法的实现和上面的 UPDATER_MESSAGE_STATE 类似,这里就不贴了。详细的实现可以看 源码

    Updater(C++) 部分只做了这些苦力工作,而具体的分析逻辑(分析getUserInfo返回的数据决定是否升级、如何升级和升级什么),下载命令的发出(调用update方法),解压成功之后的操作(比如合并新文件到就文件中,更新文件索引列表等等),全部需要lua来做。下面是一个处理Updater(C++)事件的lua函数的例子。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    function us._updateHandler(event, value)
    updater.state = event
    if event == "success" then
    updater.stateValue = value:getCString()
    -- 成功之后更新资源列表,合并新资源
    updater.updateFinalResInfo()
    -- 调用成功后的处理函数
    if us._succHandler then
    us._succHandler()
    end
    elseif event == "error" then
    updater.stateValue = value:getCString()
    elseif event == "progress" then
    updater.stateValue = tostring(value:getValue())
    elseif event == "state" then
    updater.stateValue = value:getCString()
    end
    -- us._label 是一个CCLabelTTF,用来显示进度和状态
    us._label:setString(updater.stateValue)
    assert(event ~= "error",
    string.format("Update error: %s !", updater.stateValue))
    end
     
    updater:registerScriptHandler(us._updateHandler)

    4. update包(lua)

    update包是整个项目的入口包,quick会首先载入这个包,甚至在 framework 之前。

    4.1 为update包所做的项目修改

    我修改了quick项目文件 AppDelegate.cpp 中的 applicationDidFinishLaunching 方法,使其变成这样:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    bool AppDelegate::applicationDidFinishLaunching()
    {
    // initialize director
    CCDirector *pDirector = CCDirector::sharedDirector();
    pDirector->setOpenGLView(CCEGLView::sharedOpenGLView());
    pDirector->setProjection(kCCDirectorProjection2D);
     
    // set FPS. the default value is 1.0/60 if you don't call this
    pDirector->setAnimationInterval(1.0 / 60);
     
    // register lua engine
    CCLuaEngine *pEngine = CCLuaEngine::defaultEngine();
    CCScriptEngineManager::sharedManager()->setScriptEngine(pEngine);
     
    CCLuaStack *pStack = pEngine->getLuaStack();
     
    string gtrackback = "
    function __G__TRACKBACK__(errorMessage)
    print("----------------------------------------")
    print("LUA ERROR: " .. tostring(errorMessage) .. "\n")
    print(debug.traceback("", 2))
    print("----------------------------------------")
    end";
    pEngine->executeString(gtrackback.c_str());
     
    // load update framework
    pStack->loadChunksFromZIP("res/lib/update.zip");
     
    string start_path = "require("update.UpdateApp").new("update"):run(true)";
    CCLOG("------------------------------------------------");
    CCLOG("EXECUTE LUA STRING: %s", start_path.c_str());
    CCLOG("------------------------------------------------");
    pEngine->executeString(start_path.c_str());
     
    return true;
    }

    原来位于 main.lua 中的 __G_TRACKBACK__ 函数(用于输出lua报错信息)直接包含在C++代码中了。那么现在 main.lua 就不再需要了。

    同样的,第一个载入的模块变成了 res/lib/update.zip,这个zip也可以放在quick能找到的其它路径中,使用这个路径只是我的个人习惯。

    最后,LuaStack直接执行了下面这句代码启动了 update.UpdateApp 模块:

    1
    require("update.UpdateApp").new("update"):run(true);

    4.2 update包中的模块

    update包有三个子模块,每个模块是一个lua文件,分别为:

    • update.UpdateApp 检测更新,决定启动哪个模块。
    • update.updater 负责真正的更新工作,与C++通信,下载、解压、复制。
    • update.updateScene 负责在更新过程中显示界面,进度条等等。

    对于不同的大小写,是因为在我的命名规则中,类用大写开头,对象是小写开头。 update.UpdateApp是一个类,其它两个是对象(table)。

    下面的 4.3、4.4、4.5 将分别对这3个模块进行详细介绍。

    4.3 update.UpdateApp

    下面是入口模块 UpdateApp 的内容:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    --- The entry of Game
    -- @author zrong(zengrong.net)
    -- Creation 2014-07-03
     
    local UpdateApp = {}
     
    UpdateApp.__cname = "UpdateApp"
    UpdateApp.__index = UpdateApp
    UpdateApp.__ctype = 2
     
    local sharedDirector = CCDirector:sharedDirector()
    local sharedFileUtils = CCFileUtils:sharedFileUtils()
    local updater = require("update.updater")
     
    function UpdateApp.new(...)
    local instance = setmetatable({}, UpdateApp)
    instance.class = UpdateApp
    instance:ctor(...)
    return instance
    end
     
    function UpdateApp:ctor(appName, packageRoot)
    self.name = appName
    self.packageRoot = packageRoot or appName
     
    print(string.format("UpdateApp.ctor, appName:%s, packageRoot:%s", appName, packageRoot))
     
    -- set global app
    _G[self.name] = self
    end
     
    function UpdateApp:run(checkNewUpdatePackage)
    --print("I am new update package")
    local newUpdatePackage = updater.hasNewUpdatePackage()
    print(string.format("UpdateApp.run(%s), newUpdatePackage:%s",
    checkNewUpdatePackage, newUpdatePackage))
    if checkNewUpdatePackage and newUpdatePackage then
    self:updateSelf(newUpdatePackage)
    elseif updater.checkUpdate() then
    self:runUpdateScene(function()
    _G["finalRes"] = updater.getResCopy()
    self:runRootScene()
    end)
    else
    _G["finalRes"] = updater.getResCopy()
    self:runRootScene()
    end
    end
     
    -- Remove update package, load new update package and run it.
    function UpdateApp:updateSelf(newUpdatePackage)
    print("UpdateApp.updateSelf ", newUpdatePackage)
    local updatePackage = {
    "update.UpdateApp",
    "update.updater",
    "update.updateScene",
    }
    self:_printPackages("--before clean")
    for __,v in ipairs(updatePackage) do
    package.preload[v] = nil
    package.loaded[v] = nil
    end
    self:_printPackages("--after clean")
    _G["update"] = nil
    CCLuaLoadChunksFromZIP(newUpdatePackage)
    self:_printPackages("--after CCLuaLoadChunksForZIP")
    require("update.UpdateApp").new("update"):run(false)
    self:_printPackages("--after require and run")
    end
     
    -- Show a scene for update.
    function UpdateApp:runUpdateScene(handler)
    self:enterScene(require("update.updateScene").addListener(handler))
    end
     
    -- Load all of packages(except update package, it is not in finalRes.lib)
    -- and run root app.
    function UpdateApp:runRootScene()
    for __, v in pairs(finalRes.lib) do
    print("runRootScene:CCLuaLoadChunksFromZip",__, v)
    CCLuaLoadChunksFromZIP(v)
    end
     
    require("root.RootScene").new("root"):run()
    end
     
    function UpdateApp:_printPackages(label)
    label = label or ""
    print(" pring packages "..label.."------------------")
    for __k, __v in pairs(package.preload) do
    print("package.preload:", __k, __v)
    end
    for __k, __v in pairs(package.loaded) do
    print("package.loaded:", __k, __v)
    end
    print("print packages "..label.."------------------ ")
    end
     
     
    function UpdateApp:exit()
    sharedDirector:endToLua()
    os.exit()
    end
     
    function UpdateApp:enterScene(__scene)
    if sharedDirector:getRunningScene() then
    sharedDirector:replaceScene(__scene)
    else
    sharedDirector:runWithScene(__scene)
    end
    end
     
    return UpdateApp

    我来说几个重点。

    4.3.1 没有framework

    由于没有加载 framework,class当然是不能用的。所有quick framework 提供的方法都不能使用。

    我借用class中的一些代码来实现 UpdateApp 的继承。其实我觉得这个UpdateApp也可以不必写成class的。

    4.3.2 入口函数 update.UpdateApp:run(checkNewUpdatePackage)

    run 是入口函数,同时接受一个参数,这个参数用于判断是否要检测本地有新的 update.zip 模块。

    是的,run 就是那个在 AppDelegate.cpp 中第一个调用的lua函数。

    这个函数接受一个参数 checkNewUpdatePackage ,在C++调用 run 的时候,传递的值是 true 。

    如果这个值为真,则会检测本地是否拥有新的更新模块,这个检测通过 update.updater.hasNewUpdatePackage() 方法进行,后面会说到这个方法。

    本地有更新的 update 模块,则直接调用 updateSelf 来更新 update 模块自身;若无则检测是否有项目更新,下载更新的资源,解析它们,处理它们,然后启动主项目。这些工作通过 update.updater.checkUpdate() 完成,后面会说到这个方法。

    若没有任何内容需要更新,则直接调用 runRootScene 来显示主场景了。这后面的内容就交给住场景去做了,update 模块退出历史舞台。

    从上面这个流程可以看出。在更新完成之前,主要的项目代码和资源没有进行任何载入。这也就大致达到了我们 更新一切 的需求。因为所有的东西都没有载入,也就不存在更新。只需要保证我载入的内容是最新的就行了。

    因此,只要保证 update 模块能更新,就达到我们最开始的目标了。

    这个流程还可以保证,如果没有更新,甚至根本就不需要载入 update 模块的场景界面,直接跳转到游戏的主场景即可。

    有句代码在 run 函数中至关重要:

    1
    _G["finalRes"] = updater.getResCopy()

    finalRes 这个全局变量保存了本地所有的 原始/更新 资源索引。它是一个嵌套的tabel,保存的是所有资源的名称以及它们对应的 绝对/相对 路径的对应关系。后面会详述。

    4.3.3 更新自身 update.UpdateApp:updateSelf(newUpdatePackage)

    这是本套机制中最重要的一环。理解了它,你就知道更新一切其实没什么秘密。Lua本来就提供了这样一套机制。

    由于在 C++ 中已经将 update 模块载入了内存,那么要更新自身首先要做的是清除 Lua 的载入标记。

    Lua在两个全局变量中做了标记:

    • package.preload 执行 CCLuaLoadChunksFromZIP 之后会将模块缓存在这里作为 require 的加载器;
    • package.loaded 执行 require 的时候会先查询 package.loaded,若没有则会查询 package.preload 得到加载器,利用加载器加载模块,再将加载的模块缓存到 package.loaded 中。

    详细的机制可以阅读 Lua程序设计(第2版) 15.1 require 函数。

    那么,要更新自己,只需要把 package.preload 和 package.loaded 清除,然后再用新的 模块填充 package.preload 即可。下面就是核心代码了:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    local updatePackage = {
    "update.UpdateApp",
    "update.updater",
    "update.updateScene",
    }
    for __,v in ipairs(updatePackage) do
    package.preload[v] = nil
    package.loaded[v] = nil
    end
    _G["update"] = nil
    CCLuaLoadChunksFromZIP(newUpdatePackage)
    require("update.UpdateApp").new("update"):run(false)

    如果不相信这么简单,可以用上面完整的 UpdateApp 模块中提供的 UpdateApp:_printPackages(label) 方法来检测。

    4.3.4 显示更新界面 update.UpdateApp:runUpdateScene(handler)

    update.updater.checkUpdate() 的返回是异步的,下载和解压都需要时间,在这段时间里面,我们需要一个界面。runUpdateScene 方法的作用就是显示这个界面。并在更新成功之后调用handler处理函数。

    4.3.5 显示主场景 update.UpdateApp:runRootScene()

    到了这里,update 包就没有作用了。但由于我们先前没有载入除 update 包外的任何包,这里必须先载入它们。

    我上面提到过,finalRes 这个全局变量是一个索引表,它的 lib 对象就是一个包含所有待载入的包(类似于frameworks_precompiled.zip 这种)的列表。我们通过循环将它们载入内存。

    对于 root.RootScene 这个模块来说,就是标准的quick模块了,它可以使用quick中的任何特性。

    1
    2
    3
    4
    5
    6
    for __, v in pairs(finalRes.lib) do
    print("runRootScene:CCLuaLoadChunksFromZip",__, v)
    CCLuaLoadChunksFromZIP(v)
    end
     
    require("root.RootScene").new("root"):run()

    4.3.6 怎么使用这个模块

    你如果要直接拿来就用,这个模块基本上不需要修改。因为本来它就没什么特别的功能。当然,你可以看完下面两个模块再决定。

    4.4 update.updateScene

    这个模块用于显示更新过程的进度和一些信息。所有内容如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    141
    142
    143
    144
    145
    146
    147
    148
    149
    150
    151
    152
    153
    154
    155
    156
    157
    158
    159
    160
    161
    162
    163
    164
    165
    166
    167
    168
    169
    170
    171
    172
    173
    174
    175
    176
    177
    178
    179
    180
    181
    182
    183
    184
    185
    186
    187
    188
    189
    190
    191
    192
    193
    194
    ------
    -- updateScene for update package.
    -- This is a object, not a class.
    -- In this scene, it will show download progress bar
    -- and state for uncompress.
    -- @author zrong(zengrong.net)
    -- Creation: 2014-07-03
     
    local updater = require("update.updater")
    local sharedDirector = CCDirector:sharedDirector()
     
    -- check device screen size
    local glview = sharedDirector:getOpenGLView()
    local size = glview:getFrameSize()
    local display = {}
    display.sizeInPixels = {width = size.width, height = size.height}
     
    local w = display.sizeInPixels.width
    local h = display.sizeInPixels.height
     
    CONFIG_SCREEN_WIDTH = 1280
    CONFIG_SCREEN_HEIGHT = 800
    CONFIG_SCREEN_AUTOSCALE = "FIXED_HEIGHT"
     
    local scale, scaleX, scaleY
     
    scaleX, scaleY = w / CONFIG_SCREEN_WIDTH, h / CONFIG_SCREEN_HEIGHT
    scale = scaleY
    CONFIG_SCREEN_WIDTH = w / scale
     
    glview:setDesignResolutionSize(CONFIG_SCREEN_WIDTH, CONFIG_SCREEN_HEIGHT, kResolutionNoBorder)
     
    local winSize = sharedDirector:getWinSize()
    display.contentScaleFactor = scale
    display.size = {width = winSize.width, height = winSize.height}
    display.width = display.size.width
    display.height = display.size.height
    display.cx = display.width / 2
    display.cy = display.height / 2
    display.c_left = -display.width / 2
    display.c_right = display.width / 2
    display.c_top = display.height / 2
    display.c_bottom = -display.height / 2
    display.left = 0
    display.right = display.width
    display.top = display.height
    display.bottom = 0
    display.widthInPixels = display.sizeInPixels.width
    display.heightInPixels = display.sizeInPixels.height
     
    print("# display in updateScene start")
    print(string.format("# us.CONFIG_SCREEN_AUTOSCALE = %s", CONFIG_SCREEN_AUTOSCALE))
    print(string.format("# us.CONFIG_SCREEN_WIDTH = %0.2f", CONFIG_SCREEN_WIDTH))
    print(string.format("# us.CONFIG_SCREEN_HEIGHT = %0.2f", CONFIG_SCREEN_HEIGHT))
    print(string.format("# us.display.widthInPixels = %0.2f", display.widthInPixels))
    print(string.format("# us.display.heightInPixels = %0.2f", display.heightInPixels))
    print(string.format("# us.display.contentScaleFactor = %0.2f", display.contentScaleFactor))
    print(string.format("# us.display.width = %0.2f", display.width))
    print(string.format("# us.display.height = %0.2f", display.height))
    print(string.format("# us.display.cx = %0.2f", display.cx))
    print(string.format("# us.display.cy = %0.2f", display.cy))
    print(string.format("# us.display.left = %0.2f", display.left))
    print(string.format("# us.display.right = %0.2f", display.right))
    print(string.format("# us.display.top = %0.2f", display.top))
    print(string.format("# us.display.bottom = %0.2f", display.bottom))
    print(string.format("# us.display.c_left = %0.2f", display.c_left))
    print(string.format("# us.display.c_right = %0.2f", display.c_right))
    print(string.format("# us.display.c_top = %0.2f", display.c_top))
    print(string.format("# us.display.c_bottom = %0.2f", display.c_bottom))
    print("# display in updateScene done")
     
    display.ANCHOR_POINTS = {
    CCPoint(0.5, 0.5), -- CENTER
    CCPoint(0, 1), -- TOP_LEFT
    CCPoint(0.5, 1), -- TOP_CENTER
    CCPoint(1, 1), -- TOP_RIGHT
    CCPoint(0, 0.5), -- CENTER_LEFT
    CCPoint(1, 0.5), -- CENTER_RIGHT
    CCPoint(0, 0), -- BOTTOM_LEFT
    CCPoint(1, 0), -- BOTTOM_RIGHT
    CCPoint(0.5, 0), -- BOTTOM_CENTER
    }
     
    display.CENTER = 1
    display.LEFT_TOP = 2; display.TOP_LEFT = 2
    display.CENTER_TOP = 3; display.TOP_CENTER = 3
    display.RIGHT_TOP = 4; display.TOP_RIGHT = 4
    display.CENTER_LEFT = 5; display.LEFT_CENTER = 5
    display.CENTER_RIGHT = 6; display.RIGHT_CENTER = 6
    display.BOTTOM_LEFT = 7; display.LEFT_BOTTOM = 7
    display.BOTTOM_RIGHT = 8; display.RIGHT_BOTTOM = 8
    display.BOTTOM_CENTER = 9; display.CENTER_BOTTOM = 9
     
    function display.align(target, anchorPoint, x, y)
    target:setAnchorPoint(display.ANCHOR_POINTS[anchorPoint])
    if x and y then target:setPosition(x, y) end
    end
     
    local us = CCScene:create()
    us.name = "updateScene"
     
    local localResInfo = nil
     
    function us._addUI()
    -- Get the newest resinfo in ures.
    local localResInfo = updater.getResCopy()
     
    local __bg = CCSprite:create(us._getres("res/pic/init_bg.png"))
    display.align(__bg, display.CENTER, display.cx, display.cy)
    us:addChild(__bg, 0)
     
    local __label = CCLabelTTF:create("Loading...", "Arial", 24)
    __label:setColor(ccc3(255, 0, 0))
    us._label = __label
    display.align(__label, display.CENTER, display.cx, display.bottom+30)
    us:addChild(__label, 10)
    end
     
    function us._getres(path)
    if not localResInfo then
    localResInfo = updater.getResCopy()
    end
    for key, value in pairs(localResInfo.oth) do
    print("us._getres:", key, value)
    local pathInIndex = string.find(key, path)
    if pathInIndex and pathInIndex >= 1 then
    print("us._getres getvalue:", path)
    res[path] = value
    return value
    end
    end
    return path
    end
     
    function us._sceneHandler(event)
    if event == "enter" then
    print(string.format("updateScene "%s:onEnter()"", us.name))
    us.onEnter()
    elseif event == "cleanup" then
    print(string.format("updateScene "%s:onCleanup()"", us.name))
    us.onCleanup()
    elseif event == "exit" then
    print(string.format("updateScene "%s:onExit()"", us.name))
    us.onExit()
     
    if DEBUG_MEM then
    print("----------------------------------------")
    print(string.format("LUA VM MEMORY USED: %0.2f KB", collectgarbage("count")))
    CCTextureCache:sharedTextureCache():dumpCachedTextureInfo()
    print("----------------------------------------")
    end
    end
    end
     
    function us._updateHandler(event, value)
    updater.state = event
    if event == "success" then
    updater.stateValue = value:getCString()
    updater.updateFinalResInfo()
    if us._succHandler then
    us._succHandler()
    end
    elseif event == "error" then
    updater.stateValue = value:getCString()
    elseif event == "progress" then
    updater.stateValue = tostring(value:getValue())
    elseif event == "state" then
    updater.stateValue = value:getCString()
    end
    us._label:setString(updater.stateValue)
    assert(event ~= "error",
    string.format("Update error: %s !", updater.stateValue))
    end
     
    function us.addListener(handler)
    us._succHandler = handler
    return us
    end
     
    function us.onEnter()
    updater.update(us._updateHandler)
    end
     
    function us.onExit()
    updater.clean()
    us:unregisterScriptHandler()
    end
     
    function us.onCleanup()
    end
     
    us:registerScriptHandler(us._sceneHandler)
    us._addUI()
    return us

    代码都在上面,说重点:

    4.4.1 还是没有framework

    这是必须一直牢记的。由于没有载入quick的 framework,所有的quick特性都不能使用。

    你也许会说没有framework我怎么写界面?那么想想用C++的同学吧!那个代码怎么也比Lua多吧?

    什么什么?你说有CCB和CCS?CCS你妹啊!同学我和你不是一个班的。

    例如,原来在quick中这样写:

    1
    2
    3
    display.newSprite("res/pic/init_bg.png")
    :align(display.CENTER, display.cx, display.cy)
    :addTo(self, 0)

    在没有quick framework的时候需要改成这样:

    1
    2
    3
    local __bg = CCSprite:create(us._getres("res/pic/init_bg.png"))
    display.align(__bg, display.CENTER, display.cx, display.cy)
    us:addChild(__bg, 0)

    等等!为啥我用了 display !!!笨蛋,你不会偷quick的啊啊啊!

    4.4.2 必须要偷的代码

    为了方便使用,我们可以偷一部分framework的代码过来(干嘛说得那么难听嘛,程序员怎么能用偷?程序员的事,用CV啊),注意CV来的代码用local变量来保存。由于 updateScene 已经是一个可视的场景,因此quick中关于界面缩放设置的那部分代码也是必须CV过来。不多,几十行而已。

    游戏产品绝大多数都不会做成横屏竖屏自动适应的(自己找SHI啊有木有),因此界面缩放的代码我也只保存了一个横屏的,这又省了不少。那CV的同学,注意自己改啊!

    4.4.3 update.updateScene._getres(path)

    在 update.updateScene 模块中,所有涉及到资源路径的地方,必须使用这个方法来包裹。

    这个方法先从 update.updater 模块中获取最新的资源索引列表,然后根据我们传递的相对路径从索引列表中查找到资源的实际路径(可能是原包自带的资源,也可能是更新后的资源的绝对路径),然后载入它们。这能保证我们使用的是最新的资源。

    4.4.4 update.updateScene._updateHandler(event, value)

    这个方法已经在 上面 C++ 模块中 讲过了。注意其中的 _succHandler 是在 update.UpdateApp 中定义的匿名函数。

    4.4.5 怎么使用这个模块

    如果你要使用这个模块,那么可能大部分都要重写。你可以看到,我在这个模块中只有一个背景图和一个 CCLabeTTF 来显示下载进度和状态。你当然不希望你的更新界面就是这个样子。怎么也得来个妹子做封面不是?

    4.5 update.updater

    这是整个更新系统的核心部分了。代码更长一点,但其实很好懂。

    在这个模块中,我们需要完成下面的工作:

    1. 调用C++的Updater模块来获取远程的版本号以及资源下载地址;
    2. 调用C++的Updater模块来下载解压;
    3. 合并解压后的新资源到新资源文件夹;
    4. 更新总的资源索引;
    5. 删除临时文件;
    6. 报告更新中的各种错误。

    所以说,这是一个工具模块。它提供的是给更新使用的各种工具。而 UpdateApp 和 updateScene 则分别是功能和界面模块。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    141
    142
    143
    144
    145
    146
    147
    148
    149
    150
    151
    152
    153
    154
    155
    156
    157
    158
    159
    160
    161
    162
    163
    164
    165
    166
    167
    168
    169
    170
    171
    172
    173
    174
    175
    176
    177
    178
    179
    180
    181
    182
    183
    184
    185
    186
    187
    188
    189
    190
    191
    192
    193
    194
    195
    196
    197
    198
    199
    200
    201
    202
    203
    204
    205
    206
    207
    208
    209
    210
    211
    212
    213
    214
    215
    216
    217
    218
    219
    220
    221
    222
    223
    224
    225
    226
    227
    228
    229
    230
    231
    232
    233
    234
    235
    236
    237
    238
    239
    240
    241
    242
    243
    244
    245
    246
    247
    248
    249
    250
    251
    252
    253
    254
    255
    256
    257
    258
    259
    260
    261
    262
    263
    264
    265
    266
    267
    268
    269
    270
    271
    272
    273
    274
    275
    276
    277
    278
    279
    280
    281
    282
    283
    284
    285
    286
    287
    288
    289
    290
    291
    292
    293
    294
    295
    296
    297
    298
    299
    300
    301
    302
    303
    304
    305
    306
    307
    308
    309
    310
    311
    312
    313
    314
    315
    316
    317
    318
    319
    320
    321
    322
    323
    324
    325
    326
    327
    328
    329
    330
    331
    332
    333
    334
    335
    336
    337
    338
    339
    340
    341
    342
    343
    344
    345
    346
    347
    348
    349
    350
    351
    352
    --- The helper for update package.
    -- It can download resources and uncompress it,
    -- copy new package to res directory,
    -- and remove temporery directory.
    -- @author zrong(zengrong.net)
    -- Creation 2014-07-03
     
    require "lfs"
    local updater = {}
    updater.STATES = {
    kDownStart = "downloadStart",
    kDownDone = "downloadDone",
    kUncompressStart = "uncompressStart",
    kUncompressDone = "uncompressDone",
    unknown = "stateUnknown",
    }
     
    updater.ERRORS = {
    kCreateFile = "errorCreateFile",
    kNetwork = "errorNetwork",
    kNoNewVersion = "errorNoNewVersion",
    kUncompress = "errorUncompress",
    unknown = "errorUnknown";
    }
     
    function updater.isState(state)
    for k,v in pairs(updater.STATES) do
    if v == state then
    return true
    end
    end
    return false
    end
     
    function updater.clone(object)
    local lookup_table = {}
    local function _copy(object)
    if type(object) ~= "table" then
    return object
    elseif lookup_table[object] then
    return lookup_table[object]
    end
    local new_table = {}
    lookup_table[object] = new_table
    for key, value in pairs(object) do
    new_table[_copy(key)] = _copy(value)
    end
    return setmetatable(new_table, getmetatable(object))
    end
    return _copy(object)
    end
     
    function updater.vardump(object, label, returnTable)
    local lookupTable = {}
    local result = {}
     
    local function _v(v)
    if type(v) == "string" then
    v = """ .. v .. """
    end
    return tostring(v)
    end
     
    local function _vardump(object, label, indent, nest)
    label = label or ""
    local postfix = ""
    if nest > 1 then postfix = "," end
    if type(object) ~= "table" then
    if type(label) == "string" then
    result[#result +1] = string.format("%s["%s"] = %s%s", indent, label, _v(object), postfix)
    else
    result[#result +1] = string.format("%s%s%s", indent, _v(object), postfix)
    end
    elseif not lookupTable[object] then
    lookupTable[object] = true
     
    if type(label) == "string" then
    result[#result +1 ] = string.format("%s%s = {", indent, label)
    else
    result[#result +1 ] = string.format("%s{", indent)
    end
    local indent2 = indent .. " "
    local keys = {}
    local values = {}
    for k, v in pairs(object) do
    keys[#keys + 1] = k
    values[k] = v
    end
    table.sort(keys, function(a, b)
    if type(a) == "number" and type(b) == "number" then
    return a < b
    else
    return tostring(a) < tostring(b)
    end
    end)
    for i, k in ipairs(keys) do
    _vardump(values[k], k, indent2, nest + 1)
    end
    result[#result +1] = string.format("%s}%s", indent, postfix)
    end
    end
    _vardump(object, label, "", 1)
     
    if returnTable then return result end
    return table.concat(result, " ")
    end
     
    local u = nil
    local f = CCFileUtils:sharedFileUtils()
    -- The res index file in original package.
    local lresinfo = "res/resinfo.lua"
    local uroot = f:getWritablePath()
    -- The directory for save updated files.
    local ures = uroot.."res/"
    -- The package zip file what download from server.
    local uzip = uroot.."res.zip"
    -- The directory for uncompress res.zip.
    local utmp = uroot.."utmp/"
    -- The res index file in zip package for update.
    local zresinfo = utmp.."res/resinfo.lua"
     
    -- The res index file for final game.
    -- It combiled original lresinfo and zresinfo.
    local uresinfo = ures .. "resinfo.lua"
     
    local localResInfo = nil
    local remoteResInfo = nil
    local finalResInfo = nil
     
    local function _initUpdater()
    print("initUpdater, ", u)
    if not u then u = Updater:new() end
    print("after initUpdater:", u)
    end
     
    function updater.writeFile(path, content, mode)
    mode = mode or "w+b"
    local file = io.open(path, mode)
    if file then
    if file:write(content) == nil then return false end
    io.close(file)
    return true
    else
    return false
    end
    end
     
    function updater.readFile(path)
    return f:getFileData(path)
    end
     
    function updater.exists(path)
    return f:isFileExist(path)
    end
     
    --[[
    -- Departed, uses lfs instead.
    function updater._mkdir(path)
    _initUpdater()
    return u:createDirectory(path)
    end
     
    -- Departed, get a warning in ios simulator
    function updater._rmdir(path)
    _initUpdater()
    return u:removeDirectory(path)
    end
    --]]
     
    function updater.mkdir(path)
    if not updater.exists(path) then
    return lfs.mkdir(path)
    end
    return true
    end
     
    function updater.rmdir(path)
    print("updater.rmdir:", path)
    if updater.exists(path) then
    local function _rmdir(path)
    local iter, dir_obj = lfs.dir(path)
    while true do
    local dir = iter(dir_obj)
    if dir == nil then break end
    if dir ~= "." and dir ~= ".." then
    local curDir = path..dir
    local mode = lfs.attributes(curDir, "mode")
    if mode == "directory" then
    _rmdir(curDir.."/")
    elseif mode == "file" then
    os.remove(curDir)
    end
    end
    end
    local succ, des = os.remove(path)
    if des then print(des) end
    return succ
    end
    _rmdir(path)
    end
    return true
    end
     
    -- Is there a update.zip package in ures directory?
    -- If it is true, return its abstract path.
    function updater.hasNewUpdatePackage()
    local newUpdater = ures.."lib/update.zip"
    if updater.exists(newUpdater) then
    return newUpdater
    end
    return nil
    end
     
    -- Check local resinfo and remote resinfo, compare their version value.
    function updater.checkUpdate()
    localResInfo = updater.getLocalResInfo()
    local localVer = localResInfo.version
    print("localVer:", localVer)
    remoteResInfo = updater.getRemoteResInfo(localResInfo.update_url)
    local remoteVer = remoteResInfo.version
    print("remoteVer:", remoteVer)
    return remoteVer ~= localVer
    end
     
    -- Copy resinfo.lua from original package to update directory(ures)
    -- when it is not in ures.
    function updater.getLocalResInfo()
    print(string.format("updater.getLocalResInfo, lresinfo:%s, uresinfo:%s",
    lresinfo,uresinfo))
    local resInfoTxt = nil
    if updater.exists(uresinfo) then
    resInfoTxt = updater.readFile(uresinfo)
    else
    assert(updater.mkdir(ures), ures.." create error!")
    local info = updater.readFile(lresinfo)
    print("localResInfo:", info)
    assert(info, string.format("Can not get the constent from %s!", lresinfo))
    updater.writeFile(uresinfo, info)
    resInfoTxt = info
    end
    return assert(loadstring(resInfoTxt))()
    end
     
    function updater.getRemoteResInfo(path)
    _initUpdater()
    print("updater.getRemoteResInfo:", path)
    local resInfoTxt = u:getUpdateInfo(path)
    print("resInfoTxt:", resInfoTxt)
    return assert(loadstring(resInfoTxt))()
    end
     
    function updater.update(handler)
    assert(remoteResInfo and remoteResInfo.package, "Can not get remoteResInfo!")
    print("updater.update:", remoteResInfo.package)
    if handler then
    u:registerScriptHandler(handler)
    end
    updater.rmdir(utmp)
    u:update(remoteResInfo.package, uzip, utmp, false)
    end
     
    function updater._copyNewFile(resInZip)
    -- Create nonexistent directory in update res.
    local i,j = 1,1
    while true do
    j = string.find(resInZip, "/", i)
    if j == nil then break end
    local dir = string.sub(resInZip, 1,j)
    -- Save created directory flag to a table because
    -- the io operation is too slow.
    if not updater._dirList[dir] then
    updater._dirList[dir] = true
    local fullUDir = uroot..dir
    updater.mkdir(fullUDir)
    end
    i = j+1
    end
    local fullFileInURes = uroot..resInZip
    local fullFileInUTmp = utmp..resInZip
    print(string.format('copy %s to %s', fullFileInUTmp, fullFileInURes))
    local zipFileContent = updater.readFile(fullFileInUTmp)
    if zipFileContent then
    updater.writeFile(fullFileInURes, zipFileContent)
    return fullFileInURes
    end
    return nil
    end
     
    function updater._copyNewFilesBatch(resType, resInfoInZip)
    local resList = resInfoInZip[resType]
    if not resList then return end
    local finalRes = finalResInfo[resType]
    for __,v in ipairs(resList) do
    local fullFileInURes = updater._copyNewFile(v)
    if fullFileInURes then
    -- Update key and file in the finalResInfo
    -- Ignores the update package because it has been in memory.
    if v ~= "res/lib/update.zip" then
    finalRes[v] = fullFileInURes
    end
    else
    print(string.format("updater ERROR, copy file %s.", v))
    end
    end
    end
     
    function updater.updateFinalResInfo()
    assert(localResInfo and remoteResInfo,
    "Perform updater.checkUpdate() first!")
    if not finalResInfo then
    finalResInfo = updater.clone(localResInfo)
    end
    --do return end
    local resInfoTxt = updater.readFile(zresinfo)
    local zipResInfo = assert(loadstring(resInfoTxt))()
    if zipResInfo["version"] then
    finalResInfo.version = zipResInfo["version"]
    end
    -- Save a dir list maked.
    updater._dirList = {}
    updater._copyNewFilesBatch("lib", zipResInfo)
    updater._copyNewFilesBatch("oth", zipResInfo)
    -- Clean dir list.
    updater._dirList = nil
    updater.rmdir(utmp)
    local dumpTable = updater.vardump(finalResInfo, "local data", true)
    dumpTable[#dumpTable+1] = "return data"
    if updater.writeFile(uresinfo, table.concat(dumpTable, " ")) then
    return true
    end
    print(string.format("updater ERROR, write file %s.", uresinfo))
    return false
    end
     
    function updater.getResCopy()
    if finalResInfo then return updater.clone(finalResInfo) end
    return updater.clone(localResInfo)
    end
     
    function updater.clean()
    if u then
    u:unregisterScriptHandler()
    u:delete()
    u = nil
    end
    updater.rmdir(utmp)
    localResInfo = nil
    remoteResInfo = nil
    finalResInfo = nil
    end
     
    return updater

    代码都在上面,还是说重点:

    4.5.1 就是没有framework

    我嘴巴都说出茧子了,没有就是没有。

    不过,我又从quick CV了几个方法过来:

    • clone 方法用来完全复制一个table,在复制文件索引列表的时候使用;
    • vardump 方法用来1持久化索引列表,使其作为一个lua文件保存在设备存储器上。有修改。
    • writeFile 和 readFile 用于把需要的文件写入设备中,也用它来复制文件(读入一个文件,在另一个地方写入来实现复制)
    • exists 这个和quick实现的不太一样,直接用 CCFileUtils 了。

    4.5.2 文件操作

    除了可以用 writeFile 和 readFile 来实现文件的复制操作之外,还要实现文件夹的创建和删除。

    这个功能可以使用 lfs(Lua file system) 来实现,参见:在lua中递归删除一个文件夹 。

    4.5.3 相关目录和变量

    上面的代码中定义了几个变量,在这里进行介绍方便理解:

    4.5.3.1 lres(local res)

    安装包所带的res目录;

    4.5.3.2 ures(updated res)

    保存在设备上的res目录,用于保存从网上下载的新资源;

    4.5.3.3 utmp(update temp)

    临时文件夹,用于解压缩,更新后会删除;

    4.5.3.4 lresinfo(本地索引文件)

    安装包内自带的所有资源的索引文件,所有资源路径指向包内自带的资源。打包的时候和产品包一起提供,产品包会默认使用这个资源索引文件来查找资源。它的大概内容如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    local data = {
    version = "1.0",
    update_url = "http://192.168.18.22:8080/updater/resinfo.lua",
    lib = {
    ["res/lib/config.zip"] = "res/lib/config.zip",
    ["res/lib/framework_precompiled.zip"] = "res/lib/framework_precompiled.zip",
    ["res/lib/root.zip"] = "res/lib/root.zip",
    ......
    },
    oth = {
    ["res/pic/init_bg.png"] = "res/pic/init_bg.png",
    ......
    },
    }
    return data

    从它的结构可以看出,它包含了当前包的版本(version)、在哪里获取要更新的资源索引文件(update_url)、当前包中所有的lua模块的路径(lib)、当前包中所有的资源文件的路径(oth)。

    4.5.3.5 uresinfo(更新索引文件)

    保存在 ures 中的更新后的索引文件,没有更新的资源路径指向包内自带的资源,更新后的资源路径指向ures中的资源。它的内容大致如下:

    config.zip 的路径是在 iOS 模拟器中得到的。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    local data = {
    version = "1.0",
    update_url = "http://192.168.18.22:8080/updater/resinfo.lua",
    lib = {
    ["res/lib/cc.zip"] = "res/lib/cc.zip",
    ["res/lib/config.zip"] = "/Users/zrong/Library/Application Support/iPhone Simulator/7.1/Applications/2B46FAC0-C419-42B5-92B0-B06DD16E113B/Documents/res/lib/config.zip",
    ......
    },
    oth = {
    ["res/pic/init_bg.png"] = "res/pic/init_bg.png",
    ......
    },
    }
    return data

    4.5.3.6 http://192.168.18.22:8080/updater/resinfo.lua

    getRemoteResInfo 方法会读取这个文件,然后将结果解析成lua table。对比其中的version与 lrefinfo 中的区别,来决定是否需要更新。

    若需要,则调用C++ Updater模块中的方法下载 package 指定的zip包并解压。

    它的内容如下:

    1
    2
    3
    4
    5
    local data = {
    version = "1.0.2",
    package = "http://192.168.18.22:8080/updater/res.zip",
    }
    return data

    4.5.3.7 http://192.168.18.22:8080/updater/res.zip

    zip包的文件夹结构大致如下:

    res/
    res/resinfo.lua
    res/lib/cc.zip
    res/pic/init_bg.png
    ......
    

    zip文件的下载和解压都是由C++完成的,但是下载和解压的路径需要Lua来提供。这个动作完成后,C++会通知Lua更新成功。Lua会接着进行后续操作就使用下面 4.5.4 中提到的方法来复制资源、合并 uresinfo 。

    4.5.3.8 zresinfo(zip资源索引文件)

    zip文件中也包含一个 resinfo.lua ,它用于指示哪些文件需要更新。内容大致如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    local data = {
    version = "1.0.2",
    lib = {
    "res/lib/cc.zip",
    ......
    },
    oth = {
    "res/pic/init_bg",
    ......
    },
    }
    return data

    这个文件中包含的所有文件必须能在zip解压后找到。


    4.5.4 update.updater.updateFinalResInfo()

    这是一个至关重要的方法,让我们代入用上面提到的变量名和目录来描述它的功能:

    它实现的功能是:

    1. 读取 uresinfo,若没有,则将 lresinfo 复制成 uresinfo;
    2. 从 utmp 中读取 zresinfo,注意此时zip文件已经解压;
    3. 将需要更新的资源文件从 utmp 中复制到 ures 中;
    4. 更新 uresinfo ,使其中的资源键名指向正确的资源路径(上一步复制的目标路径);
    5. 删除 utmp;
    6. 将更新后的 uresinfo 作为lua文件写入 ures 。

    4.5.5 其它方法

    对 update.updater 的调用一般是这样的顺序:

    1. 调用 checkUpdat 方法检测是否需要升级;
    2. 调用 update 方法执行升级,同时注册事件管理handler;
    3. 升级成功,调用 getResCopy 方法获取最新的 uresinfo 。

    5 对 framework 的修改

    5.1 写一个 getres 方法

    ures 中包含的就是所有素材的索引(键值对)。形式如下:

    • 键名:res/pic/init_bg.png
    • 键值(lres中): res/pic/init_bg.png
    • 键值(ures中):/Users/zrong/Library/Application Support/iPhone Simulator/7.1/Applications/2B46FAC0-C419-42B5-92B0-B06DD16E113B/Documents/res/pic/init_bg.png

    在程序中,我们一般会使用这样的写法来获取资源:

    1
    display.newSprite("pic/init_bg.png")

    或者干脆简化成了:

    1
    display.newSprite("init_bg.png")

    要上面的代码能够工作,需要为 CCFileUtils 设置搜索路径:

    1
    2
    CCFileUtils:sharedFileUtils:addSearchPath("res/")
    CCFileUtils:sharedFileUtils:addSearchPath("res/pic/")

    但是,在这套更新机制中,我不建议设置搜索路径,因为素材都是以完整路径格式保存的,这样使用起来更方便和更确定。

    如果是新项目,那么挺好,我只需要保证素材路径基于 res 提供即可,类似这样:

    1
    display.newSprite("res/pic/init_bg.png")

    但是对于已经开发了一段时间的项目来说,一个个改就太不专业了。这是我们需要扩展一个 io.getres 方法:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    res = {}
     
    function io.getres(path)
    print("io.getres originl:", path)
    if CCFileUtils:sharedFileUtils():isAbsolutePath(path) then
    return path
    end
    if res[path] then return res[path] end
    for key, value in pairs(finalRes.oth) do
    print(key, value)
    local pathInIndex = string.find(key, path)
    if pathInIndex and pathInIndex >= 1 then
    print("io.getres getvalue:", path)
    res[path] = value
    return value
    end
    end
    print("io.getres no get:", path)
    return path
    end

    然后,我们需要修改 quick framework 中的display模块让我们的旧代码不必进行任何改动就能生效。

    5.2 修改 display.newSprite

    找到该方法中的这个部分:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    if string.byte(filename) == 35 then -- first char is #
    local frame = display.newSpriteFrame(string.sub(filename, 2))
    if frame then
    sprite = spriteClass:createWithSpriteFrame(frame)
    end
    else
    if display.TEXTURES_PIXEL_FORMAT[filename] then
    CCTexture2D:setDefaultAlphaPixelFormat(display.TEXTURES_PIXEL_FORMAT[filename])
    sprite = spriteClass:create(filename)
    CCTexture2D:setDefaultAlphaPixelFormat(kCCTexture2DPixelFormat_RGBA8888)
    else
    sprite = spriteClass:create(filename)
    end
    end

    将其改为:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    if string.byte(filename) == 35 then -- first char is #
    local frame = display.newSpriteFrame(string.sub(filename, 2))
    if frame then
    sprite = spriteClass:createWithSpriteFrame(frame)
    end
    else
    local absfilename = io.getres(filename)
    if display.TEXTURES_PIXEL_FORMAT[filename] then
    CCTexture2D:setDefaultAlphaPixelFormat(display.TEXTURES_PIXEL_FORMAT[filename])
    sprite = spriteClass:create(absfilename)
    CCTexture2D:setDefaultAlphaPixelFormat(kCCTexture2DPixelFormat_RGBA8888)
    else
    sprite = spriteClass:create(absfilename)
    end
    end

    5.3 修改display.newTilesSprite

    将其中的 local sprite = CCSprite:create(filename, rect)

    改为local sprite = CCSprite:create(io.getres(filename), rect)

    5.4 修改 display.newBatchNode

    改法与上面相同。

    6. 后记

    噢!这真是一篇太长的文章了,真希望我都说清了。

    其实还有一些东西在这个机制中没有涉及,例如:

    6.1 更新的健壮性

    • 在更新 update.zip 模块自身的时候,如果新的update.zip有问题怎么办?
    • 如果索引文件找不到怎么办?zip文件解压失败怎么办?zresinfo 中的内容与zip文件解压后的内容不符怎么办?
    • 下载更新的时候网断了如何处理?如何处理断点续传?设备磁盘空间不够了怎么处理?

    6.2 更多的更新方式

    我在 需求的复杂性 里面描述了一些需求,例如:

    • 如何回滚更新?
    • 如何多个版本共存?
    • 如何对资源进行指纹码化?

    这些问题都不难解决。方法自己想,我只能写到这儿了。

    话说回来,实现了 更新一切 ,你还担心什么呢?

  • 相关阅读:
    【转】内部Handler类引起内存泄露
    检测是否存在相机硬件代码
    asp.net 过滤器
    iis 中经典和集成模式对应webconfig节点
    事务
    C# Excel操作
    一步一步部署SSIS包图解教程
    js和.net操作Cookie遇到的问题
    File,FileInfo,Directory,DirectoryInfo
    C#文件Copy
  • 原文地址:https://www.cnblogs.com/xiaoleiel/p/8301199.html
Copyright © 2011-2022 走看看