Unity 內(nèi)存管理

轉(zhuǎn)自: https://www.notion.so/Unity-f79bb1d4ccfc483fbd8f8eb859ae55fe

Best practice guides

什么是內(nèi)存

  • 物理內(nèi)存
    • CPU 訪問內(nèi)存是一個慢速過程
  • 虛擬內(nèi)存
    • 內(nèi)存交換
    • 移動設(shè)備不支持內(nèi)存交換
    • iOS 可以進(jìn)行內(nèi)存壓縮
    • Android 沒有內(nèi)存壓縮能力
image.png

內(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)部的使用情況。

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ā),消耗增加。

        Audio

    • 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、打包的速度。
  • 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 都會獲得收益。
    • 壓縮方式:

      • Lz4

        BuildCompression.LZ4

        • LZ4HC "Chunk Based" Compression. 非常快
        • 和 Lzma 相比,平均壓縮比率差 30%。也就是說會導(dǎo)致包體大一點,但是(作者說)速度能快 10 倍以上。
      • Lzma

        BuildCompression.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)把包體加大。還是要看實際用戶的情況。
  • 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)系的。(?)

      Memory Management in Unity - Unity Learn

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(可移植性)
        • 不同平臺是否可以使用
    • 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
    • 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ù)利用。
    • 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)存里。
      • Coroutines(協(xié)程)
        • 可看做閉包和匿名函數(shù)的一個特例
        • 最佳實踐:用的時候生產(chǎn)一個,不用的時候 destroy 掉。
      • Configurations(配置表)
        • 不要把整個配置表都扔進(jìn)去,是否能通過啥來切分下配置表
      • Singleton
        • 慎用
        • 游戲一開始到游戲死掉,一直在內(nèi)存中。
  • UPR 工具

    Unite 2019 | Unity UPR性能報告功能介紹 - Unity Connect

    • 免費,在中國增強(qiáng)版里
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容