背景
根據Apple官方WWDC的回答,減少記憶體可以讓使用者體驗到更快的啟動速度,不會因為記憶體過大而導緻Crash,可以讓APP存活的更久。
對于高德地圖來說,根據線上資料的分析,記憶體過高會導緻導航過程中系統強殺OOM。尤其差別于其他APP的地方是,一般APP隻需要關注前台記憶體過高的系統強殺FOOM,高德地圖有不少使用者使用背景導航,是以也需要關注背景的記憶體過高導緻的系統強殺BOOM,且背景強殺較前台強殺更為嚴重。為了提升使用者體驗,記憶體治理迫在眉睫。
原理剖析
OOM
OOM是Out of Memory的縮寫。在iOS APP中如果記憶體超了,系統會把APP直接殺死,一種另類的Crash,且無法捕獲。發現OOM時,我們可以從裝置->隐私->分析與改進->分析資料中找到以JetsamEvent開頭的日志,日志裡面記錄了很多資訊:手機裝置資訊、系統版本、記憶體大小、CPU時間等。
Jetsam
Jetsam是iOS系統的一種資源管理機制。不同于MacOS、Linux、Windows等,iOS中沒有記憶體交換空間,是以在裝置整體記憶體緊張時,系統會将一些優先級不高或者占用記憶體過大的直接Kill掉。
通過iOS開源的XNU核心源碼可以分析到:
- 每個程序在核心中都存在一個優先級清單,JetSam在受到記憶體壓力時會從優先級清單最低的程序開始嘗試殺死,直到記憶體水位恢複到正常水位。
- Jetsam是通過get_task_phys_footprint擷取到phys_footprint的值,來決定要不要殺掉應用。
Jetsam機制清理政策可以總結為以下幾點:
- 單個APP實體記憶體占用超過上限會被清理,不同的裝置記憶體水位線不一樣。
- 整個裝置實體記憶體占用受到壓力時,優先清理背景應用,再清理前台應用。
- 優先清理記憶體占用高的應用,再記憶體占用低的應用。
- 相比系統應用,會優先清理使用者應用。
Android端為Low Memory Killer:
- 根據APP的優先級和使用總記憶體的多少,系統會在裝置記憶體吃緊情況下強殺應用。
- 記憶體吃緊的判斷取決于系統RSS(實際使用實體記憶體,包含共享庫占用的全部記憶體)的大小。
- 關鍵參數有3個:
1)oom_adj:在Framework層使用,代表程序的優先級,數值越高,優先級越低,越容易被殺死。
2)oom_adj threshold:在Framework層使用,代表oom_adj的記憶體門檻值。Android Kernel會定時檢測目前剩餘記憶體是否低于這個閥值,若低于則殺死oom_adj ≥該門檻值對應的oom_adj中,數值最大的程序,直到剩餘記憶體恢複至高于該閥值的狀态。
3)oom_score_adj:在Kernel層使用,由oom_adj換算而來,是殺死程序時實際使用的參數。
資料分析
phys_footprint擷取iOS應用總的實體記憶體,具體可以參考官方說明iOS Memory Deep Dive.
std::optional<size_t> memoryFootprint()
{
task\_vm\_info\_data\_t vmInfo;
mach\_msg\_type\_number\_t count = TASK\_VM\_INFO_COUNT;
kern\_return\_t result = task\_info(mach\_task\_self(), TASK\_VM\_INFO, (task\_info_t) &vmInfo, &count);
if (result != KERN_SUCCESS)
return std::nullopt;
return static\_cast<size\_t>(vmInfo.phys_footprint);
}
Instruments-VM Tracker可以用來分析具體記憶體分類,比如Malloc部分是堆記憶體,Webkit Malloc部分是JavaScriptCore占用的記憶體等。需要注意的是每個分類的記憶體值 = Dirty Size + Swapped。
通過Instruments VM Tracker抓取導航中記憶體分布進行對比分析。導航前台靜置時,高德地圖的總記憶體數值非常高,其中IOKit、WebKit Malloc和Malloc堆記憶體為記憶體占用大頭。
在分析過程中可以使用的工具很多,各有優缺點,需要配合使用,互相彌補。我們在分析的過程中主要用到Intruments VM Tracker、Allocations、Capture GPU Frame、MemGraph、dumpsys meminfo 、Graphics API Debugger、Arm Mobile Studio、AJX 記憶體分析工具、自研Malloc分析工具等。
- IOKit記憶體為地圖渲染顯存部分。
- WebKit Malloc記憶體為AJX JS業務記憶體。
- Malloc堆記憶體,我們通過Hook Malloc配置設定記憶體的API,通過抓取堆棧分析具體記憶體消費者。
治理優化
根據上面的資料分析,很容易做出從大頭開始抓起的思路。我們在治理過程中的大體思路:
- 分析資料:從記憶體大頭開始,分析各記憶體歸屬業務,以便業務進一步分析優化。
- 記憶體治理:優化技術方案減少記憶體開銷、高低端機功能分級和智能容災(即記憶體告警時通過功能降級等政策釋放記憶體)。
分而治之
據資料分析,高德地圖三大記憶體消耗分别是地圖渲染(Graphic顯存)、功能業務(JavaScriptCore)和通用業務(Malloc)。我們也主要從這三個方面入手優化。
地圖Graphic顯存優化
Xcode自帶Debug工具Capture GPU Frame,可以分析出具體顯存占用,顯存主要分為紋理Texture部分和Buffer部分,通過詳細的位址資訊分析具體消耗。Android端類似分析顯存工具可以用Google的Graphics API Debugger。
根據分析,Texture部分我們通過FBO繪制方式調整、矢量路口大圖背景優化、圖示跨頁面釋放、文字紋理優化、低端機關閉全屏抗鋸齒等減少顯存消耗。Buffer部分通過開啟低顯存模式、關閉四叉樹預加載、切背景釋放緩存資源等。
Webkit Malloc優化
高德地圖使用的是自研的動态化方案,依賴于iOS系統提供的架構JavaScriptCore,使用的業務記憶體消耗大多會被系統歸類到WebKit Malloc,從系統工具Instruments上的VM Tracker可以看出。此處有兩個思路,一個是業務自身優化記憶體消耗,第二個是動态化引擎和架構優化記憶體消耗。
業務自身優化,動态化方案的IDE提供記憶體分析工具可以清晰的輸出具體業務記憶體消耗在什麼地方,便于業務同學分析是否合理。
動态化引擎和架構優化,我們通過優化對系統庫JavaScriptCore的使用方式,即多個JSContextRef上下文共享同一份JSContextGroupRef的方式。多個頁面可以共享一份架構代碼,進而減少記憶體開銷。
Malloc堆記憶體優化
iOS端堆記憶體配置設定基本上使用的libmalloc庫,其中包含以下幾個記憶體操作接口:
// c配置設定方法
void *malloc(size\_t \_\_size) \_\_result\_use\_check \_\_alloc_size(1);
void *calloc(size\_t \_\_count, size\_t \_\_size) \_\_result\_use\_check \_\_alloc_size(1,2);
void free(void *);
void \*realloc(void \*\_\_ptr, size\_t \_\_size) \_\_result\_use\_check \_\_alloc\_size(2);
void *valloc(size\_t) \_\_alloc_size(1);
// block配置設定方法
// Create a heap based copy of a Block or simply add a reference to an existing one.
// This must be paired with Block_release to recover memory, even when running
// under Objective-C Garbage Collection.
BLOCK\_EXPORT void \*\_Block_copy(const void \*aBlock)
\_\_OSX\_AVAILABLE\_STARTING(\_\_MAC\_10\_6, \_\_IPHONE\_3_2);
通過hook記憶體操作API記錄下記憶體配置設定的堆棧、大小,即可分析記憶體使用情況。
同時源碼中還存在一個全局鈎子函數malloc_logger ,可輸出Malloc過程中的日志,定義如下:
// We set malloc_logger to NULL to disable logging, if we encounter errors
// during file writing
typedef void(malloc\_logger\_t)(uint32_t type,
uintptr_t arg1,
uintptr_t arg2,
uintptr_t arg3,
uintptr_t result,
uint32\_t num\_hot\_frames\_to_skip);
extern malloc\_logger\_t *malloc_logger;
iOS堆記憶體分析方案,可通過hook malloc系列API,也可以設定malloc_logger的函數實作,即可記錄下堆記憶體使用情況。
此方案有幾個難點問題,每秒鐘記憶體配置設定的量級大、記憶體有配置設定有釋放需要高效查詢和堆棧反解聚合。為此我們設計了一套完整的Malloc堆記憶體分析方案,來滿足快速定位堆記憶體歸屬,以便分發到各自業務Owner分析優化。
統一管理
随着業務的增長給高德地圖這個超級APP帶來了極大資源壓力,是以我們沉澱了一套自适應資源管理架構,來滿足不同業務場景在有限資源下能夠做到功能和體驗極緻均衡。主要的設計思路是通過監測使用者裝置等級、系統狀态、目前業務場景以及使用者行為,利用排程算法進行實時推算,統一管理協調APP目前資源狀态配置設定,對使用者目前不可見的記憶體等資源進行回收。
自适應資源管理架構-記憶體部分
可以根據不同的裝置等級、業務場景、使用者行為和系統狀态來管理資源。各業務都可以很容易的接入此架構,目前已經應用到多個業務場景,均有不錯的收益。
資料驗收
通過三個版本的連續治理,前背景導航場景均有50%的收益,同時Abort率也有10%~20%的收益。整體收益算是比較樂觀,但是随之而來的挑戰是我們該如何守住成果。
長線管控
所謂打江山容易守江山難,如果沒有長線管控的方案,随着業務的版本疊代,不出三五個版本就會将先前的優化消耗。為此我們建構了一套APM性能監控平台,在研發測試階段發現并解決問題,不把問題帶上線。
APM性能監控平台
為了将APP的性能做到日常監控,我們建設了一套線下「APM性能監控平台」,平台能夠支援正常業務場景的性能監控,包括:記憶體、CPU、流量等,能夠及時的發現問題并進行報警。再配合性能跟進流程,為用戶端性能保障把好最後一關。
記憶體分析工具
Xcode memory gauge:在Xcode的Debug navigator中,可以粗略檢視記憶體占用的情況。
Instruments - Allocations:可以檢視虛拟記憶體占用、堆資訊、對象資訊、調用棧資訊、VM Regions資訊等。可以利用這個工具分析記憶體,并針對地進行優化。
Instruments - Leaks:用于檢測記憶體洩漏。
Instruments - VM Tracker:可以檢視記憶體占用資訊,檢視各類型記憶體的占用情況,比如dirty memory的大小等等,可以輔助分析記憶體過大、記憶體洩漏等原因。
Instruments - Virtual Memory Trace:有記憶體分頁的具體資訊,具體可以參考WWDC 2016 - Syetem Trace in Depth。
Memory Resource Exceptions:從Xcode 10開始,記憶體占用過大時,調試器能捕獲到EXC_RESOURCE RESOURCE_TYPE_MEMORY異常,并斷點在觸發異常抛出的地方。
Xcode Memory Debugger:Xcode中可以直接檢視所有對象間的互相依賴關系,可以非常友善的查找循環引用的問題。同時,還可以将這些資訊導出為memgraph檔案。
memgraph + 指令行指令:結合上一步輸出的memgraph檔案,可以通過一些指令來分析記憶體情況。vmmap可以列印出程序資訊,以及VMRegions的資訊等,結合grep可以檢視指定VMRegion的資訊。leaks可追蹤堆中的對象,進而檢視記憶體洩漏、堆棧資訊等。heap會列印出堆中所有資訊,友善追蹤記憶體占用較大的對象。malloc_history可以檢視heap指令得到的對象的堆棧資訊,進而友善地發現問題。
總結:malloc_history ===> Creation;leaks ===> Reference;heap & vmmap ===> Size。
MetricKit:iOS 13新推出的監控架構,用于收集和處理電池和性能名額。當使用者使用APP的時候,iOS會記錄各項名額,然後發送到蘋果服務端上,并自動生成相關的可視化報告。通過Window -> Organizer -> Metrics可查,包括電池、啟動時間、卡頓情況、記憶體情況、磁盤讀寫五部分。也可以MetricKit內建到工程裡,将資料上傳到自己的服務進行分析。
MLeaksFinder:通過判斷UIViewController被銷毀後其子view是否也都被銷毀,可以在不入侵代碼的情況下檢測記憶體洩漏。
Graphics API Debugger:Google開源的一系列的Graphics調試工具,可以檢查、微調、重播應用對圖形驅動的API調用。
Arm Mobile Studio: 專業級GPU分析工具。