1. 程式人生 > 實用技巧 >2020年度大賞 | UWA問答精選

2020年度大賞 | UWA問答精選

UWA每週推送的知識型欄目《厚積薄發 | 技術分享》已經伴隨大家走過了252個工作周。精選了2020年十大精彩問答分享給大家,期待2021年UWA問答繼續有您的陪伴。

UWA 問答社群:answer.uwa4d.com
UWA QQ群2:793972859(原群已滿員)


Q1:IL2CPP的記憶體問題

最近看問答上面有個關於IL2CPP和Mono的對比,看到IL2CPP記憶體衝高會下降。關於這個,我問了Unity的官方技術,回答是:你好,Unity有自己的GC機制,為了避免頻繁向作業系統申請/釋放記憶體,Reserved Mono值會保持在一定區間內,達到某些條件或在某些特殊情況才會觸發GC。 有人說是:“記憶體池管理邏輯都是一樣的,屬於上層管理一樣。它們只是中間語言不一樣而已,也是隻漲不降。”也有其他大佬說是IL2CPP衝高會下降。現在很困惑,求解答。

A:看下來題主說的記憶體衝高不降,涉及兩個指標,一個是Profiler裡的Reserved Mono,一個是裝置記憶體(PSS)。目前確實沒有權威的文件說明這一點,所以下面通過真實資料來說明一下。

先說第一個(Reserved Mono)。

  1. 在Script Backend是Mono的情況下,如果選擇的是舊版本里的Mono 2.x,或者新版本里的 .Net 3.5(Runtime Version),那麼這個值是隻升不降的。比如這個資料,Unused已經很高了,但也不會下降:

  2. 同樣在Script Backend是Mono的情況下,如果選擇的是.Net 4.x (Runtime Version),那麼這個值是可以下降的(但不確定具體是從哪個版本開始的)。比如這個資料,可以看出雖然會下降,也並不是頻繁執行下降操作的:

  3. 最後Script Backend是IL2CPP的情況下,那麼這個值也是可以下降的。比如這個資料,看上去和上面的情況相差不是太大:

而對於第二個,裝置記憶體。這個就和安卓系統的記憶體管理機制有關了,即使Unity把Reserved Mono降低了,減少了自身的記憶體佔用,系統也不一定會立即會把這塊記憶體釋放,所以這裡的行為就很難說清楚了。

該回答由UWA提供


Q2:載入配置記憶體過大問題

配置表太多佔用記憶體過大時,除了採用Sqlite,還有什麼好的解決辦法沒有,有沒有大佬能否指點下。FlatBuffer不用全部進記憶體嗎?如果不全部進記憶體,訪問速度如何呢?

A1:第一問題參考如下:

  1. 可以針對重複資料進行剔除,尤其是一些字串的配置。在配置匯出時把這樣的資料提取一份,其他用到的地方只是引用,會節省不少。
  2. 資料型別要合理。
  3. 可以使用類似FlatBuffer/ZeroFormatter的延遲載入的思路,在真正使用時再去反序列化。一次遊戲過程中實際用到的配置量比較有限,使用這種策略可以儘可能的減少不必要資料的載入。

第二個問題參考如下:
我們上個專案也是到後期優化時遇到類似問題,只是參考了這種思路,並沒有進行完全替換。我們當時在打包時,會對配置以行為單位,進行Offset和Length的計算,在Runtime階段,初始載入只會載入每行的ID,對應的這一行的Offset和Length,然後後續邏輯呼叫配置表介面拿資料的時候,如果發現沒有反序列化過,就根據Offset和Length再去構建一下相應的資料提供給上層。訪問速度的話肯定不如開始直接全部載入好,但我們測下來影響不大。

感謝範君@UWA問答社群提供了回答

A2:字串吃記憶體不說了,儘量少用或者複用。表格中比較多的會是那種:攻擊-1000;防禦-2000;血量-3000,每個int都是4個位元組,數量多了會頂不住。這種可以考慮用一個int32/int64/uint32/uint64 去存多個數值。

感謝蕭小俊@UWA問答社群提供了回答


Q3:Instruments如何看Mono記憶體分配

例如在分配了一個10MB陣列,對應在Unity Profiler中會看到開闢了至少10MB大小的Mono記憶體。

那麼在Instruments中,如何檢視分配的記憶體資訊呢?Allocations中的資訊是此程序中分配的所有記憶體資訊嗎,嘗試分配過100MB記憶體,Allocations中的統計沒有任何增長。

A:我這邊也做了測試:

建立了100MB大小的int陣列,Size實際應該是400MB。

然後到Profiler觀察:


可以看到ManagedHeap正確分配了這400MB的空間。

然後打包iOS後到xCode執行,執行前首先吧Run這個Scheme的Malloc Stack勾上:

Run以後點選Memory並匯出Memory Graph來觀察:

由於應用程式的記憶體都是在VirtualMemory空間分配的,因此檢視VM Regions的VM_ALLOCATE部分。

於是就可已發現128X3+16剛好400MB的分配。
呼叫堆疊也很好確定:

正式我們的測試程式碼。

然後我們來看Instruments。
首先是Allocations部分,有一點要注意,該欄的下部有一些選項:

注意最後一個選項,如果選擇第一個:
All Heap & Anonymous VM,All Heap對應App實際分配的物理空間,不包含VM,

Anonymous VM的官方解釋是:
interesting VM regions such as graphics- and Core Data-related. Hides mapped files, dylibs, and some large reserved VM regions。

因此一些比較大的預留分配空間是不會顯示的。
將這個選項切換為All VM Regions,就能看到分配的400MB了:

並且右邊詳情頁面也正確顯示了呼叫堆疊:

另外我們還可以從VM Tracker來觀察,開啟VMTracker的Snapshots:

於是就能看到這400MB的詳細分配資訊:

可以發現,Virutal Size略大於400MB,因為程式其他部分也要申請一些記憶體。而這400MB又分別儲存在Resident和Swapped內,其中Resident部分又基本等於Dirty Size,說明這部分大小的空間被標記了Dirty是不能被交換出去的,剩下240MB左右空間是Clean空間,可以暫時被交換出去以保證有足夠的物理空間能使用。這也是因為我們只是申請了這部分空間,並沒有進行具體的賦值初始化和使用。

那如果賦值使用了呢?修改程式碼測試:

執行Instruments後再觀察:

可以清楚的發現這400MB都在Dirty Size內。這種情況真正會給該App和iOS以記憶體壓力。

推薦閱讀:
《寫給Unity開發者的iOS記憶體除錯指南》
《Understanding iOS Memory (WiP)》

感謝黃程@UWA問答社群提供了回答


Q4:URP關於多個攝相機的效能優化

URP7.4.3,除開主相機外,還有一個子相機,用於將照到的模型渲到遊戲主介面UI上,在Profiler中看到以下情況:

可以看到,在子相機中也進行了包括對LOD的計算,但子相機的Cullingmask只開了一個名為RTModel的Layer,在這一層裡只有一個3D物件。按說子相機CullScriptable這塊開銷不應該有才對。

目前懷疑可能的原因是URP會對每個Base Camera都進行這部分的計算,但如果用Overlay相機,又無法用原來的方式將相機的targetTexture渲到一張RawImage上了,有人遇到過麼?

A:題主的疑惑是:子相機的CullScriptable這塊的開銷不應該有那麼大對吧(畢竟只有一個物件)?

這裡有兩個問題:

  1. Culling到底做了什麼,只有一個物件為什麼要Culling那麼久(難道只有一個物件也要做很多的準備工作)?
  2. 在Profiler裡面看到的資料真的是真實資料嗎?也就是說,子相機的Culling真的做了1.68ms嗎?

拋開這兩個問題,也可以有更好的做法:
我們一共兩個相機,主相機和UI相機,那麼UI上顯示的3D物件怎麼辦呢?
我們有個虛擬相機,所謂相機,其實就是做一個VP矩陣,做一個RT,繪製可見的物件就可以了。使用Unity的SRP,隨機選一個地方,設定VP矩陣,設定RT,接著繪製指定的物件(UI中所有的3D物件都會掛在這個物件下面),然後這個RT就可以隨意使用了。

假如一個UI上有兩個3D物件,儘量都放在一個RT上;如果不行,就放在兩個或更多的RT上,只是會多幾個繪製命令。幾個RT(還不需要是全屏的),而且會多幾個Swap RT的操作。由於我們專案沒需求需要若干RT,所以假設一下,在這種需要若干RT的情況下,也可以用一個RT加多個Viewport來解決的。這個程式碼都是現成的,參考一下Cascade Shadow Map的做法,這樣Swap RT也就省了。

綜上所述,既然你都知道自己要繪製什麼,就不要給Unity Culling的機會了。

在Development Build中連真機看到的效能資料,是真實資料嗎?目前在使用類似於HLOD的方式來減少掉這個LOD的巨大開銷。樓上說的“設定一下VP矩陣,設定RT”,還不太清楚這個VP矩陣的操作具體是個什麼,可否詳解下或者推薦些相關資料?

A:你提的HLOD和LOD和上面的Culling沒關係。VP矩陣就是view矩陣和projection矩陣。相機的作用就是提供這倆矩陣的。

如果你在管線裡面設定了相應的矩陣,然後繪製指定的物件,就可以完全不用多一個相機,畢竟多一個相機就多一個Culling。

如果你對VP矩陣不熟悉,不清楚怎麼實現,也簡單。依然用一個額外相機,關上這個相機的Culling,然後在渲染pass中,不要繪製cullingresult.visibleobject,而直接用Graphics.DrawMesh或者CommandBuffer.DrawMesh繪製你要顯示的那個3D Object的物件就好了。

感謝王爍@UWA問答社群提供了回答


Q5:關於_CameraDepthTexture的疑惑

如果開啟_CameraDepthTexture,Camera就需要渲染一遍場景內所有帶有ShadowCaster的可見物體的Pass來實現深度圖。

但是場景中的物體在開啟ZWrite的時候就把深度寫進了Depth Buffer中了,直接獲得這個Depth Buffer是不是比近乎DrawCall翻倍的方式更有效率呢?還是Unity在這方面有什麼考慮?

另外,問一個更實際的問題:
我們的專案需要渲染場景的中湖水的深度效果,所有不透明的場景物體的材質都是關聯同樣一個Shader,這個Shader是帶有ShadowCaster的。但是隻有個別插入水中的物體需要去渲染ShadowCaster的Pass,有沒有方法在不增加Shader的情況下,讓沒有插入水裡的物體不渲染Shadow Caster Pass呢?我們用的是Built-in的渲染管線。

A1:第一個問題,可以參考這個問題中Unity官方人員的回覆。
裡面講了兩個原因,第一是對於非全屏渲染的情況,本來是想拿對應相機渲染的深度,但是Depth Buffer是全屏的。第二個原因是因為很多平臺不支援直接拿Depth Buffer的資料。

參考網頁:
https://forum.unity.com/threads/poor-performance-of-updatedepthtexture-why-is-it-even-needed.197455/

另外查FrameBufferFetch相關問題的時候看到Unity論壇上另外一個貼子裡面的回答。裡面說到Unity支援了FrameBufferFetch,但是不支援DepthBuffer的獲取。

參考網頁:
https://forum.unity.com/threads/pixel-local-storage-and-frame-buffer-fetch-on-mobile-devices.604186/

第二個問題,如果不增加Shader,目前沒想到其他好的方法。
如果可以增加Shader,可以將原來的Shader複製一份,只在ShadowCaster的部分加一個“NeedDepth”這樣的Tag,將水下的物體的材質球換成這個Shader,另外做一個只有ShadowCaster並帶有“NeedDepth”這個Tag的Shader,這個Shader用來做Replace操作。

額外增加一個Camera,這個Camera跟隨主相機,或者作為主相機的子節點,建立一個RT,讓這個Camera渲染到這個RT,在Update裡面使用ReplaceShader去畫一下,那麼只有有那個Tag的ShadowCaster會進行深度渲染,後續可以對這個RT進行編碼等操作,這個RT記錄的就是水下物體的深度。整個過程看上去沒有特別多的額外工作,覺得可以一試(我沒有做過測試,但理論上是可行的)。

感謝Xuan@UWA問答社群提供了回答

A2:最近自己試著升級專案到URP,發現Game View的湖水深度效果沒有了,Scene View的是正常的,做了很多實驗發現了兩個現象。

我之前的湖水Shader的Queue是Geometry + 150,保證自己在其他不透明物體之後渲染。別的物體有Shadow Caster的Pass,湖水沒有,這樣在別的不透明物體渲染完成後自己能直接用到正確的_CameraDepthTexture。但是在URP下我必須將Render Queue設定到Transparent層才有正確效果。

發現勾選了MSAA抗鋸齒後,就有跟Scene View一樣正確的深度效果了。

URP預設優先使用Copy的方式在所有不透明Queue的物體渲染完後把深度Copy到_CameraDepthTexture上,我的湖水Queue設定在不透明層了,即使是最後渲染的,他的深度也進入了深度圖中,因此效果沒了。勾選MSAA會正常是因為MSAA會影響管線無法用Copy的方式把深度圖拷出來(需要Resolve解析),所以URP預設在這種情況下使用老方式通過渲染Depth Only (原Shadow Caster)的Pass得到深度圖,因此回到了老方式,我的Shader就又起效果了。

其實自己如果當時看到Framedebugger的時候,是用心讀的而不是隻是草草看一眼就花心思在自認為的問題原因上會更容易得到答案。

感謝題主安日天@UWA問答社群提供了回答


Q6:渲染大面積草地時,如何降低消耗

請問下大家,渲染大面積草地時,如何降低消耗呢?

A1:回答如下:

  1. 使用DrawMeshInstance;
  2. 上面這個API是不會進行視距剔除、視錐體剔除和遮擋剔除的。

下面有兩種方案:
a. 將草地按區域分組,用每組的中心點計算視距,依據距離切換網格LOD或剔除;還能用向量點乘簡單剔除在相機後方的草地(注意臨界問題)。
b. 藉助CullingGroup。
CullingGroup.onStateChanged事件繫結,通過事件觸發調整傳入;DrawMeshInstanced的Matrix順序和渲染數量(但是DrawMeshInstanced只能指定渲染前幾個Matrix);
通過cullingGroup.SetBoundingSpheres實現視錐體剔除和遮擋剔除;
通過cullingGroup.SetBoundingDistances實現視距剔除和LOD。
這個方案最好也進行區域分組,不然CullingGroup的事件監聽佔用會比較高,在中端機上4000個監聽會佔約2ms的大小。

以後如果有對比兩種方案的效能,我再進行補充。

附:

  1. 《CullingGroup API的使用說明》
  2. 《Unity 3D研究院之Lightmap支援GPU Instancing》
  3. 《如何高效使用GPU Instancing技術來進行草叢渲染》
  4. 升級Unity 2018,DrawMeshInstanced不生效的問題

感謝題主李先生@UWA問答社群提供了回答

A2:使用Indirect模式的Instancing,配合Compute Shader實現視錐剔除和遮擋剔除。

感謝鄒春毅@UWA問答社群提供了回答

A3:推薦一個使用URP製作的草海效果,親測可在Mobile端使用。
Unity URP Mobile Draw Mesh Instanced Indirect Example效能測試:

  • can handle 10 million instances on Samsung Galaxy A70 (GPU =
    adreno612, not a strong GPU), 50~60fps, performance mainly affected
    by visible grass count on screen(draw distance = 125)
  • can handle 10 million instances on Lenovo S5 (GPU = adreno506, a weak GPU), 30fps, performance mainly affected by visible grass count on screen(draw distance = 75)

感謝Vest@UWA問答社群提供了回答


Q7:Packages目錄下的Shader打包AssetBundle

Unity引入了Package Manager來進行管理外掛管理,例如URP引入Packages之後會有目錄Packages/[email protected]。請教一下各位,如何對Packages目錄下的資源進行AssetBundle打包?

例如,工程目錄中有材質球引用到URP的Shader,那麼材質球打成AssetBundle之後會將Shader包含進去,會有Shader解析耗時。

A1:我這邊是隻使用SBP而不用Addressable,這樣通過使用AssetBundleBuild是可以將Packages中的資源也打包成AssetBundle的。

將所有依賴到的Shader(包括Packages中的)都使用AssetBundleBuild設定到同一個shader.bundle的,打包後也解包確認了,Packages中的Shader也打包在shader.bundle而不會被包含在材質AssetBundle中。

感謝黃曉文@UWA問答社群提供了回答

A2:我在嘗試將現有專案轉成URP的時候,遇到和Addressable系統有些不相容問題。
在打包引用了URP的Shader的Material時會發生Shader被重複打包現象。
如果想把URP的Shader單獨打包,又會發現因為不在Assets目錄內,Addressable管不到的問題。

我的解決方案是將用到的URP的Shader拷出來,放到Assets目錄下通用Shader目錄。
當然需要將該Shader改名,並且要注意將內部引用的Shader也一併拷出管理。

不過一般專案中使用的Shader往往還是會自己編寫,直接使用官方提供總會遇到這種那種問題。因此我也會考慮儘量不用官方預設Shader,這時對於URP而言自然更加需要將Shader拷出來進行改造了。

感謝黃程@UWA問答社群提供了回答

A3:經過 黃曉文 的思路,已經解決。
打包AssetBundle最重要的,就是指定資源Path的源路徑,以及去往的目的AssetBundle地址,這個問題關鍵是需要知道資源在Packages中的源路徑。

例如一個Packages下的Shader資源,Lit.shader,通過AssetDatabase.GetAssetPath可以發現路徑是:Packages/com.unity.render-pipelines.universal/Shaders/Lit.shader,這個是正確的路徑,用它即可。

而錯誤的路徑分別是:

  1. Unity中看到的:Packages/Universal RP/Shaders/Lit.shader 錯誤。
  2. 在檔案目錄中看到的:Packages/com.unity.render-pipelines.universal@7.3.1/Shaders/Lit.shader錯誤

所以得出結論:Packages 下的資源打包,去除一下 @x.y.z 即可。

感謝題主一刀@UWA問答社群提供了回答

A4:可以試試使用Scriptableobject或Material引用到Shader檔案,然後把ScriptableObject或Material打到AssetBundle裡。

感謝上午八點@UWA問答社群提供了回答


Q8:Shared UI Mesh記憶體佔用過高

快取池中的UI如果不隱藏,Shared UI Mesh會比較高;如果隱藏,Shared UI Mesh會比較低,但是UI SetActive又有效能消耗,該如何權衡呢?

隱藏快取池中的UI時,Shared UI Mesh記憶體佔用:

不隱藏快取池UI時Shared UI Mesh記憶體佔用:

A1:Shared UI Mesh是源自UGUI框架中的一個靜態全域性變數Graphic.workerMesh:

而workerMesh主要在以下程式碼中使用:

該函式是在Rebuild單個UI元素的頂點資訊,紅框裡的FillMesh就是將更新後的頂點屬性陣列設定到workerMesh上,且每次呼叫都會先進行Clear操作。

看邏輯,這個workerMesh的記憶體大小應該只和單個UI元素的頂點量有關,但實際測試下來,是和當前所有啟用UI元素的頂點總量相關的。

所以,Shared UI Mesh很大,表示當前所有啟用UI元素的頂點總量很高。需要對部分複雜元素進行簡化。

常見的複雜元素有:

  1. Tiled模式的Image:該模式下會根據UI元素的區域和紋理解析度的大小,自動生成適當數量的四邊形,一旦紋理解析度很小,而區域很大時,就會產生大量的頂點。
  2. Outline效果的Text:Outline效果會將Text文字原來的頂點數放大為5倍。
  3. RichText,且包含了較多樣式的Text:樣式標籤部分也會產生頂點數。
    注:2和3同時使用時,樣式標籤部分的頂點數也會放大。

定位的方法:

  1. 初步定位:直接在SceneView下換到線框模式,肉眼找一下複雜元素;
  2. 通過Profiler的UI面板,檢視各個Canvas下各個Batch產生的頂點數,並檢查對應的GameObject即可。

需要注意的是,Canvas元件被禁用的情況下,Profiler裡是看不到的,但其下的啟用UI元素依然會影響Shared UI Mesh的大小。

該回答由UWA提供

A2:如果只有SetActive才能降低Shared UI Mesh,好像就沒有其他選擇了;但是如果切換layer可以降低,可以選擇該辦法。

感謝青麈@UWA問答社群提供了回答

A3:也可以試試把Canvas的Enable設定為False。

感謝Crazy_Liu@UWA問答社群提供了回答


Q9:Addressable如何刪除舊資源

目前計劃使用Addressable來實現資源熱更新,實際真機測試發現當資源更新後,舊的資源Addressable並不會把它刪除,同時可以看到App佔用的資料檔案會越來越大。請問有什麼辦法可以把指定的Group或Label的資源刪除嗎?

試了Addressable.ClearDependencyCacheAsync也不行。實際測試這個介面只能刪除最新版本的資源。當本地已經是最新版本資源時這個介面確實有效;但是如果本地需要更新資源時,這個介面應該也是嘗試去刪除最新資源,然而本地並沒有最新版的資源,所以大概就無效了。

A:呼叫Addressable.ClearDependencyCacheAsync實質是呼叫了 “Caching.ClearAllCachedVersions();”。事實上是使用了Unity的Caching系統。

在Windows編輯器環境測試了一下。
Caching的目錄為“C:\Users\UserName\AppData\LocalLow\Unity\ProjectFolder”,當正常下載AssetBundle以後,該目錄內就出現 “stage01_298bd883434eedb69ea7316cb23e0b0d\662ab7a0d2aa99bc7a2dbb7baec63872” 之類的目錄,並儲存著當前的AssetBundle版本,當更新AssetBundle並執行下載以後,該目錄也會出現其他AssetBundle的Caching目錄。

在執行下載之前,先執行了一下“Caching.ClearCache();”,這時會發現Caching目錄內已經被清空,所有版本的AssetBundle都沒有了。下載完成後,該目錄只保留了最新的AssetBundle資源。由此可推,即使不通過Addressable系統,仍然可以通過Caching把所有的資源都清理掉。

於是繼續進行第二個實驗,連續更新幾次AssetBundle以後,Caching目錄內已經有多個版本的AssetBundle目錄了,當有新的更新後執行 “Addressables.ClearDependencyCacheAsync(key);”,發現的確並沒有將舊版本的AssetBundle都刪除。因為“Caching.ClearAllCachedVersions”的引數是對應的AssetBundle名字,而Addressables的管理AssetBundle包名是帶Hash的,因為每個版本的AssetBundle檔名都不一樣的,Caching系統也就無法分辨了。

繼續做實驗,將打包名字去掉Hash,Caching目錄內的AssetBundle目錄名也不帶Hash了,然後連續更新幾個版本後發現,該AssetBundle目錄內多了幾個不同Hash版本的目錄,內部才是真正的AssetBundle。於是走“Addressables.ClearDependencyCacheAsync(key)”,這時就能正確地刪除舊版本,然後再更新新版本了。

確實不勾選Hash打包可以成功刪除了,這種方式貌似就是覆蓋式的打包,不知道會不會有其他隱患,目前來看夠用。

A:隱患就是如果按照Label來做更新檢查,本來可以只下載差異部分,但是因為同樣使用Label做清除Caching的工作就會造成重複下載原本不必要更新的部分。於是就需要遍歷所有的Location然後去檢查更新,並將有更新的AssetBundle放入列表,然後再依次清除舊快取,重新下載。這樣就和傳統方案沒太大區別了。

請問下不勾選Hash其實就不用清除了吧?名字一樣不是會直接覆蓋嗎?

A:不勾選Hash,只是在Cache的目錄內第一級資源同名子目錄是一致的,但是裡面儲存具體資料的子目錄是遞增的,因為有不同版本。每個版本都會有一個子目錄。這個是Caching系統管理的。

如果不勾選Hash,CDN有可能不會更新檔案,所以要結合自己的專案使用的CDN情況來確定如何管理這塊。

我是用Addressables.ClearDependencyCacheAsync(key)並沒有清除Cache多次更新後越來越多。上面所說的“將打包名字去掉Hash”,是指配置中Bundle Naming選擇Fliename選項嘛?所使用的Key是指本次更新的列表嗎?

A:就是“配置中Bundle Naming選擇Fliename”,這個Key其實是應該是這個Group名字,對應到打包後的Bundle名字,讓Caching系統搜尋。貌似1.15.X後面這塊有一些更新,還沒確認過。

感謝黃程@UWA問答社群提供了回答

A:在論壇裡看到一個方案(11樓),修改了Addressables.ClearDependencyCacheAsync(key)的實現:

首先在Addressables.ClearDependencyCacheForKey中對當前使用的資源進行Cache標記,然後在Addressables.ClearDependencyCacheAsync裡清除遊戲執行之後未使用的資源。

修改AddressablesImpl.cs檔案中的以下四個方法:

  • ClearDependencyCacheForKey(object key)
  • ClearDependencyCacheAsync(object key)
  • ClearDependencyCacheAsync(IList locations)
  • ClearDependencyCacheAsync(IList keys)

需要注意呼叫Addressables.ClearDependencyCacheAsync的時機。

        internal void ClearDependencyCacheForKey(object key)
        {
#if ENABLE_CACHING
            IList<IResourceLocation> locations;
            if (key is IResourceLocation && (key as IResourceLocation).HasDependencies)
            {
                foreach (var dep in (key as IResourceLocation).Dependencies)
                    Caching.ClearAllCachedVersions(Path.GetFileName(dep.InternalId));
            }
            else if (GetResourceLocations(key, typeof(object), out locations))
            {
                foreach (var loc in locations)
                {
                    if (loc.HasDependencies)
                    {
              foreach (var dep in loc.Dependencies){
              // added by Lukas
                            AssetBundleRequestOptions options;
                            if ((options = dep.Data as AssetBundleRequestOptions) != null)
                            {
                   //對當前依賴資源進行標記
                                Caching.MarkAsUsed(dep.InternalId, Hash128.Parse(options.Hash));
                            }
                            //原方法無法刪除舊版本的ab
              //Caching.ClearAllCachedVersions(Path.GetFileName(dep.InternalId));
                        }   
                    }
                }
            }
#endif
        }

  

        public AsyncOperationHandle<bool> ClearDependencyCacheAsync(object key)
        {
            if (ShouldChainRequest)
                return ResourceManager.CreateChainOperation(ChainOperation, op => ClearDependencyCacheAsync(key));

            ClearDependencyCacheForKey(key);
            // added to ClearCache 
        Caching.ClearCache((int) Time.realtimeSinceStartup + 10);
            var completedOp = ResourceManager.CreateCompletedOperation(true, string.Empty);
            Release(completedOp);
            return completedOp;
        }

  

感謝小魔女紗代醬@UWA問答社群提供了回答

A:我們用的是覆蓋式更新的流程(不是增量更新)。Addressables版本是1.15.1。

在把Bundle Naming設定為Filename後發現,在Caching中的AssetBundle目錄還是帶有Hash值的,這個和樓上的解釋不一致,不知道是不是版本的原因。

圖中可看到AssetBundle包名已經是Group的名字了,但是下載到Caching中還是有 Hash。

然後我們是這麼解決的。還是開啟檔名的Hash,將Caching中的AssetBundle資料夾名儲存到PlayerPrefs中,當檢測到有下載的時候,讀出PlayerPrefs中的值,把舊的對應AssetBundle包刪除,並更新PlayerPrefs。

獲取當前Catalog中所有AssetBundle資料夾名的方法,是從Addressables中複製出來的。

    // 獲得當前catalog中所有 assetbundle 儲存的資料夾名
    // 這個函式中引用到的方法沒有列出,可以去 addressables 中原始碼中找 
    // 示例:CollectBundleNames(new string[]{ "SkllIcons", "ItemIcons", "AvatarIcons" })
    private static List<string> CollectBundleNames(object[] keys)
    {
        List<string> result = new List<string>();
#if ENABLE_CACHING
        foreach(var key in keys)
        {
            IList<IResourceLocation> locations;
            if (key is IResourceLocation resourceLocation && resourceLocation.HasDependencies)
            {
                foreach (var dep in resourceLocation.Dependencies)
                {
                    if (dep.Data is AssetBundleRequestOptions options)
                    {
                        result.Add(options.BundleName);
                    }
                }
            }
            else if (GetResourceLocations(key, typeof(object), out locations))
            {
                var deps = GatherDependenciesFromLocations(locations);
                foreach (var dep in deps)
                {
                    if (dep.Data is AssetBundleRequestOptions options)
                    {
                        result.Add(options.BundleName);
                    }
                }
            }
        }
#endif
        return result;
    }

  

刪除AssetBundle包資料夾的方法:

// 這裡的 bundleName 就是 CollectBundleNames 的返回值
private static void ClearCacheForBundle(string bundleName)
    {
        List<Hash128> hashList = new List<Hash128>();
        Caching.GetCachedVersions(bundleName, hashList);
        foreach (Hash128 hash in hashList)
        {
            Caching.ClearCachedVersion(bundleName, hash);
        }
    }

  

需要注意的是呼叫CollectBundleNames的時機,如果已經更新了Catalog,那麼返回的是即將要寫入Caching中的AssetBundle資料夾名。如果要得到當前AssetBundle資料夾名,要在更新Catalog之前呼叫。

感謝jim@UWA問答社群提供了回答


Q10:LuaJIT效能熱點函式優化

專案中的這個函式耗時非常嚴重,有什麼優化的方法嗎?

A1:這個是table.get,對應獲取欄位或者訪問陣列時呼叫的函式:

  1. 優先使用連續陣列而不是英文名欄位,可以顯著提升訪問效率並降低記憶體消耗,很多團隊喜歡使用Class的寫法,可以這樣改造:
    local obj = ClassA.New()
    obj.abc = 1
    obj.cde = "test"
    變成:
    local obj = ClassA.New()
    obj1= 1
    obj2= "test"
    這個方法可以針對使用頻率較高的程式碼進行改造。

  2. 自己開發工具,在編譯Lua之前,將Lua程式碼中的常量從英文名變數轉換為數值,這個可以結合1使用,就可以在開發期寫英文名欄位名,然後編譯時轉換為陣列。

感謝招文勇@UWA問答社群提供了回答

A2:字串應該是雜湊值計算的消耗,這樣的開銷應該是很頻繁地呼叫了。

感謝王歡@UWA問答社群提供了回答

今天的分享就到這裡。當然,生有涯而知無涯。在漫漫的開發週期中,您看到的這些問題也許都只是冰山一角,我們早已在UWA問答網站上準備了更多的技術話題等你一起來探索和分享。歡迎熱愛進步的你加入,也許你的方法恰能解別人的燃眉之急;而他山之“石”,也能攻你之“玉”。

官網:www.uwa4d.com
官方技術部落格:blog.uwa4d.com
官方問答社群:answer.uwa4d.com
UWA學堂:edu.uwa4d.com
官方技術QQ群:793972859(原群已滿員)