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提供,歡迎大家轉至社區交流:
https://answer.uwa4d.com/question/5e1d877ffd2e373ffa7eab3b


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問答社區提供了回答,歡迎大家轉至社區交流:
https://answer.uwa4d.com/question/5fc8254010a17c6c2b09d48c


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問答社區提供了回答,歡迎大家轉至社區交流:
https://answer.uwa4d.com/question/5f40a5849424416784ef1d4c


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問答社區提供了回答,歡迎大家轉至社區交流:
https://answer.uwa4d.com/question/5f914de75d9aea5aa9719769


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問答社區提供了回答,歡迎大家轉至社區交流:
https://answer.uwa4d.com/question/5fa4f888dc477370c2c1f07f


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問答社區提供了回答,歡迎大家轉至社區交流:
https://answer.uwa4d.com/question/5e919a528cabe84a011afcde


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問答社區提供了回答,歡迎大家轉至社區交流:
https://answer.uwa4d.com/question/5f3d10b19424416784ef1c82


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問答社區提供了回答,歡迎大家轉至社區交流:
https://answer.uwa4d.com/question/5efbe2cf965c87248fa413e0


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問答社區提供了回答,歡迎大家轉至社區交流:
https://answer.uwa4d.com/question/5ee9ea195d07d80acd262421


Q10:LuaJIT性能熱點函數優化

項目中的這個函數耗時非常嚴重,有什麼優化的方法嗎?

 

A1:這個是table.get,對應獲取字段或者訪問數組時調用的函數:

  1. 優先使用連續數組而不是英文名字段,可以顯著提升訪問效率並降低內存消耗,很多團隊喜歡使用Class的寫法,可以這樣改造:
    local obj = ClassA.New()
    obj.abc = 1
    obj.cde = "test"
    變成:
    local obj = ClassA.New()
    obj[1] = 1
    obj[2] = "test"
    這個方法可以針對使用頻率較高的代碼進行改造。







  2. 自己開發工具,在編譯Lua之前,將Lua代碼中的常量從英文名變量轉換爲數值,這個可以結合1使用,就可以在開發期寫英文名字段名,然後編譯時轉換爲數組。

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

A2:字符串應該是哈希值計算的消耗,這樣的開銷應該是很頻繁地調用了。

感謝王歡@UWA問答社區提供了回答,歡迎大家轉至社區交流:
https://answer.uwa4d.com/question/5f1ee0acaac90465cae9404c

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

官網:www.uwa4d.com
官方技術博客:blog.uwa4d.com
官方問答社區:answer.uwa4d.com
UWA學堂:edu.uwa4d.com
官方技術QQ羣:793972859(原羣已滿員)



發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章