[size=5]4.5 update.updater[/size]
这是整个更新系统的核心部分了。代码更长一点,但其实很好懂。
在这个模块中,我们需要完成下面的工作:
[list=1][li]调用C++的Updater模块来获取远程的版本号以及资源下载地址;[/li][li]调用C++的Updater模块来下载解压;[/li][li]合并解压后的新资源到新资源文件夹;[/li][li]更新总的资源索引;[/li][li]删除临时文件;[/li][li]报告更新中的各种错误。[/li][/list]所以说,这是一个工具模块。它提供的是给更新使用的各种工具。而 UpdateApp 和 updateScene 则分别是功能和界面模块。
--- 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, "\n")
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, "\n")) 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[/code]
什么什么?你说有CCB和CCS?CCS你妹啊!同学我和你不是一个班的。
例如,原来在quick中这样写:
代码都在上面,还是说重点:
[b]4.5.1 就是没有framework[/b]
我嘴巴都说出茧子了,没有就是没有。
不过,我又从quick CV了几个方法过来:
[list][li]clone 方法用来完全复制一个table,在复制文件索引列表的时候使用;[/li][li]vardump 方法用来1持久化索引列表,使其作为一个lua文件保存在设备存储器上。有修改。[/li][li]writeFile 和 readFile 用于把需要的文件写入设备中,也用它来复制文件(读入一个文件,在另一个地方写入来实现复制)[/li][li]exists 这个和quick实现的不太一样,直接用 CCFileUtils 了。[/li][/list][b]4.5.2 文件操作[/b]
除了可以用 writeFile 和 readFile 来实现文件的复制操作之外,还要实现文件夹的创建和删除。
这个功能可以使用 lfs(Lua file system) 来实现,参见:[url=http://zengrong.net/post/2129.htm]在lua中递归删除一个文件夹[/url] 。
[b]4.5.3 相关目录和变量[/b]
上面的代码中定义了几个变量,在这里进行介绍方便理解:
[b]4.5.3.1 lres(local res)[/b]
安装包所带的res目录;
[b]4.5.3.2 ures(updated res)[/b]
保存在设备上的res目录,用于保存从网上下载的新资源;
[b]4.5.3.3 utmp(update temp)[/b]
临时文件夹,用于解压缩,更新后会删除;
[b]4.5.3.4 lresinfo(本地索引文件)[/b]
安装包内自带的所有资源的索引文件,所有资源路径指向包内自带的资源。打包的时候和产品包一起提供,产品包会默认使用这个资源索引文件来查找资源。它的大概内容如下:
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
[code]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[/code]
从它的结构可以看出,它包含了当前包的版本(version)、在哪里获取要更新的资源索引文件(update_url)、当前包中所有的lua模块的路径(lib)、当前包中所有的资源文件的路径(oth)。
[b]4.5.3.5 uresinfo(更新索引文件)[/b]
保存在 ures 中的更新后的索引文件,没有更新的资源路径指向包内自带的资源,更新后的资源路径指向ures中的资源。它的内容大致如下:
config.zip 的路径是在 iOS 模拟器中得到的。
[code]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[/code]
[b]4.5.3.6 [url]http://192.168.18.22:8080/updater/resinfo.lua[/url][/b]
getRemoteResInfo 方法会读取这个文件,然后将结果解析成lua table。对比其中的version与 lrefinfo 中的区别,来决定是否需要更新。
若需要,则调用C++ Updater模块中的方法下载 package 指定的zip包并解压。
它的内容如下:
[code]local data = {
    version = "1.0.2",
    package = "http://192.168.18.22:8080/updater/res.zip",
}
return data[/code]
[b]4.5.3.7 [url]http://192.168.18.22:8080/updater/res.zip[/url][/b]
zip包的文件夹结构大致如下:
[code]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 ,它用于指示哪些文件需要更新。内容大致如下:
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()
这是一个至关重要的方法,让我们代入用上面提到的变量名和目录来描述它的功能:
它实现的功能是:
[list=1][li]读取 uresinfo,若没有,则将 lresinfo 复制成 uresinfo;[/li][li]从 utmp 中读取 zresinfo,注意此时zip文件已经解压;[/li][li]将需要更新的资源文件从 utmp 中复制到 ures 中;[/li][li]更新 uresinfo ,使其中的资源键名指向正确的资源路径(上一步复制的目标路径);[/li][li]删除 utmp;[/li][li]将更新后的 uresinfo 作为lua文件写入 ures 。[/li][/list]4.5.5 其它方法
对 update.updater 的调用一般是这样的顺序:
[list=1][li]调用 checkUpdat 方法检测是否需要升级;[/li][li]调用 update 方法执行升级,同时注册事件管理handler;[/li][li]升级成功,调用 getResCopy 方法获取最新的 uresinfo 。[/li][/list]