热更新的代码实现

本篇介绍热更的Lua实现

1.UI界面 UpdateLayer.lua

local LocalStorage = require("framework.storage.LocalStorage")

local UpdateLayer = class("UpdateLayer", function()
    return cc.Layer:create()
end)

local director = cc.Director:getInstance()
local view = director:getOpenGLView()

function UpdateLayer:ctor(hasInitSDK, autoOpenLogin)
    self._hasInitSDK    = hasInitSDK
    self._autoOpenLogin = autoOpenLogin

	self._curProgress = 0
    self._tarProgress = 0

    --是否已经开始热更新
    self._isUpdate = nil
    self._md5DownloadStart = false
    --是否开始拉取整包更新的url
    self._showUpdateClient = nil

    
    self._updateCheck = require("version.UpdateCheck").new(
        handler(self, self._onCheckProgress), 
        handler(self, self._onCheckError),
        handler(self, self._setRemoteVersion)
    )
    G_updateCheck = self._updateCheck
    self._getUpdateUrl = require("version.UpdateUrl").new(
        handler(self, self._onGetUrlProgress), 
        handler(self, self._onGetUrlError)
    )

	self:registerScriptHandler(
		function(state)
	        if state == "enter" then
	        	self:_onEnter()
	        elseif state == "exit" then
                if self._updateHandler then
                    G_Scheduler.remove(self._updateHandler)
                    self._updateHandler = nil
                    self._curProgress = 0
                end
	        end
	    end
	)
    G_EventManager = require("framework.event.EventManager").new()
    G_EventID = require("app.event.EventID")
end

function UpdateLayer:_onEnter()
	local viewsize = director:getWinSize()

	local bg = cc.Sprite:create("ui/images/scene/start/start/start_bg1.png")
	bg:setPosition(viewsize.width/2,viewsize.height/2)
	self:addChild(bg)

	local logo = cc.Sprite:create("ui/images/scene/start/start/start_logo.png")
	logo:setPosition(568,377)
	self:addChild(logo)

	local tips = newLabelTTF("",nil,20)
	tips:setPosition(viewsize.width/2,21)
	self:addChild(tips)

	local node = cc.Node:create()
	node:setPosition(512.5,21.6)
	self:addChild(node)

	local flowerBg = cc.Sprite:create("ui/images/scene/start/start/start_loding_bg.png")
	flowerBg:setPosition(55.93,86.72)
	node:addChild(flowerBg)

	local sp_progress = cc.Sprite:create("game_assets/images/start/start_loding.png")--粉色花背景
    self._progress = cc.ProgressTimer:create(sp_progress)
    self._progress:setType(cc.PROGRESS_TIMER_TYPE_RADIAL)--设为圆形进度条
    node:addChild(self._progress)
    self._progress:setPosition(flowerBg:getPositionX(), flowerBg:getPositionY())
    self._progress:setPercentage(0)

	local localVersion = newLabelTTF("Text_version",nil,15)
	localVersion:setPosition(1126,27)
	localVersion:setAnchorPoint({x=1,y=0.5})
    localVersion:setString("LOCAL: " .. G_Version:getLocalVersionString())
	self:addChild(localVersion)

	self._remoteVersion = newLabelTTF("Text_version",nil,15)
	self._remoteVersion:setPosition(1126,9)
	self._remoteVersion:setAnchorPoint({x=1,y=0.5})
    self._remoteVersion:setVisible(false)
	self:addChild(self._remoteVersion)

	self._text_per = newLabelTTF("",nil,20)
	self._text_per:setPosition(568,100)
    self._text_per:setString("0%")
	self:addChild(self._text_per)
    local ret = G_Version:removeHotDirForNewPackage()
    if ret then
        return
    end
    self.text_tips = tips
    local packageIndex = G_ChannelManager.channel():getPackageIndex()
    local allowPA = LocalStorage.getBool("HAS_ALLOW_PA", false, false)
    local channelId = G_ChannelManager.getChannelId()
    local opid = G_ChannelManager.channel():getOpId()
    print("UpdateLayer:_onEnter","packageIndex:",packageIndex,"allowPA:",allowPA, "channelId:",channelId,"opid:",opid)
    if packageIndex < 20211217 then
        --老包
        LocalStorage.setBool("HAS_ALLOW_PA", true, false)   --修复老包->新包,覆盖安装卡热更的bug
        self:startHotUpdateCheck()
    elseif allowPA then
        --新包已同意隐私条例从这里发起热更
        --新包未同意隐私条例从BaseChannel:_onCheckPA发起热更
        self:_buglyInit()
        self:startHotUpdateCheck()
    end
end

function UpdateLayer:startHotUpdateCheck()
    print("*** do UpdateLayer:startHotUpdateCheck", self._updateStarted)
    print(debug.traceback())
    if self._updateStarted then
        return
    end
    self._updateStarted = true
    if G_ChannelManager.isUpdateOpen() then
        G_updateCheck:getVersionInfo()
    else
        self:_enterGame()
    end
end

function UpdateLayer:_startUpdateClient()
    self._curProgress = 0
    self._tarProgress = 0

    self._progress:setPercentage(0)
    self._text_per:setString("0%")
    self.text_tips:setString("下载版本...(0%)")
    
    self._getUpdateUrl:start(url)
end
 
function UpdateLayer:_onUpdateProgress(initPer, curIndex, notCalculate)
end

function UpdateLayer:_startFrameCheck(progress, tipWord)
    self._tarProgress = math.ceil(progress)
    self.text_tips:setString(tipWord)
    if not self._updateHandler then
        --G_Scheduler.setUpdate的回调只有在客户端空闲时会调度!
        self._updateHandler = G_Scheduler.setUpdate(handler(self, self._frameUpdate))
    end
end

function UpdateLayer:_frameUpdate()
    if self._curProgress < self._tarProgress then
        local before = self._curProgress
        self._curProgress = self._curProgress + math.ceil((self._tarProgress - self._curProgress) / 10)
        --print("########### _frame1Update", before, self._curProgress, self._tarProgress, os.time())
        if self._progress:getPercentage() ~= self._curProgress then
            self._progress:setPercentage(self._curProgress)
            self._text_per:setString(self._curProgress .. "%")
        end
    end
    if self._curProgress >= 100 then
       self:_onComplete()
    end
end

function UpdateLayer:_onComplete()
    G_Scheduler.remove(self._updateHandler)
    self._updateHandler = nil
    self._curProgress = 0
    
    local packageIndex = G_ChannelManager.channel():getPackageIndex()
    
    --不需要更新
    print("*** UpdateLayer:_onComplete()", self._updateCheck._updateFlag)
    if self._updateCheck:isForceHotNoDownload() or self._updateCheck:isNotNeedUpdate() then
        self:_enterGame()
    --有热更新有下载
    elseif self._updateCheck:isDownloadUpdate() then
        if not self._md5DownloadStart then
            self:_showUpdateAlert()
        end
    --有热更无下载
    elseif self._updateCheck:isNoDownloadUpdate() then
        self._updateCheck:doNoDownloadUpdate()
        G_ChannelManager.channel():onGameInnerUpdateEnd(true)
        G_Version:restartGame()
    --热更拷贝文件中断
    elseif self._updateCheck:isCopyBreak() then
        G_ChannelManager.channel():onGameInnerUpdateEnd(true)
        G_Version:restartGame()
    --需要更新整包
    elseif self._updateCheck:isNeedUpdateClient() then
        if not self._showUpdateClient then
            self:_showUpdatePackageAlert()
            self._showUpdateClient = true
        else
            G_Version:updateClient()
        end
    end
end

function UpdateLayer:_onCheckProgress(percent, strTip)
    self:_startFrameCheck(percent, strTip)--热更直接取percent
end

function UpdateLayer:_getErrMsgByIndex(index)
    local msg = {
        "失败,请检查网络",
        "失败,请检查网络", --尝试三次仍然失败
        "失败,稍后将重试,请勿退出",--前两次失败
        "失败,请重试"
    }
    return msg[index] or ""
end

function UpdateLayer:_onCheckError(error, curIndex)
    local curTask = self._updateCheck:getTaskTips(curIndex)
    local errMsg = self:_getErrMsgByIndex(error)

    self.text_tips:setString(string.format("%s(%s)",curTask,errMsg))
end

--整包更新进度条变化
function UpdateLayer:_onGetUrlProgress(initPercent, curIndex)
    local subPercent = 0
    if initPercent and initPercent ~= 0 then
        initPercent = initPercent > 1 and initPercent / 100 or initPercent
        subPercent = 100 / self._getUpdateUrl:getTaskCount() * initPercent
    else
        initPercent = 0
    end

    -- 进度条从0开始计算,所以需要减1
    local targetProgress = (curIndex - 1) / self._getUpdateUrl:getTaskCount() * 100
    if targetProgress < 0 then
        targetProgress = 0
    end
    targetProgress = targetProgress + subPercent
    --整包更新targetProgress走计算逻辑
    self:_startFrameCheck(targetProgress, self._getUpdateUrl:getTaskTips(curIndex) .. "(" .. math.floor(initPercent * 100) .. "%)")
end

function UpdateLayer:_onGetUrlError(error, curIndex)
    local curTask = self._getUpdateUrl:getTaskTips(curIndex)
    local errMsg = self:_getErrMsgByIndex(error)

    self.text_tips:setString(string.format("%s(%s)",curTask,errMsg))
end

function UpdateLayer:_enterGame()
    cc.UserDefault:getInstance():setBoolForKey("MD5HOTUPDATED", true)
    local packageIndex = G_ChannelManager.channel():getPackageIndex()
    cc.UserDefault:getInstance():setIntegerForKey("LastLoginPkgIdx", packageIndex)

    --##下面这段用做覆盖安装
    local pkgVerFilePath = cc.FileUtils:getInstance():fullPathForFilename("version.data")
    local pkgVerFileData = G_Version._readVersionInfoFromFile(pkgVerFilePath)
    local pkgVerFileId = pkgVerFileData:getID()
    cc.UserDefault:getInstance():setIntegerForKey("LastPkgId", pkgVerFileId)
    --##

    self:_init()
    self:_setDebug()
    cc.disable_global()
    G_BusinessCenter.start(G_BusinessConst.LOGOUT)

    if CC_SHOW_FPS then
        cc.Director:getInstance():setDisplayStats(false)
    end

    if G_ChannelManager.channel():isAdReportOpen() then
        require("app.adReport.AdReport").getInstance():onStartGame(os.time())
    end

    require("app.share.ShareConfig").getInstance():setPlatformsByOpId()

    local sdkInitStatus = G_ChannelManager.channel():initStatus()
    if sdkInitStatus == 0 then
        G_ChannelManager.channel():init(handler(self, self._onSdkInitComplete), handler(self, self._onSdkInitError))
    elseif sdkInitStatus == 1 then
        G_ChannelManager.channel():init(handler(self, self._onSdkInitComplete), handler(self, self._onSdkInitError))
        self:_onSdkInitComplete()
    else
        self:_onSdkInitError()
    end
end

function UpdateLayer:_initSDK()
    G_ChannelManager.channel():init(nil,nil)
end

function UpdateLayer:_onSdkInitComplete()
    if G_SoundEffect ~= nil then
        G_SoundEffect:preload()
    end
    self:_showTip()
end


function UpdateLayer:_onSdkInitError()
    local msg = "游戏账号系统初始化异常!\n请联系工作人员进行处理。"
    
    local Alert = require("app.scenes.common.Alert")

    --可能当前场景初始化尚未完成,做个延时弹框防止程序出错。
    G_Scheduler.setDelay(function()
        Alert.show(msg, 2)
    end, 0.001)
end

--健康游戏忠告UI
function UpdateLayer:_showTip()
    local viewsize = director:getWinSize()
    local scene = cc.Scene:create()
    scene:addTouchEffect()
    local panel = display.getPanel(viewsize.width,viewsize.height,true)
    scene:addChild(panel)

    local bg = cc.Sprite:create("ui/tip.png")
    bg:setPosition(viewsize.width/2,viewsize.height/2)
    panel:addChild(bg)
    bg:setOpacity(0)
    bg:runAction(cc.FadeIn:create(0.3))

    local timer
    local autoOpenLogin = self._autoOpenLogin
    local function skip()
        bg:runAction(
            cc.Sequence:create(
                cc.FadeOut:create(0.5),
                cc.CallFunc:create(
                    function ()
                        G_SceneManager.switchSceneWithTween(G_SceneManager.SCENE_LOADING, false, autoOpenLogin)
                        -- 给游戏逻辑打补丁,每隔一段时间请求一次
                        require("patch.PatchTool"):instance():repeatPatching()
                        if timer then
                            G_Scheduler.remove(timer)
                            timer = nil
                        end
                    end
                )
            )
        )
    end
    panel:addClickEventListenerEx(skip)
    timer = G_Scheduler.setDelay(skip, 2)
    cc.Director:getInstance():replaceScene(scene)
end

function UpdateLayer:_init()
    require("framework.platform.init")
    G_Lang = require("framework.lang.Lang")
    
    G_Lang.add(require("lang.LangTemplate"))
    G_Lang.add(require("lang.LangTemplate_1"))

    G_FileCache = require("framework.cache.FileCache")
    G_BusinessCenter = require("framework.business.BusinessCenter")
    G_ChannelManager = require("framework.channel.ChannelManager")
    G_EventManager = require("framework.event.EventManager").new()

    G_EventID = require("app.event.EventID")
    G_GameSettingManager = require("app.setting.GameSettingManager").new()
    G_Colors = require("app.setting.Colors")
    G_Setting = require("app.setting.Setting")
    G_Url = require("app.setting.Url")
    G_YDManager = require("app.data.YDManager")
    G_DataCenter.init()
    G_SceneManager = require("app.scenes.SceneManager")
    G_SceneManager.collectLog()
    
    G_SwordVoice = require("app.audio.SwordVoice")
    G_SoundEffect = require("app.audio.SoundEffect")

    G_Tutorial = require("app.scenes.tutorial.TutorialManager")
    G_Tutorial.init()

    G_AudioManager:initData(G_Url:getAudioDic())
    G_Help = require("app.scenes.common.help.HelpManager")

    G_NotificationMgr = require("app.scenes.common.notification.NotificationMgr")

    G_ClickEffectPool = require("app.scenes.effect.ClickEffectPool").Singlton()
    -- 
end

function UpdateLayer:_setDebug()
    if device.platform == "android" or device.platform == "ios" then
        __G__TRACKBACK__ = 
        function(msg)
            local message = msg
            local msg = debug.traceback(msg, 3)
            print(msg)
            if G_DataCenter.login and G_DataCenter then
                buglySetUserId(G_DataCenter.login:getUserId())
            end
            buglyReportLuaException(tostring(message), debug.traceback())
            return msg
        end
    end

    --捕获前端报错用面板显示 方便机器测试
    -- 判断是不是debug包
    local debugPkg = false
    if G_ChannelManager.channel():getPackageIndex() >= 20220530 then
        if device.platform == "android" then
            local ok
            local luaj = require "cocos.cocos2d.luaj"
            ok, debugPkg = luaj.callStaticMethod("org.cocos2dx.lua.GameUtil", "isDebugPackage", {}, "()Z")
            print(tostring(ok), tostring(debugPkg))
        end
    end
    if G_ChannelManager.isShowErrorAlert() or debugPkg then
        local __G__TRACKBACK__OLD = __G__TRACKBACK__
        function __G__TRACKBACK__(msg)
            require("app.scenes.common.Alert").showSystemError(msg)
            __G__TRACKBACK__OLD(msg)
        end
    end
end

--热更弹窗
function UpdateLayer:_showUpdateAlert()
    local updateAlert
    local function onSelect(index)
        updateAlert:hideComplete() --掉hide效果不太好,停留时间有点长
        self._progress:setPercentage(0)
        self._text_per:setString("0%")
        self._updateCheck:startDownload(index)
        self._md5DownloadStart = true
    end

    local alert = require("version.UpdateAlert").new(onSelect, self._updateCheck)
    updateAlert = require("version.AlertControl").new(alert)
    updateAlert:show()
end

--整包更新弹窗
function UpdateLayer:_showUpdatePackageAlert()
    local updateAlert
    local alert = require("version.UpdatePackageAlert").new(
        function ()
             updateAlert:hide()
            self:_startUpdateClient()
        end
    )
    updateAlert = require("version.AlertControl").new(alert)
    updateAlert:show()
end

function UpdateLayer:_setRemoteVersion()
    self._remoteVersion:setVisible(true)
    self._remoteVersion:setString("REMOTE: " .. G_Version:getRemoteVersionString())
end

--必须要在同意隐私之后才可以接入bugly,避免bugly提前收集用户信息导致不过审
function UpdateLayer:_buglyInit()
    if version.Version.initBugly then
        version.Version:initBugly()
        print("*** buglyInit end")
    end
end

return UpdateLayer

2.热更逻辑文件:UpdateCheck.lua

local cjson = require("cjson")
local UpdateCheck = class("UpdateCheck")

local downPath = cc.FileUtils:getInstance():getWritablePath() .. "down/"            --新文件下载目录
local hotPath = cc.FileUtils:getInstance():getWritablePath() .. "version/"          --热更文件所在目录
local midPath = "/version"
local AssetJsonPath = "res/versionAsset.json"   --资源文件(获取下载内容)这里不能加斜杠:/res/versionAsset.json

function UpdateCheck:ctor(_processFunc,_errorFunc,_setRemoteUI)
    self._updateFlag = 0
    self._versionString = ""        --远程版本号
    self._maxHttpNum = 5            --最大请求数量
    self._curHttpNum = 0            --当前请求数量
    self._maxErrorNum = 3           --下载错误最大次数
    self._errorMap = {}             --下载错误次数(md5对不上)
    self._downSize = 0              --已下载大小
    self._totalDownSize = 0         --下载总大小
    self._totalGetSize = 0          --已下载大小
    self._toDownloadFiles = {}      --需要下载未下载文件 --{{"res/cfg/error_msg_master.luac", 6075, md5},{...}}
    self._toDeleteFiles = {}        --需要删除文件
    self._downloadedFileNames = {}  --已下载文件
    self._iosCommitMode = false     --ios提审状态
 	self._processFunc = _processFunc
	self._errorFunc   = _errorFunc
    self._setRemoteUI = _setRemoteUI
    self._flagHotUpdateTestWIn32 = false --开发机测试用(上传svn时必须为false别忘了改)
    if self._flagHotUpdateTestWIn32 then
        local cltVersionObj = G_Version:getLocalVersionInfo()
        cltVersionObj._id = 999999
    end
    self._timeTipShowed = false
	self:_init()
end

function UpdateCheck:_init()
    local function createDir(path)
        if not cc.FileUtils:getInstance():isDirectoryExist(path) then
            cc.FileUtils:getInstance():createDirectory(path)
        end
    end
    createDir(hotPath)
    createDir(hotPath..'src/')
    createDir(hotPath..'res/')
end

--注意下载发起下载文件的时候用newRequest不要用sendHttpRequest
function UpdateCheck:_sendHttpRequest(fileName, fileIndex, url, successCb)
    local xhr = cc.XMLHttpRequest:new()
    xhr.responseType = cc.XMLHTTPREQUEST_RESPONSE_STRING--cc.XMLHTTPREQUEST_RESPONSE_ARRAY_BUFFER
    xhr:setRequestHeader("Content-Type","application/x-www-form-urlencoded;charset=UTF-8")
    xhr:open("GET", url)
    xhr:registerScriptHandler(function()
        self._curHttpNum = self._curHttpNum - 1
        if xhr.readyState == 4 and 200 <= xhr.status and xhr.status < 207 then
            successCb(xhr.response, 1)
        else
            --这里做失败处理,失败了重新下载相同文件
            self:_httpDownloadErrorFunc(fileName, fileIndex)
        end
        xhr:unregisterScriptHandler()
    end)
    xhr:send()
end

--通过消息获取远程版本号
function UpdateCheck:getVersionInfo()
    local function completeCall()
        G_Version:checkRemoteVersion(self._versionString, self._processFunc)
    end
    if G_ChannelManager.isUpdateOpen() then
        local request = require("version.GetVersionRequest").new()
        request:send(nil, function(status, data)
            if 0 == status then
                self._versionString = data.ver
                self._iosInAppStore = tonumber(data.ver_ios_audit) or 1   
                local cltVersionObj = G_Version:getLocalVersionInfo()
                local cltID = tonumber(cltVersionObj:getID())
                print("IOS提审:","平台为IOS:",device.platform == "ios","clientId:",cltID, "下发id:",self._iosInAppStore)
                if device.platform == "ios" and cltID == self._iosInAppStore then
                    --IOS提审功能,约定:服务端下发版号和包内版号一致时为提审状态
                    --举例:IOS线上版本596,IOS新包版本597,新包提审时,服务器发597,过了提审后,服务器发非597,非596
                    self._iosCommitMode = true
                    self._updateFlag = 0
                    self._processFunc(100, "加载中...(100%)")
                else
                    G_Scheduler.setDelay(completeCall, 0.5, true)
                end
            end
        end)
    else
        G_Scheduler.setDelay(completeCall, 0.5, true)
    end
end

function UpdateCheck:startHotUpdateLogic(completeCall, errorCall, progressCall)
    local cltVersionObj = G_Version:getLocalVersionInfo()
    local svrVersionObj = G_Version:getRemoteVersionInfo()
    local cltID = cltVersionObj:getID()
    local svrID = svrVersionObj:getID()
    local cltTag = cltVersionObj:getTag()
    local svrTag = svrVersionObj:getTag()
    self._md5HotUpdated = cc.UserDefault:getInstance():getBoolForKey("MD5HOTUPDATED", false)

    print("***","startHotUpdateLogic",cltID.."."..cltTag,svrID.."."..svrTag,"已md5热更:",self._md5HotUpdated,"整包更新:",cltID < svrID,"热更新:",cltTag < svrTag)
    print("***","是否白名单:", svrVersionObj:getIsWhiteList())
    if not self._md5HotUpdated then
        --强制热更新
        self:_getServerAssetRequest()
    elseif cltID < svrID then
        --整包更新
        self._updateFlag = 3
        self._processFunc(100, "检查更新...(100%)")
    elseif cltTag < svrTag then
        --热更新
        self:_getServerAssetRequest()
    else
        --不用更新
        self._updateFlag = 0
        self._processFunc(100, "检查更新...(100%)")
    end
    if self._setRemoteUI then
        self._setRemoteUI()
    end
end

function UpdateCheck:_getServerAssetRequest()
    --获取本地资源文件
    --取develop/res/versionAsset.json时,AssetJsonPath取/res/versionAsset.json时在ios设备上读不到,需要去掉/
    local versionFilePath = AssetJsonPath
    if cc.FileUtils:getInstance():isFileExist(hotPath .. AssetJsonPath) then
        versionFilePath = hotPath .. AssetJsonPath
    end
    local isExist = cc.FileUtils:getInstance():isFileExist(versionFilePath)
    local jsonData = cc.FileUtils:getInstance():getFileData(versionFilePath)
    self._clientAssetData = cjson.decode(jsonData)
    print("*** _getServerAssetRequest", self._clientAssetData == nil, jsonData == nil, isExist, versionFilePath)
    self:_downloadFileAssetPath()
end

--获取服务器资源文件
function UpdateCheck:_downloadFileAssetPath()
    local url = self:_getHttpUrl(1, "/" .. AssetJsonPath)
    if url == nil then
        return
    end
    print("### AssetJsonPath url:", url)
    self:_sendHttpRequest(AssetJsonPath, 0, url, function(response)
        --接收到资源文件大约需要3秒
        self:_saveFile(downPath .. AssetJsonPath, response, AssetJsonPath)
        self._processFunc(55, "检查更新...(55%)")
        G_Scheduler.setDelay(handler(self,function()
            self._responseAsset = response
            local serverAssetData = cjson.decode(response)
            --不验证md5的情况下,生成差异文件大约需要2秒
            self:_getUpdateFiles(self._clientAssetData, serverAssetData)
        end),0.8)
    end)
end

function UpdateCheck:_getUpdateFiles(cltData, svrData)
    --oneCltFile:   "res/cfg/error_msg_master.luac" = {"285bbfb9c55c313fea68a6c546939ac5", 6075}
    --cltData有的文件在APP中有的在hot中
    local function checkDownload(fileName, size, md5)
        local ret = self:_isNeedDownload(fileName, md5)
        if 1 == ret then
            return true
        elseif true == ret then
            table.insert(self._toDownloadFiles, {fileName, size, md5})
            self._totalDownSize = self._totalDownSize + size
        end
        return false
    end
    local function showTip()
        self._processFunc(68, "上次更新中断,正在进行文件校验,此过程可能需要较长时间...(68%)")
        self._toDownloadFiles = {}
        self._downloadedFileNames = {}
        self._totalDownSize = 0
    end
    for fileName, oneSvrFile in pairs(svrData.assets) do
        local oneCltFile = cltData.assets[fileName]
        if oneCltFile then
            --客户端有这个文件且MD5不同
            if oneCltFile[1] ~= oneSvrFile[1] then
                if checkDownload(fileName, oneSvrFile[2], oneSvrFile[1]) then
                    showTip()
                    break
                end
            end
        else
            --客户端没有这个文件
            if checkDownload(fileName, oneSvrFile[2], oneSvrFile[1]) then
                showTip()
                break
            end
        end
    end
    
    local function calcLogic()
        if self._timeTipShowed then
            for fileName, oneSvrFile in pairs(svrData.assets) do
                local oneCltFile = cltData.assets[fileName]
                if oneCltFile then
                    --客户端有这个文件且MD5不同
                    if oneCltFile[1] ~= oneSvrFile[1] then
                        checkDownload(fileName, oneSvrFile[2], oneSvrFile[1])
                    end
                else
                    --客户端没有这个文件
                    checkDownload(fileName, oneSvrFile[2], oneSvrFile[1])
                end
            end
        end
        if 0 < table.nums(self._toDownloadFiles) then
            self._updateFlag = 1
        elseif 0 == table.nums(self._toDownloadFiles) and not self._md5HotUpdated then
            --强制热更,且没有更新文件
            self._updateFlag = 5
            table.insert(self._downloadedFileNames, AssetJsonPath)
            self:_handleFiles()
            cc.UserDefault:getInstance():setBoolForKey("MD5HOTUPDATED", true)
        elseif 0 == table.nums(self._toDownloadFiles) and self._md5HotUpdated then
            self._updateFlag = 2
        end
        for fileName, oneCltFile in pairs(cltData.assets) do
            local oneSvrFile = svrData.assets[fileName]
            if not oneSvrFile then
                --服务端没有这个文件
                table.insert(self._toDeleteFiles, fileName)
            end
        end
        self:_checkCopyBreak()
        self._processFunc(100, "检查更新...(100%)")
    end
    local time = self._timeTipShowed and 0.5 or 0
    G_Scheduler.setDelay(calcLogic, time ,true)
end

--判断该文件是否需要下载,实现断点续传
function UpdateCheck:_isNeedDownload(name, md5)
    local downloadFilePath = downPath .. name
    local md5Value = md5
    if version.Version.getFileMD5Hash then
        md5Value = version.Version:getFileMD5Hash(downloadFilePath)
    end
    local fileExist = cc.FileUtils:getInstance():isFileExist(downloadFilePath)
    if fileExist and md5Value == md5 then
        if self._timeTipShowed then
            table.insert(self._downloadedFileNames, name)
            return false
        else
            --走这里代表上次下载中断
            self._timeTipShowed = true
            return 1
        end
    else
        return true
    end
end

function UpdateCheck:_copyFile(src, dest, index)
    local sourceFile, errorString = io.open(src, "rb")
	if sourceFile == nil then
        return
    end
 	local len = sourceFile:seek("end")
	sourceFile:seek("set", 0)
	local data = sourceFile:read(len)
	sourceFile:close()
    if not data then    --不应该走进来
        print("_copyFile error ", src, dest)
        return
    end
    local pInfo = io.pathinfo(dest)
    if not cc.FileUtils:getInstance():isFileExist(dest) then
        cc.FileUtils:getInstance():createDirectory(pInfo.dirname)
    end

	local destFile = io.open(dest, "wb")
	destFile:write(data)
	destFile:close()
end

function UpdateCheck:_saveFile(absolutePath, response, relativePath)
    if relativePath ~= AssetJsonPath then
        table.insert(self._downloadedFileNames, relativePath)
    end
    local pInfo = io.pathinfo(absolutePath)
    cc.FileUtils:getInstance():createDirectory(pInfo.dirname)
    io.writefile(absolutePath, response, "wb")
end

--请求文件下载并检测完成,这里注意实际下载完成和进度条走到100%不同步,需要保证任意时间点尤其是100%时断线重连的逻辑正常
function UpdateCheck:_frameCheck()
    if 0 == table.nums(self._toDownloadFiles) then
        --下载完成
        --将版本文件放在最后,为了让其最后被拷贝,这样在拷贝过程中断会检测出来
        table.insert(self._downloadedFileNames, AssetJsonPath)

        G_Scheduler.remove(self._frameHandler)
        self:_handleFiles()
        self:downloadComplete()
        G_ChannelManager.channel():onGameInnerUpdateEnd(true)
        G_Version:restartGame()
    else
        if self._curHttpNum < self._maxHttpNum and self._curHttpNum < table.nums(self._toDownloadFiles) then
            for index, downElement in pairs(self._toDownloadFiles) do
                if not downElement[4] then --[1]文件名 [2]大小 [3]md5 [4]是否正在下载(发起下载时downElement[4] = true)
                    self:_newRequest(index)
                    break
                end
            end
        end
    end
end

function UpdateCheck:_handleFiles()
    --将下载目录下文件移到hotPath目录中
    for k, fileName in ipairs(self._downloadedFileNames) do
        self:_copyFile(downPath..fileName, hotPath..fileName, k)
    end
    local downPath = cc.FileUtils:getInstance():getWritablePath() .. "down/"
    cc.FileUtils:getInstance():removeDirectory(downPath)
    self:_replaceVersionFile()
    --在hotPath下删除不需要的文件
    for k, fileName in ipairs(self._toDeleteFiles) do
        cc.FileUtils:getInstance():removeFile(hotPath..fileName)
    end
end

function UpdateCheck:_replaceVersionFile()
    --删除老版号文件
    os.remove(cc.FileUtils:getInstance():getWritablePath() .. "version.data")
    --生效新版号文件
    local tempVersionFile = cc.FileUtils:getInstance():getWritablePath() .. "version.data.temp"
    local versionFile = cc.FileUtils:getInstance():getWritablePath() .. "version.data"
    os.rename(tempVersionFile, versionFile)
end

function UpdateCheck:downloadComplete()
    self:_clearData()
    cc.Director:getInstance():getActionManager():removeAllActions()
    cc.Director:getInstance():getTextureCache():removeAllTextures()
    cc.SpriteFrameCache:getInstance():removeSpriteFrames()
end

function UpdateCheck:_getHttpUrl(selIndex, fileName)
    local svrVersionObj = G_Version:getRemoteVersionInfo()
    local svrTag = svrVersionObj:getTag()
    local postfix = "?" .. svrTag  --url增加后缀可以每次请求到最新的文件(不加的话,SVN修改已有文件后上传,需要运维刷新缓存才会访问到最新的)
    local remoteData = G_Version:getRemoteVersionInfo()
    local whiteListStr = remoteData:getIsWhiteList() and "/white" or ""
    local url = remoteData:getUpdateURL(selIndex).. midPath .. whiteListStr .. fileName .. postfix
    if device.platform  == "windows" then--开发机上url用固定值,因为从文件中读到的不对
        url = "https://cdn-xf-studio.uuzuonline.com/171/mobile/innerup/youzu" .. midPath .. whiteListStr .. fileName .. postfix
    end
    return url
end

--请求文件下载
function UpdateCheck:_newRequest(index)
    local downElement = self._toDownloadFiles[index]
    local downFileName = "/"..downElement[1]    --这里需要加正斜杠
    local downFileSize = downElement[2]
    local downFileMd5 = downElement[3]
    local selUrlIdx = self._urlIndex or 1
    local url = self:_getHttpUrl(selUrlIdx, downFileName)
    downElement[4] = true
    self._curHttpNum = self._curHttpNum + 1
    self:_sendHttpRequest(downFileName, index, url, function(response)
        self:_saveFile(downPath .. downFileName, response, downFileName)
        local md5Value = downFileMd5
        if version.Version.getFileMD5Hash then
            md5Value = version.Version:getFileMD5Hash(downPath .. downFileName)
        end
        if md5Value == downFileMd5 then
            --进行md5验证,因为下载的文件有可能是错误的(网络不好,被劫持,作弊等情况)
            --不验证的话,文件可能下载到的内容为nil,copy文件时会报错
            self._errorMap[downFileName] = nil
            self._totalGetSize = self._totalGetSize + downFileSize
            self._toDownloadFiles[index] = nil
            local percent = math.floor(self._totalGetSize / self._totalDownSize  * 100)
            local mb = self._totalGetSize / 1024 / 1024
            local tmb = self._totalDownSize / 1024 / 1024
            local size1 = string.format("%.2f", mb)
            local size2 = string.format("%.2f", tmb)
            self._processFunc(percent, "下载版本...(" .. percent .. "%)")
        else
            print("***md5 error:"..index, downFileName,os.time())
            self:_httpDownloadErrorFunc(downFileName, index)
        end
    end)
end

function UpdateCheck:_httpDownloadErrorFunc(downFileName, fileIndex)
    if self._errorMap[downFileName] then
        self._errorMap[downFileName] = self._errorMap[downFileName] + 1
    else
        self._errorMap[downFileName] = 1
    end
    local function downloadFileAgain()
        if 0 == fileIndex then  --versionAsset.json下载失败
            self:_downloadFileAssetPath()
        else
            self:_newRequest(fileIndex)
        end
    end

    if self._errorMap[downFileName] < self._maxErrorNum then
        downloadFileAgain()
    else
        print(downFileName .. " download error "..self._maxErrorNum .. " times")
        local msg = "文件" .. downFileName .. "多次下载失败,请确保网络通畅后,点击确定重新下载"
        local SceneManager = require("app.scenes.SceneManager")
        if not self._alertPanel then
            G_Scheduler.remove(self._frameHandler)
            self._alertPanel = SceneManager.alert(msg, 0, function()
                G_Version:restartGame()
            end)
        end
    end
end

--请求文件下载并检测完成
function UpdateCheck:startDownload(urlIndex)
    --G_Scheduler.setUpdate的回调只有在客户端空闲时会调度!
    self._urlIndex = urlIndex
    self._frameHandler = G_Scheduler.setUpdate(handler(self, self._frameCheck))
end

function UpdateCheck:_clearData()
    self._errorMap = {}
    self._toDownloadFiles = {}
    self._toDeleteFiles = {}
    self._downloadedFileNames = {}
end

--无需更新
function UpdateCheck:isNotNeedUpdate()
    return self._updateFlag == 0
end

--有下载的热更新
function UpdateCheck:isDownloadUpdate()
    return self._updateFlag == 1
end

--无下载的热更新
function UpdateCheck:isNoDownloadUpdate()
    return self._updateFlag == 2
end

--整包更新
function UpdateCheck:isNeedUpdateClient()
    return self._updateFlag == 3
end

--拷贝中断
function UpdateCheck:isCopyBreak()
    return self._updateFlag == 4
end

--强制热更新且没有文件需要下载
function UpdateCheck:isForceHotNoDownload()
    return self._updateFlag == 5
end

--检测复制文件中断的情况
function UpdateCheck:_checkCopyBreak()
    if 1 < table.nums(self._downloadedFileNames) and 0 == table.nums(self._toDownloadFiles) then
        print("######################  警告:上次拷贝中断,将继续拷贝")
        table.insert(self._downloadedFileNames, AssetJsonPath)
        self._updateFlag = 4
        self:_handleFiles()
    end
end

--检测无下载文件的更新
function UpdateCheck:doNoDownloadUpdate()
    self:_copyFile(downPath..AssetJsonPath, hotPath..AssetJsonPath)
    cc.FileUtils:getInstance():removeDirectory(downPath)
    for k, fileName in ipairs(self._toDeleteFiles) do
        cc.FileUtils:getInstance():removeFile(hotPath..fileName)
    end
    self:_replaceVersionFile()
    self:downloadComplete()
end

function UpdateCheck:getTotalDownSize()
    return self._totalDownSize
end

function UpdateCheck:getIOSCommitMode()
    return self._iosCommitMode
end

return UpdateCheck

3.热更辅助文件Version.lua

require("version.tools")
local SceneManager = require("app.scenes.SceneManager")
local Version = class("Version")

Version.VERSION_FILE_NAME = "version.data"              --本地版号文件名, tag(SVN的版号)用来判断热更,id用来判断整包更新
Version.VERSION_FILE_TEMP_NAME = "version.data.temp"    --下载的版号文件名
Version.URL_FILE_NAME = "urlconfig"
Version.LOCALVERPATH = cc.FileUtils:getInstance():getWritablePath() .. Version.VERSION_FILE_NAME
Version.LOCALTEMPPATH = cc.FileUtils:getInstance():getWritablePath() .. Version.VERSION_FILE_TEMP_NAME
Version.NO_ERROR = 0
Version.ERROR_NET = 1
Version.ERROR_UNZIP = 2

--预启动
function Version.preSetup()
    --!!!注意addSearchPath之前执行的一定是包体内代码
    --为了避免覆盖安装后,热更目录中的热更之前代码错误,新包则无法修正
    --要求新包第一次成功热更之前不要执行热更目录代码
    local fileUtil = cc.FileUtils:getInstance()
    local localVersionDir = fileUtil:getWritablePath() .. "version/"
    --包体内的版号文件:develop/res/version.data
    local pkgVerFilePath = fileUtil:fullPathForFilename(Version.VERSION_FILE_NAME)
    local pkgVerFileData = Version._readVersionInfoFromFile(pkgVerFilePath)
    local pkgVerFileId = pkgVerFileData:getID()
    --热更目录下的版号文件:develop/version.data
    --isVerExist为true代表经历过一次成功热更
    local isVerExist = cc.FileUtils:getInstance():isFileExist(Version.LOCALVERPATH)
    --isVerExist为true代表热更中途退出
    local isVerTempExist = cc.FileUtils:getInstance():isFileExist(Version.LOCALTEMPPATH)
    print("Version.preSetup() isVerExist=",isVerExist,"isVerTempExist=",isVerTempExist)

    --lastPkgId可以理解为发起热更时记录的包内版号文件的大版本号
    local lastPkgId = cc.UserDefault:getInstance():getIntegerForKey("LastPkgId", 0)
    
    local packageChanged = false
    if lastPkgId == 0 and (not isVerExist) and (not isVerTempExist) then
        --新包裸装
        packageChanged = false
        print("Version.preSetup() A 新包裸装")
    elseif lastPkgId == 0 and (not isVerExist) and isVerTempExist then
        --旧包没有经历一次成功热更就覆盖安装
        packageChanged = true
        print("Version.preSetup() A 旧包没有经历一次成功热更就覆盖安装")
    elseif lastPkgId == 0 and isVerExist then
        --没有在20220411之后热更过,覆盖安装
        packageChanged = true
        print("Version.preSetup() A 没有在20220411之后热更过,覆盖安装")
    elseif 0 < lastPkgId and lastPkgId ~= pkgVerFileId then
        --在20220411之后热更过,覆盖安装
        packageChanged = true
        print("Version.preSetup() A 在20220411之后热更过,覆盖安装")
    elseif 0 < lastPkgId and lastPkgId == pkgVerFileId then
        --正常进入游戏
        packageChanged = false
        print("Version.preSetup() A 正常进入游戏", lastPkgId, pkgVerFileId)
    end
    print("Version.preSetup() B", "packageChanged =", packageChanged,"lastPkgId =", lastPkgId, "pkgVerFileId =", pkgVerFileId)
    if packageChanged then
        --0.覆盖安装(老包不删除,安装新包)走这里
        fileUtil:removeDirectory(localVersionDir)
        os.remove(fileUtil:getWritablePath() .. "version.data")--删除版号文件    
        print("Version.preSetup() C removeDirectory")
    end
    Version.addVerSearchPath()
    cc.UserDefault:getInstance():setIntegerForKey("LastPkgId", pkgVerFileId)
    
    --销毁自身缓存,便于在setup中加载最新的version.lua
    package.loaded["version.Version"] = nil
    package.loaded["version.VersionInfo"] = nil
    fileUtil:purgeCachedEntries() --清理文件查找缓存
end

function Version.addVerSearchPath()
    local localVersionDir = cc.FileUtils:getInstance():getWritablePath() .. "version/"
    cc.FileUtils:getInstance():addSearchPath(localVersionDir .. "src/", true)
    cc.FileUtils:getInstance():addSearchPath(localVersionDir .. "res/", true)
end
--启动:
-- 初始化自身;
-- 导入cocos\framework\config;
-- 启动游戏;
-- 警告:方法名不能修改,否则无法启动
function Version.setup(hasInitSDK, autoOpenLogin)
    --- 初始化自身 ---
    Version.enableGlobal()
    G_Version = require("version.Version").new()
    G_Version:init()

    require "config"
    require "cocos.init"
    require "framework.Framework"

    --- 启动游戏 ---
    local function main()
        require("app.MyApp").new():run(hasInitSDK, autoOpenLogin)
    end

    local status, msg = 
    xpcall(main, 
        function (msg)
            local msg = debug.traceback(msg, 3)
            print(msg)
            return msg
        end
    )
    
    if not status then
        print(msg)
    end
end

function Version.enableGlobal()
    setmetatable(_G, {})
end

function Version:ctor()
    self._localVersionInfo = nil                --本地版号文件读取的对象
    self._remoteVersionInfo = nil               --远程版号文件读取的对象
    self._versionCompareCode = 0
    self._hasAddVersionPathToSearchPath = false
    self._remoteSimpleVersion = 0
end

--移除非系统全局变量和非系统模块
function Version:_cleanEnv()

    local needRemoveLoad = {}
    for k, v in pairs(package.loaded) do
        if not _originalLoaded[k] then
            table.insert(needRemoveLoad, k)
        end
    end
    for i = 1, #needRemoveLoad do
        package.loaded[needRemoveLoad[i]] = nil
    end

    local needRemoveG = {}
    for k, v in pairs(_G) do
        if not _originalG[k] then
            table.insert(needRemoveG, k)
        end
    end
    for i = 1, #needRemoveG do
        _G[needRemoveG[i]] = nil
    end
end

--让新包覆盖安装可以进行完整热更新
--老包更新不执行,新包裸装不执行,老包不删除安装新包后只执行一次
function Version:removeHotDirForNewPackage()
    local packageIndex = G_ChannelManager.channel():getPackageIndex()   --2022.3出的包为20211217
    if not packageIndex then return false end
    print("delete version logic step 1","packageIndex", packageIndex, "go on", packageIndex and 20211217 <= packageIndex)
    if 20211217 <= packageIndex then
        local lastLoginPkgIdx = cc.UserDefault:getInstance():getIntegerForKey("LastLoginPkgIdx", 0)
        print("delete version logic step 2", "lastLoginPkgIdx", lastLoginPkgIdx, "go on", lastLoginPkgIdx ~= 0 and lastLoginPkgIdx < packageIndex)
        if lastLoginPkgIdx ~= 0 and lastLoginPkgIdx < packageIndex then
            local localVersionDir = cc.FileUtils:getInstance():getWritablePath() .. "version/"
            local versionDirExist = cc.FileUtils:getInstance():isDirectoryExist(localVersionDir)
            print("delete version logic step 3", "versionDirExist", versionDirExist)
            if versionDirExist then
                print("delete version logic step 4", "remove versionDir executing")
                cc.FileUtils:getInstance():removeDirectory(localVersionDir)
                os.remove(cc.FileUtils:getInstance():getWritablePath() .. "version.data")--删除版号文件
                cc.UserDefault:getInstance():setIntegerForKey("LastLoginPkgIdx", packageIndex)
                self:restartGame()
                return true
            else
                cc.UserDefault:getInstance():setIntegerForKey("LastLoginPkgIdx", packageIndex)
                return false
            end
        end
    end
    return false
end

--还原游戏运行环境,重启游戏
function Version:restartGame(autoOpenLogin)
    local node = cc.Director:getInstance():getNotificationNode()
    if node then
        cc.Director:getInstance():setNotificationNode(nil)
    end
    
    -- 清理延时逻辑
    G_Scheduler.removeAll()
    -- 清理hold纹理
    G_Texture.unholdAllTexture()
    -- 清理文件查找缓存
    cc.FileUtils:getInstance():purgeCachedEntries()
    -- 清理音效
    G_AudioManager:destroy()
    --清理聊天历史消息管理类
    require("app.scenes.chat.tool.ChatHistoryStore").getInstance():dispose()


    local scene = cc.Scene:create()
    cc.Director:getInstance():replaceScene(scene)
    scene:registerScriptHandler(
        function(state)
            if state == "enterTransitionFinish" then
                self:_cleanEnv()
                Version.setup(false, autoOpenLogin)
            end
        end
    )
end

--初始化本地版本信息
function Version:init()
    local fileUtil    = cc.FileUtils:getInstance()
    local downloadedFilePath = fileUtil:getWritablePath() .. Version.VERSION_FILE_NAME
    local originalFilePath = fileUtil:fullPathForFilename(Version.VERSION_FILE_NAME)
    --已下载的版号文件:develop/version.data
    local downloadedFileData = Version._readVersionInfoFromFile(downloadedFilePath)
    --包内自带的版号文件:develop/res/version.data
    local originalFileData = Version._readVersionInfoFromFile(originalFilePath) 
    if downloadedFileData then
        self._hasAddVersionPathToSearchPath = true
        self._localVersionInfo = downloadedFileData
    else
        self._localVersionInfo = originalFileData
    end
end

--获取本地最新版本信息
function Version:getLocalVersionInfo()
    return self._localVersionInfo
end

--获取远程版本信息
function Version:getRemoteVersionInfo()
    return self._remoteVersionInfo
end

--初始化远程版本地址
function Version:setRemoteURL(remoteURL)
    self._remoteURL = remoteURL
end

function Version:getRemoteURL()
    return self._remoteURL
end

function Version:getLocalVersionString()
    return self._localVersionInfo:getID().. "." .. self._localVersionInfo:getTag()
end

function Version:getRemoteVersionString()
    if self._remoteVersionInfo then
        return self._remoteVersionInfo:getID().. "." .. self._remoteVersionInfo:getTag()
    else
        return ""
    end
end


--[[
    版号文件处理流程:
    0.包体内存在的版号文件为develop/res/version.data
    1.下载后生效的版号文件为develop/version.data
    2.下载后未生效的版号文件为develop/version.data.temp
    3.develop/version.data存在, 本地版号文件就取develop/version.data
    4.develop/version.data不存在, 本地版号文件就取develop/res/version.data
    5.下载完成后删除:develop/version.data
    6.将develop/version.data.temp改名为develop/version.data
--]]
--请求服务器版号文件, 保存为develop下的version.data.temp
function Version:checkRemoteVersion(versionString, processFunc)
    self._processFunc = processFunc
    local game_id = G_ChannelManager.channel():getGameId()
    local opgame_id = G_ChannelManager.channel():getOpgameId()
    local op_id = G_ChannelManager.channel():getOpId()
    self._loader = require("version.loader.Loader").new()
    self._remoteUrl = string.format("%s/%d_%d_%d_%s.data?%s",self._remoteURL,game_id,opgame_id,op_id,versionString,tostring(os.time()))
    self._errorTimes = 0
    self._loader:add(self._remoteUrl, Version.LOCALTEMPPATH)
    self._loader:load(handler(self, self._downRemoteComplete), handler(self, self._downRemoteError))
end

function Version:_downRemoteComplete()
    self:_checkRemoteRight()
end

function Version:setRemoteSimpleVersion(data)
    self._remoteSimpleVersion = data    --这里会赋值一个number,eg:706.53456
end

function Version:getRemoteSimpleVersion()
    return self._remoteSimpleVersion
end

function Version:_downRemoteError()
    --已知情况:当连续调用2次loader:load()时候会出现error
    print("******************!!!!!! checkRemoteVersion download error")
end

function Version:_checkRemoteRight()
    self._remoteVersionInfo = Version._readVersionInfoFromFile(Version.LOCALTEMPPATH)
    if self._remoteVersionInfo:getUpdateURL(1) then
        self._processFunc(9, "检查更新...(9%)")
        G_updateCheck:startHotUpdateLogic()
    else
        --网络不好时下载的self._remoteVersionInfo的各字段都为0
        --这里反复下载避免玩家热更过程报错,卡0%,这里逻辑很重要
        self._errorTimes = self._errorTimes + 1
        if 3 <= self._errorTimes then
            self._alertPanel = SceneManager.alert("下载的版号文件多次错误,请网络稳定后点击确定重进游戏", 0, function()
                self:restartGame()
            end)
        else
            self._loader:add(self._remoteUrl, Version.LOCALTEMPPATH)
            self._loader:load(handler(self, self._downRemoteComplete), handler(self, self._downRemoteError))
        end
    end
end

--下载远程版本,并更新至最新版本
function Version:downloadRemoteVersion(remoteUrl, completeCall, progressCall, errorCall)
    local loader = require("version.loader.Loader").new()
    local url = string.format("%s/version/%d_%d.zip", remoteUrl, self._remoteVersionInfo:getID(), self._localVersionInfo:getID())
    local localPath = string.format("%sversion/update.zip", cc.FileUtils:getInstance():getWritablePath())
    loader:add(url, localPath)

    local function onLoadProgress()
        progressCall(loader:getContentLengthLoaded() / loader:getContentLengthTotal())
    end

    local function onLoadError()
        errorCall(Version.ERROR_NET)
    end

    loader:load(completeCall, onLoadError, onLoadProgress)
end

--解压已下载版本文件
function Version:decompressVersion(completeCall, progressCall, errorCall)
    local localPath = string.format("%sversion/update.zip", cc.FileUtils:getInstance():getWritablePath())
    local localVersionDirTemp = cc.FileUtils:getInstance():getWritablePath() .. "version_temp/"
    local localVersionDir = cc.FileUtils:getInstance():getWritablePath() .. "version/"
    local function onUnCompressCall(result)
        if result then
            progressCall(0.6)
            local function onComplete(value)
                local result = cc.FileUtils:getInstance():removeDirectory(localVersionDirTemp)
                if value then
                    --- 添加搜索
                    if not self._hasAddVersionPathToSearchPath then
                        cc.FileUtils:getInstance():addSearchPath(localVersionDir .. "src/", true)
                        cc.FileUtils:getInstance():addSearchPath(localVersionDir .. "res/", true)
                    end
                    progressCall(0.7)
                    --- 删除更新包
                    os.remove(localPath)
                    progressCall(0.8)
                    --- 删除老版本信息
                    os.remove(cc.FileUtils:getInstance():getWritablePath() .. Version.VERSION_FILE_NAME)
                    progressCall(0.9)

                    --- 生效新版本
                    local tempVersionFile = cc.FileUtils:getInstance():getWritablePath() .. Version.VERSION_FILE_TEMP_NAME
                    local versionFile = cc.FileUtils:getInstance():getWritablePath() .. Version.VERSION_FILE_NAME
                    os.rename(tempVersionFile, versionFile)
                    progressCall(1)
                    self._localVersionInfo = self._remoteVersionInfo
                    completeCall()
                else
                    errorCall(Version.ERROR_UNZIP)
                end
            end
            version.Version:copyDir(localVersionDirTemp, localVersionDir, onComplete)
        else
            errorCall(Version.ERROR_UNZIP)
        end
    end
    version.Version:uncompressDir(localPath, localVersionDirTemp, onUnCompressCall)
end

--更新客户端
function Version:updateClient()
    cc.Application:getInstance():openURL(self._updateClientURL)
end

function Version._readVersionInfoFromFile(path)
    if cc.FileUtils:getInstance():isFileExist(path) then
        local data = cc.FileUtils:getInstance():getFileData(path)
        if data then
            return Version._parserVersionInfo(data)
        end
    end
    return nil
end

function Version._parserVersionInfo(data)
    local versionInfo = require("version.VersionInfo").new()
    versionInfo:setData(data)
    return versionInfo
end


--获取更新整包地址
function Version:getUpdateUrl(completeCall, progressCall, errorCall)
    local loader = require("version.loader.Loader").new()
    local url = string.format("%s/%s?%s", 
        self._remoteURL, 
        Version.URL_FILE_NAME,
        tostring(os.time()))

    local localPath = cc.FileUtils:getInstance():getWritablePath() .. Version.URL_FILE_NAME
    if cc.FileUtils:getInstance():isFileExist(localPath) then
        os.remove(localPath)
    end
    loader:add(url, localPath)

    local function onLoadComplete()
        local size = cc.FileUtils:getInstance():getFileSize(localPath)
        if size == 0 then
            errorCall(Version.ERROR_NET)
        else
            completeCall()
        end
    end

    local function onLoadError()
        errorCall(Version.ERROR_NET)
    end

    loader:load(onLoadComplete, onLoadError)
end

--根据op_id和包名解析整包下载地址
function Version:parseUpdateUrl(completeCall, progressCall, errorCall)
    local localPath = cc.FileUtils:getInstance():getWritablePath() .. Version.URL_FILE_NAME
    if not cc.FileUtils:getInstance():isFileExist(localPath) then
        errorCall(Version.ERROR_NET)
    end

    local str = cc.FileUtils:getInstance():getStringFromFile(localPath)
    local ret,msg = loadstring(str)
    local url
    if ret then
        local config = ret()
        if config and type(config) == "table" then
            local bounder_id = G_ChannelManager.channel():getPackageName()
            local op_id = G_ChannelManager.channel():getOpId()
            for i=1,#config do
                if tostring(config[i][1]) == tostring(op_id)  and tostring(config[i][2]) == tostring(bounder_id) then
                    url = tostring(config[i][4])
                    break
                end
            end
        end
    end
    if not url then
        errorCall(1)
    else
        self._updateClientURL = url
        completeCall()
    end
end

return Version

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

ellis1970

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值