tft每日頭條

 > 生活

 > ios微信占内存

ios微信占内存

生活 更新时间:2024-11-23 04:15:50

ios微信占内存(iOS微信内存監控)1

WeTest 導讀

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


FOOM(Foreground Out Of Memory),是指App在前台因消耗内存過多引起系統強殺。對用戶而言,表現跟crash一樣。Facebook早在2015年8月提出FOOM檢測辦法,大緻原理是排除各種情況後,剩餘的情況是FOOM。

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

一、實現原理

微信内存監控最初版本是使用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為參考,着重四個方面優化,分别是數據收集、存儲、上報及展現。

1.數據收集

16年9月底為了解決ios10 nano crash,研究了libmalloc源碼,無意中發現這幾個接口:

ios微信占内存(iOS微信内存監控)2

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

ios微信占内存(iOS微信内存監控)3

另外為了更好的歸類數據,每個内存對象應該有它所屬的分類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]來獲取:

ios微信占内存(iOS微信内存監控)4

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

ios微信占内存(iOS微信内存監控)5

通過上面方式,我們的監控數據來源基本跟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。所以用伸展樹管理最适合不過了。

傳統二叉樹是用鍊表方式實現,每次添加/删除結點,都會申請/釋放内存。為了減少内存操作,可以用數組實現二叉樹。具體做法是父結點的左右孩子由以往的指針類型改成整數類型,代表孩子在數組的下标;删除結點時,被删除的結點存放上一個被釋放的結點所在數組下标。

ios微信占内存(iOS微信内存監控)6

堆棧存儲

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

ios微信占内存(iOS微信内存監控)7

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

ios微信占内存(iOS微信内存監控)8

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

ios微信占内存(iOS微信内存監控)9

3.數據上報

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

首先把所有對象按Category進行歸類,統計每個Category的對象數和分配内存大小。這列表數據很少,可以做全量上報。接着對Category下所有相同堆棧做合并,計算每種堆棧的對象數和内存大小。對于某些Category,如分配大小TOP N,或者UI相關的(如UIViewController、UIView之類的),它裡面分配大小TOP M的堆棧才做上報。上報格式類似這樣:

ios微信占内存(iOS微信内存監控)10

4.頁面展現

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

ios微信占内存(iOS微信内存監控)11

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

● UIViewController數量是否異常

● UIView數量是否異常

● UIImage數量是否異常

● 其它Category分配大小是否異常,對象個數是否異常

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

ios微信占内存(iOS微信内存監控)12

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

一級分類

ios微信占内存(iOS微信内存監控)13

二級分類

ios微信占内存(iOS微信内存監控)14

5.運營策略

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

二、降低誤判

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

ios微信占内存(iOS微信内存監控)15

1.App沒有升級

2.App沒有調用exit()或abort()退出

3.App沒有出現crash

4.用戶沒有強退App

5.系統沒有升級/重啟

6.App當時沒有後台運行

7.App出現FOOM

1、2、4、5比較容易判斷,3依賴于自身CrashReport組件的crash回調,6、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前台卡死導緻重啟”,列入重點關注。

三、成果

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

ios微信占内存(iOS微信内存監控)16

ios微信占内存(iOS微信内存監控)17

四、常見問題

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來規避這類問題。

大圖片處理

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

ios微信占内存(iOS微信内存監控)18

但處理大分辨率圖片時,往往容易出現OOM,原因是-[UIImage drawInRect:]在繪制時,先解碼圖片,再生成原始分辨率大小的bitmap,這是很耗内存的。解決方法是使用更低層的ImageIO接口,避免中間bitmap産生:

ios微信占内存(iOS微信内存監控)19

大視圖

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


騰訊WeTest iOS預審工具

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

現将騰訊内部産品的過審經驗,以線上工具的形式共享給各位。在WeTest騰訊質量開放平台上可以在線使用。進入We Test官網即可體驗!

,

更多精彩资讯请关注tft每日頭條,我们将持续为您更新最新资讯!

查看全部

相关生活资讯推荐

热门生活资讯推荐

网友关注

Copyright 2023-2024 - www.tftnews.com All Rights Reserved