iOS性能優(yōu)化實(shí)踐:頭條抖音如何實(shí)現(xiàn)OOM崩潰率下降50%+

轉(zhuǎn)載:字節(jié)跳動(dòng)

iOS OOM 崩潰在生產(chǎn)環(huán)境中的歸因一直是困擾業(yè)界已久的疑難問(wèn)題冬耿,字節(jié)跳動(dòng)旗下的頭條圣猎、抖音等產(chǎn)品也面臨同樣的問(wèn)題。

在字節(jié)跳動(dòng)性能與穩(wěn)定性保障團(tuán)隊(duì)的研發(fā)實(shí)踐中,我們自研了一款基于內(nèi)存快照技術(shù)并且可應(yīng)用于生產(chǎn)環(huán)境中的 OOM 歸因方案——線上 Memory Graph精拟∈愁恚基于此方案滚躯,3 個(gè)月內(nèi)頭條抖音 OOM 崩潰率下降 50%+胃惜。

本文主要分享下該解決方案的技術(shù)背景,技術(shù)原理以及使用方式鼎姐,旨在為這個(gè)疑難問(wèn)題提供一種新的解決思路钾麸。

OOM 崩潰背景介紹

OOM

OOM 其實(shí)是Out Of Memory的簡(jiǎn)稱,指的是在 iOS 設(shè)備上當(dāng)前應(yīng)用因?yàn)閮?nèi)存占用過(guò)高而被操作系統(tǒng)強(qiáng)制終止炕桨,在用戶側(cè)的感知就是 App 一瞬間的閃退饭尝,與普通的 Crash 沒(méi)有明顯差異。但是當(dāng)我們?cè)谡{(diào)試階段遇到這種崩潰的時(shí)候谋作,從設(shè)備設(shè)置->隱私->分析與改進(jìn)中是找不到普通類型的崩潰日志芋肠,只能夠找到Jetsam開頭的日志,這種形式的日志其實(shí)就是 OOM 崩潰之后系統(tǒng)生成的一種專門反映內(nèi)存異常問(wèn)題的日志遵蚜。那么下一個(gè)問(wèn)題就來(lái)了帖池,什么是Jetsam奈惑?

Jetsam

Jetsam是 iOS 操作系統(tǒng)為了控制內(nèi)存資源過(guò)度使用而采用的一種資源管控機(jī)制。不同于MacOS睡汹,Linux肴甸,Windows等桌面操作系統(tǒng),出于性能方面的考慮囚巴,iOS 系統(tǒng)并沒(méi)有設(shè)計(jì)內(nèi)存交換空間的機(jī)制原在,所以在 iOS 中,如果設(shè)備整體內(nèi)存緊張的話彤叉,系統(tǒng)只能將一些優(yōu)先級(jí)不高或占用內(nèi)存過(guò)大的進(jìn)程直接終止掉庶柿。

[圖片上傳失敗...(image-aeea8d-1603760345328)]

<figcaption style="margin: 5px 0px 0px; padding: 0px; max-width: 100%; box-sizing: border-box !important; word-wrap: break-word !important; text-align: justify; color: rgb(136, 136, 136); font-size: 14px;">Jetsam 日志解讀
</figcaption>

上圖是截取一份Jetsam日志中最關(guān)鍵的一部分。關(guān)鍵信息解讀:

  • pageSize:指的是當(dāng)前設(shè)備物理內(nèi)存頁(yè)的大小秽浇,當(dāng)前設(shè)備是iPhoneXs Max浮庐,大小是 16KB,蘋果 A7 芯片之前的設(shè)備物理內(nèi)存頁(yè)大小則是 4KB柬焕。
  • states:當(dāng)前應(yīng)用的運(yùn)行狀態(tài)审残,對(duì)于Heimdallr-Example這個(gè)應(yīng)用而言是正在前臺(tái)運(yùn)行的狀態(tài),這類崩潰我們稱之為FOOM(Foreground Out Of Memory)斑举;與此相對(duì)應(yīng)的也有應(yīng)用程序在后臺(tái)發(fā)生的 OOM 崩潰搅轿,這類崩潰我們稱之為BOOM(Background Out Of Memory)。
  • rpages:是resident pages的縮寫富玷,表明進(jìn)程當(dāng)前占用的內(nèi)存頁(yè)數(shù)量璧坟,Heimdallr-Example 這個(gè)應(yīng)用占用的內(nèi)存頁(yè)數(shù)量是 92800,基于 pageSize 和 rpages 可以計(jì)算出應(yīng)用崩潰時(shí)占用的內(nèi)存大小:16384 * 92800 / 1024 /1024 = 1.4GB赎懦。
  • reason:表明進(jìn)程被終止的的原因沸柔,Heimdallr-Example這個(gè)應(yīng)用被終止的原因是超過(guò)了操作系統(tǒng)允許的單個(gè)進(jìn)程物理內(nèi)存占用的上限。

Jetsam機(jī)制清理策略可以總結(jié)為下面兩點(diǎn):

  1. 單個(gè) App 物理內(nèi)存占用超過(guò)上限2. 整個(gè)設(shè)備物理內(nèi)存占用收到壓力按照下面優(yōu)先級(jí)完成清理:

  2. 后臺(tái)應(yīng)用>前臺(tái)應(yīng)用

  3. 內(nèi)存占用高的應(yīng)用>內(nèi)存占用低的應(yīng)用

  4. 用戶應(yīng)用>系統(tǒng)應(yīng)用

Jetsam的代碼在開源的XNU代碼中可以找到铲敛,這里篇幅原因就不具體展開了,具體的源碼解析可以參考本文最后第 2 和第 3 篇參考文獻(xiàn)会钝。

為什么要監(jiān)控 OOM 崩潰

前面我們已經(jīng)了解到伐蒋,OOM 分為FOOMBOOM兩種類型,顯然前者因?yàn)橛脩舻母兄黠@迁酸,所以對(duì)用戶的體驗(yàn)的傷害更大先鱼,下文中提到的 OOM 崩潰僅指的是FOOM。那么針對(duì) OOM 崩潰問(wèn)題有必要建立線上的監(jiān)控手段嗎奸鬓?

答案是有而且非常有必要的焙畔!原因如下:

  1. 重度用戶也就是使用時(shí)間更長(zhǎng)的用戶更容易發(fā)生FOOM,對(duì)這部分用戶體驗(yàn)的傷害導(dǎo)致用戶流失的話對(duì)業(yè)務(wù)損失更大串远。
  2. 頭條宏多,抖音等多個(gè)產(chǎn)品線上數(shù)據(jù)均顯示FOOM量級(jí)比普通崩潰還要多儿惫,因?yàn)檫^(guò)去缺乏有效的監(jiān)控和治理手段導(dǎo)致問(wèn)題被長(zhǎng)期忽視。
  3. 內(nèi)存占用過(guò)高即使沒(méi)導(dǎo)致FOOM也可能會(huì)導(dǎo)致其他應(yīng)用BOOM的概率變大伸但,一旦用戶發(fā)現(xiàn)從微信切換到我們 App 使用肾请,再切回微信沒(méi)有停留在之前微信的聊天頁(yè)面而是重新啟動(dòng)的話,對(duì)用戶來(lái)說(shuō)更胖,體驗(yàn)是非常糟糕的铛铁。

OOM 線上監(jiān)控

Jetsam 強(qiáng)殺代碼截圖

翻閱XNU源碼的時(shí)候我們可以看到在Jetsam機(jī)制終止進(jìn)程的時(shí)候最終是通過(guò)發(fā)送SIGKILL異常信號(hào)來(lái)完成的。

define SIGKILL 9 kill (cannot be caught or ignored)

從系統(tǒng)庫(kù) signal.h 文件中我們可以找到SIGKILL這個(gè)異常信號(hào)的解釋却妨,它不可以在當(dāng)前進(jìn)程被忽略或者被捕獲饵逐,我們之前監(jiān)聽異常信號(hào)的常規(guī) Crash 捕獲方案肯定也就不適用了。那我們應(yīng)該如何監(jiān)控 OOM 崩潰呢彪标?

正面監(jiān)控這條路行不通倍权,2015 年的時(shí)候Facebook提出了另外一種思路,簡(jiǎn)而言之就是排除法捐下。具體流程可以參考下面這張流程圖:

排除法判定OOM崩潰的流程

我們?cè)诿看?App 啟動(dòng)的時(shí)候判斷上一次啟動(dòng)進(jìn)程終止的原因账锹,那么已知的原因有:

  • App 更新了版本
  • App 發(fā)生了崩潰
  • 用戶手動(dòng)退出
  • 操作系統(tǒng)更新了版本
  • App 切換到后臺(tái)之后進(jìn)程終止

如果上一次啟動(dòng)進(jìn)程終止的原因不是上述任何一個(gè)已知原因的話,就判定上次啟動(dòng)發(fā)生了一次FOOM崩潰坷襟。

曾經(jīng)Facebook旗下的Fabric也是這樣實(shí)現(xiàn)的奸柬。但是通過(guò)我們的測(cè)試和驗(yàn)證,上述這種方式至少將以下幾種場(chǎng)景誤判:

  • WatchDog 崩潰
  • 后臺(tái)啟動(dòng)
  • XCTest/UITest 等自動(dòng)化測(cè)試框架驅(qū)動(dòng)
  • 應(yīng)用 exit 主動(dòng)退出

在字節(jié)跳動(dòng) OOM 崩潰監(jiān)控上線之前婴程,我們已經(jīng)排除了上面已知的所有誤判場(chǎng)景廓奕。需要說(shuō)明的是,因?yàn)榕懦ó吘箾](méi)有直接的監(jiān)控來(lái)的那么精準(zhǔn)档叔,或多或少總有一些 bad case桌粉,但是我們會(huì)保證盡量的準(zhǔn)確。

自研線上 Memory Graph衙四,OOM 崩潰率下降 50%+

OOM 生產(chǎn)環(huán)境歸因

目前在 iOS 端排查內(nèi)存問(wèn)題的工具主要包括 Xcode 提供的 Memory Graph 和 Instruments 相關(guān)的工具集铃肯,它們能夠提供相對(duì)完備的內(nèi)存信息,但是應(yīng)用場(chǎng)景僅限于開發(fā)環(huán)境传蹈,無(wú)法在生產(chǎn)環(huán)境使用押逼。由于內(nèi)存問(wèn)題往往發(fā)生在一些極端的使用場(chǎng)景,線下開發(fā)測(cè)試一般無(wú)法覆蓋對(duì)應(yīng)的問(wèn)題惦界,Xcode 提供的工具無(wú)法分析處理大多數(shù)偶現(xiàn)的疑難問(wèn)題挑格。

對(duì)此,各大公司都提出了自己的線上解決方案沾歪,并開源了例如MLeaksFinder漂彤、OOMDetectorFBRetainCycleDetector等優(yōu)秀的解決方案。

在字節(jié)跳動(dòng)內(nèi)部的使用過(guò)程中挫望,我們發(fā)現(xiàn)現(xiàn)有工具各有側(cè)重立润,無(wú)法完全滿足我們的需求。主要的問(wèn)題集中在以下兩點(diǎn):

  • 基于 Objective-C 對(duì)象引用關(guān)系找循環(huán)引用的方案士骤,適用范圍比較小范删,只能處理部分循環(huán)引用問(wèn)題,而內(nèi)存問(wèn)題通常是復(fù)雜的拷肌,類似于內(nèi)存堆積到旦,Root Leak,C/C++層問(wèn)題都無(wú)法解決巨缘。
  • 基于分配堆棧信息聚類的方案需要常駐運(yùn)行添忘,對(duì)內(nèi)存、CPU 等資源存在較大消耗若锁,無(wú)法針對(duì)有內(nèi)存問(wèn)題的用戶進(jìn)行監(jiān)控搁骑,只能廣撒網(wǎng)又固,用戶體驗(yàn)影響較大仲器。同時(shí)肢扯,通過(guò)某些比較通用的堆棧分配的內(nèi)存無(wú)法定位出實(shí)際的內(nèi)存使用場(chǎng)景,對(duì)于循環(huán)引用等常見(jiàn)泄漏也無(wú)法分析欢摄。

為了解決頭條熬丧,抖音等各產(chǎn)品日益嚴(yán)峻的內(nèi)存問(wèn)題,我們自行研發(fā)了一款基于內(nèi)存快照技術(shù)的線上方案怀挠,我們稱之為——線上 Memory Graph析蝴。上線后接入了集團(tuán)內(nèi)幾乎所有的產(chǎn)品害捕,幫助各產(chǎn)品修復(fù)了多年的歷史問(wèn)題,OOM 率降低一個(gè)數(shù)量級(jí)闷畸,3 個(gè)月之內(nèi)抖音最新版本 OOM 率下降了 50%尝盼,頭條下降了 60%。線上突發(fā) OOM 問(wèn)題定位效率大大提升佑菩,徹底告別了線上 OOM 問(wèn)題歸因“兩眼一抹黑”的時(shí)代盾沫。

線上 Memory Graph 核心的原理是掃描進(jìn)程中所有 Dirty 內(nèi)存,通過(guò)內(nèi)存節(jié)點(diǎn)中保存的其他內(nèi)存節(jié)點(diǎn)的地址值建立起內(nèi)存節(jié)點(diǎn)之間的引用關(guān)系的有向圖殿漠,用于內(nèi)存問(wèn)題的分析定位赴精,整個(gè)過(guò)程不使用任何私有 API。這套方案具備的能力如下:

  1. 完整還原用戶當(dāng)時(shí)的內(nèi)存狀態(tài)绞幌。
  2. 量化線上用戶的大內(nèi)存占用和內(nèi)存泄漏蕾哟,可以精確的回答 App 內(nèi)存到底大在哪里這個(gè)問(wèn)題。
  3. 通過(guò)內(nèi)存節(jié)點(diǎn)符號(hào)和引用關(guān)系圖回答內(nèi)存節(jié)點(diǎn)為什么存活這個(gè)問(wèn)題莲蜘。
  4. 嚴(yán)格控制性能損耗谭确,只有當(dāng)內(nèi)存占用超過(guò)異常閾值的時(shí)候才會(huì)觸發(fā)分析。沒(méi)有運(yùn)行時(shí)開銷票渠,只有采集時(shí)開銷逐哈,對(duì) 99.9%正常使用的用戶幾乎沒(méi)有任何影響。
  5. 支持主要的編程語(yǔ)言庄新,包括 OC鞠眉,C/C++,Swift择诈,Rust 等械蹋。
線上 Memory Graph 采集及上報(bào)流程示意圖

內(nèi)存快照采集

線上 Memory Graph 采集內(nèi)存快照主要是為了獲取當(dāng)前運(yùn)行狀態(tài)下所有內(nèi)存對(duì)象以及對(duì)象之間的引用關(guān)系,用于后續(xù)的問(wèn)題分析羞芍。主要需要獲取的信息如下:

  • 所有內(nèi)存的節(jié)點(diǎn)哗戈,以及其符號(hào)信息(如OC/Swift/C++ 實(shí)例類名,或者是某種有特殊用途的 VM 節(jié)點(diǎn)的 tag 等)荷科。
  • 節(jié)點(diǎn)之間的引用關(guān)系唯咬,以及符號(hào)信息(偏移,或者實(shí)例變量名)畏浆,OC/Swift成員變量還需要記錄引用類型胆胰。

由于采集的過(guò)程發(fā)生在程序正常運(yùn)行的過(guò)程中,為了保證不會(huì)因?yàn)椴杉瘍?nèi)存快照導(dǎo)致程序運(yùn)行異常刻获,整個(gè)采集過(guò)程需要在一個(gè)相對(duì)靜止的運(yùn)行環(huán)境下完成蜀涨。因此,整個(gè)快照采集的過(guò)程大致分為以下幾個(gè)步驟:

  1. 掛起所有非采集線程。
  2. 獲取所有的內(nèi)存節(jié)點(diǎn)厚柳,內(nèi)存對(duì)象引用關(guān)系以及相應(yīng)的輔助信息氧枣。
  3. 寫入文件。
  4. 恢復(fù)線程狀態(tài)别垮。

下面會(huì)分別介紹整個(gè)采集過(guò)程中一些實(shí)現(xiàn)細(xì)節(jié)上的考量以及收集信息的取舍便监。

內(nèi)存節(jié)點(diǎn)的獲取

程序的內(nèi)存都是由虛擬內(nèi)存組成的,每一塊單獨(dú)的虛擬內(nèi)存被稱之為VM Region碳想,通過(guò) mach 內(nèi)核的vm_region_recurse/vm_region_recurse64函數(shù)我們可以遍歷進(jìn)程內(nèi)所有VM Region烧董,并通過(guò)vm_region_submap_info_64結(jié)構(gòu)體獲取以下信息:

  • 虛擬地址空間中的地址和大小。
  • Dirty 和 Swapped 內(nèi)存頁(yè)數(shù)移袍,表示該VM Region的真實(shí)物理內(nèi)存使用解藻。
  • 是否可交換,Text 段葡盗、共享 mmap 等只讀或隨時(shí)可以被交換出去的內(nèi)存螟左,無(wú)需關(guān)注。
  • user_tag觅够,用戶標(biāo)簽胶背,用于提供該VM Region的用途的更準(zhǔn)確信息。

大多數(shù) VM Region 作為一個(gè)單獨(dú)的內(nèi)存節(jié)點(diǎn)喘先,僅記錄起始地址和 Dirty钳吟、Swapped 內(nèi)存作為大小,以及與其他節(jié)點(diǎn)之間的引用關(guān)系窘拯;而 libmalloc 維護(hù)的堆內(nèi)存所在的 VM Region 則由于往往包含大多數(shù)業(yè)務(wù)邏輯中的 Objective-C 對(duì)象红且、C/C++對(duì)象、buffer 等涤姊,可以獲取更詳細(xì)的引用信息暇番,因此需要單獨(dú)處理其內(nèi)部節(jié)點(diǎn)、引用關(guān)系思喊。

在 iOS 系統(tǒng)中為了避免所有的內(nèi)存分配都使用系統(tǒng)調(diào)用產(chǎn)生性能問(wèn)題壁酬,相關(guān)的庫(kù)負(fù)責(zé)一次申請(qǐng)大塊內(nèi)存,再在其之上進(jìn)行二次分配并進(jìn)行管理恨课,提供給小塊需要?jiǎng)討B(tài)分配的內(nèi)存對(duì)象使用舆乔,稱之為堆內(nèi)存。程序中使用到絕大多數(shù)的動(dòng)態(tài)內(nèi)存都通過(guò)堆進(jìn)行管理剂公,在 iOS 操作系統(tǒng)上希俩,主要的業(yè)務(wù)邏輯分配的內(nèi)存都通過(guò)libmalloc進(jìn)行管理,部分系統(tǒng)庫(kù)為了性能也會(huì)使用自己的單獨(dú)的堆管理纲辽,例如WebKit內(nèi)核使用bmalloc颜武,CFNetwork也使用自己獨(dú)立的堆贫母,在這里我們只關(guān)注libmalloc內(nèi)部的內(nèi)存管理狀態(tài),而不關(guān)心其它可能的堆(即這部分特殊內(nèi)存會(huì)以VM Region的粒度存在盒刚,不分析其內(nèi)部的節(jié)點(diǎn)引用關(guān)系)。

我們可以通過(guò)malloc_get_all_zones獲取libmalloc內(nèi)部所有的zone绿贞,并遍歷每個(gè)zone中管理的內(nèi)存節(jié)點(diǎn)因块,獲取 libmalloc 管理的存活的所有內(nèi)存節(jié)點(diǎn)的指針和大小。

符號(hào)化

獲取所有內(nèi)存節(jié)點(diǎn)之后籍铁,我們需要為每個(gè)節(jié)點(diǎn)找到更加詳細(xì)的類型名稱涡上,用于后續(xù)的分析。其中拒名,對(duì)于 VM Region 內(nèi)存節(jié)點(diǎn)吩愧,我們可以通過(guò) user_tag 賦予它有意義的符號(hào)信息;而堆內(nèi)存對(duì)象包含 raw buffer增显,Objective-C/Swift雁佳、C++等對(duì)象。對(duì)于 Objective-C/Swift同云、C++這部分糖权,我們通過(guò)內(nèi)存中的一些運(yùn)行時(shí)信息,嘗試符號(hào)化獲取更加詳細(xì)的信息炸站。

Objective/Swift 對(duì)象的符號(hào)化相對(duì)比較簡(jiǎn)單星澳,很多三方庫(kù)都有類似實(shí)現(xiàn),Swift在內(nèi)存布局上兼容了Objective-C旱易,也有isa指針禁偎,objc相關(guān)方法可以作用于兩種語(yǔ)言的對(duì)象上。只要保證 isa 指針合法阀坏,對(duì)象實(shí)例大小滿足條件即可認(rèn)為正確如暖。

C++對(duì)象根據(jù)是否包含虛表可以分成兩類。對(duì)于不包含虛表的對(duì)象全释,因?yàn)槿狈\(yùn)行時(shí)數(shù)據(jù)装处,無(wú)法進(jìn)行處理。

對(duì)于對(duì)于包含虛表的對(duì)象浸船,在調(diào)研 mach-o 和 C++的 ABI 文檔后妄迁,可以通過(guò) std::type_info 和以下幾個(gè) section 的信息獲取對(duì)應(yīng)的類型信息。

  • type_name string - 類名對(duì)應(yīng)的常量字符串李命,存儲(chǔ)在__TEXT/__RODATA段的__const section中登淘。
  • type_info - 存放在__DATA/__DATA_CONST段的__const section中。
  • vtable - 存放在__DATA/__DATA_CONST段的__const section中封字。
C++實(shí)例以及 vtable 的引用關(guān)系示意圖

在 iOS 系統(tǒng)內(nèi)黔州,還有一類特殊的對(duì)象耍鬓,即CoreFoundation。除了我們熟知的CFString流妻、CFDictionary外等牲蜀,很多很多系統(tǒng)庫(kù)也使用 CF 對(duì)象,比如CGImage绅这、CVObject等涣达。從它們的 isa 指針獲取的Objective-C類型被統(tǒng)一成__NSCFType。由于 CoreFoundation 類型支持實(shí)時(shí)的注冊(cè)证薇、注銷類型度苔,為了細(xì)化這部分的類型,我們通過(guò)逆向拿到 CoreFoundation 維護(hù)的類型 slot 數(shù)組的位置并讀取其數(shù)據(jù)浑度,保證能夠安全的獲取準(zhǔn)確的類型寇窑。

CoreFoundation 類型獲取

引用關(guān)系的構(gòu)建

整個(gè)內(nèi)存快照的核心在于重新構(gòu)建內(nèi)存節(jié)點(diǎn)之間的引用關(guān)系。在虛擬內(nèi)存中箩张,如果一個(gè)內(nèi)存節(jié)點(diǎn)引用了其它內(nèi)存節(jié)點(diǎn)甩骏,則對(duì)應(yīng)的內(nèi)存地址中會(huì)存儲(chǔ)指向?qū)Ψ降闹羔樦怠伏钠;谶@個(gè)事實(shí)我們?cè)O(shè)計(jì)了以下方案:

  1. 遍歷一個(gè)內(nèi)存節(jié)點(diǎn)中所有可能存儲(chǔ)了指針的范圍獲取其存儲(chǔ)的值 A横漏。
  2. 搜索所有獲得的節(jié)點(diǎn),判斷 A 是不是某一個(gè)內(nèi)存節(jié)點(diǎn)中任何一個(gè)字節(jié)的地址熟掂,如果是缎浇,則認(rèn)為是一個(gè)引用關(guān)系。
  3. 對(duì)所有內(nèi)存節(jié)點(diǎn)重復(fù)以上操作赴肚。

對(duì)于一些特定的內(nèi)存區(qū)域素跺,為了獲取更詳細(xì)的信息用于排查問(wèn)題,我們對(duì)棧內(nèi)存以及 Objective-C/Swift 的堆內(nèi)存進(jìn)行了一些額外的處理誉券。

其中指厌,棧內(nèi)存也以VM Region的形式存在,棧上保存了臨時(shí)變量和 TLS 等數(shù)據(jù)踊跟,獲取相應(yīng)的引用信息可以幫助排查諸如 autoreleasepool 造成的內(nèi)存問(wèn)題踩验。由于棧并不會(huì)使用整個(gè)棧內(nèi)存,為了獲取 Stack 的引用關(guān)系商玫,我們根據(jù)寄存器以及棧內(nèi)存獲取當(dāng)前的椈叮可用范圍,排除未使用的棧內(nèi)存造成的無(wú)效引用拳昌。

棧使用范圍

而對(duì)于Objective-C/Swift對(duì)象袭异,由于運(yùn)行時(shí)包含額外的信息,我們可以獲得Ivar的強(qiáng)弱引用關(guān)系以及Ivar的名字炬藤,帶上這些信息有助于我們分析問(wèn)題御铃。通過(guò)獲得Ivar的偏移碴里,如果找到的引用關(guān)系的偏移和Ivar的偏移一致,則認(rèn)為這個(gè)引用關(guān)系就是這個(gè)Ivar上真,可以將Ivar相關(guān)的信息附加上去咬腋。

數(shù)據(jù)上報(bào)策略

我們?cè)?App 內(nèi)存到達(dá)設(shè)定值后采集 App 當(dāng)時(shí)的內(nèi)存節(jié)點(diǎn)和引用關(guān)系,然后上傳至遠(yuǎn)端進(jìn)行分析睡互,可以精準(zhǔn)的反映 App 當(dāng)時(shí)的內(nèi)存狀態(tài)帝火,從而定位問(wèn)題,總的流程如下:

線上 Memory Graph 整體工作流程

整個(gè)線上 Memory Graph 模塊工作的完整流程如上圖所示湃缎,主要包括:

  1. 后臺(tái)線程定時(shí)檢測(cè)內(nèi)存占用,超過(guò)設(shè)定的危險(xiǎn)閾值后觸發(fā)內(nèi)存分析蠢壹。
  2. 內(nèi)存分析后數(shù)據(jù)持久化嗓违,等待下次上報(bào)。
  3. 原始文件壓縮打包图贸。
  4. 檢查后端上報(bào)許可蹂季,因?yàn)閱蝹€(gè)文件很大,后端可能會(huì)做一些限流的策略疏日。
  5. 上報(bào)到后端分析偿洁,如果成功后清除文件,失敗后會(huì)重試沟优,最多三次之后清除涕滋,防止占用用戶太多的磁盤空間。

后臺(tái)分析

這是字節(jié)監(jiān)控平臺(tái) Memory Graph 單點(diǎn)詳情頁(yè)的一個(gè) case:

線上 Memory Graph 詳情頁(yè)概覽

我們可以看到這個(gè)用戶的內(nèi)存占用已經(jīng)將近 900MB挠阁,我們分析時(shí)候的思路一般是:

  1. 從對(duì)象數(shù)量和對(duì)象內(nèi)存占用這兩個(gè)角度嘗試找到類列表中最有嫌疑的那個(gè)類宾肺。
  2. 從對(duì)象列表中隨機(jī)選中某個(gè)實(shí)例,向它的父節(jié)點(diǎn)回溯引用關(guān)系侵俗,找到你認(rèn)為最有嫌疑的一條引用路徑锨用。
  3. 點(diǎn)擊引用路徑模塊右上角的Add Tag來(lái)判斷當(dāng)前選中的引用路徑在同類對(duì)象中出現(xiàn)過(guò)多少次。
  4. 確認(rèn)有問(wèn)題的引用路徑之后再判斷究竟是哪個(gè)業(yè)務(wù)模塊發(fā)生的問(wèn)題隘谣。
當(dāng)前引用路徑在同類型對(duì)象中出現(xiàn)頻率統(tǒng)計(jì)

通過(guò)上圖中引用路徑的分析我們發(fā)現(xiàn)增拥,所有的圖片最終都被TTImagePickController這個(gè)類持有,最終排查到是圖片選擇器模塊一次性把用戶相冊(cè)中的所有圖片都加載到內(nèi)存里寻歧,極端情況下會(huì)發(fā)生這個(gè)問(wèn)題掌栅。

整體性能和穩(wěn)定性

采集側(cè)優(yōu)化策略

由于整個(gè)內(nèi)存空間一般包含的內(nèi)存節(jié)點(diǎn)從幾十萬(wàn)到幾千萬(wàn)不等,同時(shí)程序的運(yùn)行狀態(tài)瞬息萬(wàn)變熄求,采集過(guò)程有著很大的性能和穩(wěn)定性的壓力渣玲。

我們?cè)谇懊娴幕A(chǔ)上還進(jìn)行了一些性能優(yōu)化:

  • 寫出采集數(shù)據(jù)使用mmap映射,并自定義二進(jìn)制格式保證順序讀寫弟晚。
  • 提前對(duì)內(nèi)存節(jié)點(diǎn)進(jìn)行排序忘衍,建立邊引用關(guān)系時(shí)使用二分查找逾苫。通過(guò)位運(yùn)算對(duì)一些非法內(nèi)存地址進(jìn)行提前快速剪枝。

對(duì)于穩(wěn)定性部分枚钓,我們著重考慮了下面幾點(diǎn):

  • 死鎖

由于無(wú)法保證 Objective-C 運(yùn)行時(shí)鎖的狀態(tài)铅搓,我們將需要通過(guò)運(yùn)行時(shí) api 獲取的信息在掛起線程前提前緩存。同時(shí)搀捷,為了保證libmalloc鎖的狀態(tài)安全星掰,在掛起線程后我們對(duì) libmalloc 的鎖狀態(tài)進(jìn)行了判斷,如果已經(jīng)鎖住則恢復(fù)線程重新嘗試掛起嫩舟,避免堆死鎖氢烘。

  • 非法內(nèi)存訪問(wèn)

在掛起所有其他線程后,為了減少采集本身分配的內(nèi)存對(duì)采集的影響家厌,我們使用了一個(gè)單獨(dú)的malloc_zone管理采集模塊的內(nèi)存使用播玖。

性能損耗

因?yàn)樵跀?shù)據(jù)采集的時(shí)候需要掛起所有線程,會(huì)導(dǎo)致用戶感知到卡頓饭于,所以字節(jié)模塊還是有一定性能損耗的蜀踏,經(jīng)過(guò)我們測(cè)試,在iPhone8 Plus設(shè)備上掰吕,App 占用 1G 內(nèi)存時(shí)果覆,采集用時(shí) 1.5-2 秒,采集時(shí)額外內(nèi)存消耗 10-20MB殖熟,生成的文件 zip 后大小在 5-20MB局待。

為了嚴(yán)格控制性能損耗,線上 Memory Graph 模塊會(huì)應(yīng)用以下策略菱属,避免太頻繁的觸發(fā)打擾用戶正常使用燎猛,避免自身內(nèi)存和磁盤等資源過(guò)多的占用:

性能損耗控制策略

穩(wěn)定性

該方案已經(jīng)在字節(jié)全系產(chǎn)品線上穩(wěn)定運(yùn)行了 6 個(gè)月以上,穩(wěn)定性和成功率得到了驗(yàn)證照皆,目前單次采集成功率可以達(dá)到 99.5%重绷,剩下的失敗基本都是由于內(nèi)存緊張?zhí)崆?OOM,考慮到大多數(shù)應(yīng)用只有不到千分之一的用戶會(huì)觸發(fā)采集膜毁,這種情況屬于極低概率事件昭卓。

試用路徑

目前,線上 Memory Graph 已搭載在字節(jié)跳動(dòng)火山引擎旗下應(yīng)用性能管理平臺(tái)(APMInsight)上賦能給外部開發(fā)者使用瘟滨。

APMInsight 的相關(guān)技術(shù)經(jīng)過(guò)今日頭條候醒、抖音、西瓜視頻等眾多應(yīng)用的打磨杂瘸,已沉淀出一套完整的解決方案倒淫,能夠定位移動(dòng)端、瀏覽器败玉、小程序等多端問(wèn)題敌土,除了支持崩潰镜硕、錯(cuò)誤、卡頓返干、網(wǎng)絡(luò)等基礎(chǔ)問(wèn)題的分析兴枯,還提供關(guān)聯(lián)到應(yīng)用啟動(dòng)、頁(yè)面瀏覽矩欠、內(nèi)存優(yōu)化的眾多功能财剖。目前 Demo 已開放大部分能力,歡迎各位注冊(cè)賬號(hào)試用:https://www.volcengine.cn/product/apminsight

加入我們

本技術(shù)方案由字節(jié)跳動(dòng) APM 中臺(tái)和抖音基礎(chǔ)技術(shù)團(tuán)隊(duì)深度合作聯(lián)合打造癌淮,歡迎對(duì)我們兩個(gè)團(tuán)隊(duì)感興趣的同學(xué)加入:

APM 中臺(tái)

字節(jié)跳動(dòng) APM 中臺(tái)目前致力于提升整個(gè)集團(tuán)內(nèi)全系產(chǎn)品的性能和穩(wěn)定性表現(xiàn)躺坟,技術(shù)棧覆蓋 iOS/Android/Flutter/Web/Hybrid/PC/游戲/小程序等,工作內(nèi)容包括但不限于線上監(jiān)控乳蓄,線上運(yùn)維瞳氓,深度優(yōu)化,線下防劣化等栓袖。長(zhǎng)期期望為業(yè)界輸出更多更有建設(shè)性的問(wèn)題發(fā)現(xiàn)和深度優(yōu)化手段。

歡迎各位有識(shí)之士加入我們店诗,一起為了“更快裹刮,更穩(wěn),更省庞瘸,更有品質(zhì)”的極致目標(biāo)攜手前行捧弃。我們?cè)诒本钲趦傻鼐姓衅感枨蟛聊遥?jiǎn)歷投遞郵箱:** tech@bytedance.com 违霞;郵件標(biāo)題:姓名 - 工作年限 - APM 中臺(tái) - 技術(shù)棧方向(如 iOS/Android/Web/后端)**。

抖音基礎(chǔ)技術(shù)

我們是負(fù)責(zé)抖音客戶端基礎(chǔ)能力研發(fā)和新技術(shù)探索的團(tuán)隊(duì)瞬场。我們?cè)诠こ?業(yè)務(wù)架構(gòu)买鸽,研發(fā)工具,編譯系統(tǒng)等方向深耕贯被,支撐業(yè)務(wù)快速迭代的同時(shí)眼五,保證超大規(guī)模團(tuán)隊(duì)的研發(fā)效能和工程質(zhì)量。在性能/穩(wěn)定性等方面不斷探索彤灶,努力為全球數(shù)億用戶提供最極致的基礎(chǔ)體驗(yàn)看幼。

如果你對(duì)技術(shù)充滿熱情,歡迎加入抖音基礎(chǔ)技術(shù)團(tuán)隊(duì)幌陕,讓我們共建億級(jí)全球化 App诵姜。目前我們?cè)谏虾!⒈本┎ā⒑贾菖锼簟⑸钲诰姓衅感枨笙境啵瑑?nèi)推可以聯(lián)系郵箱:** tech@bytedance.com** ;郵件標(biāo)題: **姓名 - 工作年限 - 抖音 - 基礎(chǔ)技術(shù) - iOS/Android **瑟俭。

參考文獻(xiàn)

[1] https://zhuanlan.zhihu.com/p/49829766

[2] http://satanwoo.github.io/2017/10/18/abort/

[3] https://jinxuebin.cn/2019/07/OOM底層原理探究/

[4] https://engineering.fb.com/ios/reducing-fooms-in-the-facebook-ios-app/

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末翎卓,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子摆寄,更是在濱河造成了極大的恐慌失暴,老刑警劉巖,帶你破解...
    沈念sama閱讀 219,039評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件微饥,死亡現(xiàn)場(chǎng)離奇詭異逗扒,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)欠橘,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,426評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門矩肩,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人肃续,你說(shuō)我怎么就攤上這事黍檩。” “怎么了始锚?”我有些...
    開封第一講書人閱讀 165,417評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵刽酱,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我瞧捌,道長(zhǎng)棵里,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,868評(píng)論 1 295
  • 正文 為了忘掉前任姐呐,我火速辦了婚禮殿怜,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘曙砂。我一直安慰自己头谜,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,892評(píng)論 6 392
  • 文/花漫 我一把揭開白布鸠澈。 她就那樣靜靜地躺著乔夯,像睡著了一般。 火紅的嫁衣襯著肌膚如雪款侵。 梳的紋絲不亂的頭發(fā)上末荐,一...
    開封第一講書人閱讀 51,692評(píng)論 1 305
  • 那天,我揣著相機(jī)與錄音新锈,去河邊找鬼甲脏。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的块请。 我是一名探鬼主播娜氏,決...
    沈念sama閱讀 40,416評(píng)論 3 419
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼墩新!你這毒婦竟也來(lái)了贸弥?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,326評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤海渊,失蹤者是張志新(化名)和其女友劉穎绵疲,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體臣疑,經(jīng)...
    沈念sama閱讀 45,782評(píng)論 1 316
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡盔憨,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,957評(píng)論 3 337
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了讯沈。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片郁岩。...
    茶點(diǎn)故事閱讀 40,102評(píng)論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖缺狠,靈堂內(nèi)的尸體忽然破棺而出问慎,到底是詐尸還是另有隱情,我是刑警寧澤挤茄,帶...
    沈念sama閱讀 35,790評(píng)論 5 346
  • 正文 年R本政府宣布如叼,位于F島的核電站,受9級(jí)特大地震影響驮樊,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜片酝,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,442評(píng)論 3 331
  • 文/蒙蒙 一囚衔、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧雕沿,春花似錦练湿、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,996評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至疾渣,卻和暖如春篡诽,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背榴捡。 一陣腳步聲響...
    開封第一講書人閱讀 33,113評(píng)論 1 272
  • 我被黑心中介騙來(lái)泰國(guó)打工杈女, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,332評(píng)論 3 373
  • 正文 我出身青樓达椰,卻偏偏與公主長(zhǎng)得像翰蠢,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子啰劲,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,044評(píng)論 2 355