更新

  • 2020/01/19 增加合并方法链接
  • 2019/12/08 初次发布

介绍

Unity AssetBundle 中的资源非常容易被提取,如果想要阻止简单的提取行为,可以尝试在打包时将文件分割,在运行时将文件合并。

这种方法存在运行时过于耗时的问题,因此在 Unity AssetBundle 合并 - 狂飙 使用了另一种方法来解决这个问题。

打包

打包时将文件分割成多个文件,这个可以根据需要指定规则处理。例如可以使用随机文件名、分割成随机的份数。

加载

LoadFromStream

Unity 2017.4 Unity 2018.4 Unity 2019.3 Unity 2020.1 或更高版本新增了一个从流读取 AssetBundle 的 API,那么可以将多个文件合并为一个文件流供 API 使用。

虽然通过这个 API 可以自定义 AssetBundle 加载方式,包括加密、多文件读取、内存中读取等等。但是有一个致命缺点:使用时占用文件打开数量,而操作系统对文件打开数量是有上限的。

5.5.4. iOS file handle overuse
Current versions of Unity are not affected by this issue.
In versions prior to Unity 5.3.2p2, Unity would hold an open file handle to an AssetBundle the entire time that the AssetBundle is loaded. This is not a problem on most platforms. However, iOS limits the number of file handles a process may simultaneously have open to 255. If loading an AssetBundle causes this limit to be exceeded, the loading call will fail with a “Too Many Open File Handles” error.
This was a common problem for projects trying to divide their content across many hundreds or thousands of AssetBundles.

具体需要在真机测试,可以尝试在 Update 中一直打开文件,然后看界面显示的打开文件数量,到达某一数字后进程会被强制结束。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
using System.Collections.Generic;
using System.IO;
using UnityEngine;

public class Test : MonoBehaviour
{
    List<FileStream> _openFileStreams = new List<FileStream>();

    void OnGUI()
    {
        GUILayout.Label(_openFileStreams.Count.ToString());
    }

    void Update()
    {
        _openFileStreams.Add(File.Create(_openFileStreams.Count.ToString()));
    }
}

以下链接中提供了一个可用的 MultiStream 实现,建议参考使用。经测试可以在 Unity 2018.4.12f1 中正确加载 AssetBundle 并读取其中资源进行实例化显示到场景中。

建议:由于受文件打开数量上限影响,可以考虑只为关键资源使用 MultiStream 方式加载。

LoadFromFile

如果使用 LoadFromFile API,那么需要提前对文件进行合并,这一步骤可以放在首次启动时处理。

对文件进行合并的操作可以放在后台线程中处理,另外可以将文件划分优先级,只有马上要用的文件优先处理,处理完成后进入游戏;剩下的文件在后台继续处理。

验证

在合并文件时可以使用大小及 MD5 验证。

MD5 验证看起来不错,但是在 Unity 5.6.6f2 + Xcode 11 环境下测试时发现 Debug 版本耗时超长,Release 版本中不受影响。

实际结果是 1800+ 文件共计 180+MB 在 Debug 下需要 150 多秒验证,怀疑 MD5 代码中存在数据竞争问题。

考虑到 MD5 验证意义不大,因为打包时可以保证文件都是正确的,因此这步可以跳过,只做大小验证。

测试

可以使用 Instruments 工具对耗时进行测试,通过工具可以轻易地看到耗时过长的调用。

大概的使用方法可以参考 Unity 文章:

优化

在合并文件时,要注意尽可能地减少无用操作,例如:

  • 合并的缓存 Buffer 可以在外部预先创建好,然后传递给合并方法使用,减少 GC 次数与时间。
  • 增大缓存 Buffer 大小,减少系统调用次数。
  • 减少 IO 操作,包括不限于检查目录是否存在、创建目录、创建文件等等。
  • 使用线程时需要指定线程的优先级为最高,可以为线程多增加一些时间片使用。
  • 使用 StopWatch 处理时,注意 StopWatch 提供的是墙上时间,而不是 CPU 时间,不要被误导。
  • 减少拆分文件的数量,文件数量的多少直接影响 IO 操作的数量。