微信團隊原創分享:iOS版微信的記憶體監控系統技術實踐

jsjsjjs發表於2018-03-05

本文來自微信開發團隊yangyang的技術分享。

一、前言

FOOM(Foreground Out Of Memory),是指App在前臺因消耗記憶體過多引起系統強殺。對使用者而言,表現跟crash一樣。Facebook早在2015年8月提出FOOM檢測辦法,大致原理是排除各種情況後,剩餘的情況是FOOM,具體連結:https://code.facebook.com/posts/1146930688654547/reducing-fooms-in-the-facebook-ios-app/

微信自15年年底上線FOOM上報,從最初資料來看,每天FOOM次數與登入使用者數比例接近3%,同期crash率1%不到。而16年年初某東老大反饋微信頻繁閃退,在艱難拉取2G多日誌後,才發現kv上報頻繁打log引起FOOM。接著16年8月不少外部使用者反饋微信啟動不久後閃退,分析大量日誌還是不能找到FOOM原因。微信急需一個有效的記憶體監控工具來發現問題。

(本文同步釋出於:http://www.52im.net/thread-1422-1-1.html

二、實現原理

微信記憶體監控最初版本是使用Facebook的FBAllocationTracker工具監控OC物件分配,用fishhook工具hook malloc/free等介面監控堆記憶體分配,每隔1秒,把當前所有OC物件個數、TOP 200最大堆記憶體及其分配堆疊,用文字log輸出到本地。該方案實現簡單,一天內完成,通過給使用者下發TestFlight,最終發現聯絡人模組因遷移DB載入大量聯絡人導致FOOM。

不過這方案有不少缺點:

1)監控粒度不夠細,像大量分配小記憶體引起的質變無法監控,另外fishhook只能hook自身app的C介面呼叫,對系統庫不起作用;

2)打log間隔不好控制,間隔過長可能丟失中間峰值情況,間隔過短會引起耗電、io頻繁等效能問題;

3)上報的原始log靠人工分析,缺少好的頁面工具展現和歸類問題。

所以二期版本以Instruments的Allocations為參考,著重四個方面優化,分別是資料收集、儲存、上報及展現。

2.1、資料收集

16年9月底為了解決ios10 nano crash,研究了libmalloc原始碼,無意中發現這幾個介面:

當malloc_logger和__syscall_logger函式指標不為空時,malloc/free、vm_allocate/vm_deallocate等記憶體分配/釋放通過這兩個指標通知上層,這也是記憶體除錯工具malloc stack的實現原理。有了這兩個函式指標,我們很容易記錄當前存活物件的記憶體分配資訊(包括分配大小和分配堆疊)。分配堆疊可以用backtrace函式捕獲,但捕獲到的地址是虛擬記憶體地址,不能從符號表dsym解析符號。所以還要記錄每個image載入時的偏移slide,這樣符號表地址=堆疊地址-slide。

另外為了更好的歸類資料,每個記憶體物件應該有它所屬的分類Category,如上圖所示。對於堆記憶體物件,它的Category名是“Malloc ”+分配大小,如“Malloc 48.00KiB”;對於虛擬記憶體物件,呼叫vm_allocate建立時,最後的引數flags代表它是哪類虛擬記憶體,而這個flags正對應於上述函式指標__syscall_logger的第一個引數type,每個flag具體含義可以在標頭檔案找到;對於OC物件,它的Category名是OC類名,我們可以通過hook OC方法+[NSObject alloc]來獲取:

但後來發現,NSData建立物件的類靜態方法沒有呼叫+[NSObject alloc],裡面實現是呼叫C方法NSAllocateObject來建立物件,也就是說這類方式建立的OC物件無法通過hook來獲取OC類名。最後在蘋果開原始碼CF-1153.18找到了答案,當__CFOASafe=true並且__CFObjectAllocSetLastAllocEventNameFunction!=NULL時,CoreFoundation建立物件後通過這個函式指標告訴上層當前物件是什麼型別:

通過上面方式,我們的監控資料來源基本跟Allocations一樣了,當然是藉助了私有API。如果沒有足夠的“技巧”,私有API帶不上Appstore,我們只能退而求其次。修改malloc_default_zone函式返回的malloc_zone_t結構體裡的malloc、free等函式指標,也是可以監控堆記憶體分配,效果等同於malloc_logger;而虛擬記憶體分配只能通過fishhook方式。

2.2、資料儲存

2.2.1 存活物件管理

APP在執行期間會大量申請/釋放記憶體。以上圖為例,微信啟動10秒內,已經建立了80萬物件,釋放了50萬,效能問題是個挑戰。另外在儲存過程中,也儘量減少記憶體申請/釋放。所以放棄了sqlite,改用了更輕量級的平衡二叉樹來儲存。

伸展樹(Splay Tree),也叫分裂樹,是一種二叉排序樹,不保證樹是平衡,但各種操作平均時間複雜度是O(logN),可近似看作平衡二叉樹。相比其他平衡二叉樹(如紅黑樹),其記憶體佔用較小,不需要儲存額外資訊。伸展樹主要出發點是考慮到區域性性原理(某個剛被訪問的結點下次又被訪問,或者訪問次數多的結點下次可能被訪問),為了使整個查詢時間更少,被頻繁查詢的結點通過“伸展”操作搬移到離樹根更近的地方。大部分情況下,記憶體申請很快又被釋放,如autoreleased物件、臨時變數等;而OC物件申請記憶體後緊接著會更新它所屬Category。所以用伸展樹管理最適合不過了。

傳統二叉樹是用連結串列方式實現,每次新增/刪除結點,都會申請/釋放記憶體。為了減少記憶體操作,可以用陣列實現二叉樹。具體做法是父結點的左右孩子由以往的指標型別改成整數型別,代表孩子在陣列的下標;刪除結點時,被刪除的結點存放上一個被釋放的結點所在陣列下標。

2.2.2 堆疊儲存

據統計,微信執行期間,backtrace的堆疊有成百萬上千萬種,在捕獲最大棧長64情況下,平均棧長35。如果36bits儲存一個地址(armv8最大虛擬記憶體地址48bits,實際上36bits夠用了),一個堆疊平均儲存長度157.5bytes,1M個堆疊需要157.5M儲存空間。但通過斷點觀察,實際上大部分堆疊是有共同字尾,例如下面的兩個堆疊後7個地址是一樣的:

為此,可以用Hash Table來儲存這些堆疊。思路是整個堆疊以連結串列的方式插入到table裡,連結串列結點存放當前地址和上一個地址所在table的索引。每插入一個地址,先計算它的hash值,作為在table的索引,如果索引對應的slot沒有儲存資料,就記錄這個連結串列結點;如果有儲存資料,並且資料跟連結串列結點一致,hash命中,繼續處理下一個地址;資料不一致,意味著hash衝突,需要重新計算hash值,直到滿足儲存條件。舉個例子(簡化了hash計算):

1)Stack1的G、F、E、D、C、A、依次插入到Hash Table,索引1~6結點資料依次是(G, 0)、(F, 1)、(E, 2)、(D, 3)、(C, 4)、(A, 5)。Stack1索引入口是6;

2)輪到插入Stack2,由於G、F、E、D、C結點資料跟Stack1前5結點一致,hash命中;B插入新的7號位置,(B, 5)。Stack2索引入口是7;

3)最後插入Stack3,G、F、E、D結點hash命中;但由於Stack3的A的上一個地址D索引是4,而不是已有的(A, 5),hash不命中,查詢下一個空白位置8,插入結點(A, 4);B上一個地址A索引是8,而不是已有的(B, 5),hash不命中,查詢下一個空白位置9,插入結點(B, 9)。Stack3索引入口是9。

經過這樣的字尾壓縮儲存,平均棧長由原來的35縮短到5不到。而每個結點儲存長度為64bits(36bits儲存地址,28bits儲存parent索引),hashTable空間利用率60%+,一個堆疊平均儲存長度只需要66.7bytes,壓縮率高達42%。

2.2.3 效能資料

經過上述優化,記憶體監控工具在iPhone6Plus執行佔用CPU佔用率13%不到,當然這是跟資料量有關,重度使用者(如群過多、訊息頻繁等)可能佔用率稍微偏高。而儲存資料記憶體佔用量20M左右,都用mmap方式把檔案對映到記憶體。有關mmap好處可自行google之。

2.3、資料上報

由於記憶體監控是儲存了當前所有存活物件的記憶體分配資訊,資料量極大,所以當出現FOOM時,不可能全量上報,而是按某些規則有選擇性的上報。

首先把所有物件按Category進行歸類,統計每個Category的物件數和分配記憶體大小。這列表資料很少,可以做全量上報。接著對Category下所有相同堆疊做合併,計算每種堆疊的物件數和記憶體大小。對於某些Category,如分配大小TOP N,或者UI相關的(如UIViewController、UIView之類的),它裡面分配大小TOP M的堆疊才做上報。上報格式類似這樣:

2.4、頁面展現

頁面展現參考了Allocations,可看出有哪些Category,每個Category分配大小和物件數,某些Category還能看分配堆疊。

為了突出問題,提高解決問題效率,後臺先根據規則找出可能引起FOOM的Category(如上面的Suspect Categories),規則有:

● UIViewController數量是否異常

● UIView數量是否異常

● UIImage數量是否異常

● 其它Category分配大小是否異常,物件個數是否異常

接著對可疑的Category計算特徵值,也就是OOM原因。特徵值是由“Caller1”、“Caller2”和“Category, Reason”組成。Caller1是指申請記憶體點,Caller2是指具體場景或業務,它們都是從Category下分配大小第一的堆疊提取。Caller1提取儘量是有意義的,並不是分配函式的上一地址。例如:

所有report計算出特徵值後,可以對它們進行歸類了。一級分類可以是Caller1,也可以是Category,二級分類是與Caller1/Category有關的特徵聚合。效果如下。

一級分類:

二級分類:

2.5、運營策略

上面提到,記憶體監控會帶來一定的效能損耗,同時上報的資料量每次大概300K左右,全量上報對後臺有一定壓力,所以對現網使用者做抽樣開啟,灰度包使用者/公司內部使用者/白名單使用者做100%開啟。本地最多隻保留最近三次資料。

三、降低誤判

1)先回顧Facebook如何判定上一次啟動是否出現FOOM:

a) App沒有升級;

b) App沒有呼叫exit()或abort()退出;

c) App沒有出現crash;

d) 使用者沒有強退App;

e) 系統沒有升級/重啟;

f) App當時沒有後臺執行;

g) App出現FOOM。

1、2、4、5比較容易判斷,3依賴於自身CrashReport元件的crash回撥,6、7依賴於ApplicationState和前後臺切換通知。

微信自上線FOOM資料上報以來,出現不少誤判,主要情況有下面幾種。

2)ApplicationState不準:

部分系統會在後臺短暫喚起app,ApplicationState是Active,但又不是BackgroundFetch;執行完didFinishLaunchingWithOptions就退出了,也有收到BecomeActive通知,但很快也退出;整個啟動過程持續5~8秒不等。解決方法是收到BecomeActive通知一秒後,才認為這次啟動是正常的前臺啟動。這方法只能減少誤判概率,並不能徹底解決。

3)群控類外掛:

這類外掛是可以遠端控制iPhone的軟體,通常一臺電腦可以控制多臺手機,電腦畫面和手機螢幕實時同步操作,如開啟微信,自動加好友,發朋友圈,強制退出微信,這一過程容易產生誤判。解決方法只能通過安全後臺打擊才能減少這類誤判。

4)CrashReport元件出現crash沒有回撥上層:

微信曾經在17年5月底爆發大量GIF crash,該crash由記憶體越界引起,但收到crash訊號寫crashlog時,由於記憶體池損壞,元件無法正常寫crashlog,甚至引起二次crash;上層也無法收到crash通知,因此誤判為FOOM。目前改成不依賴crash回撥,只要本地存在上一次crashlog(不管是否完整),就認為是crash引起的APP重啟。

5)前臺卡死引起系統watchdog強殺:

也就是常見的0x8badf00d,通常原因是前臺執行緒過多,死鎖,或CPU使用率持續過高等,這類強殺無法被App捕獲。為此我們結合了已有卡頓系統,當前臺執行最後一刻有捕獲到卡頓,我們認為這次啟動是被watchdog強殺。同時我們從FOOM劃分出新的重啟原因叫“APP前臺卡死導致重啟”,列入重點關注。

四、成果顯著

微信自2017年三月上線記憶體監控以來,解決了30多處大大小小記憶體問題,涉及到聊天、搜尋、朋友圈等多個業務,FOOM率由17年年初3%,降到目前0.67%,而前臺卡死率由0.6%下降到0.3%,效果特別明顯。

五、常見問題

1)UIGraphicsEndImageContext:

UIGraphicsBeginImageContext和UIGraphicsEndImageContext必須成雙出現,不然會造成context洩漏。另外XCode的Analyze也能掃出這類問題。

2)UIWebView:

無論是開啟網頁,還是執行一段簡單的js程式碼,UIWebView都會佔用APP大量記憶體。而WKWebView不僅有出色的渲染效能,而且它有自己獨立程式,一些網頁相關的記憶體消耗移到自身程式裡,最適合取替UIWebView。

3)autoreleasepool:

通常autoreleased物件是在runloop結束時才釋放。如果在迴圈裡產生大量autoreleased物件,記憶體峰值會猛漲,甚至出現OOM。適當的新增autoreleasepool能及時釋放記憶體,降低峰值。

4)互相引用:

比較容易出現互相引用的地方是block裡使用了self,而self又持有這個block,只能通過程式碼規範來避免。另外NSTimer的target、CAAnimation的delegate,是對Obj強引用。目前微信通過自己實現的MMNoRetainTimer和MMDelegateCenter來規避這類問題。

5)大圖片處理:

舉個例子,以往圖片縮放介面是這樣寫的:

但處理大解析度圖片時,往往容易出現OOM,原因是-[UIImage drawInRect:]在繪製時,先解碼圖片,再生成原始解析度大小的bitmap,這是很耗記憶體的。解決方法是使用更低層的ImageIO介面,避免中間bitmap產生:

6)大檢視:

大檢視是指View的size過大,自身包含要渲染的內容。超長文字是微信裡常見的炸群訊息,通常幾千甚至幾萬行。如果把它繪製到同一個View裡,那將會消耗大量記憶體,同時造成嚴重卡頓。最好做法是把文字劃分成多個View繪製,利用TableView的複用機制,減少不必要的渲染和記憶體佔用。

六、推薦幾個iOS記憶體技術相關的連結

● Memory Usage Performance Guidelines

https://developer.apple.com/library/content/documentation/Performance/Conceptual/ManagingMemory/ManagingMemory.html#//apple_ref/doc/uid/10000160-SW1

● No pressure, Mon!

http://www.newosxbook.com/articles/MemoryPressure.html

附錄:微信、QQ文章彙總

[1] QQ、微信團隊原創技術文章:

微信團隊原創分享:iOS版微信的記憶體監控系統技術實踐

讓網際網路更快:新一代QUIC協議在騰訊的技術實踐分享

iOS後臺喚醒實戰:微信收款到賬語音提醒技術總結

騰訊技術分享:社交網路圖片的頻寬壓縮技術演進之路

微信團隊分享:視訊影像的超解析度技術原理和應用場景

微信團隊分享:微信每日億次實時音視訊聊天背後的技術解密

QQ音樂團隊分享:Android中的圖片壓縮技術詳解(上篇)

QQ音樂團隊分享:Android中的圖片壓縮技術詳解(下篇)

騰訊團隊分享:手機QQ中的人臉識別酷炫動畫效果實現詳解

騰訊團隊分享 :一次手Q聊天介面中圖片顯示bug的追蹤過程分享

微信團隊分享:微信Android版小視訊編碼填過的那些坑》 

微信手機端的本地資料全文檢索優化之路》 

企業微信客戶端中組織架構資料的同步更新方案優化實戰

微信團隊披露:微信介面卡死超級bug“15。。。。”的來龍去脈

QQ 18年:解密8億月活的QQ後臺服務介面隔離技術

月活8.89億的超級IM微信是如何進行Android端相容測試的

以手機QQ為例探討移動端IM中的“輕應用”

一篇文章get微信開源移動端資料庫元件WCDB的一切!

微信客戶端團隊負責人技術訪談:如何著手客戶端效能監控和優化

微信後臺基於時間序的海量資料冷熱分級架構設計實踐

微信團隊原創分享:Android版微信的臃腫之困與模組化實踐之路

微信後臺團隊:微信後臺非同步訊息佇列的優化升級實踐分享

微信團隊原創分享:微信客戶端SQLite資料庫損壞修復實踐》 

騰訊原創分享(一):如何大幅提升行動網路下手機QQ的圖片傳輸速度和成功率》 

騰訊原創分享(二):如何大幅壓縮行動網路下APP的流量消耗(下篇)》 

騰訊原創分享(二):如何大幅壓縮行動網路下APP的流量消耗(上篇)》 

微信Mars:微信內部正在使用的網路層封裝庫,即將開源》 

如約而至:微信自用的移動端IM網路層跨平臺元件庫Mars已正式開源》 

開源libco庫:單機千萬連線、支撐微信8億使用者的後臺框架基石 [原始碼下載]》 

微信新一代通訊安全解決方案:基於TLS1.3的MMTLS詳解》 

微信團隊原創分享:Android版微信後臺保活實戰分享(程式保活篇)》 

微信團隊原創分享:Android版微信後臺保活實戰分享(網路保活篇)》 

Android版微信從300KB到30MB的技術演進(PPT講稿) [附件下載]》 

微信團隊原創分享:Android版微信從300KB到30MB的技術演進》 

微信技術總監談架構:微信之道——大道至簡(演講全文)

微信技術總監談架構:微信之道——大道至簡(PPT講稿) [附件下載]》 

如何解讀《微信技術總監談架構:微信之道——大道至簡》

微信海量使用者背後的後臺系統儲存架構(視訊+PPT) [附件下載]

微信非同步化改造實踐:8億月活、單機千萬連線背後的後臺解決方案》 

微信朋友圈海量技術之道PPT [附件下載]》 

微信對網路影響的技術試驗及分析(論文全文)》 

一份微信後臺技術架構的總結性筆記》 

架構之道:3個程式設計師成就微信朋友圈日均10億釋出量[有視訊]》 

快速裂變:見證微信強大後臺架構從0到1的演進歷程(一)

快速裂變:見證微信強大後臺架構從0到1的演進歷程(二)》 

微信團隊原創分享:Android記憶體洩漏監控和優化技巧總結》 

全面總結iOS版微信升級iOS9遇到的各種“坑”》 

微信團隊原創資源混淆工具:讓你的APK立減1M》 

微信團隊原創Android資源混淆工具:AndResGuard [有原始碼]》 

Android版微信安裝包“減肥”實戰記錄》 

iOS版微信安裝包“減肥”實戰記錄》 

移動端IM實踐:iOS版微信介面卡頓監測方案》 

微信“紅包照片”背後的技術難題》 

移動端IM實踐:iOS版微信小視訊功能技術方案實錄》 

移動端IM實踐:Android版微信如何大幅提升互動效能(一)

移動端IM實踐:Android版微信如何大幅提升互動效能(二)

移動端IM實踐:實現Android版微信的智慧心跳機制》 

移動端IM實踐:WhatsApp、Line、微信的心跳策略分析》 

移動端IM實踐:谷歌訊息推送服務(GCM)研究(來自微信)

移動端IM實踐:iOS版微信的多裝置字型適配方案探討》 

信鴿團隊原創:一起走過 iOS10 上訊息推送(APNS)的坑

騰訊信鴿技術分享:百億級實時訊息推送的實戰經驗

>> 更多同類文章 ……

[2] 有關QQ、微信的技術故事:

2017微信資料包告:日活躍使用者達9億、日發訊息380億條

騰訊開發微信花了多少錢?技術難度真這麼大?難在哪?

技術往事:創業初期的騰訊——16年前的冬天,誰動了馬化騰的程式碼》 

技術往事:史上最全QQ圖示變遷過程,追尋IM巨人的演進歷史》 

技術往事:“QQ群”和“微信紅包”是怎麼來的?》 

開發往事:深度講述2010到2015,微信一路風雨的背後》 

開發往事:微信千年不變的那張閃屏圖片的由來》 

開發往事:記錄微信3.0版背後的故事(距微信1.0釋出9個月時)》 

一個微信實習生自述:我眼中的微信開發團隊

首次揭祕:QQ實時視訊聊天背後的神祕組織

>> 更多同類文章 ……

(本文同步釋出於:http://www.52im.net/thread-1422-1-1.html


相關文章