quick-cocos2d-x基于源码加密打包功能的更新策略(2)

前一篇:quick-cocos2d-x基于源码加密打包功能的更新策略(1)

二、更新原理讨论及更复杂的更新功能

1.更新原理

在前面的更新过程中,从服务器取文件列表,并根据文件列表再更新相关的文件,这都是很好理解的。当然其中还有些流程细节关系到健壮性、续传、文件版本分发等,我们可以后面再讨论。

对于一些刚开始学习Quick-x的朋友来说,可能希望了解的是,这一更新机制的替换原理是什么,为什么新的文件下载后能够替代原来的代码生效呢?

从前面我们参考的源文件编译及加密的相关文章,可以清楚的看到,初始代码打包成的game.zip文件,是在AppDelegate.ccp中,在程序启动前通过loadChunksFromZIP载入的。这一载入工作实际上已经将所有代码都加载了,之后调用require时,将会直接 调用而不会再去找代码文件来载入。也正因为如此,一些朋友会感到迷惑,即使下载了新的代码文件,又如何让它生效呢?

其实很简单,loadChunksFromZIP是可以多次调用的,而且如果第二次载入的包中的代码模块与之前载入的模块有重名,新的模块会覆盖旧的模块。

在Quick-x的Lua代码中,对应的调用接口是CCLuaLoadChunksFromZIP。有了这个接口,我们下载新的代码包后,就可以自己加载了。在update.lua里,在下载完成后,会自动将act标记为load的文件加载一次,这样,模块的新代码就取代了旧的代码,也就实现了我们之前看到的更新功能。

原理很简单,似乎没问题了?可惜问题没这么简单。这是因为require的机制问题。如果在第二次载入包之前,某一个模块文件已经被require过,那么,虽然第二次载入后,这一模块的代码已经被更新了,但再次require时,由于此模块不会被真正调用而是直接取原来的调用结果,将会造成更新不生效。

下面我们通过调试一个新的更新情景来说明这个问题及解决方法。

2.已经require过的模块的更新

假设现在我们希望更新一下界面上的图片。

如果只是更换个别图片,这是很简单的,只需要在代码里newSprite里在图片文件名前加上完整的下载目录路径,然后将新的图片放到服务器上,在flist文件中添加图片文件名即可。不建议在要更新的代码里使用addSearchPath来添加搜索路径,因为在图片同名的情况下并不能保证优先查找到的是新的文件。

如果是更新纹理图,仍然可以使用上述明确指定资源路径的方式,同时也是建议的方式。因此,对于我们之前MainScene.lua的代码,可以做以下修改(假设device.writablePath是下载路径):

display.addSpriteFramesWithFile(device.writablePath..GAME_TEXTURE_DATA_FILENAME, device.writablePath..GAME_TEXTURE_IMAGE_FILENAME)
self.bg = display.newSprite("#logo.png", display.cx-200, display.cy-200)
self:addChild(self.bg)

当然这不是唯一的改法。为了说明主题,我们换成修改config模块的方式。这次不修改MainScene.lua,而是在config.lua中,修改原来的纹理图定义如下:

GAME_TEXTURE_DATA_FILENAME = device.writablePath.."game.plist"
GAME_TEXTURE_IMAGE_FILENAME = device.writablePath.."game.png"

要注意的是,如果不是更新代码,这样写是有问题的。通常config模块都是最早被require的,甚至还在framework.init模块之前,此时device都还没有定义,所以如果是正常流程就会出错。但这里是更新代码,在被调用之前,update模块已经调用过framework.init,所以device已经被初始化过了。

这就引出了我们的问题,既然config模块在早期已经被调用,那么,当新的config下载到客户端后被加载时,再require实际上已经不能让它生效了!

最直接的解决办法是,在重新require之前,调用以下的代码:

package.loaded["config"] = nil

即将config的调用标记清除。这样,再次require时就能让Lua程序重新去执行新的config模块了。我们将这条语句加到appentry模块的开头,这是最合适的地方了。

我们仍然将修改后的config.lua和appentry.lua打包成update.bin,和新的图片资源文件一起放到服务器上,这次服务器的flist文件如下:

local list = {
  ver = "1.0.3",
  stage = {
   {name="game.plist", code="29d103e9831720c1be12d8b33a1ea762", },
   {name="game.png", code="e9dd2797018cad79186e03e8c5aec8dc", },
   {name="update.bin", code="a681c6a002989832645ed26b766c7afa", act="load"},
  },
  remove = {
  },
}
return list
在客户端启动程序,版本自动被更新,进入MainScene显示的图片变成了新的,成功了!

大家可以自行验证不清除调用标记的情况。要注意的是如果是在player上,显示的图片一样会变成新的,这是因为device.writablePath在player上刚好是最优先的搜索路径,所以下载后的图片会被先搜到。因此这个测试最好下载到其他目录下,或者在新的config模块里加一条调试输出语句,这样就很容易确认新的模块是否真的被执行了。

由上述例子可以看到,更新模块时,困难的地方就在于判断重加载前有没有已经被调用的模块。特别是作在应用运行过程中动态更新的方案时,更需要对流程有清楚的把握。

即使是现在实现的这个预更新的方案,在解决了config模块的重载调用问题后,仍然要面对一个相对更困难的问题:如果framework模块需要更新,应该怎么办?

3.深层逻辑更新的处理方式:强制覆盖式调用

framework模块是Quick-x的核心之一,旧版本里是在main.lua里载入,现在的版本更是将它的载入位置提前到了Lua程序入口被调用之前的C++代码中,而通常framework的init模块也是早早会被调用。即使是update模块,也是先调用了init才能方便的实现更新功能。虽然这一模块更新的机率很小,但作为解决方案,还是要考虑清楚应该如何对它进行更新。

首先会想到的,是象之前处理config模块一样,清除调用标记。然而仔细考虑,会觉得不太行得通。因为看代码可以知道,init模块还调用了framework里很多的其他模块,在修改之后要理清哪些模块需要重调用是很繁琐的,而繁琐就容易出错。因此这不是最好的解决方案。

经过考虑,我觉得最方便的是这样一种解决方式:如果需要更新framework模块,那么,在打包时,不将代码放在framework目录下,而是其他的目录,如“framework1”。这样,所有的模块就变成了framework1.init、framework1.device等等,在新的代码里调用“require "framework1.int"”,将会真正重新运行框架代码,而不必担心有哪个模块没有重新运行生效。

这就是“强制覆盖式调用”的含义,它在很多情形下可以避免过于复杂的更新逻辑的判断,推荐给大家。

再举个例子,如果更新模块本身需要更新,那应该怎么办呢?如果要为此在更新模块中预留更新逻辑,会非常复杂难解。然而,如果跳出来看,其实完全可以在修改完update模块后,打包成一个updateNew模块放到服务器,让原更新模块下载后强制调用,这样一来,整个处理就简单了。

通过上面的讨论,现在我们已经能确认,update模块的更新功能是有效的。接下来我们还要讨论更新方案的另一个要点:安全性。

如果因为更新失败造成客户端无法启动,那无疑是致命的。幸运的是,Quick-x的打包机制能让我们保有一个原始版本,即使是在最不幸的出错情况下,程序仍然能回到最初的版本而不至于崩渍。

后一篇:

quick-cocos2d-x基于源码加密打包功能的更新策略(3)

你可能感兴趣的:(更新,自动更新,quick-cocos2d-x,源码加密)