热更新

简述

Unity热更新指的是不重新安装游戏而直接更新资源和Lua代码(本质上也是一种资源)

基本原理

更新无非就是两种途径:

  1. 直接下载新的资源然后替换旧资源,但是很多时候旧资源都是只读的,替换不了
  2. 下载新的资源后放在一个固定的地方,加载资源时优先查找是否有新的资源,没有新资源时采取加载旧资源。Lua代码的加载也是如此。

基于安卓平台的安装文件只读的特性,一般就是走第二条路。

具体流程

  1. 资源打包:所有需要热更新的资源全部打包到 Application.streamingAssetsPath 文件里

  2. 在游戏启动时,需要对比版本号。如果有大版本更新,则需要更新安装包;如果有小版本更新,则下载更新文件

    CheckUpdate

    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
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    141
    142
    143
    144
    145
    146
    147
    148
    public IEnumerator CheckUpdate()
    {
    var VERSION_FILE_NAME = "version.txt";

    // 临时保存协程结果
    var versionResult = new GetVersionResult();

    // 获取服务器版本
    var serverVersionFileURL = GetServerFileURL(VERSION_FILE_NAME);
    yield return GetVersion(serverVersionFileURL, versionResult);
    var serverVersion = versionResult.version;

    // 获取App版本号
    var appVersionFileURL = GetAppFileURL(VERSION_FILE_NAME);
    yield return GetVersion(appVersionFileURL, versionResult);
    var appVersion = versionResult.version;

    // 获取数据版本号
    var dataVersionFileURL = GetDataFileURL(VERSION_FILE_NAME);
    yield return GetVersion(dataVersionFileURL, versionResult);
    var dataVersion = versionResult.version;

    // 对比版本号

    }

    /// <summary>
    /// 获取安装包文件访问路径,用于System.File
    /// </summary>
    /// <param name="fileName">相对文件路径</param>
    /// <returns></returns>
    public static string GetAppFilePath(string fileName)
    {
    #if UNITY_EDITOR
    // 取决于编辑器资源放在哪
    return "";

    #elif UNITY_ANDROID
    // 安卓平台安装包文件只有文件链接,不能用路径访问
    return null;

    #elif UNITY_STANDALONE || UNITY_IOS
    return $"{Application.streamingAssetsPath}/{fileName}";

    #endif

    // 未知平台
    return null;
    }

    /// <summary>
    /// 获取安装包文件访问链接,用于UnityWebRequest
    /// </summary>
    /// <param name="fileName">相对文件路径</param>
    /// <returns></returns>
    public static string GetAppFileURL(string fileName)
    {
    #if UNITY_ANDROID
    // 自带文件协议头
    return $"{Application.streamingAssetsPath}/{fileName}";

    #elif UNITY_EDITOR || UNITY_STANDALONE || UNITY_IOS
    // 都是可以直接访问的文件
    return $"file:///{GetAppFilePath(fileName)}";

    #endif

    // 未知平台
    return null;
    }

    /// <summary>
    /// 获取下载的数据文件访问路径,用于System.File
    /// </summary>
    /// <param name="fileName">相对文件路径</param>
    /// <returns></returns>
    public static string GetDataFilePath(string fileName)
    {
    return $"{Application.persistentDataPath}/{fileName}";
    }

    /// <summary>
    /// 获取下载的数据文件访问链接,用于UnityWebRequest
    /// </summary>
    /// <param name="fileName">相对文件路径</param>
    /// <returns></returns>
    public static string GetDataFileURL(string fileName)
    {
    return $"file:///{GetDataFilePath(fileName)}";
    }

    /// <summary>
    /// 获取服务器文件访问链接,用于UnityWebRequest
    /// </summary>
    /// <param name="fileName">相对文件路径</param>
    /// <returns></returns>
    public static string GetServerFileURL(string fileName)
    {
    // 临时用本地文件进行测试
    const string SERVER_URL = "file:///{D:/tmp/HotUpdate}";
    return $"{SERVER_URL}/{fileName}";
    }

    /// <summary>
    /// 解析版本号,1.2.3
    /// </summary>
    /// <param name="version"></param>
    /// <returns></returns>
    static int[] ParseVersionString(string version)
    {
    var veri = new int[3];
    var vers = version.Split('.');
    for (var i = 0; i < 3; i++)
    veri[i] = int.Parse(vers[i]);
    return veri;
    }

    class GetTextResult { public string text; } // 临时保存协程结果
    static IEnumerator GetText(string url, GetTextResult result)
    {
    using (var req = UnityWebRequest.Get(url))
    {
    yield return req.SendWebRequest();
    if (req.isDone && req.result == UnityWebRequest.Result.Success)
    {
    result.text = req.downloadHandler.text;
    }
    else
    {
    result.text = null;
    }
    }
    }

    class GetVersionResult { public int[] version; } // 临时保存协程结果
    static IEnumerator GetVersion(string url, GetVersionResult result)
    {
    var textResult = new GetTextResult();
    yield return GetText(url, textResult);
    if (textResult.text == null)
    {
    result.version = null;
    }
    else
    {
    result.version = ParseVersionString(textResult.text);
    }
    }

  3. 下载APK:从服务器下载安装包(就是二进制数据),保存到数据文件路径,然后调用java的方法来安装

  4. 更新文件:从服务器下载文件列表 files.txt,里面的每行分别是相对文件路径、文件MD5、文件字节大小,比如:

    1
    2
    3
    4
    lua/lua.unity3d|60813be4a561c30dbfb8e37923788405|49687
    StreamingAssets|7dc37bfbe7d153d83b71d579c4a9cd56|37135
    StreamingAssets.manifest|95750ba9aee6b40eca7d6759e5947db7|153908
    version.txt|01105f7fb6caff1985eba568aefddcf1|14

    遍历文件列表,判断文件是否存在本地,以及文件MD5是否发生了变化(跟版本号一样,本地也有两个文件列表需要对比),是的话则直接下载相应文件到数据文件目录(如果是以前就有的数据文件,会直接被覆盖掉)

  5. 资源文件加载:用 File.Exists 判断是否有对应下载的新数据文件(数据文件路径可以直接读写,自然也可以判断文件存在与否),如果有,则加载新文件,否则用安装包里的

  6. Lua代码加载:需要扩展Lua文件加载函数,也是先判断是否有对应的数据文件,然后再加载。以ToLua为例,需要继承 LuaFileUtils 。如果Lua文件都是打包成AssetBundle的话,则需要重写 AddBundle 方法。

    关于Lua自定义文件路径

    ToLua.OpenLibs 的第一步就是注册了 ToLua.Loader 作为一个加载函数到 package.loaders (参考此处)。而这个加载函数里会使用 LuaFileUtils.ReadZipFile 加载AssetBudle中的指定文件,核心实现为:

    1
    2
    3
    4
    5
    TextAsset luaCode = zipFile.LoadAsset<TextAsset>(fileName);
    if (luaCode != null) {
    buffer = luaCode.bytes;
    Resources.UnloadAsset(luaCode);
    }