Unity Asset的一生
Asset
Asset资源分为两部分:文件本身和.meta文件
文件本身存储原始数据;对应的.meta文件存储一些unity用到的额外信息
Asset可分为两种:第三方工具产生的 和 Unity自身产生的
第三方工具产生的,如:Maya、3DMax等;
Unity自身产生的,如:Prefab、Script等
这两者的.meta文件所存储的信息是不相同的
Asset可分为两种:运行时Runtime Asset 和 编辑器Editor Asset
Runtime Asset(比如纹理、声音、动画等)在最终打包时会被打入包,被玩家直接看到
Editor Asset,比如一些数据内容,参与编辑或生成包的过程,但是最终没有被打包
Asset文件与.meta文件
.Meta文件:
.meta文件很重要。
由Unity产生的资源对应的.meta文件
prefab的.meta文件;和material的.meta文件
fileFormatVersion: 无需关注,表示当前meta文件的格式(基本上一直会是2)
guid:当导入一个asset时,unity会分配一个唯一id作为标识,这个标识也用于关联到Library中的对应资源
PrefabImpoter:导入管理的相关信息(也是AssetImporter处理的内容,也可在Inspector中对应看到)
Impoter下的一些键值对是可以在Inspector面板下
.prefab文件(资源文件本身) -- (每一个Asset的数据都是这种格式)
YAML文件格式
000 !u!1 &4309454636272863991:一般称为ObjectID
1表示类型,比如这里是1一定表示是GameObject,下面4一定是Transform(unity内置枚举)
4309454636272863991表示该组件的fileID
最上面是GameObject;其中的一些字段可以在Inspector面板中打开Debug进行查看
m_Component下面有四个数据,会发现这四个数据对应的是该GameObject下挂载的四个组件自己对应的ID
Unity找寻该fileID对应的数据段,将这个数据段填充到这个位置
实用方法:有的时候会出现script里面引用missing的情况,多数是因为.meta文件丢失后生成了新的meta导致id对不上了
这时如果有旧版本,就可以通过fileID来重新赋值引用(在这里改数据的优势就是批量化)
诡异技巧:在打包时比如想要移除掉一些脚本,也可通过python这样处理(移除数据块和引用)
Library文件夹:
所有Asset资源最终(build)都会被放入Library文件夹 -- 异常庞大的文件夹
源文件会根据unity的导出设置进行格式转换并放入Library文件夹,这也就是为什么源文件永远是那个源文件,即使导出设置改变了源文件也不变的原因
所以比如声音文件,放什么格式的最好呢,按道理wav格式是最好的,因为无损、原始采样率最高,unity导出后只进行了一次压缩;如果放的是mp3,最终音效质量就没那么好,进行了二次压缩
这里提到一个Unity现在有两种版本,在ProjectSettings -> Editor -> Asset Pipeline -> Mode 里可选Version1和Version2,
Version1和Version2的主要区别是Version1其实是一个对应索引,而Version2是一个DB
选择Version1时是这样的
Library/metadata 目录下为很多这种编号的文件夹,上面提到的guid数字就可以在这里被对应上
比如上面第一部分提到的prefab对应的在.meta中记录的guid: 368406572aed14c9da2edd5fe4bedc67
前两位数36表示可以在36文件夹中找到两个对应文件
之前提到,Unity会将源文件基于一些配置设置导入到Library文件夹下,这些文件就存在这里
这里面文件的修改时间可以被作为一些操作的参考依据,比如判断是否需要assetbundle重新打入包
选择Version2时会发现reimport的时间大大缩短,此时是这样的
在Library文件夹下没有了meta文件夹,但是有一个Artifacts文件夹下也都是编号文件夹,不过在36文件夹中也找不到对应guid的文件
会发现在Library下多了很多DB文件,如ArtifactDB, SourceAssetDB等LMDB数据库文件,这也是reimport时间
StreamingAssets文件夹:
1. 被原封不动打进包里 -- 也意味着不做压缩(Unity在打安卓包的时候会对所有SteamingAssets文件夹下的文件标记为不压缩)
2. 在安卓系统上可以直接被读取
害羞的波浪线:
(一个小技巧)
在Unity中,凡是以~为后缀的文件或文件夹,都是会直接被无视跳过的,不会被导入工程
这个小技巧在做工程管理的时候比较有用,比如某些文件夹在某些场合下不想用到,这个时候直接改名加后缀即可
AssetBundle
AssetBundle的原理:
AssetBundle其实就是一个压缩包
既然是一个压缩包,那直接用文件不行吗?是可以的,但是AssetBundle包含了资源文件依赖关系、还有一些文件查重等功能
可以做到跨平台,对应不同平台可以打出对应的包
可以做出快速索引
本质上是Unity的一套虚拟文件系统
既然是一个压缩包,那就可以分成两部分
体:被压缩的内容
头:对应的一些摘要信息
加载一个AB包的时候,头会被立刻加载,而里面的内容(Asset资源本身)是按需加载的,使用到的时候才会被加载入内存
AssetBundle的参数:
BuildPipeline.BuildAssetBundles(path, BuildAssetBundleOptions, BuildTarget)
BuildAssetBundleOptions:
BuildAssetBundleOptions.ChunkBasedCompression: 以chunk-based LZ4进行包体的压缩,在LZ4的基础上做了一些改良
BuildAssetBundleOptions.DisableWriteTypeTree: 可减少AB包体大小,同时减小使用的内存大小,和加载AB包的使用时间
BuildAssetBundleOptions.DisableLoadAssetByFileName | DisableLoadAssetByFileNameWithExtension
在加载AB包时,AssetBundle.LoadFromFile(Path.Combine(Application.streamingAssetsPath, "cubebundle")
可以传入路径/包名,也可以只写包名,或加上扩展名,但是是有代价的,在写入的时候是需要加上哈希的,所以在寻找的时候会耗费更多的cpu时间与内存开销。如果确定加载方式是存路径加载的话,就可以把这个哈希寻找关闭掉
做个小实验:创建一个简单场景,场景中只有一个cube
ChunkBasedCompression: 包体大小85KB
ChunkBasedCompression | DisableWriteTypeTree: 包体大小73KB (一个简单Cube的typetree就占了有12KB)
ChunkBasedCompression | DisableLoadAssetByFileName: 包体大小仍为85KB,因为小场景中只有一个AB包,所以差别不大,而且这个选项更侧重的是内存和CPU上的消耗
另一个小实验:对应以上不同打包方式,进行AB包的加载,并使用Profiler进行性能消耗的查看
Build成可执行文件后运行,连接上Editor中的Profiler,点击按钮进行AB包的加载,并在Profiler中TakeSample,
查看SerializedFiles与其下的archive所对应的Memory值(是AB包头的大小)
ChunkBasedCompression: 273.5KB
ChunkBasedComprerssion | DisableWriteTypeTree: 206.4KB (打出的包相差了67KB,就一个简单的cube对应的typetree就有这么大)
不过一个cube所关联的有许多资源,比如material texture等等,这些都是需要被进行打包的
AssetBundle的识别:
有些人会去算AssetBundle打出来的包的MD5值,这种方式是不推荐的,因为在Unity打包的过程中并不是稳定的,有可能导致两次打出来的AB包的内容即使是一致的,但是Binary是有差异的。
那怎么识别呢?算打包之前的
可以算Library里的文件;可以算打包前的文件本体以及文件对应的meta的哈希值
图方便的话也可以直接使用Unity打包出来的AB包对应的.manifest文件里对应的值
AssetBundle的策略:
不要走极端,AB包过大过小都不好。
官方推荐大小为
需要经过网上下载的AB包(比如手游资源)1MB~2MB一个包
本地的包的5MB~10MB一个包,不超过10MB
过大缺点:下载慢
过小缺点:每个AB包内的资源很少,但是头文件大小相对应的会变大,且导致加载到内存后的有效数据变少,很多为头文件信息
Asset的加载及管理:
编辑器内和运行时的加载机制不同:
因为在Editor中,unity会优先保证使用的流畅度,并且基本上都是在资源充裕的电脑上运行的,因此会尽量把许多资源都提前加载好,甚至会加载一些额外数据以方便并加速编辑和制作过程
在Runtime时,unity遵循按需加载的加载规则,尽量减少目标设备上内存和cpu的使用
-- 不要用Editor期的Profiler去作为最终的衡量标准,一定要去profiler真机
序列化与反序列化:
两个场景:
场景1中有三个Cube gameobject
场景2中有三个来自于同一个Cube Prefab的gameobject
将场景文件.unity用文本方式打开,可以发现
场景1对应的文件大小会大于场景2对应的文件大小
打开会看到,场景1中每一个gameobject都会存储对应的信息
而场景2中的gameobject会对应到同一个prefab里
这就导致了unity在加载场景1时,会有更多的时间开销和内存开销
在加载场景2时,会优先将使用到的prefab解析出来,并且让场景中对应的游戏物体gameobject的引用指向这一块内存
而在加载场景1时,会认为这三个实际上一样的gameobject是不同的,因此会解析3次
-- 结论,能用到prefab的地方尽量用prefab
TypeTree:
上面提到这个数据会使得AB包体变大许多,那它的作用是什么呢?
为了Unity的跨版本时做兼容的
找一个meta文件查看
可以看到serializedVersion:6字段,表示当前格式之前,Unity至少改了5次数据格式
Unity在打AB包的时候,如果开着TypeTree,则首先第一步会遍历所有的文件,并把对应的数据内容的字段先写一遍
比如上图的defaultSettings中,在6这个版本里会把字段loadType, sampleRateSetting, sampleRateOverride等先写一遍
然后在第二遍里再去写字段对应的值
在读取的时候,如果当前Unity版本不同了,serializedVersion比如说是5,那么则会根据version5的格式进行反向解析
先开始解析TypeTree,发现里面的loadType不认识,是version5中没有的字段,这时候这个字段就会被跳过而不去解析
如果解析TypeTree时发现应该有的一项aabb在TypeTree里没有找到,则会用默认值去填充该字段
好处:Unity通过TypeTree,实现了跨版本的兼容性
缺点:如果在打包的时候使用了TypeTree,
AB包中会额外增加TypeTree的信息(存储);
而且在加载的时候消耗cpu时间去额外遍历TypeTree(cpu);
并在内存中存储了TypeTree的数据结构(内存)
结论:当确认Unity版本一致时,比如打的apk和ab包都是2019.1.1版本打出来的,此时关闭typetree即可
-- 绝大部分项目都可以关闭,除非需要做跨版本兼容
同步与异步:
什么时候选用更多的是策略,而没有哪一种更好
同步意味着更快,在那一帧内,主线程所有的CPU全部都可以使用;
但是同时,可能造成主线程卡顿
异步的最大优点是主线程可以保持尽量不卡顿;
但是异步永远至少比同步慢一帧 -- 这一帧发起的异步,最快也得等到下一帧才会开始执行
异步需要一些额外的逻辑,在保证没有加载完之前,会进行一些对应情况的处理
还有一种情况是可以手动分帧进行同步的处理
但是,异步和同步混合使用的时候,会导致大问题:Preload与Presistent问题
Preload与Presistent:
Unity引擎内部,有两个模块是主要负责加载工作的:PreloadManager和PresistentManager
PreloadManager负责调度任务,PresistentMnanager负责把数据从硬盘读取到内存中,同时给这块数据分配一个ID
当上层有一个任务下来,形成一个option,这个option会给到PreloadManager;
在PreloadManager中有一个队列,每一帧会从这个队列中取出一个任务(opt)去执行;
在执行opt的过程中,会使用到PresistantManager。
上面说到异步和同步混合使用会导致的问题就是这么来的
当preloadManager加载了异步的任务,而下一帧加载了同步的任务,这时异步的任务也在跑,这时同步任务和异步任务会去抢着使用PresistentManager;而PresistentManager分配ID等等的操作是阻断线程的,一次只能对应操作同一块内存,对应一个ID,这时候就会被block掉(异步工作可能会被同步工作阻断,同步工作也可能被异步工作阻断)
-- 但是在2020版本中的Unity解决了这个问题
两个任务都需要分配ID时,需要分先后
Asset的卸载:
UnloadUnusedAssets:
这个和加载一样,是归PreloadManager管理的
unity在一次load的开始阶段,就已经确定了哪一些资源是需要被load的,但是如果在load的过程中又发生了unload操作,那么会发生一些已经确定了要用的asset而且已经load了却被unload卸载掉,最终导致出错
-- 因此UnloadUnusedAssets是一个同步的方法,所以会造成卡顿
而Unity在切换scene的过程中,会自动调用一次UnloadUnusedAssets。
AssetBundle.Unload()
这个不归PreloadManager管理
它会遍历当前加载过的资源,并进行unload;
如果是Unload(true),则会把AssetBundle本身和加载了的相关Asset一起卸载掉;在不合适的时机,是会导致Runtime错误的
如果是Unload(false),则只是把AssetBundle卸载掉;而这个会导致当再次加载该AB包的时候,一些asset可能会在内存中存在两份,因为在当把AssetBundle卸载掉的时候,AB包与对应的asset之间的关系也消失了
在Unity内部,很多时候Asset并不是大家想的是有reference的,而是靠的遍历
这个正在解决,可以看看新的AddressableAsset