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