引言:之前有个同事问我,说为什么Unity资源加载要用到AssetBundle这个东西,不用行不行?这个问题挺有意思,遂从资源加载的逻辑开始进行必要性探路。
如果每个资源都视为一个单独的对象,统一用File.ReadAllBytes进行字节读取,转为对应的资源类型呢?会有什么问题呢?
首先,I/O读取是有成本的,涉及到用户态到内核态的转换。读取100个1k的小文件要比读取1个100k的大文件慢。
有点像DrawCall,有感觉吗?
其实SpriteAtlas/AssetBundle都是一样的思想。
新建两个txt:a.txt只有一个字母a,大小是1个Byte;b.txt有很多个字母a,大小是984,920,040个Byte。
Stopwatch sw = new();
sw.Restart();
for (int i = 0; i < 100000; i++)
{
File.ReadAllBytes("G:/a.txt");
}
sw.Stop();
Console.WriteLine(sw.ElapsedTicks);
sw.Restart();
File.ReadAllBytes("G:/b.txt");
sw.Stop();
Console.WriteLine(sw.ElapsedTicks);
输出结果:
16557786 4341952
可以看到读取十万次1Byte(100KB)的小文件用的时间,比读取一个差不多1GB的文件用的时间,多了一个数量级。
打开Profile进行分析,发现热点函数System.IO.File.ReadAllBytes占了90%以上的CPU时间。Kernel态占据了98.2%的时间。
可以看到,大量的时间耗在了I/O处理中。
存在A资源依赖B资源的情况,可以构建出整个依赖图,是一个双向图。一个中等规模的项目,单资源可能有几万到几十万个,其依赖图是巨大的。不考虑循环引用的情况下,对于n个资源,其最大的依赖项为2^(n-1)-1。此外,在运行过程中,需要动态记录每个资源被引用的次数,对于n个资源,就要记录n个对象。
Unity的做法其实是很常见的思想:归类与合并。将若干个资源打包成Bundle,资源加载以Bundle为最小单元进行加载,资源依赖以Bundle为单位。
有的兄弟有的。
首先是更新问题。单文件可以做到谁改变谁更新,但是Bundle是以Bundle为最小单元进行更新的。举个例子,假设一个角色的语音包是一个Bundle,如果角色修改了中间一条语音,那么必须下载整个语音包Bundle。
其次是依赖冗余问题。对于Bundle来说,如果A资源依赖C,B资源依赖C,A打包进BundleA中,B打包进BundleB中,如果没有把C打包进Bundle,那么C资源会自动打进BundleA和BundleB中,造成资源冗余。诚然我们可以通过优化打包逻辑来尽量规避这个问题,但是缺陷客观存在。如果是单文件就存在冗余问题。