利用多进程并行化加速Unity资源构建

利用多进程并行化加速Unity资源构建

这是侑虎科技第448篇文章,感谢作者江卓浩供稿。欢迎转发分享,未经作者授权请勿转载。如果您有任何独到的见解或者发现也欢迎联系我们,一起探讨。(QQ群465082844)

作者主页:https://github.com/jiangzhhhh
作者也是U Sparkle活动参与者,UWA欢迎更多开发朋友加入U Sparkle开发者计划,这个舞台有你更精彩!


长久以来,Unity的资源构建时间过慢是不少用户的痛点之一,特别是项目大了,构建时间更是慢得影响工作效率。

作者所在的Unity项目属于手游中的巨无霸,Assets文件夹32G,5w个资源文件,资源构建时间本来就相当感人。最近项目在准备大版本更新,引擎组正抓紧时间做优化尝试,经常性批量改贴图导入格式,导致增量构建近乎失效,每次都相当于全量构建。项目组不吝啬硬件投入,配置了比较好的打包机,但即便是在SSD+i7配置的机器上进行一次全量资源构建仍然需要5个小时(312分钟)之久。一遍“修改->打包->验证修改”的流程走完,一天时间就过去了。

作者早期发现了Unity的构建流水线无法很好地利用计算资源,具体表现为:Unity在进行构建资源时,CPU占用率、磁盘IO都非常低,计算机还有大把空闲的资源可供利用,徒增用户的等待时间。能否利用多进程并行化进行资源构建?最近尝试着实现了一下。

静心分析,要实现多进程构建需要做两件事:

- 对构建工程多开实例,保证资源跟项目设置是一致的;
- 对构建列表进行正确拆分,保证有依赖关系的资源同属同一构建任务。

如何对单个Unity工程进行实例多开呢?无法做到只能退而求其次,将工程原样复制多遍了。由于项目比较大,为了省点储存空间,作者在Windows上通过Mklink(Osx上使用ln-s),对Unity的Assets、ProjectSetting两个文件夹进行符号链接,伪装出新的工程出来。而Library文件夹不可共享,则需要由子工程自行生成。考虑到冷启动的话,初次导入资源生成Library时间可能太长,可以考虑从原始工程直接拷贝一份使用。

请输入图片描述

接着就需要对构建列表进行任务分组了。Unity 5开始提供的这套资源构建流水线是相当智能化的,用户只需要提供形式如AssetBundleName:[AssetName]的映射列表,而无需再像Unity 4一样手动描述AssetBundle间的依赖关系,引擎就会自动收集每个Asset依赖关系构建出正确的资源,并通过Manifest的形式给出AssetBundle间的依赖。

对构建列表进行任务分组最重要的就是必须保证有依赖关系的AssetBundle处于同一构建任务,否则会令资源被重复构建造成冗余。

请输入图片描述
资源C将被错误的重复打包

通过AssetDatabase.GetDependencies(asset, true)接口的运用,可以很简单地构建出资源依赖树。

遍历构建列表中的Asset,称为A;使用AssetDatabase.GetDependencies接口得出包含直接、间接的依赖Asset,称为B。检查B的Asset是否也存在于构建列表中并且属于不同的AssetBundle,则可以判定A所属的AssetBundle依赖于B所属的AssetBundle。这样我们就在真实进行构建前就得出所有AssetBundle间的依赖关系了,接下来需要将有依赖关系的AssetBundle归纳为一组。用下面的例子就能很简单说清楚:

假设构建列表中包含了ABCDEF个AssetBundle文件(AssetBundle自然又包含一个或多个Asset文件,在这层面不关心,不列出),各自的依赖关系如图。则根据依赖关系,(A,B,C),(D,E)各自必须同属一组任务,(F)了无牵挂则自为一组。

作者实现这个算法比较粗暴:

- 图中每个代表AssetBundle的节点各自独立分配到一个组;
- 遍历所有节点的依赖,将依赖节点的组跟当前节点的组合并为一个组。

请输入图片描述

此时,构建列表已经根据依赖情况分拆为多个任务了,一般情况下,多进程并行构建的总用时取决于此时任务列表中包含资源数目最多、计算最重的一组。

有了任务列表后,一般我们预设用于构建的子工程数目都会远小于任务数,所以需要合并任务,直到总任务数不大于子工程数为止。

合并的具体过程,我们可以顺便对任务列表做下简单的负载均衡,好让各个子工程的构建时间相对均衡一些。

作者粗略的根据资源的类型(通过文件后缀识别)分配一个预设权重值,每个构建任务中所有Asset的权重总和就是这个任务的总权重,表达了构建这个任务的计算繁重程度,每轮任务合并前先根据任务的总权重排序,每次合并权重最小的两个任务。

请输入图片描述

此时重要环节都做完了,将任务写成Json格式输出到子工程目录下,通过BatchMode启动Unity子进程载入子工程,读取任务Json进行构建即可。

当然,为了流水线的易用性考虑,主进程还需要等待所有子进程构建完毕后将输出结果合并汇总,便于使用者对构建结果进行跟踪。

最后,在同样SSD+i7的PC上,4个子工程+1个主工程共5节点进行并行构建,将原本需要312分钟的资源构建时间缩短至42分钟,效率提升了7.4倍。


文末,再次感谢江卓浩的分享,如果您有任何独到的见解或者发现也欢迎联系我们,一起探讨。(QQ群:465082844)。
也欢迎大家来积极参与U Sparkle开发者计划,简称"US",代表你和我,代表UWA和开发者在一起!

  • Chiang 发表在 10月1日 回复

    这样每次更新资源之后都需要重新映射,如果资源量大的话,映射的时间也会特别长。