高德地圖駕車導航記憶體優化原理與實戰
背景
根據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分析工具。