Android記憶體洩漏監控和優化技巧總結

IT技術精選文摘發表於2018-05-14

前言


對於Android平臺的應用程式來說,記憶體優化一直是個熱門話題,與傳統PC應用程式不同,Android上的應用一旦出現各種異常時系統預設會以最嚴厲的“崩潰”方式反饋給使用者,如果處理不當,將嚴重影響使用者體驗。

叢所周之,移動裝置的軟硬體資源無法與傳統PC相提並論(至少目前是這樣),因而開發人員在編寫應用時,需要有更多技巧、更精深的技術來應對各種局面。這其中尤以記憶體OOM(記憶體溢位)等涉及記憶體洩漏這樣的問題最為常見。

本文著重總結降低應用記憶體佔用的技巧以及對應的解決方案。

先來談談記憶體洩漏的監控機制


記憶體洩露:簡單來說物件由於編碼錯誤或系統原因,仍然存在著對其直接或間接的引用,導致系統無法進行回收。記憶體洩露,容易留下邏輯隱患,同時增加了應用記憶體峰值與發生OOM的概率。它屬於bug issue,是我們一定要修改的。下面是造成記憶體洩露的一些常見原因,但是如何建立一套發現記憶體洩露、解決記憶體洩露的閉環方案,才是我們工作的重點。

1監控方案


Square的開源庫leakcanry是一個非常不錯的選擇,它通過弱引用方式偵查Activity或物件的生命週期,若發現記憶體洩露自動dump Hprof檔案,通過HAHA庫得到洩露的最短路徑,最後通過notification展示。

記憶體洩露判斷與處理的流程如下圖 ,各自執行的程式空間(主程式通過idlehandler,HAHA分析使用的是單獨的程式):

微信在leakcanry推出之前已經有了自己的記憶體洩露監控體系,與leakcanry大致有以下的區別:

事實上,通過對leakcanry做簡單的定製,我們就可以實現以下一個記憶體洩露監控閉環。 

2記憶體洩露後的挽救措施


Activity洩漏會導致該Activity引用到的Bitmap、DrawingCache等無法釋放,對記憶體造成大的壓力,挽救措施是指對於已洩漏Activity,嘗試回收其持有的資源,洩漏的僅僅是一個Activity空殼,從而降低對記憶體的壓力。

做法也非常簡單,在Activity onDestory時候從view的rootview開始,遞迴釋放所有子view涉及的圖片,背景,DrawingCache,監聽器等等資源,讓Activity成為一個不佔資源的空殼,洩露了也不會導致圖片資源被持有。 

總的來說,我們不是隻懂得一些記憶體洩露解決方法就可以,更重要的是通過日常測試與監控,得到記憶體洩露檢測與修改的一整套閉環體系。

如何降低執行記憶體的佔用


1Android系統何時會發生OOM?

2按照慣例:優化Bitmap佔用的記憶體效果最為明顯


說到記憶體,bitmap必然是這裡的大頭。對於bitmap記憶體佔用,想說的有以下幾點:

一個好的imageLoader,可以將2.X、4.X或5.X對圖片載入的處理對使用者隱藏,同時也可以將自適應大小、質量等放於框架中。

3記憶體佔用情況實時監測


對於系統函式onLowMemory等函式是針對整個系統而已的,對於本程式來說,其dalvik記憶體距離OOM的差值並沒有體現,也沒有回撥函式供我們及時釋放記憶體。假若能有那麼一套機制,可以實時監控程式的堆記憶體使用率,達到設定值即關於通知相關模組進行記憶體釋放,這會大大的降低OOM。

- 實現原理:

4程式隔離


對於webview,相簿等,由於存在記憶體系統洩露或者佔用記憶體過多的問題,我們可以採用單獨的程式。微信當前也會把它們放在單獨的tools程式中。

5OOM錯誤資訊上報機制


當系統發生OOM的crash時,我們應當上傳更加詳細的記憶體相關資訊,方便我們定位當時記憶體的具體情況。其他例如使用large heap、inBitmap、SparseArray、Protobuf等不再一一細述,對程式碼採用優化--埋坑--優化--埋坑的方式並不推薦。我們應該著力於建立一套合理的框架與監控體系,能及時的發現諸如bitmap過大、畫素浪費、記憶體佔用過大、應用OOM等問題。

記憶體回收(GC)優化


1頻繁的GC會帶來何種影響


Java擁有GC的機制,不同的系統版本GC的實現可能有比較大的差異。但是無論哪種版本,大量的GC操作則會顯著佔用幀間隔時間(16ms)。如果在幀間隔時間裡面做了過多的GC操作,那麼自然其他類似計算,渲染等操作的可用時間就變得少了。 

2GC的型別介紹


GC的型別有以下幾種,其中GC_FOR_ALLOC是同步方式進行,對應用幀率的影響最大。

- GC_FOR_ALLOC:
當堆記憶體不夠的時候容易被觸發,尤其是new一個物件的時候,很容易被觸發到,所以如果要加速啟動,可以提高dalvik.vm.heapstartsize的值,這樣在啟動過程中可以減少GC_FOR_ALLOC的次數。注意這個觸發是以同步的方式進行的。如果GC後仍然沒有空間,則堆進行擴張

- GC_EXPLICIT:
這個gc是被可以呼叫的,比如system.gc, 一般gc執行緒的優先順序比較低,所以這個垃圾回收的過程不一定會馬上觸發, 千萬不要認為呼叫了system.gc,記憶體的情況就能有所好轉

- GC_CONCURRENT:
當分配的物件大小超過384K時觸發,注意這是以非同步的方式進行回收的.如果發現大量反覆的Concurrent GC出現,說明系統中可能一直有大於384K的物件被分配,而這些往往是一些臨時物件,被反覆觸發了。給到我們的暗示是:物件的複用不夠。

- GC_EXTERNAL_ALLOC(在3.0系統之後被廢了):
Native層的記憶體分配失敗了,這類GC就會被觸發。如果GPU的紋理、bitmap、或者java.nio.ByteBuffers的使用沒有釋放,這種型別的GC往往會被頻繁觸發。

3記憶體抖動


Memory Churn記憶體抖動,記憶體抖動是因為在短時間內大量的物件被建立又馬上被釋放。瞬間產生大量的物件會嚴重佔用記憶體區域,當達到閥值,剩餘空間不夠的時候,會觸發GC從而導致剛產生的物件又很快被回收。即使每次分配的物件佔用了很少的記憶體,但是他們疊加在一起會增加Heap的壓力,從而觸發更多其他型別的GC。這個操作有可能會影響到幀率,並使得使用者感知到效能問題。

通過Memory Monitor,我們可以跟蹤整個app的記憶體變化情況。若短時間發生了多次記憶體的漲跌,這意味著很有可能發生了記憶體抖動。

4GC優化方案


通過Heap Viewer,我們可以檢視當前記憶體快照,便於對比分析哪些物件有可能發生了洩漏。更重要的工具是Allocation Tracker,追蹤記憶體物件的型別、堆疊、大小等。手Q有做一個統計工具,對Allocation Tracker的原始資料,按照(型別&堆疊)的組合(堆疊取棧頂的5層)統計某一種物件分配的大小、次數。同時按照次數、大小的排序,從多/大到少/小結合程式碼分析,並自頂向下的逐輪進行優化。

這樣,我們就可以快速知道發生記憶體抖動時,是因為哪些變數的建立造成頻繁GC。一般來說我們需要注意以下幾個方面:

- 字串拼接優化:

- 資源重用:
建立全球快取池,對頻繁申請、釋放的物件型別重用

- 減少不必要或不合理的物件:
例如在ondraw、getview中應減少物件申請,儘量重用。更多是一些邏輯上的東西,例如迴圈中不斷申請區域性變數等

- 選用合理的資料格式:
使用SparseArray, SparseBooleanArray, and LongSparseArray來代替Hashmap

寫在最後


我們並不能將記憶體優化中用到的所有技巧都一一說明,而且隨著Android版本的更替,可能很多方法都會變的過時。我在想更重要的是我們能持續的發現問題,精細化的監控,而不是一直處於"哪個有坑填哪裡的"的窘況。在這裡給大家的建議有:


  • 率先考慮採用已有的工具;中國人喜歡重複造輪子,我們更推薦花精力去優化已有工具,為廣大碼農做貢獻。生活已不易,碼農何為為難碼農!

  • 不拘泥於點,更重要在於如何建立合理的框架避免發生問題,或者是能及時的發現問題。


當前微信記憶體監控體系中也存在一些不盡人意的地方,在未來的日子裡也同樣需要努力去優化。

公眾號推薦:


相關文章