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

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM