iOS微信內存監控

歡迎你們前往雲+社區,獲取更多騰訊海量技術實踐乾貨哦~html

做者:楊津,騰訊移動客戶端開發 高級工程師
WeTest質量開放平臺團隊 發佈在雲+社區
商業轉載請聯繫騰訊WeTest得到受權,非商業轉載請註明出處。
原文連接: http://wetest.qq.com/lab/view/367.html

WeTest 導讀

目前iOS主流的內存監控工具是Instruments的Allocations,但只能用於開發階段。本文介紹如何實現離線化的內存監控工具,用於App上線後發現內存問題。ios

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緣由。微信急需一個有效的內存監控工具來發現問題。sql

1、實現原理

微信內存監控最第一版本是使用Facebook的FBAllocationTracker工具監控OC對象分配,用fishhook工具hook malloc/free等接口監控堆內存分配,每隔1秒,把當前全部OC對象個數、TOP 200最大堆內存及其分配堆棧,用文本log輸出到本地。該方案實現簡單,一天內完成,經過給用戶下發TestFlight,最終發現聯繫人模塊因遷移DB加載大量聯繫人致使FOOM。數據庫

不過這方案有很多缺點:數組

一、監控粒度不夠細,像大量分配小內存引發的質變沒法監控,另外fishhook只能hook自身app的C接口調用,對系統庫不起做用;安全

二、打log間隔很差控制,間隔過長可能丟失中間峯值狀況,間隔太短會引發耗電、io頻繁等性能問題;微信

三、上報的原始log靠人工分析,缺乏好的頁面工具展示和歸類問題。app

因此二期版本以Instruments的Allocations爲參考,着重四個方面優化,分別是數據收集、存儲、上報及展示。機器學習

###1.數據收集
16年9月底爲了解決ios10 nano crash,研究了libmalloc源碼,無心中發現這幾個接口:

1.png

當malloc_logger和__syscall_logger函數指針不爲空時,malloc/free、vm_allocate/vm_deallocate等內存分配/釋放經過這兩個指針通知上層,這也是內存調試工具malloc stack的實現原理。有了這兩個函數指針,咱們很容易記錄當前存活對象的內存分配信息(包括分配大小和分配堆棧)。分配堆棧能夠用backtrace函數捕獲,但捕獲到的地址是虛擬內存地址,不能從符號表dsym解析符號。因此還要記錄每一個image加載時的偏移slide,這樣符號表地址=堆棧地址-slide。

2.png

另外爲了更好的歸類數據,每一個內存對象應該有它所屬的分類Category,如上圖所示。對於堆內存對象,它的Category名是「Malloc 」+分配大小,如「Malloc 48.00KiB」;對於虛擬內存對象,調用vm_allocate建立時,最後的參數flags表明它是哪類虛擬內存,而這個flags正對應於上述函數指針__syscall_logger的第一個參數type,每一個flag具體含義能夠在頭文件<mach/vm_statistics.h>找到;對於OC對象,它的Category名是OC類名,咱們能夠經過hook OC方法+NSObject alloc來獲取:

3.png

但後來發現,NSData建立對象的類靜態方法沒有調用+NSObject alloc,裏面實現是調用C方法NSAllocateObject來建立對象,也就是說這類方式建立的OC對象沒法經過hook來獲取OC類名。最後在蘋果開源代碼CF-1153.18找到了答案,當CFOASafe=true而且CFObjectAllocSetLastAllocEventNameFunction!=NULL時,CoreFoundation建立對象後經過這個函數指針告訴上層當前對象是什麼類型:

4.png

經過上面方式,咱們的監控數據來源基本跟Allocations同樣了,固然是藉助了私有API。若是沒有足夠的「技巧」,私有API帶不上Appstore,咱們只能退而求其次。修改malloc_default_zone函數返回的malloc_zone_t結構體裏的malloc、free等函數指針,也是能夠監控堆內存分配,效果等同於malloc_logger;而虛擬內存分配只能經過fishhook方式。

2.數據存儲

存活對象管理

APP在運行期間會大量申請/釋放內存。以上圖爲例,微信啓動10秒內,已經建立了80萬對象,釋放了50萬,性能問題是個挑戰。另外在存儲過程當中,也儘可能減小內存申請/釋放。因此放棄了sqlite,改用了更輕量級的平衡二叉樹來存儲。

伸展樹(Splay Tree),也叫分裂樹,是一種二叉排序樹,不保證樹是平衡,但各類操做平均時間複雜度是O(logN),可近似看做平衡二叉樹。相比其餘平衡二叉樹(如紅黑樹),其內存佔用較小,不須要存儲額外信息。伸展樹主要出發點是考慮到局部性原理(某個剛被訪問的結點下次又被訪問,或者訪問次數多的結點下次可能被訪問),爲了使整個查找時間更少,被頻繁查詢的結點經過「伸展」操做搬移到離樹根更近的地方。大部分狀況下,內存申請很快又被釋放,如autoreleased對象、臨時變量等;而OC對象申請內存後緊接着會更新它所屬Category。因此用伸展樹管理最適合不過了。

傳統二叉樹是用鏈表方式實現,每次添加/刪除結點,都會申請/釋放內存。爲了減小內存操做,能夠用數組實現二叉樹。具體作法是父結點的左右孩子由以往的指針類型改爲整數類型,表明孩子在數組的下標;刪除結點時,被刪除的結點存放上一個被釋放的結點所在數組下標。

5.png

堆棧存儲

據統計,微信運行期間,backtrace的堆棧有成百萬上千萬種,在捕獲最大棧長64狀況下,平均棧長35。若是36bits存儲一個地址(armv8最大虛擬內存地址48bits,實際上36bits夠用了),一個堆棧平均存儲長度157.5bytes,1M個堆棧須要157.5M存儲空間。但經過斷點觀察,實際上大部分堆棧是有共同後綴,例以下面的兩個堆棧後7個地址是同樣的:

6.png

爲此,能夠用Hash Table來存儲這些堆棧。思路是整個堆棧以鏈表的方式插入到table裏,鏈表結點存放當前地址和上一個地址所在table的索引。每插入一個地址,先計算它的hash值,做爲在table的索引,若是索引對應的slot沒有存儲數據,就記錄這個鏈表結點;若是有存儲數據,而且數據跟鏈表結點一致,hash命中,繼續處理下一個地址;數據不一致,意味着hash衝突,須要從新計算hash值,直到知足存儲條件。舉個例子(簡化了hash計算):

7.png

  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%。

性能數據

通過上述優化,內存監控工具在iPhone6Plus運行佔用CPU佔用率13%不到,固然這是跟數據量有關,重度用戶(如羣過多、消息頻繁等)可能佔用率稍微偏高。而存儲數據內存佔用量20M左右,都用mmap方式把文件映射到內存。有關mmap好處可自行google之。8.png

3.數據上報

因爲內存監控是存儲了當前全部存活對象的內存分配信息,數據量極大,因此當出現FOOM時,不可能全量上報,而是按某些規則有選擇性的上報。

首先把全部對象按Category進行歸類,統計每一個Category的對象數和分配內存大小。這列表數據不多,能夠作全量上報。接着對Category下全部相同堆棧作合併,計算每種堆棧的對象數和內存大小。對於某些Category,如分配大小TOP N,或者UI相關的(如UIViewController、UIView之類的),它裏面分配大小TOP M的堆棧才作上報。上報格式相似這樣:9.png

4.頁面展示

頁面展示參考了Allocations,可看出有哪些Category,每一個Category分配大小和對象數,某些Category還能看分配堆棧。10.png

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

  • UIViewController數量是否異常
  • UIView數量是否異常
  • UIImage數量是否異常
  • 其它Category分配大小是否異常,對象個數是否異常

接着對可疑的Category計算特徵值,也就是OOM緣由。特徵值是由「Caller1」、「Caller2」和「Category, Reason」組成。Caller1是指申請內存點,Caller2是指具體場景或業務,它們都是從Category下分配大小第一的堆棧提取。Caller1提取儘可能是有意義的,並非分配函數的上一地址。例如:11.png

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

一級分類12.png
二級分類
13.png

5.運營策略

上面提到,內存監控會帶來必定的性能損耗,同時上報的數據量每次大概300K左右,全量上報對後臺有必定壓力,因此對現網用戶作抽樣開啓,灰度包用戶/公司內部用戶/白名單用戶作100%開啓。本地最多隻保留最近三次數據。

2、下降誤判

先回顧Facebook如何斷定上一次啓動是否出現FOOM:14.jpg

  1. App沒有升級
  2. App沒有調用exit()或abort()退出
  3. App沒有出現crash
  4. 用戶沒有強退App
  5. 系統沒有升級/重啓
  6. App當時沒有後臺運行
  7. App出現FOOM

一、二、四、5比較容易判斷,3依賴於自身CrashReport組件的crash回調,六、7依賴於ApplicationState和先後臺切換通知。微信自上線FOOM數據上報以來,出現很多誤判,主要狀況有:

ApplicationState不許

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

羣控類外掛

這類外掛是能夠遠程控制iPhone的軟件,一般一臺電腦能夠控制多臺手機,電腦畫面和手機屏幕實時同步操做,如開啓微信,自動加好友,發朋友圈,強制退出微信,這一過程容易產生誤判。解決方法只能經過安全後臺打擊才能減小這類誤判。

CrashReport組件出現crash沒有回調上層

微信曾經在17年5月底爆發大量GIF crash,該crash由內存越界引發,但收到crash信號寫crashlog時,因爲內存池損壞,組件沒法正常寫crashlog,甚至引發二次crash;上層也沒法收到crash通知,所以誤判爲FOOM。目前改爲不依賴crash回調,只要本地存在上一次crashlog(無論是否完整),就認爲是crash引發的APP重啓。

前臺卡死引發系統watchdog強殺

也就是常見的0x8badf00d,一般緣由是前臺線程過多,死鎖,或CPU使用率持續太高等,這類強殺沒法被App捕獲。爲此咱們結合了已有卡頓系統,當前臺運行最後一刻有捕獲到卡頓,咱們認爲此次啓動是被watchdog強殺。同時咱們從FOOM劃分出新的重啓緣由叫「APP前臺卡死致使重啓」,列入重點關注。

3、成果

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

4、常見問題

UIGraphicsEndImageContext

UIGraphicsBeginImageContext和UIGraphicsEndImageContext必須成雙出現,否則會形成context泄漏。另外XCode的Analyze也能掃出這類問題。

UIWebView

不管是打開網頁,仍是執行一段簡單的js代碼,UIWebView都會佔用APP大量內存。而WKWebView不只有出色的渲染性能,並且它有本身獨立進程,一些網頁相關的內存消耗移到自身進程裏,最適合取替UIWebView。

autoreleasepool

一般autoreleased對象是在runloop結束時才釋放。若是在循環裏產生大量autoreleased對象,內存峯值會猛漲,甚至出現OOM。適當的添加autoreleasepool能及時釋放內存,下降峯值。

互相引用

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

大圖片處理

舉個例子,以往圖片縮放接口是這樣寫的:17.png

但處理大分辨率圖片時,每每容易出現OOM,緣由是-UIImage drawInRect:在繪製時,先解碼圖片,再生成原始分辨率大小的bitmap,這是很耗內存的。解決方法是使用更低層的ImageIO接口,避免中間bitmap產生:18.png
大視圖

大視圖是指View的size過大,自身包含要渲染的內容。超長文本是微信裏常見的炸羣消息,一般幾千甚至幾萬行。若是把它繪製到同一個View裏,那將會消耗大量內存,同時形成嚴重卡頓。最好作法是把文本劃分紅多個View繪製,利用TableView的複用機制,減小沒必要要的渲染和內存佔用。

推薦文章最後推薦幾個iOS內存相關的連接:

騰訊WeTest iOS預審工具

爲了提升IEG蘋果審覈經過率,騰訊專門成立了蘋果審覈測試團隊,打造出iOS預審工具這款產品。通過1年半的內部運營,騰訊內部應用的iOS審覈經過率從平均35%提高到90%+。

現將騰訊內部產品的過審經驗,以線上工具的形式共享給各位。在WeTest騰訊質量開放平臺上能夠在線使用。點擊便可當即體驗!

若是使用當中有任何疑問,歡迎聯繫騰訊WeTest企業QQ:800024531

iOS預審服務

【掃描工具】上傳IPA包、圖片、視頻、應用描述便可進行測試; 多維度自動掃描提審材料的被拒風險;1小時內反饋全面的掃描報告。
【專家預審】騰訊專家爲您遍歷App全部功能模塊;全面暴露App內容被拒風險;跟進問題直至上線(需提供官方拒絕郵件)。
【專家諮詢】資深預審專家一對一服務; 諮詢時間靈活可選,按需購買;有的放矢解 決審覈問題。
【ASO優化】專業團隊多維度深度剖析App的ASO現狀;圍繞App目標用戶羣篩選高 度關聯的關鍵詞;幫助提高App在蘋果應用商店中的曝光率。

相關閱讀

2017年數據庫技術盤點
機器學習算法之旅
Android圖像處理 - 高斯模糊的原理及實現


此文已由做者受權雲加社區發佈,轉載請註明文章出處

相關文章
相關標籤/搜索