作者:Ali Ansari & Grzegorz Pstrucha
本文由CocoaChina譯者小袋子(博客)翻譯自Facebook的博客
原文:Reducing FOOMs in the Facebook iOS app
在 Facebook,我們一直致力於讓應用穩定、快速、可靠。在 Facebook 的 iOS 應用上,我們已經做了很多工作去減少應用的崩潰率以及全面提高應用的穩定性。此前,大多數的崩潰都是由於常規性錯誤,一般都會伴隨著相應代碼行的棧回溯信息,並且提供了可能導致問題所在的提示信息。
當我們繼續解決崩潰問題時,我們觀察到需要解決的崩潰比例正在下降,但是我們注意到 App Store 指出社區繼續出現令人失望的應用崩潰。我們深入研究了用戶報告,並且從理論上說明內存不足(out-of-memory events (OOMs))可能正在發生。OOMs 一般發生在系統運行在低內存的環境下,OS 為了回收內存而終止應用。它既可能發生在前台,也可以是後台。我們在內部稱之為 FOOMs 和 BOOMs — 當我們說應用爆炸(BOOM)了,好像很好玩的樣子。
從用戶的角度來看,一個前台內存不足導致的崩潰和常規的崩潰是不好分辨的。一般分為幾種情況,應用異常終止,似乎消失,以及用戶返回設備主屏幕。如果內存的消耗速度急速增長,那麼應用會在不接到任何通知的情況下被終止掉。在 iOS 中,OS 會將內存警告發給應用,但是不能保證 OS 一定會在終止應用之前給應用發送警告信息。這就導致我們無法輕易地知道應用是否是由於內存壓力而被 OS 終止。
分析問題
為了掌握應用由於 OOM 崩潰而終止的頻率,我們從所有已知的途徑列舉應用可能終止的情況並記錄他們。這樣問題就轉變為“導致應用重啟的是什麼?”
應用需要重啟的原因如下:
應用已經更新
應用退出或終止
應用崩潰
用戶強制退出應用
設備重啟(包括 OS 升級)
應用在前台或者後台內存不足(OOM)
通過排除處理,尋找區別於其他重啟原因的實例,借此我們可以找出 OOM 發生的時間。此外,我們還追蹤應用進入後台和前台的時間,借此我們可以精確地把 OOMs 分為 BOOMs 和 FOOMs。
日志顯示在設備處於低內存狀態下,OOMs發生的比率很高 。當應用進程在受內存限制的設備上像驅逐一樣被終止,真的非常令人沮喪。查看相關的日志記錄幫助我們驗證排除法的效果,並且能繼續提高日志記錄(我們無法准確驗證所有的事例,例如應用升級)。
我們最初在減少 OOMs 所做的努力,是試圖在應用不再需要內存時,就盡可能快地主動縮小應用的內存占用。不幸的是,我們沒有發現 OOM 崩潰的數量沒有有切實的改變,所以我們把關注點轉移到大的內存分配上,開始觀察那些可能被洩露的內存(沒有清理干淨的),尤其是潛在的循環引用。
內存使用分析
當我們開始解決內存洩露問題時,我們看到 OOM 崩潰率有所降低,但是依然沒有達到我們預期。緊接著,我們深入研究 Apple 的 Instruments 應用的 memory profiler,並且注意到只要應用打開任何 web 網頁,一個重復樣式的 UIWebView 就會分配大量的內存。我們還發現內存經常沒有回收,即使在用戶離開了網頁並且 web 視圖被關閉的情況下。
我們試圖做過大量的優化,例如清理緩存和內容,但是應用進程的內存占用在跳轉向 web 視圖時總是顯著增長。iOS包含一個新的類 — WKWebView — 它把大多數的工作都放在了分開的進程裡,這意味著大多數跟內存相關的 web 視圖使用將不會分配給我們的進程。在低內存的事件中,web 視圖的進程將會被終止,但是我們的應用有很大可能會繼續存活下去。在我們把應用遷移為 WKWebView 後,我們確切地看到 OOMs 發生的比率有了顯著的降低。Yay!
內存分配比率
當通過 Instruments 分析內存使用時,我們還發現應用中分配了大量的臨時內存(~30 MB),然後馬上釋放掉。如果 CPU 在這個分配過程中是空閒的,那麼 OS 會終止程序。我們要禁止此類臨時分配,這可以幫助我們在 30% 確定場景中減少 OOM 崩潰,我們還實驗並發現,相較於重復分配和釋放內存,分配一次然後管理內存對於應用的可靠性是更好的。
阻止內存惡化
即使用了 WKWebView,我們仍然發現一點點內存洩露都能夠顯著地導致影響 OOM 的發生比率。在我們通常的發布計劃和貢獻給應用的許多的團隊中,在發布的應用中捕獲和阻止內存洩露是非常重要的。我們改變了掃描設備,獨創性地設計了用於測試移動性能,為了記錄大量進程中的常駐內存,允許掃描設備去標記惡化情況,只要它們被添加了。這已經幫助我們把 OOM 發生比率保持在比最初解決問題時低得多的水平上。
應用內部的內存分析器
上一個在這個項目中我們使用的關鍵技術是去構造一個應用內部的內存分析器,通過追蹤所有的 Objective-C 對象的內存分配進而快速分析應用。我們把這個配置在掃描儀上,然後在裡面建立我們的應用。
它是如何工作的:對於系統中的每一個類,維護一個當前活動的實例的數量。我們可以在任何點要求它打印出每一個類對象的現存數目。然後我們就可以分析這些數據任何異常的 release-to-release 用以辨認我們應用中總體上的內存分配模式,如果計數急劇變化,這一般可以驗證為內存洩露。我們准備去用一種性能足夠用並且不會產生對用戶有影響的方法去實現。
下面簡要說明我們的策略以及我們是如何追蹤 NSObject 的內存分配。
我們一開始創建一個內存分配追蹤類。這是個超級直接和簡單的類,有統計實例數量的公共方法用於統計實例數量的增加和減少。我們使用 C++ 而不是Objective-C,是由於那樣可以最小化追蹤器的內存分配和 CPU 占有率。
class AllocationTracker { static AllocationTracker* tracker(); void incrementInstanceCountForClass(Class aCls); void decrementInstanceCountForClass(Class aCls); std::vector countsSnapshot(); ... }
然後我們可以使用 iOS 的方法調配技術(稱為“swizzling”,使用runtime 的class_replaceMethod方法),用-fb_originalAlloc 和 -fb_originalDealloc方法去替換標准的iOS方法 +alloc 和+dealloc。
然後我們用新實現的增加和減少的分配和釋放實例數量的方法相應地替代+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
然後,當應用運行時,我們可以調用快照方法有規律地打印當前存活實例的數量。
應用可靠性
一旦我們在 Facebook 的 iOS 應用中實施更改去解決內存問題,我們會看到 (F)OOMs 和用戶的應用崩潰報告有顯著的降低。OOM 崩潰對於我們來說是盲點,因為沒有正式的體系或者 API 可以隨意檢測到它們。沒有人喜歡一個應用突然關閉。但是使用某些工具,或者最新的 iOS 技術,以及一些靈巧的方法去解決這個問題,能夠讓我們的應用更加可靠,並且保證你不會在打開 web 視圖查看一篇有趣的文章(就像你在看的這篇文章)時突然關閉。
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.