Facebook如何降低應(yīng)用中的FOOMs
在 Facebook,我們一直致力于讓應(yīng)用穩(wěn)定、快速、可靠。在 Facebook 的 iOS 應(yīng)用上,我們已經(jīng)做了很多工作去減少應(yīng)用的崩潰率以及全面提高應(yīng)用的穩(wěn)定性。此前,大多數(shù)的崩潰都是由于常規(guī)性錯(cuò)誤,一般都會(huì)伴隨著相應(yīng)代碼行的棧回溯信息,并且提供了可能導(dǎo)致問(wèn)題所在的提示信息。
當(dāng)我們繼續(xù)解決崩潰問(wèn)題時(shí),我們觀察到需要解決的崩潰比例正在下降,但是我們注意到 App Store 指出社區(qū)繼續(xù)出現(xiàn)令人失望的應(yīng)用崩潰。我們深入研究了用戶報(bào)告,并且從理論上說(shuō)明內(nèi)存不足(out-of-memory events (OOMs))可能正在發(fā)生。OOMs 一般發(fā)生在系統(tǒng)運(yùn)行在低內(nèi)存的環(huán)境下,OS 為了回收內(nèi)存而終止應(yīng)用。它既可能發(fā)生在前臺(tái),也可以是后臺(tái)。我們?cè)趦?nèi)部稱之為 FOOMs 和 BOOMs — 當(dāng)我們說(shuō)應(yīng)用爆炸(BOOM)了,好像很好玩的樣子。
從用戶的角度來(lái)看,一個(gè)前臺(tái)內(nèi)存不足導(dǎo)致的崩潰和常規(guī)的崩潰是不好分辨的。一般分為幾種情況,應(yīng)用異常終止,似乎消失,以及用戶返回設(shè)備主屏幕。如果內(nèi)存的消耗速度急速增長(zhǎng),那么應(yīng)用會(huì)在不接到任何通知的情況下被終止掉。在 iOS 中,OS 會(huì)將內(nèi)存警告發(fā)給應(yīng)用,但是不能保證 OS 一定會(huì)在終止應(yīng)用之前給應(yīng)用發(fā)送警告信息。這就導(dǎo)致我們無(wú)法輕易地知道應(yīng)用是否是由于內(nèi)存壓力而被 OS 終止。
分析問(wèn)題
為了掌握應(yīng)用由于 OOM 崩潰而終止的頻率,我們從所有已知的途徑列舉應(yīng)用可能終止的情況并記錄他們。這樣問(wèn)題就轉(zhuǎn)變?yōu)?ldquo;導(dǎo)致應(yīng)用重啟的是什么?”
應(yīng)用需要重啟的原因如下:
應(yīng)用已經(jīng)更新
應(yīng)用退出或終止
應(yīng)用崩潰
用戶強(qiáng)制退出應(yīng)用
設(shè)備重啟(包括 OS 升級(jí))
應(yīng)用在前臺(tái)或者后臺(tái)內(nèi)存不足(OOM)
通過(guò)排除處理,尋找區(qū)別于其他重啟原因的實(shí)例,借此我們可以找出 OOM 發(fā)生的時(shí)間。此外,我們還追蹤應(yīng)用進(jìn)入后臺(tái)和前臺(tái)的時(shí)間,借此我們可以精確地把 OOMs 分為 BOOMs 和 FOOMs。
日志顯示在設(shè)備處于低內(nèi)存狀態(tài)下,OOMs發(fā)生的比率很高 。當(dāng)應(yīng)用進(jìn)程在受內(nèi)存限制的設(shè)備上像驅(qū)逐一樣被終止,真的非常令人沮喪。查看相關(guān)的日志記錄幫助我們驗(yàn)證排除法的效果,并且能繼續(xù)提高日志記錄(我們無(wú)法準(zhǔn)確驗(yàn)證所有的事例,例如應(yīng)用升級(jí))。
我們最初在減少 OOMs 所做的努力,是試圖在應(yīng)用不再需要內(nèi)存時(shí),就盡可能快地主動(dòng)縮小應(yīng)用的內(nèi)存占用。不幸的是,我們沒(méi)有發(fā)現(xiàn) OOM 崩潰的數(shù)量沒(méi)有有切實(shí)的改變,所以我們把關(guān)注點(diǎn)轉(zhuǎn)移到大的內(nèi)存分配上,開始觀察那些可能被泄露的內(nèi)存(沒(méi)有清理干凈的),尤其是潛在的循環(huán)引用。
內(nèi)存使用分析
當(dāng)我們開始解決內(nèi)存泄露問(wèn)題時(shí),我們看到 OOM 崩潰率有所降低,但是依然沒(méi)有達(dá)到我們預(yù)期。緊接著,我們深入研究 Apple 的 Instruments 應(yīng)用的 memory profiler,并且注意到只要應(yīng)用打開任何 web 網(wǎng)頁(yè),一個(gè)重復(fù)樣式的 UIWebView 就會(huì)分配大量的內(nèi)存。我們還發(fā)現(xiàn)內(nèi)存經(jīng)常沒(méi)有回收,即使在用戶離開了網(wǎng)頁(yè)并且 web 視圖被關(guān)閉的情況下。
我們?cè)噲D做過(guò)大量的優(yōu)化,例如清理緩存和內(nèi)容,但是應(yīng)用進(jìn)程的內(nèi)存占用在跳轉(zhuǎn)向 web 視圖時(shí)總是顯著增長(zhǎng)。iOS包含一個(gè)新的類 — WKWebView — 它把大多數(shù)的工作都放在了分開的進(jìn)程里,這意味著大多數(shù)跟內(nèi)存相關(guān)的 web 視圖使用將不會(huì)分配給我們的進(jìn)程。在低內(nèi)存的事件中,web 視圖的進(jìn)程將會(huì)被終止,但是我們的應(yīng)用有很大可能會(huì)繼續(xù)存活下去。在我們把應(yīng)用遷移為 WKWebView 后,我們確切地看到 OOMs 發(fā)生的比率有了顯著的降低。Yay!
內(nèi)存分配比率
當(dāng)通過(guò) Instruments 分析內(nèi)存使用時(shí),我們還發(fā)現(xiàn)應(yīng)用中分配了大量的臨時(shí)內(nèi)存(~30 MB),然后馬上釋放掉。如果 CPU 在這個(gè)分配過(guò)程中是空閑的,那么 OS 會(huì)終止程序。我們要禁止此類臨時(shí)分配,這可以幫助我們?cè)?30% 確定場(chǎng)景中減少 OOM 崩潰,我們還實(shí)驗(yàn)并發(fā)現(xiàn),相較于重復(fù)分配和釋放內(nèi)存,分配一次然后管理內(nèi)存對(duì)于應(yīng)用的可靠性是更好的。
阻止內(nèi)存惡化
即使用了 WKWebView,我們?nèi)匀话l(fā)現(xiàn)一點(diǎn)點(diǎn)內(nèi)存泄露都能夠顯著地導(dǎo)致影響 OOM 的發(fā)生比率。在我們通常的發(fā)布計(jì)劃和貢獻(xiàn)給應(yīng)用的許多的團(tuán)隊(duì)中,在發(fā)布的應(yīng)用中捕獲和阻止內(nèi)存泄露是非常重要的。我們改變了掃描設(shè)備,***性地設(shè)計(jì)了用于測(cè)試移動(dòng)性能,為了記錄大量進(jìn)程中的常駐內(nèi)存,允許掃描設(shè)備去標(biāo)記惡化情況,只要它們被添加了。這已經(jīng)幫助我們把 OOM 發(fā)生比率保持在比最初解決問(wèn)題時(shí)低得多的水平上。
應(yīng)用內(nèi)部的內(nèi)存分析器
上一個(gè)在這個(gè)項(xiàng)目中我們使用的關(guān)鍵技術(shù)是去構(gòu)造一個(gè)應(yīng)用內(nèi)部的內(nèi)存分析器,通過(guò)追蹤所有的 Objective-C 對(duì)象的內(nèi)存分配進(jìn)而快速分析應(yīng)用。我們把這個(gè)配置在掃描儀上,然后在里面建立我們的應(yīng)用。
它是如何工作的:對(duì)于系統(tǒng)中的每一個(gè)類,維護(hù)一個(gè)當(dāng)前活動(dòng)的實(shí)例的數(shù)量。我們可以在任何點(diǎn)要求它打印出每一個(gè)類對(duì)象的現(xiàn)存數(shù)目。然后我們就可以分析這些數(shù)據(jù)任何異常的 release-to-release 用以辨認(rèn)我們應(yīng)用中總體上的內(nèi)存分配模式,如果計(jì)數(shù)急劇變化,這一般可以驗(yàn)證為內(nèi)存泄露。我們準(zhǔn)備去用一種性能足夠用并且不會(huì)產(chǎn)生對(duì)用戶有影響的方法去實(shí)現(xiàn)。
下面簡(jiǎn)要說(shuō)明我們的策略以及我們是如何追蹤 NSObject 的內(nèi)存分配。
我們一開始創(chuàng)建一個(gè)內(nèi)存分配追蹤類。這是個(gè)超級(jí)直接和簡(jiǎn)單的類,有統(tǒng)計(jì)實(shí)例數(shù)量的公共方法用于統(tǒng)計(jì)實(shí)例數(shù)量的增加和減少。我們使用 C++ 而不是Objective-C,是由于那樣可以最小化追蹤器的內(nèi)存分配和 CPU 占有率。
- class AllocationTracker {
- static AllocationTracker* tracker();
- void incrementInstanceCountForClass(Class aCls);
- void decrementInstanceCountForClass(Class aCls);
- std::vector countsSnapshot();
- ...
- }
然后我們可以使用 iOS 的方法調(diào)配技術(shù)(稱為“swizzling”,使用runtime 的class_replaceMethod方法),用-fb_originalAlloc 和 -fb_originalDealloc方法去替換標(biāo)準(zhǔn)的iOS方法 +alloc 和+dealloc。
然后我們用新實(shí)現(xiàn)的增加和減少的分配和釋放實(shí)例數(shù)量的方法相應(yīng)地替代+alloc 和 +dealloc。
- @implementation NSObject (AllocationTracker)
- + (id)fb_newAlloc
- {
- id object = [self fb_originalAlloc];
- AllocationTracker::tracker()->incrementInstanceCountForClass([object class]);
- return object;
- }
- - (void)fb_newDealloc
- {
- AllocationTracker::tracker()->decrementInstanceCountForClass([object class]);
- [self fb_originalDealloc];
- }
- @end
然后,當(dāng)應(yīng)用運(yùn)行時(shí),我們可以調(diào)用快照方法有規(guī)律地打印當(dāng)前存活實(shí)例的數(shù)量。
應(yīng)用可靠性
一旦我們?cè)?Facebook 的 iOS 應(yīng)用中實(shí)施更改去解決內(nèi)存問(wèn)題,我們會(huì)看到 (F)OOMs 和用戶的應(yīng)用崩潰報(bào)告有顯著的降低。OOM 崩潰對(duì)于我們來(lái)說(shuō)是盲點(diǎn),因?yàn)闆](méi)有正式的體系或者 API 可以隨意檢測(cè)到它們。沒(méi)有人喜歡一個(gè)應(yīng)用突然關(guān)閉。但是使用某些工具,或者***的 iOS 技術(shù),以及一些靈巧的方法去解決這個(gè)問(wèn)題,能夠讓我們的應(yīng)用更加可靠,并且保證你不會(huì)在打開 web 視圖查看一篇有趣的文章(就像你在看的這篇文章)時(shí)突然關(guān)閉。
Additional thanks to Linji Yang, Anoop Chaurasiya, Flynn Heiss, Parthiv Patel, Justin Pasqualini, Cloud Xu, Gautham Badrinathan, Ari Grant, and many others for helping reduce the FOOM rate.