扣丁書屋

百度APP iOS端內存優化實踐-大塊內存監控方案

?

一. 背景

?內存不足引發的APP崩潰通常稱為OOM(Out Of Memory),iOS端無法捕獲OOM異常,也得不到任何堆棧信息,給我們排查和解決問題帶來很多困擾。引起OOM的原因歸根結底就是因為內存分配不合理引起的,尤其是內存處于危險水位時單次內存分配過大引起Jetsam機制開始生效而殺掉進程,通過我們線上數據監控,百度APP客戶端單次內存分配超過30M的case很多。

針對這種潛在的引起OOM的隱患,我們開發了一種大內存分配監控方案,充分利用線上監控優勢(豐富真實的用戶場景和用戶路徑)和線下流水線優勢(可獲取更多的堆棧信息),其中線上環境除了功能實現外,還要重點考慮穩定性,不能引入額外的性能問題,經過技術探索我們解決了此類難題,線上監控和線下流水線監控相結合實現對百度APP大塊內存的監控。

二. 技術方案綜述

大塊內存監控大體分為兩個功能模塊,缺一不可:

  • 獲取內存分配詳情。判斷單次內存分配是否超過閾值,若超過閾值,說明是大內存分配行為;
  • 獲取堆棧信息。豐富的堆棧信息可直接幫助開發同學定位到產生大內存分配的具體代碼,定位分配不合理的case。

最終可通過優化內存分配不合理的case,達到降低OOM率的目標。

三. 獲取內存分配詳情

3.1 方案對比

關于獲取iOS端每次內存分配信息,有如下解決方案:

  1. 通過 hook 內存分配函數 alloc 方法獲取,用 swizzle 方法實現 hook,存在的缺點是監控范圍不夠全面,只能監控 OC 對象,不能監控 C/C++ 對象。
  2. hook 庫libsystem_malloc 內存分配函數 malloc_zone_malloc、malloc_zone_calloc、malloc_zone_valloc、malloc_zone_realloc來獲取內存信息。這種方案對于 OC 對象和 C/C++ 對象都可監控,但是因為要 hook 系統 C/C++ 方法而不是 OC 方法,目前的技術條件需要使用 fishhook。

百度APP 采用的技術方案如下圖所示,首先通過重置 libsystem_malloc 庫中的malloc_logger 函數指針獲取內存活動詳情(分配和釋放兩種活動),然后通過Type類型過濾出內存分配的活動, 最后獲取內存分配大小,該方案可監控 OC 對象和 C/C++ 對象,對iOS框架系統沒有侵入性,沒有用 fishhook 庫所以沒有對 mach-o 文件做任何修改,也沒有 hook 任何底層分配內存系統方法,從 APP 性能和質量角度來說是最好的選擇。

3.2 libsystem_malloc源碼分析

libsystem_malloc.dylib 是iOS系統虛擬內存管理的核心庫之一,任何涉及到OC、C/C++ 對象的內存分配都會調用該庫的API,由它去調用操作系統Mach內核提供的接口去分配或釋放內存。具體來說 libsystem_malloc 提供了 malloc_zone_malloc,malloc_zone_calloc,malloc_zone_valloc,malloc_zone_realloc,malloc_zone_free 五個API來實現內存分配和釋放,在 iOS 系統中所有涉及到的內存活動都會調用如上接口,當我們App進程需要創建新的對象時,如調用 [NSObject alloc],或釋放對象調用 release 方法時(編譯器會添加),請求先會走到 libsystem_malloc.dylib 的上述函數。

Apple 已開源此庫,從如下地址可以下載到源碼:https://opensource.apple.com/source/libmalloc/

void *
malloc_zone_malloc(malloc_zone_t *zone, size_t size)
{
  MALLOC_TRACE(TRACE_malloc | DBG_FUNC_START, (uintptr_t)zone, size, 0, 0);

  void *ptr;
  if (malloc_check_start && (malloc_check_counter++ >= malloc_check_start)) {
    internal_check();
  }
  if (size > MALLOC_ABSOLUTE_MAX_SIZE) {
    return NULL;
  }

  ptr = zone->malloc(zone, size);    // if lite zone is passed in then we still call the lite methods

  if (malloc_logger) {
    malloc_logger(MALLOC_LOG_TYPE_ALLOCATE | MALLOC_LOG_TYPE_HAS_ZONE, (uintptr_t)zone, (uintptr_t)size, 0, (uintptr_t)ptr, 0);
  }

  MALLOC_TRACE(TRACE_malloc | DBG_FUNC_END, (uintptr_t)zone, size, (uintptr_t)ptr, 0);
  return ptr;
}

3.3 關鍵函數malloc_logger

從源碼中我們發現malloc_zone_malloc、malloc_zone_calloc、malloc_zone_valloc、malloc_zone_realloc、malloc_zone_free五個API,在每次調用mach內核函數進行內存分配和釋放后都有如下函數調用:


if (malloc_logger) {
  malloc_logger(MALLOC_LOG_TYPE_ALLOCATE | MALLOC_LOG_TYPE_HAS_ZONE, (uintptr_t)zone, (uintptr_t)size, 0, (uintptr_t)ptr, 0);
}

先判斷malloc_logger函數指針是否為空,如果不為空會調用上述函數,將內存活動的詳細信息通過該函數傳遞進來,從源碼分析的角度來看這是iOS系統提供的一個日志函數,具體函數定義如下所示:


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;

根據源碼我們對malloc_logger函數的入參做如下分析:

參數名稱 參數意義
type 類別,不同函數入參都不同
arg1 分配內存后zone地址
arg2 malloc_zone_realloc時為零,其他情況代表分配內存大小
arg3 malloc_zone_realloc時代表內存分配大小,其他情況為0
result 新內存起始地址ptr
num_hot_frames_to_skip 值都為0

對于第一個type字段,不同函數都不同,具體值后面詳細解釋,此外,我們發現malloc_logger生命為extern類型,在C++中聲明extern關鍵字的全局變量和函數可以使得它們能夠跨文件被訪問。

3.4 通過重置malloc_logger函數指針獲取內存活動詳情

在前面章節我們說過malloc_logger為extern全局變量,所以通過以下步驟可以重置該變量獲取內存活動詳情;

1. 引入libmalloc頭文件malloc/malloc.h

2. 定義函數bba_malloc_stack_logger,參數定義與源碼定義完全一致,這樣做的目的是防止實參傳遞的時候出現類型和參數個數不一致的問題。

3. 先保存malloc_logger函數指針的值到一個臨時變量origin_malloc_logger,目的是保存系統原始調用方法,交換函數指針后還要調用此方法。


#import <malloc/malloc.h>  
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); 
//定義函數bba_malloc_stack_logger
void bba_malloc_stack_logger(uint32_t type, uintptr_t arg1, uintptr_t arg2, uintptr_t arg3, uintptr_t result, uint32_t backtrace_to_skip); 
// 保存malloc_logger到臨時變量origin_malloc_logger
orgin_malloc_logger = malloc_logger;

4. 將malloc_logger賦值為自定義函數bba_malloc_stack_logger,在定義函數中先調用原始的系統方法origin_malloc_logger,該方法的調用保證了本方案對系統沒有侵入性,接下來做大塊內存檢測。


//malloc_logger賦值為自定義函數bba_malloc_stack_logger
malloc_logger = (malloc_logger_t *)bba_malloc_stack_logger; 
//bba_malloc_stack_logger具體實現
void bba_malloc_stack_logger(uint32_t type, uintptr_t arg1, uintptr_t arg2, uintptr_t arg3, uintptr_t result, uint32_t backtrace_to_skip)
{
    if (orgin_malloc_logger != NULL) {
        orgin_malloc_logger(type, arg1, arg2, arg3, result, backtrace_to_skip);
    }
    //大塊內存監控
    ......
}

經過上面四個步驟,malloc_zone_malloc、malloc_zone_calloc、malloc_zone_valloc、malloc_zone_realloc每次內存分配結束后,調用如下函數,因為malloc_logger現在不為空,具體值為bba_malloc_stack_logger,所以在bba_malloc_stack_logger中可以獲取內存分配活動詳情。

if (malloc_logger) {
    malloc_logger(MALLOC_LOG_TYPE_ALLOCATE | MALLOC_LOG_TYPE_HAS_ZONE, (uintptr_t)zone, (uintptr_t)size, 0, (uintptr_t)ptr, 0);
}

3.5 通過type類型過濾出內存分配詳情

通過上面章節我們知道malloc_zone_malloc、malloc_zone_calloc、malloc_zone_valloc、malloc_zone_realloc、malloc_zone_free五個API都會調用bba_malloc_stack_logger,其中的API實現又各有不同,malloc_zone_malloc、malloc_zone_calloc、malloc_zone_valloc代表內存分配,malloc_zone_realloc代表內存先釋放再分配,malloc_zone_free代表內存釋放,不同的API調用是通過入參type來區分的,所以本技術方案通過type反解析來獲取內存分配,過濾掉內存釋放。

API 含義 type
malloc_zone_malloc 分配內存給一個對象 MALLOC_LOG_TYPE_ALLOCATE MALLOC_LOG_TYPE_HAS_ZONE
malloc_zone_calloc 分配內存給多個對象 MALLOC_LOG_TYPE_ALLOCATE MALLOC_LOG_TYPE_HAS_ZONE MALLOC_LOG_TYPE_CLEARED
malloc_zone_valloc 分配內存給一個對象 MALLOC_LOG_TYPE_ALLOCATE MALLOC_LOG_TYPE_HAS_ZONE
malloc_zone_realloc 重新分配內存 MALLOC_LOG_TYPE_ALLOCATE MALLOC_LOG_TYPE_DEALLOCATE MALLOC_LOG_TYPE_HAS_ZONE
malloc_zone_free 釋放內存 MALLOC_LOG_TYPE_DEALLOCATE MALLOC_LOG_TYPE_HAS_ZONE

3.6 獲取單次內存分配大小并判斷是否超過閾值

根據源碼我們知道malloc_logger函數的入參arg2,arg3代表內存分配大小,不同type代表含義不同,具體見下面表格分析。

參數名稱 說明
arg2 type值malloc_zone_realloc時為零,type值為malloc_zone_malloc、malloc_zone_calloc、malloc_zone_valloc代表分配內存大小
arg3 type值malloc_zone_realloc時代表內存分配大小,type值為malloc_zone_malloc、malloc_zone_calloc、malloc_zone_valloc為0

接下來判斷是否超過我們設定的閾值的大小,在iOS端根據經驗單次內次分配8M就是普遍認為的大塊內存,當然這個值由服務端下發可靈活修改,客戶端寫個默認值即可,但是這個值不建議很小,太小會多次觸發大塊內存監控邏輯影響我們手機app的性能,超過閾值大小就進入下面的環節獲取堆棧信息。

四. 獲取堆棧信息

4.1 百度App采用的技術方案

調用系統方法backtrace_symbols可直接獲取堆棧信息,但是存在兩個問題,第一、方法具有線程屬性,必須要在獲取堆棧信息的當前線程調用;第二、耗時嚴重,實測在中高端機(iPhone8以上)有30ms耗時,在低端機(iPhone8以下)有100ms的耗時。如果大塊內存是在主線程分配的,上述耗時會引起主線程卡頓問題,故此方案無法針在線上生產環境使用。

針對這個問題,百度App采用的方案如下所示,結合客戶端和服務端雙端優勢,首先利用dyld庫生成了APP所有庫的起始地址和結束地址,將獲取堆棧函數地址信息詳情步驟獲取完全放在獨立子線程中實現,在客戶端拼接成crash日志格式,充分利用服務端做堆棧詳情反解析操作,客戶端只需要在專屬子線程執行耗時較少的堆棧函數地址比較操作即可,這樣做不會影響所屬線程任何操作,更不會引入性能問題,完全克服了系統方法的不足,具體操作流程如下圖所示:

4.2 生成所有庫的地址范圍(dyld)

生成APP中mach-o文件的所有庫的信息,該信息包括庫名稱、庫起始地址和結束地址,該操作主要利用dyld庫函數在子線程實現,不會占用主線程和內存分配所屬線程任何資源。dyld庫提供了豐富的api可以獲取上述數據,具體來說,_dyld_image_count可獲取所有模塊數目,dyld_get_image_name 可獲取模塊名稱,dyld_get_image_header可獲取每個模塊的起始地址,_dyld_get_image_vmaddr_slide獲取單個模塊的隨機基址。

4.3 backtrace獲取堆棧地址

當檢測到大塊內存時,在分配內存所屬線程調用backtrace方法獲取堆棧函數地址,代碼示例:

//返回值depth表示實際上獲取的堆棧的深度,stacks用來存儲堆棧地址信息,20表示指定堆棧深度。
size_t depth = backtrace((void**)stacks, 20);

那么backtrace耗時究竟如何?通過實踐數據證明,堆棧深度設置為20,實測高端機耗時在3ms以內,對性能影響基本可以忽略,此外,不是每次內存分配都需要調用backtrace獲取堆棧,只有單次內存分配大小符合大塊內存標準才會去獲取堆棧。

因此我們在線上生產環境堆棧深度設置為20并且只對高端機開放,深度值太小獲取的堆棧信息有限,太大會明顯增加backtrace方法耗時,線上數據證明堆棧深度設置為20既能滿足性能要求又能解析出合理的堆棧信息,在線下流水線場景下堆棧深度為40以獲取更豐富的堆棧信息,兩個場景各自發揮自己的優勢并相互補充。

4.4 獲取每個地址詳細信息

經過4.3步驟,我們獲取了堆棧地址,但是這個還不夠,為了方便服務端直接可以解析出堆棧信息,我們在客戶端需要將堆棧地址拼裝成下圖所示堆棧格式(類似Crash堆棧),libsystem_kernel.dylib 0x1b8a9dcf8 0x1b8a73000 + 175352 ,第一項是庫名稱,第二項是堆棧函數地址(十六進制),第三項是動態庫的起始地址(十六進制),第四項是十進制偏移量,其中第二項堆棧函數地址是通過4.3步驟的backtrace獲取的,下面的重點是獲取每個地址對應的動態庫名稱和相對于動態庫起始地址的偏移量。

對于上述詳細信息的獲取,本技術方案將該操作完全放在子線程去實現,因為我們已經在4.2構建好了每個庫的起始地址和結束地址,只需要遍歷一遍全量庫,判斷地址是否大于該庫起始地址并小于該庫的結束地址,那說明該地址就是屬于這個庫,從而得到該地址的詳細信息,堆棧地址和動態庫其實地址做差值可獲取偏移量信息。

4.5 atos和dsym解析堆棧

經過前面的步驟生成了類似crash日志格式的堆棧,上報服務端后,最后在服務端通過atos命令和dsym文件就可以反解還原出對應的堆棧內容,如:

BaiduBoxApp  0x000000010ff0ceb4 +[BBAJSONSerialization dataFromJSONObject:error:] + 256

通過這種方式可以把耗時較高的符號還原工作放到服務器端,客戶端只需要執行耗時較少的堆棧函數地址比較操作即可,放在子線程隊列執行,不會影響所屬線程任何操作,更不會引入性能問題。

五. 總結

本文主要介紹百度APP大塊內存監控方案,目前在生產環境和線下流水線環境均已部署,通過該方案實現了如下三個目標:

  1. 降低OOM率:如果內存分配不合理,優化后對降低OOM率有幫助;
  2. 數據摸底,讓我們明確知道百度APP哪些場景有大塊內存分配;
  3. 起到預防的作用,因為我們有明確的監控機制,督促每個開發同學創建內存對象時采用適量原則避免無節制分配。

六. 參考鏈接

[1] libsystem_malloc.dylib源碼

https://opensource.apple.com/source/libmalloc/

[2] Mach-O文檔介紹

https://developer.apple.com/library/archive/documentation/DeveloperTools/Conceptual/MachOTopics/0-Introduction/introduction.html

[3] Mach-O源碼

https://opensource.apple.com/source/xnu/xnu-1228.0.2/EXTERNAL_HEADERS/mach-o/loader.h.auto.html

[4]fishhook

https://github.com/facebook/fishhook


https://mp.weixin.qq.com/s/ke9bMHTfe1Ioq0eVfAxr2A

最多閱讀

iOS 性能檢測新方式?——AnimationHitches 1年以前  |  22531次閱讀
快速配置 Sign In with Apple 3年以前  |  6305次閱讀
APP適配iOS11 4年以前  |  5059次閱讀
App Store 審核指南[2017年最新版本] 4年以前  |  4899次閱讀
所有iPhone設備尺寸匯總 4年以前  |  4796次閱讀
使用 GPUImage 實現一個簡單相機 3年以前  |  4561次閱讀
開篇 關于iOS越獄開發 4年以前  |  4186次閱讀
在越獄的iPhone設置上使用lldb調試 4年以前  |  4093次閱讀
給數組NSMutableArray排序 4年以前  |  4009次閱讀
使用ssh訪問越獄iPhone的兩種方式 4年以前  |  3708次閱讀
UITableViewCell高亮效果實現 4年以前  |  3705次閱讀
關于Xcode不能打印崩潰日志 4年以前  |  3632次閱讀
iOS虛擬定位原理與預防 1年以前  |  3629次閱讀
使用ssh 訪問越獄iPhone的兩種方式 4年以前  |  3447次閱讀

手機掃碼閱讀
18禁止午夜福利体验区,人与动人物xxxx毛片人与狍,色男人窝网站聚色窝
<蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <文本链> <文本链> <文本链> <文本链> <文本链> <文本链>