背景
根據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分析工具。