Unity官方直播--Unity Asset的一生

Unity Asset的一生

https://unity.cn/projects/zhi-bo-yu-gao-unityzi-shen-ji-zhu-zhuan-jia-gao-chuan-wei-nin-xiang-jie-unity-assetde-yi-sheng

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

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

原文地址:https://www.cnblogs.com/FudgeBear/p/13416080.html