文章目錄
  1. 1. 前言
  2. 2. 热更新
    1. 2.1. What
    2. 2.2. Why
    3. 2.3. How
      1. 2.3.1. 版本强更
        1. 2.3.1.1. 实战
      2. 2.3.2. 资源热更
      3. 2.3.3. 热更新辅助工具
      4. 2.3.4. 代码热更
        1. 2.3.4.1. XLua
      5. 2.3.5. 资源服务器
        1. 2.3.5.1. 实战
  3. 3. Reference
  4. 4. Git

前言

本篇文章是为了记录学习游戏开发过程中,版本强更和资源热更相关知识。

关于资源AssetBundle加载模块相关学习参考:
Unity Resource Manager
AssetBundle资源打包加载管理

热更新

首先还是让我们从What,Why,How三个点来学习理解热更新。

What

什么是热更新?
从前端的角度来说,我个人理解热更新是指无需通过商店审核上传最新版本就能通过游戏内热更下载最新资源和代码的形式更新最新功能。

从服务器的角度来说,可能是无需关闭服务器,不停机状态下修复漏洞,更新资源等,重点是更新逻辑代码。

本人是前端程序,所以侧重点是前端热更这一块,而非服务器热更新。
接下来主要以以下三点来深入学习:

  1. 版本强更(类似于商店下载最新版本的完整包安装更新)
  2. 资源热更(游戏内资源热更新下载替换本地游戏内容)
  3. 代码热更(游戏内代码热更新下载替换本地游戏代码内容)

Why

为什么需要热更新?

  1. 商店审核时间不可控(紧急bug严重修复时会造成阻碍)
  2. 手机游戏更新频繁,改善用户更新体验(不必每次都走商店版本强更)
  3. 快速修复bug和更新功能

How

接下来会针对前面提到的三点来分别讨论:
针对版本强更和资源热更,先来看一下我整理的流程图:
HotUpdateFlowChat

版本强更

版本强更主要是游戏内通过服务器获取最新版本号判定当前游戏版本是否需要更新,然后引导用户更新(可以是引导到对应商店下载,也可以是直接游戏内直接下载安装包)最新的包。

  1. 引导到对应商店下载
    问题:
    需要判断出用户当前下载的安装包是从哪个应用商店下载的,然后跳转到对应商店,如果玩家不是从应用商店下载或者说玩家本地没有对应应用商店,那么就会出现无法成功跳转对应应用商店的情况。
    方案:
    考虑到Android应用商店繁多,基于上述问题,这里我们采用第二种方案,直接引导游戏内CDN下载最新包的形式强更版本(这里指的Android,IOS官方AppStore只有一个本地肯定有AppStore,直接跳转AppStore即可)
  2. 游戏内直接下载(cdn下载)
    游戏内直接下载可以通过下载对应CDN(不同渠道分不同CDN地址即可划分渠道)上的安装包覆盖安装即可。

实战

Android:
通过查询相关资料,了解到Android APK安装在Android N(7)之前主要是通过Itent结合File的形式。
Android N(7)之后主要是通过FileProvider组件

CS层热更下载APK代码:

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
string testresourceurl = "*/HotUpdate.apk";
var webrequest = UnityWebRequest.Get(testresourceurl);
yield return webrequest.SendWebRequest();
if (webrequest.isNetworkError)
{
Debug.LogError(string.Format("{0}安装包下载出错!", testresourceurl));
Debug.LogError(webrequest.error);
if (webrequest.isHttpError)
{
Debug.LogError(string.Format("responseCode : ", webrequest.responseCode));
}
}
else
{
Debug.Log(string.Format("{0} webrequest.isDone:{1}!", testresourceurl, webrequest.isDone));
Debug.Log(string.Format("{0}安装包下载完成!", testresourceurl));
var downloadfilefolderpath = Application.persistentDataPath + "/download/";
var downloadfilesavepath = downloadfilefolderpath + "app.apk";
Debug.Log("downloadfilefolderpath = " + downloadfilefolderpath);
Debug.Log("downloadfilesavepath = " + downloadfilesavepath);
if(!Directory.Exists(downloadfilefolderpath))
{
Directory.CreateDirectory(downloadfilefolderpath);
}
if (File.Exists(downloadfilesavepath))
{
File.Delete(downloadfilesavepath);
}
using (var fs = File.Create(downloadfilesavepath))
{
fs.Write(webrequest.downloadHandler.data, 0, webrequest.downloadHandler.data.Length);
fs.Flush();
fs.Close();
Debug.Log(downloadfilesavepath + "文件写入完成!");
}
}

原生APK安装代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Intent install = new Intent(Intent.ACTION_VIEW);
install.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
File apkFile = new File(mContext.getExternalFilesDir(null).getPath() + "/download/" + "app.apk");

Uri uri = null;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
install.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
install.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
install.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
uri = FileProvider.getUriForFile(mContext, mPackagename + ".fileprovider", apkFile);
} else {
uri = Uri.fromFile(apkFile);
}
install.setDataAndType(uri, "application/vnd.android.package-archive");
startActivity(install);

AndroidMainifest.xml定义相关FileProvider:
res下新建xml目录以及file_paths.xml文件
AndroidHotUpdateFilePath

1
2
3
4
5
6
7
8
9
10
//强更部分
<provider
android:name="android.support.v4.content.FileProvider"
android:authorities="应用包名.fileprovider"
android:exported="false"
android:grantUriPermissions="true">

<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths"/>

</provider>

配置file_paths.xml:

1
2
3
4
5
6
7
8
9
10
11
12
<?xml version="1.0" encoding="utf-8"?>
<resources>
//下载安装包目录
<paths>
<external-path
name="download"
path="" />

<external-files-path
name="download"
path="" />

</paths>
</resources>

AndroidManifest.xml权限相关设置:

1
2
3
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES"/>

Android相关路径定义知识:
以下内容来源:
关于 Android 7.0 适配中 FileProvider 部分的总结

:内部存储空间应用私有目录下的 files/ 目录,等同于 Context.getFilesDir() 所获取的目录路径;

:内部存储空间应用私有目录下的 cache/ 目录,等同于 Context.getCacheDir() 所获取的目录路径;

:外部存储空间根目录,等同于 Environment.getExternalStorageDirectory() 所获取的目录路径;

:外部存储空间应用私有目录下的 files/ 目录,等同于 Context.getExternalFilesDir(null) 所获取的目录路径;

:外部存储空间应用私有目录下的 cache/ 目录,等同于 Context.getExternalCacheDir();

最后让我们来看下运行效果:
运行界面
下载安装包
版本强更安装

注意事项:

  1. 下载保存APK时需要android.permission.WRITE_EXTERNAL_STORAGE权限
  2. 安装APK时需要android.permission.REQUEST_INSTALL_PACKAGES和android.permission.READ_EXTERNAL_STORAGE权限
  3. Android 6.0以后敏感权限需要主动请求
  4. 从Android 7.0开始,系统修改了安全机制: 限定应用在默认情况下只能访问自身应用数据。所以当我们想通过File对象访问其它package数据时,就需要借助于ContentProvider、FileProvider这些组件,否则会报FileUriExposedException异常。
  5. Android下载和读取安装APK的目录需要是有可读写权限的外部存储目录(e.g. Application.persistentDataPath || Application.temporaryCachePath)

IOS:
IOS正版只有AppStore(暂时不考虑越狱渠道),所以IOS直接打开应用商店跳转即可。
IOS跳转AppStore商店的代码就简单很多了:

1
Application.OpenURL("itms-apps://itunes.apple.com/app/id" + appID);

Android打开商店的代码和IOS类似:

1
Application.OpenURL("market://details?id=" + appID);

Note:
IOS appID可以通过itunes的链接获取到,比如我们的游戏链接为:https://itunes.apple.com/app/id1238589899,1238589899 为IOS的appID
google play 的ID通过商店链接获取到,比如我们的游戏链接为:https://play.google.com/store/apps/details?id=com.oasis.movten.android, com.oasis.movten.android 为 Android的appID

资源热更

资源热更是指在游戏内直接下载最新资源,然后本地游戏通过使用最新下载的资源更新到最新的游戏资源并使用。

资源热更下载老版本Unity提供了WWW接口,但这个接口官方已经不推荐使用了,取而代之的是UnityWebRequest(http访问资源服务器的形式)。

资源下载跟前面提到的版本强更APK下载是一个意思,只不过需要通过比较本地更新的资源数据和服务器上的资源数据对比出需要更新的资源列表。这里不厌其烦的再放一次版本强更和资源热更的流程图加深印象:
HotUpdateFlowChat
代码这里就不贴了,感兴趣的可以直接上Git下载了解:
AssetBundleLoadManager

热更新辅助工具

Tools->HotUpdate->热更新操作工具

HotUpdateToolsUI

主要分为以下4个步骤:

  1. 版本资源文件MD5计算(文件名格式:MD5+版本号+资源版本号+平台+时间戳+.txt)

    AssetBundleMD5Caculation

  2. 对比两个版本的MD5文件信息得出需要热更新的AB文件信息

  3. 执行热更新AB准备操作自动复制需要热更新的AB到热更新准备目录(然后手动拷贝需要强更或热更的资源到真正的热更新目录)
  4. 执行热更新准备操作,生成热更新所需的最新资源热更新信息文件(ResourceUpdateList.txt)和服务器最新版本信息文件(ServerVersionConfig.json)

代码热更

代码热更和版本强更不一样,版本强更一般来说是通过更新下载完整的包来实现整个游戏功能版本更新,而代码热更是指我们通过技术手段(e.g. XLua, ILRuntime, ToLua ……),通过热更代码的形式实现热更功能代码。

主流的方式有两种:

  1. Lua(Lua方案)
    Lua是解析执行,有自己的虚拟机,对于代码热更有天然的优势,我们完全可以以资源热更(AssetBundle)的形式热更Lua代码。
  2. ILRuntime(CS方案)
    这里不是非常了解ILRuntime底层原理,想了解详情的可以参考官网:ILRuntime项目为基于C#的平台(例如Unity)提供了一个纯C#实现,快速、方便且可靠的IL运行时,使得能够在不支持JIT的硬件环境(如iOS)能够实现代码的热更新
    但ILRuntime有一点很大的优势就是开发期也是用CS语言,不用写Lua。

这里因为项目主要用到了XLua,所以后续都是以XLua作为代码热更来学习。
选XLua的原因主要有以下几点:

  1. 腾讯开源,有大公司支持,稳定
  2. 除了支持纯Lua开发,还支持CS通过改写Lua的形式热修复Bug

Note:

  1. 苹果禁止了C#的部分反射操作,禁止JIT(即时编译,程序运行时创建并运行新代码),不允许逻辑热更新,只允许使用AssetBundle进行资源热更新。

XLua

官网:
XLua

待续……

资源服务器

前面提到的版本强更和资源热更都离不开资源服务器,这里说的资源服务器是指静态资源服务器,接下来通过实战学习了解静态资源服务器相关的概念以及静态资源服务器选择和使用。

静态资源服务器:

我们把资源放到指定静态资源服务器上,静态资源服务器开启Http或者其他的连接服务,允许用户通过对应方式(Http或者其他)去访问拉取服务器资源。(个人理解,如果有误欢迎指出)

静态资源服务器是资源热更的一个前提。

CND:

内容分发网络(英语:Content delivery network或Content distribution network,缩写CDN)是指一种透过互联网互相连接的计算机网络系统,利用最靠近每位用户的服务器,更快、更可靠地将音乐、图片、影片、应用程序及其他文件发送给用户,来提供高性能、可扩展性及低成本的网络内容传递给用户。

从上面可以看出CDN是独立于静态资源服务器,网络传输层的一个概念,主要目的是为了加快和提供高性能的资源传输。

实战

这里我选择了阿里云的OSS作为静态资源服务器。

OSS详情:

阿里云对象存储服务(Object Storage Service,简称 OSS)为您提供基于网络的数据存取服务。使用 OSS,您可以通过网络随时存储和调用包括文本、图片、音频和视频等在内的各种非结构化数据文件。

OSS静态资源服务器搭建:

  1. 注册阿里云账号
  2. 账号实名认证
  3. 开通OSS服务
  4. 创建Bucket
  5. 上传文件

OSS详细使用说明

待续……

Reference

Unity手游之路手游资源热更新策略探讨
Unity 大版本更新之APK的下载与覆盖安装
Unity3D热更新方案总结
使用Intent安装APK方法(兼容Android N)
关于 Android 7.0 适配中 FileProvider 部分的总结
Unity app 如何打开商店

Git

AssetBundleLoadManager

文章目錄
  1. 1. 前言
  2. 2. 热更新
    1. 2.1. What
    2. 2.2. Why
    3. 2.3. How
      1. 2.3.1. 版本强更
        1. 2.3.1.1. 实战
      2. 2.3.2. 资源热更
      3. 2.3.3. 热更新辅助工具
      4. 2.3.4. 代码热更
        1. 2.3.4.1. XLua
      5. 2.3.5. 资源服务器
        1. 2.3.5.1. 实战
  3. 3. Reference
  4. 4. Git