轉(zhuǎn)自: https://www.notion.so/Unity-f79bb1d4ccfc483fbd8f8eb859ae55fe
什么是內(nèi)存
- 物理內(nèi)存
- CPU 訪問內(nèi)存是一個慢速過程
- 減少 Cache Miss
- ECS 和 DOTS
- (本文寫手 OS:可以看看我的博文《Unity DOTS 走馬觀花》
- CPU 訪問內(nèi)存是一個慢速過程
- 虛擬內(nèi)存
- 內(nèi)存交換
- 移動設(shè)備不支持內(nèi)存交換
- iOS 可以進(jìn)行內(nèi)存壓縮
- Android 沒有內(nèi)存壓縮能力

內(nèi)存殺手 low memory killer(AKA lmk)
[圖片上傳失敗...(image-4a0625-1598248679425)]
- 內(nèi)存不足時,killer 會出現(xiàn),從上圖底層一層一層地向上殺。(Cached-Previous-Home...)
Unity 內(nèi)存管理
- Unity 是一個 C++ 引擎
- 底層代碼完全由 C++ 寫成
- 通過 Wrapper 提供給用戶 API
- 用戶代碼會轉(zhuǎn)換為 CPP 代碼 (il2cpp)
- VM 仍然存在(il2cpp vm)
- Unity 內(nèi)存按照分配方式分為:
- Native Memory
- Managed Memory
- Editor & Runtime 是不同的
- 不止是統(tǒng)計看到的內(nèi)存大小不同,甚至是內(nèi)存分配時機(jī)和方式也不同
- Asset 在 Runtime 中如果不讀取,是不會進(jìn)內(nèi)存的,但 Editor 打開就占內(nèi)存。因為 Editor 不注重 Runtime 的表現(xiàn),更注重編輯器中編輯時的流暢。
- 但如果游戲龐大到幾十個 G,如果第一次打開項目,會消耗很多時間,有的大的會幾天,甚至到一周。
- Unity 內(nèi)存按照管理者分為:
- 引擎管理內(nèi)存
- 用戶管理內(nèi)存(應(yīng)優(yōu)先考慮)
- Unity 檢測不到的內(nèi)存
- 用戶分配的 native 內(nèi)存
- 自己寫的 Native 插件(C++ 插件), Unity 無法分析已經(jīng)編譯過的 C++ 是如何去分配和使用內(nèi)存的。
- Lua 完全由自己管理內(nèi)存,Unity 無法統(tǒng)計到內(nèi)部的使用情況。
- 用戶分配的 native 內(nèi)存
Unity Native Memory 管理
Unity 重載了所有分配內(nèi)存的操作符(C++ alloc、new),使用這些重載的時候,會需要一個額外的 memory label (Profiler-shaderlab-object-memory-detail-snapshot,里面的名字就是 label:指當(dāng)前內(nèi)存要分配到哪一個類型池里面)
- 使用重載過的分配符去分配內(nèi)存時,Allocator 會根據(jù)你的 memory label 分配到不同 Allocator 池里面,每個 Allocator 池 單獨做自己的跟蹤。因此當(dāng)我們?nèi)?Runtime get memory label 下面的池時就可以問 Allocator,里面有多少東西 多少兆。
- Allocator 在 NewAsRoot (Memory “island”(沒聽清)) 中生成。在這個 Memory Root 下面會有很多子內(nèi)存:shader:當(dāng)我們加載一個 Shader 進(jìn)內(nèi)存的時候,會生成一個 Shader 的 root。Shader 底下有很多數(shù)據(jù):sub shader、Pass 等會作為 memory “island” (root) 的成員去依次分配。因此當(dāng)我們最后統(tǒng)計 Runtime 的時候,我們會統(tǒng)計 Root,而不會統(tǒng)計成員,因為太多了沒法統(tǒng)計。
- 因為是 C++ 的,因此當(dāng)我們 delete、free 一個內(nèi)存的時候會立刻返回內(nèi)存給系統(tǒng),與托管內(nèi)存堆不一樣。
最佳實踐 Native 內(nèi)存
-
Scene
- Unity 是一個 C++ 引擎,所有實體最終都會反映在 C++ 上,而不是托管堆里面。因此當(dāng)我們實例化一個 GameObject 的時候,在 Unity 底層會構(gòu)建一個或多個 Object 來存儲這個 GameObject 的信息,例如很多 Components。因此當(dāng) Scene 有過多 GameObject 的時候,Native 內(nèi)存就會顯著上升。
- 當(dāng)我們看 Profiler,發(fā)現(xiàn) Native 內(nèi)存大量上升的時候,應(yīng)先去檢查 Scene。
-
Audio
-
DSP buffer (聲音的緩沖)
當(dāng)一個聲音要播放的時候,它需要向 CPU 去發(fā)送指令——我要播放聲音。但如果聲音的數(shù)據(jù)量非常小,就會造成頻繁地向 CPU 發(fā)送指令,會造成 I\O。
當(dāng) Unity 用到 FMOD 聲音引擎時(Unity 底層也用到 FMOD),會有一個 Buffer,當(dāng) Buffer 填充滿了,才會向 CPU 發(fā)送“我要播放聲音”的指令。
-
DSP buffer 會導(dǎo)致兩種問題:
- 如果(設(shè)置的) buffer 過大,會導(dǎo)致聲音的延遲。要填充滿 buffer 是要很多聲音數(shù)據(jù)的,但聲音數(shù)據(jù)又沒這么大,因此會導(dǎo)致一定的聲音延遲。
- 如果 DSP buffer 太小,會導(dǎo)致 CPU 負(fù)擔(dān)上升,滿了就發(fā),消耗增加。
-
Force to mono
- 在導(dǎo)入聲音的時候有一個設(shè)置,很多音效師為了聲音質(zhì)量,會把聲音設(shè)為雙聲道。但 95% 的聲音,左右聲道放的是完全一樣的數(shù)據(jù)。這導(dǎo)致了 1M 的聲音會變成 2M,體現(xiàn)在包體里和內(nèi)存里。因此一般對于聲音不是很敏感的游戲,會建議改成 Force to mono,強(qiáng)制單聲道。
Format
Compression Format(看文檔,有使用建議)
-
-
Code Size
- C++ 模板泛型的濫用會影響到 Code Size、打包的速度。
- 可以參考 Memory Management in Unity 3.IL2CPP & Mono 的 Generic Sharing 部分。
- C++ 模板泛型的濫用會影響到 Code Size、打包的速度。
-
AssetBundle
-
TypeTree
- Unity 的每一種類型都有很多數(shù)據(jù)結(jié)構(gòu)的改變,為了對此做兼容,Unity 會在生成數(shù)據(jù)類型序列化的時候,順便會生成 TypeTree:當(dāng)前我這一個版本里用到了哪些變量,對應(yīng)的數(shù)據(jù)類型是什么。在反序列化的時候,會根據(jù) TypeTree 來進(jìn)行反序列化。
- 如果上一個版本的類型在這個版本中沒有,TypeTree 就沒有它,因此不會碰到它。
- 如果要用一個新的類型,但在這個版本中不存在,會用一個默認(rèn)值來序列化,從而保證了不會在不同的版本序列化中出錯,這個就是 TypeTree 的作用。
- Build AssetBundle 中有開關(guān)可以關(guān)掉 TypeTree。當(dāng)你確認(rèn)當(dāng)前 AssetBundle 的使用和 Build Unity 的版本一模一樣,這時候可以把 TypeTree 關(guān)掉。
- 例如如果用同樣的 Unity 打出來的 AssetBundle 和 APP,TypeTree 則完全可以關(guān)掉。
- TypeTree 好處:
- 內(nèi)存減少。TypeTree 本身是數(shù)據(jù),也要占內(nèi)存。
- 包大小會減少,因為 TypeTree 會序列化到 AssetBundle 包中,以便讀取。
- Build 和運行時會變快。源代碼中可以看到,因為每一次 Serialize 東西的時候,如果發(fā)現(xiàn)需要 Serialize TypeTree,則會 Serialize 兩次:
- 第一次先把 TypeTree Serialize 出來
- 第二次把實際的東西 Serialize 出來
- 反序列化也會做同樣的事情,1. TypeTree 反序列化,2. 實際的東西反序列化。
- 因此如果確定 TypeTree 不會對兼容性造成影響,可以把它關(guān)掉。這樣對 Size 大小和 Build Runtime 都會獲得收益。
- Unity 的每一種類型都有很多數(shù)據(jù)結(jié)構(gòu)的改變,為了對此做兼容,Unity 會在生成數(shù)據(jù)類型序列化的時候,順便會生成 TypeTree:當(dāng)前我這一個版本里用到了哪些變量,對應(yīng)的數(shù)據(jù)類型是什么。在反序列化的時候,會根據(jù) TypeTree 來進(jìn)行反序列化。
-
壓縮方式:
-
Lz4
- LZ4HC "Chunk Based" Compression. 非常快
- 和 Lzma 相比,平均壓縮比率差 30%。也就是說會導(dǎo)致包體大一點,但是(作者說)速度能快 10 倍以上。
-
Lzma
- Lzma 基本上就不要用了,因為解壓和讀取速度上都會比較慢。
- 還會占大量內(nèi)存
- 因為是 Steam based 而不是 Chunk Based 的,因此需要一次全解壓
- Chunk Based 可以一塊一塊解壓
- 如果發(fā)現(xiàn)一個文件在第 5-10 塊,那么 LZ4 會依次將 第 5 6 7 8 9 10 塊分別解壓出來,每次(chunk 的)解壓會重用之前的內(nèi)存,來減少內(nèi)存的峰值。
預(yù)告:中國版 Unity 會在下個版本(1月5號或2月份)推出新的功能:基于 LZ4 的 AssetBundle 加密,只支持 LZ4。
-
Size & count
- AssetBundle 包打多大是很玄學(xué)的問題,但每一個 Asset 打一個 Bundle 這樣不太好。
- 有一種減圖片大小的方式,把 png 的頭都提出來。因為頭的色板是通用的,而數(shù)據(jù)不通用。AssetBundle 也一樣,一部分是它的頭,一部分是實際打包的部分。因此如果每個 Asset 都打 Bundle 會導(dǎo)致 AssetBundle 的頭比數(shù)據(jù)還要大。
- 官方的建議是每個 AssetBundle 包大概 1M~2M 左右大小,考慮的是網(wǎng)絡(luò)帶寬。但現(xiàn)在 5G 的時候,可以考慮適當(dāng)把包體加大。還是要看實際用戶的情況。
- AssetBundle 包打多大是很玄學(xué)的問題,但每一個 Asset 打一個 Bundle 這樣不太好。
-
-
-
Resource 文件夾(Do not use it. 除非在 debug 的時候)
- Resource 和 AssetBundle 一樣,也有頭來索引。Resource 在打進(jìn)包的時候會做一個紅黑樹,來幫助 Resource 來檢索資源在什么位置,
- 如果 Resource 非常大,那么紅黑樹也會非常大。
- 紅黑樹是不可卸載的。在剛開始游戲的時候就會加載進(jìn)內(nèi)存中,會持續(xù)對游戲造成內(nèi)存壓力。
- 會極大拖慢游戲的啟動時間。因為紅黑樹沒加載完,游戲不能啟動。
-
Texture
upload buffer,和聲音的很像:填滿多大,就向 CPU push 一次。
-
r/w
- Texture 沒必要就不要開 read and write。正常 Texture 讀進(jìn)內(nèi)存,解析完了,放到 upload buffer 里后,內(nèi)存里的就會 delete 掉。
- 但如果檢測到你開了 r/w 就不會 delete 了,就會在顯存和內(nèi)存中各一份。
-
Mip Maps
- UI 沒必要開,可以省大量內(nèi)存。
-
Mesh
- r/w
- compression
- 有些版本 Compression 開了不如不開,內(nèi)存占用可能更嚴(yán)重,具體需要自己試。
-
Assets
- Assets 的數(shù)量實際上和 asset 整個的紋理是有關(guān)系的。(?)
Unity Managed Memory
Understanding the managed heap
-
VM 內(nèi)存池
- mono 虛擬機(jī)的內(nèi)存池
- VM 會返還內(nèi)存給 OS 嗎?
- 會
- 返還條件是什么?
- GC 不會把內(nèi)存返還給系統(tǒng)
- 內(nèi)存也是以 Block 來管理的。當(dāng)一個 Block 連續(xù)六次 GC 沒有被訪問到,這塊內(nèi)存才會被返還到系統(tǒng)。(mono runtime 基本看不到,IL2cpp runtime 可能會看到多一點)
- 不會頻繁地分配內(nèi)存,而是一次分配一大塊。
-
GC 機(jī)制(BOEHM Non-generational 不分代的)
-
GC 機(jī)制考量
- Throughput((回收能力)
- 一次回收,會回收多少內(nèi)存
- Pause times(暫停時長)
- 進(jìn)行回收的時候,對主線程的影響有多大
- Fragmentation(碎片化)
- 回收內(nèi)存后,會對整體回收內(nèi)存池的貢獻(xiàn)有多少
- Mutator overhead(額外消耗)
- 回收本身有 overhead,要做很多統(tǒng)計、標(biāo)記的工作
- Scalability(可擴(kuò)展性)
- 擴(kuò)展到多核、多線程會不會有 bug
- Protability(可移植性)
- 不同平臺是否可以使用
- Throughput((回收能力)
-
BOEHM
-
Non-generational(不分代的)
[圖片上傳失敗...(image-af6cab-1598248679398)]
- 分代是指:大塊內(nèi)存、小內(nèi)存、超小內(nèi)存是分在不同內(nèi)存區(qū)域來進(jìn)行管理的。還有長久內(nèi)存,當(dāng)有一個內(nèi)存很久沒動的時候會移到長久內(nèi)存區(qū)域中,從而省出內(nèi)存給更頻繁分配的內(nèi)存。
-
Non-compacting(非壓縮式)
[圖片上傳失敗...(image-15c273-1598248679397)]
- 當(dāng)有內(nèi)存被回收的時候,壓縮內(nèi)存會把上圖空的地方重新排布。
- 但 Unity 的 BOEHM 不會!它是非壓縮式的。空著就空著,下次要用了再填進(jìn)去。
- 歷史原因:Unity 和 Mono 合作上,Mono 并不是一直開源免費的,因此 Unity 選擇不升級 Mono,與實際 Mono 版本有差距。
- 下一代 GC
- Incremental GC(漸進(jìn)式 GC)
- 現(xiàn)在如果我們要進(jìn)行一次 GC,主線程被迫要停下來,遍歷所有 GC Memory “island”(沒聽清),來決定哪些 GC 可以回收。
- Incremental GC 把暫停主線程的事分幀做了。一點一點分析,主線程不會有峰值??傮w GC 時間不變,但會改善 GC 對主線程的卡頓影響。
- SGen 或者升級 Boehm?
- SGen 是分代的,能避免內(nèi)存碎片化問題,調(diào)動策略,速度較快
- IL2CPP
- 現(xiàn)在 IL2CPP 的 GC 機(jī)制是 Unity 自己重新寫的,是升級版的 Boehm
- Incremental GC(漸進(jìn)式 GC)
-
-
Memory fragmentation 內(nèi)存碎片化
[圖片上傳失敗...(image-3aebc7-1598248679407)]
- 為什么內(nèi)存下降了,但總體內(nèi)存池還是上升了?
- 因為內(nèi)存太大了,內(nèi)存池沒地方放它,雖然有很多內(nèi)存可用。(內(nèi)存已被嚴(yán)重碎片化)
- 當(dāng)開發(fā)者大量加載小內(nèi)存,使用釋放*N,例如配置表、巨大數(shù)組,GC 會漲一大截。
- 建議先操作大內(nèi)存,再操作小內(nèi)存,以保證內(nèi)存以最大效率被重復(fù)利用。
- 為什么內(nèi)存下降了,但總體內(nèi)存池還是上升了?
-
Zombie Memory(僵尸內(nèi)存)
- 內(nèi)存泄露說法是不對的,內(nèi)存只是沒有任何人能夠管理到,但實際上內(nèi)存沒有被泄露,一直在內(nèi)存池中,被 zombie 掉了,這種叫 Zombie 內(nèi)存。
- 無用內(nèi)容
- Coding 時候或者團(tuán)隊配合的時候有問題,加載了一個東西進(jìn)來,結(jié)果從頭到尾只用了一次。
- 有些開發(fā)者寫了隊列調(diào)度策略,但是策略寫的不好,導(dǎo)致一些他覺得會被釋放的東西,沒有被釋放掉。
- 找是否有活躍度實際上并不高的內(nèi)存。
- 沒有釋放
- 通過代碼管理和性能工具分析
-
最佳實踐
- Don't Null it, but Destroy it(顯式用 Destory,別用 Null)
- Class VS Struct
- Pool In Pool(池中池)
- VM 本身有內(nèi)存池,但建議開發(fā)者對高頻使用的小部件,自己建一個內(nèi)存池。例如子彈等。
- Closures and anonymous methods(閉包和匿名函數(shù))
- 如果看 IL,所有匿名函數(shù)和閉包會 new 成一個 class,因此所有變量和要 new 的東西都是要占內(nèi)存的。這樣會導(dǎo)致協(xié)程。
- 有些開發(fā)者會在游戲開始啟用一個協(xié)程,直到游戲結(jié)束才釋放,這是錯誤的。
- 只要協(xié)程不被釋放掉,所有內(nèi)存都會在內(nèi)存里。
- 如果看 IL,所有匿名函數(shù)和閉包會 new 成一個 class,因此所有變量和要 new 的東西都是要占內(nèi)存的。這樣會導(dǎo)致協(xié)程。
- Coroutines(協(xié)程)
- 可看做閉包和匿名函數(shù)的一個特例
- 最佳實踐:用的時候生產(chǎn)一個,不用的時候 destroy 掉。
- Configurations(配置表)
- 不要把整個配置表都扔進(jìn)去,是否能通過啥來切分下配置表
- Singleton
- 慎用
- 游戲一開始到游戲死掉,一直在內(nèi)存中。
-
-
UPR 工具
Unite 2019 | Unity UPR性能報告功能介紹 - Unity Connect
- 免費,在中國增強(qiáng)版里