UE 手遊在 iOS 平臺執行時記憶體佔用太高?試試這樣著手優化

Leonn發表於2020-09-03
效能優化,對遊戲開發來說是一個需要不斷鑽研的課題,效能越好,遊戲才會執行的更加順暢,玩家的體驗感才會更好。騰訊遊戲學院專家、遊戲客戶端開發 Leonn,將和大家分享 UE 手遊在 iOS 平臺上的記憶體分佈和優化。(本文首發於騰訊遊戲學院專家團月刊《EXP 手冊》)

文 | Leonn

騰訊遊戲學院專家 遊戲客戶端開發

對於在 iOS 平臺上執行的 UE 程式,經常會出現記憶體佔用較高,xcode 的記憶體統計和 UE 的統計有偏差的問題。本文將討論下 iOS 上記憶體的管理機制,記憶體的組成,iOS 特有的一些資源管理特性,以及 UE 程式針對 iOS 常見的記憶體瓶頸和優化。

iOS 程式記憶體分配原理

1.1 iOS 系統記憶體分配原理

iOS 也是一個類 linux 的系統,所以基本的記憶體分配還是走的虛擬記憶體系統中 page mapping 和 swap out 那套,即虛擬記憶體訪問缺頁產生對實體記憶體的實際佔用,以及對實體記憶體的喚入喚出(詳細可以看前面的《Android 記憶體分佈和優化》的文章)。iOS 上的 page size 為 16k,

Swap out 和 compressed memory

關於 swap out 機制,由於移動端記憶體的壽命問題,沒有實現傳統虛擬記憶體那樣的 swap in/out 機制,只有一些 read-only 的資料(例如程式碼)在被 swap out 的時候,會被直接從實體記憶體移除,而不會 back up 到磁碟檔案上,下次 swap in 就直接重新載入,也就是非 read only 的 page 只要被訪問就永遠不可能喚出實體記憶體。

UE 手遊在 iOS 平臺執行時記憶體佔用太高?試試這樣著手優化

此外 iOS 還有額外的 memory compress 機制,即將一些不常用的記憶體壓縮儲存在記憶體中。

UE 手遊在 iOS 平臺執行時記憶體佔用太高?試試這樣著手優化

所以 iOS 中有個特有的表示記憶體佔用的說法 memory footprint 就是值得壓縮前的總大小,而不是當前的實際實體記憶體大小。

當系統記憶體吃緊的時候,系統會通過 didrecievememorywarning 通知程式,讓程式自願的釋放一些記憶體,這時候如果程式仍然不能有效降低記憶體,程式就會被殺掉。

VM Object

在 iOS 核心中,使用一個叫做 VM Object 的物件表示在虛擬記憶體空間的一塊被對映的記憶體區域(region),一個 region 是由幾個連續的 page 組成,所以一個 vm object region 的起始地址必定是某個虛擬空間上 page 的起始地址。VM Object 還記錄了其他的一些資訊,包括繼承關係,讀寫許可權,是否是 wired(能不能被 swap-out)。此外它還關聯了一個 pager,用來做記憶體對映,這個 pager 是 default pager 或者 vnode pager 的一種, default pager 負責將虛地址 VA 跟實體地址 PA 做對映(即訪問 va 缺頁後將開闢實體記憶體),vnode pager 直接將檔案對映到虛地址空間(這樣不經過記憶體直接讀寫檔案)。

VM_Copy

Vm object 的 pager 除了可以是 default pager 或者 vnode pager 之外,還可以直接對映另外一個 vm object,這時是為了做 copy-on-write 優化。這允許不同的 vm object 對映同一段 page 區域,直到其中一個 vm object 需要發生寫入它才會 copy 出一個新的。在 iOS 系統下我們可以直接呼叫 vm_copy 代替 memcpy 來執行這種 copy-on-write 的 copy,只要你 copy 後面不寫入,就一直沒有實際的 copy 開銷。Vm_copy 的唯一缺點是如果發生寫入那麼 copy 會存在延時,所以對於頻繁的小記憶體的 copy 還是推薦直接 memcpy。

UE 手遊在 iOS 平臺執行時記憶體佔用太高?試試這樣著手優化

UE 手遊在 iOS 平臺執行時記憶體佔用太高?試試這樣著手優化
Vm_copy 和 memcpy

1.2 iOS 系統記憶體的分配方式

在《Android 記憶體分佈和優化》中我們講了使用 malloc 和 mmap 分配系統記憶體的原理和區別。這裡我們詳細討論下他們在 iOS 上的特點。

vm_allocate

首先 iOS 上做 mmap 的函式是 vm_allocate,它同 Android 上的 mmap 用法相當,即分配一個虛存,做實體記憶體或者檔案對映。

Malloc

直接從堆上分配記憶體,並且這些記憶體會立即從虛擬記憶體對映實體記憶體,並且不會初始化記憶體內容。在 iOS 上 malloc 的底層實現細節如下:

小記憶體:小於幾個 pagesize 的記憶體,malloc 會從一個 pool 上分配,這個 pool 本身是由 vm_allocate 分配的虛存,這些虛存可能都是已經存在實體記憶體對映的了,這個池分配的粒度都是按照 16 位元組對齊,所以我們用 malloc 也儘量 16 位元組對齊,否則就存在了浪費。這個小記憶體池的預分配的大小有多大要取決於系統策略。

大記憶體:對於大於幾個 pagesize 的記憶體,Malloc 自動使用 vm_allocate,它只分配虛存,不立即對映實體記憶體,分配粒度為 1 個 page 大小(即 16k),因為不同的 vmobject 是由不同的獨立的 page 組成,這時如果 malloc 的大小沒有 16k 對齊,也會產生較多的記憶體浪費。在這種情況,使用 malloc 和直接使用 vm_allocate 是相當的。

Calloc

calloc 同 malloc 不同的是,它在分配記憶體後,在使用前會保證將記憶體初始化為 0。他比用 malloc+memset 要優化,因為 memset 發生時會立即產生缺頁造成實體記憶體佔用,然後初始化 0,而 calloc 則是延遲的,它不會馬上產生實體記憶體佔用,而是要等得到真正這塊記憶體被使用之前。我們應該完全使用 calloc 代替 malloc+memset。

Malloc zone

iOS 上所有的記憶體分配都是來自於某個 malloc zone 的,每個 zone 有獨立的記憶體池,預設的分配都是在 default malloc zone 上的。使用 malloc zone 有個好處是減少小記憶體池的浪費。我們知道記憶體池的浪費主要有兩種來源,一種是對齊浪費,即為了匹配記憶體池的分配粒度,沒有對齊的記憶體產生的浪費,如 malloc 一個 17 byte 的記憶體其實需要 malloc 32byte,另一種則來源於對頁的空白浪費,例如在頻繁的分配記憶體後,會開闢大量的新 page,這樣在後面即使先後發生了一些釋放,但是因為釋放不集中在一個 page 上,也導致了很多 page 上只被少量的 block 佔據,導致大量的空白部分的浪費。如果我們可以知道某些記憶體的生命週期是相同的,那麼我們可以把它們在同樣的一個 zone 上分配,這樣我們在確定他們的生命週期全都到期後,可以對整個 zone 執行釋放的操作,這樣就杜絕了這兩種浪費。在 iOS 下使用 malloc zone 的相關介面是:

  • malloc_create_zone 建立一個 zone
  • malloc_zone_malloc 再某個 zone 上分配
  • malloc_destroy_zone 釋放整個 zone


UE 程式在 iOS 上的記憶體組成清單

瞭解了 iOS 上的基本記憶體分配原理後,我們來統計我們 iOS 上的 UE 程式的記憶體組成。在對 UE 程式進行記憶體分析和優化過程中,我們要做的的第一件事就是獲取一個完整的關於你程式的記憶體組成清單。UE 的引擎內部提供了 LLM,memreport 等記憶體統計工具,但是這些只是 UE 能感知到的記憶體,我們需要能明確整個程式的記憶體被花到哪裡了,以及為什麼程式會因記憶體過高而產生問題。

2.1 iOS 記憶體組成統計口徑

Memory footprint

Android 上我們一般使用 PSS,即程式(按分攤統計共享庫)分配的實際實體記憶體大小來定義記憶體開銷。iOS 有所不同,iOS 上通常使用 memory footprint(下面簡寫為 mem foot)這一個概念來定義記憶體開銷,mem foot 同實際佔用的實體記憶體之間有一定差別。Mem foot 在 iOS 上的定義是程式實際佔用的實體記憶體+程式被壓縮了的記憶體在壓縮前的大小,即 mem foot = resident + swapped (這裡的 swapped 不是指 swap out 的意思,是前文說到的 iOS 的記憶體壓縮機制)。所以從定義上看,所謂 mem foot 是指你的程式所可能觸碰到的所有實體記憶體大小(儘管部分已經被壓縮),這就是腳印的意思。

在 xcode 的 allocater 中,我們可以計算 vm tracker 中 all 中的 resident+swapped 的大小來得到 mem foot 值。如果是在程式碼中,則可以通過 darwin 核心的介面 task_info 獲取 TASK_VM_INFO 來獲取其中的 phys_footprint 來獲得,darwin 原始碼中關於 phys_footprint 的定義是

UE 手遊在 iOS 平臺執行時記憶體佔用太高?試試這樣著手優化

其中 internal 即除了視訊記憶體外的 resident 記憶體,internal_compressed 即指除了視訊記憶體外的 swapped 部分,iokit_mapped 一般就是(其實是不能使用 purgeable memory 的)視訊記憶體,後面的 purgeable 是指使用 purgeable memory 中屬性為 nontvolatile 的。關於 purgeable memory 後文再說。

可以說 memery footprint 是 iOS 上統計記憶體佔用的金標準。

XCode Gauge

當我們使用 xcode 執行遊戲,會看到一個實時的顯示記憶體的儀表盤,如下圖

UE 手遊在 iOS 平臺執行時記憶體佔用太高?試試這樣著手優化

這個叫做 xcode memory gauge,它統計的又是什麼記憶體呢,其實它嚴格來說統計的不是 memory footprint,它統計的是 vmtracker 裡面 dirty+swapped 的值,那麼什麼是 dirty 記憶體呢,dirty 是指實際佔用的實體記憶體(resident)中那些一定不能被 swap out 出去的記憶體,前面提到 iOS swap out 機制時說,iOS 上只有那些可讀的檔案等才能被 swap out,這些能 swap out 的記憶體通常危害不大,在記憶體吃緊的時候可以部分被系統排程出實體記憶體,他們一般是各種檔案對映,程式碼庫,符號檔案等,所以 dirty 才是程式動態分配的需要考慮的記憶體,xcode gause 統計的是真正使用者能夠決定的記憶體腳印大小。他要比 mem foot 小一些,小了那些程式碼和檔案的記憶體佔用。

2.2 iOS 程式主要記憶體構成

我們以 memory footprint 為統計標準來得到 iOS 的完整記憶體構成,最正確方便的方法是使用 xcode 的 allocations 工具,裡面有個 vm tracker,vm tracker 就是用來跟蹤程式的每個 vmobject 的,即每個虛擬記憶體區域的分配情況。在 vm tracker 裡面做一個 snapshots,就可以得到當前記憶體分配的一個快照。

UE 手遊在 iOS 平臺執行時記憶體佔用太高?試試這樣著手優化
一個典型的 vm tracker 快照截圖

其中 All 是指總的分類,all 下面是各種細類,右面的 resident dirty swapped 分別指實際實體記憶體,不能被 swap out 出去的實體記憶體,以及被壓縮的記憶體的壓縮前大小,最後面的 virtual size 是虛存大小。我們把 all 中的 resident+swapped 就是總 memory foot print。

下面是佔據大頭的幾個細類:

  • IOKIt 和 IOSurface:通常就是指我們 GPU 需要訪問的記憶體,即視訊記憶體
  • Performance tool data:是實際執行沒有的,Profile 工具本身記憶體。
  • Mappedfile:檔案對映,用於讀寫的檔案,一般不佔用很多 dirty
  • __LINKEDIT 和 __TEXT:程式碼段部分,即程式碼段記憶體,只讀,他們一般不佔 dirty
  • __DATA:程式碼的資料段,包括可讀寫的全域性變數等。

Malloc_NANO/TINY/SMALL/LARGE :這就是前面提到的 iOS 的 malloc 小記憶體池,nano/TINY 指的就是文章最前提到的 malloc zone。雖然預設的 malloc 是在 default zone 上分配的,但是系統還是會根據不同的大小再選擇不同的 zone。對於 0-256B 的 malloc,系統會使用 nano zone,nano zone 比較特殊,它專門為小記憶體而優化,並且預先就 vm_allocate 了一塊 512M 的虛擬記憶體空間做這個 nano zone 的 pool,這塊空間處於堆底。對於更大一些的小記憶體分配,則會根據情況使用到 tiny small large 這三個 zone 的 pool。所以我們推薦大家在 iOS 上對於 256b 以內的記憶體分配直接走 malloc,而不是 UE 的 malloc,可能會得到更多的收益。

VM_ALLOCATE:這個就是通過 vm_allocate 方法申請虛存後缺頁觸發的記憶體佔用,在 UE 裡面一定是大頭,因為 UE 自己的 Fmalloc 在 iOS 上就是走的 vm_allocate。又因為 UE 的 fmalloc 在做 vm_allocate 的時候傳遞的 tag 是 255,所以在 vmtracker 中,所有體現為 memory tag 255 的 vm 就是 UE 的 fmalloc。

2.3 UE 程式記憶體組成清單

從 vm tracker 出發,在配合我們在《Android 記憶體分佈和優化》中提到的 UE 的自帶的 LLM 機制,我們就可以構建 UE 程式在 iOS 平臺上的記憶體完整清單了。它至少應該被分割成以下幾個大部分:

UE 手遊在 iOS 平臺執行時記憶體佔用太高?試試這樣著手優化

這是我們對於任何一個 UE 程式,可以得到的在 iOS 上的詳細的記憶體分佈情況,這裡面有幾個問題需要注意:

實際的總 mem foot 和下面各自項加起來可能是存在一定偏差的。因為 LLm 中各個從 UE Fmalloc 出來的子項的總和其實也只是個 vm_allocate 的虛存大小,它實際上佔用的實體記憶體腳印是要小一些的,另外 LLM 裡面對 metal texture 和 buffer 的記憶體計算也是估計的,但是一般情況不會差別過大,我們只要瞭解這其中存在差值即可。

2.4 視訊記憶體大小的統計

Llm 統計的 metal tex 和 metal buffer 是 UE 統計的 gpu 訪問的資源量,它同實際值是有偏差的,比如 UE 未考慮到使用 purgeable memory,memryless 等資源的記憶體減少,此外視訊記憶體上還有除了 tex 和 buffer 之外的其他資源。所以如果想確定真實的 gpu 資源使用還是要看 IOKIT 的值,只是我們可以用 metal tex 和 buffer 估計下大致的比例。另外在最新版本 xcode 的截幀工具中我們也可以看到一個細節的 tex 和 buffer 的視訊記憶體,如下:、

UE 手遊在 iOS 平臺執行時記憶體佔用太高?試試這樣著手優化

它可以顯示詳細的 tex 和 buffer 使用情況,但是記憶體值是明顯偏大的,因為這裡顯示的是虛存值,不是實體記憶體,所以也只能參考。

UE 程式在 iOS 上的主要瓶頸和優化

我們從上面的清單上找到一些記憶體的大頭。在一個大型 3D 專案中,記憶體較大的塊一般集中在在程式碼段部分,GPU 訪問記憶體,Uobject,和 Fmalloc 記憶體池浪費上。本章節也著重講這幾塊的針對 iOS 的常用優化方法。很多平臺通用的優化方法在文章《Android 記憶體分佈和優化》中已經說到了,這裡就不重複,主要將針對 iOS 平臺的優化手段。

3.1 GPU 訪問記憶體

也可以稱為視訊記憶體,視訊記憶體的主要組成部分包括 buffer, texture 和 shader。視訊記憶體的資源維護在 iOS 上就有一些特有的優化手段。

Purgeable memory

iOS 上的視訊記憶體資源 MTLResource(mtlbuffer,mtltexture)使用的都是 purgeable memory。所謂 purgeable memory 是指這種記憶體有三種 purgeable state,分別為 volatile,none-volatile 和 empty。

Volatile:該記憶體資源是暫時不被使用的,系統將在記憶體吃緊的時候回收掉它,使用這種型別資源前要查詢該資源是否已經無效了(變成 empty 狀態)。

Non_volatile:該記憶體資源一直有用,不能被回收。

Empty:該記憶體資源明確不用了,需要立即釋放。

重要的一點是 volatile 和 empty 狀態的資源不計入程式自己的 mem footprint,它算系統的 cache 記憶體。

通過 purgeable state iOS 系統等於為我們提供了一層 pool 或 cache 機制,我們應該儘量利用它。事實上理想情況我們應該把大部分程式用到的可反覆建立的視訊記憶體資源用 purgeable state 來管理。就像一個快取池一樣,我們不用這個資源就把他標記為 volatile 的,我們想用就從池拿出來,判斷它是否為 empty,被釋放了就重新建立否則直接用。iOS 也開闢了大片的記憶體為這個 purgeable 的資源池,除非我們需要考慮重新建立的成本,否則你的視訊記憶體資源都應該是在不用後做成 volatile 的。

在 UE 程式中,我們基本會用到 texure streaming pool 去做 texure 的 streaming,用 mesh streaming pool 去做 mesh 的 streaming,還有各種 rt pool 等等,事實上這些 pool 裡所有的資源都應該走 volatile 的機制。這對視訊記憶體總量的節省是巨大的,並且更加科學,iOS 系統會自動在記憶體壓力下幫我釋放 cache。

UE 手遊在 iOS 平臺執行時記憶體佔用太高?試試這樣著手優化
基於 purgeable state 的資源池管理方式

Memoryless Resource

除了 purgeable state 之外,metal 的 resource 還可以指定它的 storage mode。Storage mode 用於指定 mtl 資源的被 cpu 和 gpu 訪問的途徑和儲存優化。對於用於做 rt 的 texture,有一個特殊的儲存模式叫做 memoryless。

我們知道對於 tbdr 的裝置,我們在建立一個 rt 之後,rt 的真正訪問是在 gpu 的 cache 上的,除非我們顯示的需要讀取它才需要把整個資源從 gpu cache resolve 到 memory 上的,所以在很多情況,我們是根本不需要存在一個 memory 上的那一份 rt 的。例如你的只用作深度測試的深度圖。這樣的資源在 iOS 上可以宣告為 memoryless 的 storage mode,這樣整個 mtltexture 物件在建立後其實並不會產生一個 memory 上的記憶體佔用,只會在 gpu 的 cache 上產生臨時的物件,並且用後也不會 resolve 回記憶體,相當於我們節省了整個 rt 的記憶體開銷。

UE 手遊在 iOS 平臺執行時記憶體佔用太高?試試這樣著手優化

在 iOS 上對於所有的不需要 resolve 的 rt(或 storeaction 設定為 don’t care)都應該設定為 memoryless。注意如果宣告瞭 meoryless 但是實際又去讀取了它則會產生 crash。

Memoryless 的資源同樣不計入 mem foot。

Metal resource heap

iOS 中 xture 和 buffer 等資源通常可以直接從 mtldevice 上建立。但是能帶來更多記憶體優化的方法是從 mtlheap 上建立。

Metal Resouce Heap 是一個抽象的用於建立 GPU 資源的 heap,它其實是維護了一個記憶體池。我們可以從 1 個 MTLHeap 上 subllocate 多個 texure 或者 buffer,這樣做有很多好處:

首先它減輕了資源建立的時間開銷,因為 heap 的後面是一個可複用的記憶體池。

然後因為 mtlheap 的記憶體可能被系統動態的壓縮不常用的區域,所以基於 mtlheap 可以減少記憶體佔用。

一個 mtlheap 上的 subllocate 資源擁有相同的 storage mode 和 cpu cache mode。另外 mtlheap 需要整體設定 purgeable state,而不能每個資源單獨設定。所以實際使用中我們也要有很多的 mtlheap 組成的池,那些 storage mode 相同,purgeable state 相同的資源從一個 heap 上分配,另外 metal 文件提到單個 heap 也不能過大,因為對 heap 的壓縮將影響其效能。

另外 mtlheap 上分配的資源支援 alias 機制,下面一段會講。

Resource Alias

從 mtlheap 上建立的資源支援 alias 機制,alias 也是 iOS 上對 gpu 資源的一個獨有優化機制,它是指一個被標記為 alias 的資源 A 可以被 mtlheap 重用,只要重用的資源 B 同 A 有相同的資源格式,只是內容不一致,並且邏輯上要保證 A 和 B 不會被 GPU 同時使用。

一個典型的使用場合是後處理鏈,在這裡面要涉及很多後處理階段,每個後處理階段用到不同的 rt,但是這些 rt 不會被同時使用,我們就可以把這 rt 做成 alias 的,然後在後面階段不斷被重用,但是分配的記憶體一直都是那一個。這個過程要注意使用 fence 或 event 來保證共享這塊記憶體的 rt 不會被同時使用到,這裡我們的做法應該是從 mtlheap 建立一個 rt,然後執行第一步後處理,然後插入一個 fence,呼叫它的 makealiasable,然後再建立第二個 rt,執行第二步後處理…依次下去。

iOS 通過 alias 為我們在保持邏輯層有多個資源的同時,做到了一個底層的記憶體共享。

關於 shader

Shader 也會佔用較多的視訊記憶體。除了常規的減少 shader 變體之外,我們還應該利用 metal 的 shaderlibrary 的預編譯,預先將 metal shader 編譯成 native 的 mtllibrary,執行時從 library 中載入 shader function,而不是動態從原始碼編譯 shader 。

首先從 mtllibrary 載入要更快,另外 mtllibrary 本身不佔用實體記憶體,只佔用虛存,只會在我們用到哪些 shader 的時候才產生記憶體佔用,且由於 native code 本身體積也很小,佔用記憶體少。而動態編譯 shader 會將原始碼載入記憶體進行便於,原始碼體積大本身就會產生更大的記憶體腳印。

3.2 UE 的 fmalloc

對於 fmalloc 的分配,除了常規的減少記憶體分配次數,儘量對齊記憶體,減少 traray 的 resize,用 inline allocater 等棧模擬等方法之外,在 iOS 上還有一些額外可以嘗試的操作。

UE 使用一個自帶的記憶體池(Fmalloc)去進行記憶體的分配釋放管理,預先分配整段記憶體,避免 malloc 產生磁碟碎片,這個過程會產生前面提到的所有記憶體池都會有的對齊浪費和頁空白浪費。這部分浪費的記憶體顯示在了 LLm 的 malloc unused 專案裡。

其實對於 iOS 來說,iOS 底層已經實現了類似的 malloc 小記憶體池,所以在專案裡可以實驗一下在 iOS 上不採用 UE 的 fmalloc 而直接用 malloc 交給 iOS 的記憶體池管理,對比下記憶體用量的區別,對於不同的專案這個哪個更好不好說,但是可以試一下。即使最終發現使用 UE 的 fmalloc 還是更優的話,還是可以試一下對於 256B 以內的小記憶體直接使用 iOS 的 malloc 對比測試一下,因為 iOS 的 nano malloc 對於小記憶體還做了額外的優化。

Vm_copy

前面提到了 iOS 上使用 vm_copy 來明確的使用 copy on write 優化,所以我們應該在程式碼裡大量的對於大塊記憶體的 memcpy 換成 vm_copy,除非你發現這裡的 copy 時間是個瓶頸。這種優化對於圖形程式的收益是很大的,一個典型的場景,我們從檔案載入模型資料,然後將其 copy 到申請的一個 mtlbuffer 上,在 copy 之後我們幾乎不會對這個記憶體做更改,如果使用 vm_copy,這個 copy 的操作就剩下了,也減少了記憶體腳印。

3.3 其他

程式碼段記憶體

另外對於 iOS 程式來說,程式碼段本身也有可能是個大頭,這部分可以被 swap out,所以當記憶體真正吃緊的時候危害相對沒這麼大,但還是可以想辦法減小。包括減少程式碼體積,減少模板的使用,strip 掉除錯符號,將一些 iOS 上不會用到的 UE 的 plugin 去掉等。

對 iOS 系統 lowmemorywarning 的響應

iOS 系統會根據不同的機型制定該機型記憶體告警的級別,如 xcode memory gauge 上面顯示的一樣,到達紅色區域(如 iphonexr 到達 2.1G)就會觸發記憶體告警,你的程式不能無視記憶體告警,如果在記憶體告警到達時不能有效的儘快減輕記憶體負擔,系統將會很快結束該程式以回收記憶體。我們需要在收到 didreceivememorywarning 的時候額外釋放大量任何可以釋放但不會導致程式異常的記憶體,例如你的各種 cache,這也可以大大減少系統異常退出的機率。

iOS 記憶體問題排查常用工具

UE 自帶的 memoryreport

這是最簡單方便的估計 UE 主要資源的方法,裡面整合了一些指令,可以看到常用資源如貼圖,uobject 等的詳細清單,記憶體,但是這裡面的記憶體值都是估計的,只供參考。

UE 自帶的 obj list 指令

用於列出任意 uobject 型別的例項清單,對於記憶體洩露和因 uobject 產生的記憶體優化很重要。

UE 自帶的 obj refs 指令

用於列出任何 uobject 例項的引用鏈條,可以在我們通過 obj list 找到洩露的物件後繼續追查它未被 GC 的原因。

UE 自帶的 LLM 工具

在《Android 記憶體分析和優化》中詳細講了 UE 這個工具的實現,它可以將 UE 範疇內分配的記憶體按 tag 列出,對視訊記憶體資源也能做出較準確的估計。

Xcode 的 allocation

這個就是平臺層面的工具,但是也是最全面的,它獲取整個 iOS 程式的記憶體情況,進行記憶體分佈分析,洩露查詢。這裡面可以按照各種 tag 列出整個虛存空間的分配情況,還可以看到如下圖整個程式的虛存空間每個地址上的分配情況,tag,大小,對映的實體記憶體大小,類似於 Android 平臺上的 pmap。

UE 手遊在 iOS 平臺執行時記憶體佔用太高?試試這樣著手優化

還可以插入 generation,來定位在某個時間段之內增長的記憶體。

注意這個工具裡面的幾個分類表述,all heap 是指從 malloc 途徑的分配,anonymous VM 是指所有不帶 tag 的 vm_allocate,而 UE 的 fmalloc 因為帶了 255 的 tag,所以不在 anonymous VM 分類下,而是在 all vm region 下面的 memory tag 255 下。

UE 手遊在 iOS 平臺執行時記憶體佔用太高?試試這樣著手優化

此外 Xcode 中的 leaker,graph capture 中的 gpu memory,以及 memorygraph 都是比較有用的排查 iOS 記憶體問題和做優化的工具。Memory graph 裡面就列出了所有程式範疇內的 vmobject 分佈及之間的關係。

總的來說,相比較與其他平臺,iOS 是一個從作業系統層面就極度追求優化的一個平臺,提供了大量平臺特有的記憶體優化手段,這使得同樣的程式可以在 iOS 上比其他平臺都有少的多的記憶體佔用,使及時對於大量只有 2G 記憶體的 iOS 裝置仍然能夠良好的體驗遊戲,而我們不能無視這些手段,需要利用好 iOS 提供給我們的武器去優化程式的記憶體使用。



相關文章