梦想网络游戏平台.三层更新|虚拟磁盘|游戏菜单|游戏菜单源码 小勇作品QQ: 官方网站
梦想网络游戏平台.三层更新|虚拟磁盘|游戏菜单|游戏菜单源码 小勇作品QQ: 官方网站
同时上传多个
正在读取...
空间:1000M
正在读取...
最新版本即将免费发布!.
欢迎使用小勇作品 QQ: 官方网站游戏内热更新实现方案
(window.slotbydup=window.slotbydup || []).push({
id: '2611110',
container: s,
size: '240,200',
display: 'inlay-fix'
您当前位置: &
[ 所属分类
| 时间 2015 |
作者 红领巾 ]
其实打算做游戏内热更新也是几个月之前的事情了,在方案经历了数次变迁之后,最近才终于应用到了外网的bugfix中。
但是就目前数据来看,热更新由于要下载资源,会使新用户的进入门槛变高,所以留存收到了一定影响,基本降低了10个点。
当然,也可能是热更新的功能存在bug。
好了,我们还是进入正题吧!
最早的时候,我们想用一种类似打patch的方式来更新。
即将lua代码和资源打成一个zip包,而每个zip包只要代码或者资源发生变化,都会有一个自增的小版本号(大版本号为打在apk或者ipa里的版本号)。
不同的zip包之间diff就可以生成一个patc***件,而为了开发的简单,这个patch列表只会将文件路径列出来,之后客户端要去完整的下载新的文件覆盖。
然而这个方案有很多的问题。
小版本号不好维护因为代码和资源会不停的修改,而如果人工来维护小版本号,会极其复杂。如果通过机器来维护也是个很麻烦的工程
patch包太多
如果我们将小版本号的patch包先生成好放到服务器上,那么如果我们有100个小版本,就会有非常多 1-&100, 2-&100, 3-&100 … 这种格式的patch包。
并且如果小版本号继续增长,patch包的增长基本会失控。
消耗大量流量,速度缓慢也许有同学会说,不如只有 1-&2, 2-&3, 3-&4 这样的单个小版本之间的升级patch,客户端自己挨个下载就好。
但是这样客户端如果是个很小的版本号,就不会不停的重复下载,导致迟迟进入不了游戏,并且流量也大量消耗。
综上所述,方案一有太多的弊端。这也是为什么我们做完了之后迟迟不敢上线的原因,那么我们来看看方案二。
方案二相对就简单了很多。
说白了,我们希望有一种方式能够快速的判断出客户端的代码资源,与服务器端的代码资源是否完全一致。
怎么做呢?对的,md5.
依托于我们目前的打包系统,我们现在每个渠道打出来的apk里面的代码资源都是不尽相同的。而我在其打包之后,自动将所有的代码资源,进行了一次md5,生成了一个如下格式的文件md5.txt:
b0e03cbf33ab|src/cache/User.lua|498217aefb1c5905|src/cache/WordMsg.lua|2441
b0e03cbf33ab|src/cache/User.lua|4982
17aefb1c5905|src/cache/WordMsg.lua|2441
第一列为文件的md5值,用来判断文件是否有变化。
第二列为文件路径,拼接上服务器返回的url前缀即为完整的下载地址。因为我们使用cdn存储文件,所以url前缀随时可能变化。
第三列为文件大小。当提示用户更新的时候,可以算出看到更新的总大小,并在下载中显示文件进度。
那么我们怎么判断客户端的代码资源和服务器是否完全一致呢?
我们只需要判断在相同渠道和大版本的情况下,客户端md5.txt的md5值与服务器的是否相等即可。
为了节省流量,客户端每次进入游戏的时候会将本地的md5.txt的md5值发给服务器,服务器在判断之后告知客户端更新状态是无需更新、建议更新、还是强制更新。如果不是无需更新,就要把服务器端md5.txt的下载路径返回给客户端,客户端下载下来之后,将两个md5.txt对比后列出不同的文件列表,并计算需要下载的总大小。之后提示用户更新或者直接进入更新逻辑。
大概的说明就是这样,我们接下来说下服务器端和客户端代码的具体实现。
我们后台是使用django的,所以天生带了一个管理后台,在models中添加我们自己的逻辑也比较简单。大家如果是别的框架,其实原理也是一样的。
class LuaSource(models.Model): file = models.FileField(u'文件', upload_to=partial(get_source_zip_path, 'lua_source', 'zip')) version = models.IntegerField(u"大版本号", blank=True, default=0, null=True) channel = models.CharField(u"渠道", max_length=128, blank=True, default='') md5sum = models.CharField(u"列表md5值", max_length=128, blank=True, default='') status = models.IntegerField(u'更新状态', default=config.PACKAGE_STATUS_NOT_UPDATE, choices=config.PACKAGE_UPDATE_STATUS) create_time = models.DateTimeField(u'创建时间', default=datetime.datetime.now)
class LuaSource(models.Model):
file = models.FileField(u'文件', upload_to=partial(get_source_zip_path, 'lua_source', 'zip'))
version = models.IntegerField(u"大版本号", blank=True, default=0, null=True)
channel = models.CharField(u"渠道", max_length=128, blank=True, default='')
md5sum = models.CharField(u"列表md5值", max_length=128, blank=True, default='')
status = models.IntegerField(u'更新状态', default=config.PACKAGE_STATUS_NOT_UPDATE,
choices=config.PACKAGE_UPDATE_STATUS)
create_time = models.DateTimeField(u'创建时间', default=datetime.datetime.now)
input: channel 渠道 version 大版本号 md5 md5.txt的md5值output: status 更新状态 url_prefix url前缀 file_list 新的md5.txt下载链接 md5 新的md5.txt的md5值。这个客户端要用来做存储目录的。
channel 渠道
version 大版本号
md5md5.txt的md5值
status 更新状态
url_prefixurl前缀
file_list新的md5.txt下载链接
md5新的md5.txt的md5值。这个客户端要用来做存储目录的。
客户端的代码逻辑要稍微复杂一些:
在cpp层,创建一个update目录,并将lua的search优先级调到最前面。
这里我们很不幸的踩过一个坑,就是我们把这个update目录的名字固定为update。但这样会带来一个问题,就是当用户更新了大版本之后,还是会优先去读取这个update目录的代码,从而导致进入不了游戏。
正确的方式应该是将这个将渠道和大版本号也拼上,比如 update_CN_IOS_APP_91。
ios和android的具体代码如下:
std::string strChannel = "UNKNOWN";std::string strVersion = "UNKNOWN";#if (CC_TARGET_PLATFORM == CC_PLATFORM_IOS)NSString * channel = [[[NSBundle mainBundle] infoDictionary] objectForKey:@"Channel"];NSString * version = [[[NSBundle mainBundle] infoDictionary] objectForKey:@"CFBundleShortVersionString"];strChannel = [channel UTF8String];strVersion = [version UTF8String];#endif#if (CC_TARGET_PLATFORM == CC_PLATFORM_ANDROID)JniMethodInfo methodVif (JniHelper::getStaticMethodInfo(methodVersion, J***A_CLASS_NAME.c_str(), "getVersionCode", "()I")) { jint version = methodVersion.env-&CallStaticIntMethod(methodVersion.classID, methodVersion.methodID); std:: ss && strVersion = ss.str(); methodVersion.env-&DeleteLocalRef(methodVersion.classID);}JniMethodInfo methodCif (JniHelper::getStaticMethodInfo(methodChannel, J***A_CLASS_NAME.c_str(), "getChannel", "()Ljava/lang/S")) { jstring str = (jstring)methodChannel.env-&CallStaticObjectMethod(methodChannel.classID, methodChannel.methodID); strChannel = JniHelper::jstring2string(str); methodChannel.env-&DeleteLocalRef(methodChannel.classID); methodChannel.env-&DeleteLocalRef(str);}#endif_updatePath = CCFileUtils::getInstance()-&getWritablePath() + UPDATE + "_" + strChannel + "_" +strV
std::string strChannel = "UNKNOWN";
std::string strVersion = "UNKNOWN";
#if (CC_TARGET_PLATFORM == CC_PLATFORM_IOS)
NSString * channel = [[[NSBundlemainBundle] infoDictionary] objectForKey:@"Channel"];
NSString * version = [[[NSBundlemainBundle] infoDictionary] objectForKey:@"CFBundleShortVersionString"];
strChannel = [channelUTF8String];
strVersion = [versionUTF8String];
#if (CC_TARGET_PLATFORM == CC_PLATFORM_ANDROID)
JniMethodInfomethodV
if (JniHelper::getStaticMethodInfo(methodVersion, J***A_CLASS_NAME.c_str(), "getVersionCode", "()I")) {
jintversion = methodVersion.env-&CallStaticIntMethod(methodVersion.classID, methodVersion.methodID);
strVersion = ss.str();
methodVersion.env-&DeleteLocalRef(methodVersion.classID);
JniMethodInfomethodC
if (JniHelper::getStaticMethodInfo(methodChannel, J***A_CLASS_NAME.c_str(), "getChannel", "()Ljava/lang/S")) {
jstringstr = (jstring)methodChannel.env-&CallStaticObjectMethod(methodChannel.classID, methodChannel.methodID);
strChannel = JniHelper::jstring2string(str);
methodChannel.env-&DeleteLocalRef(methodChannel.classID);
methodChannel.env-&DeleteLocalRef(str);
_updatePath = CCFileUtils::getInstance()-&getWritablePath()+ UPDATE + "_" + strChannel + "_" +strV
当与服务器进行网络交互后,服务器告知客户端需要更新时。
先创建一个以新md5值为名的目录,并将新的md5.txt下载其中,之后将所有不同的文件下载到这个目录里。
当所有文件下载完成后,将新的md5.txt文件拷贝update目录,之后重新进入游戏。
由于lua操作文件系统的限制,我们需要将一个lfs的库编译近来,具体可以去cocos2d-quick中找。
另外要注意,当更新完毕重新进入游戏时,需要将所有引入的lua代码删除再重新导入。而如果热更新的代码本身被修改了的话,则需要将热更新代码本身也重新导入。并重新走一遍热更新判断逻辑。
基本就是这样子了,很多细节代码不方便放出,大家自己想想应该写起来也不难。
本文网络安全相关术语:网络安全工程师 网络信息安全 网络安全技术 网络安全知识
转载请注明本文标题:本站链接:
分享请点击:
1.凡CodeSecTeam转载的文章,均出自其它媒体或其他官网介绍,目的在于传递更多的信息,并不代表本站赞同其观点和其真实性负责;
2.转载的文章仅代表原创作者观点,与本站无关。其原创性以及文中陈述文字和内容未经本站证实,本站对该文以及其中全部或者部分内容、文字的真实性、完整性、及时性,不作出任何保证或承若;
3.如本站转载稿涉及版权等问题,请作者及时联系本站,我们会及时处理。
登录后可拥有收藏文章、关注作者等权限...
CodeSecTeam微信公众号
把一件件小事做好,最后就做成了大事。
手机客户端
,专注代码审计及安全周边编程,转载请注明出处:http://www.codesec.net
转载文章如有侵权,请邮件 admin[at]codesec.net页面没有找到
页面没有找到 5秒钟之后将会带您进入新浪首页!