Android內存優化雜談

      Android內存優化是咱們性能優化工做中比較重要的一環,這裏其實主要包括兩方面的工做:
  1. 優化RAM,即下降運行時內存。這裏的目的是防止程序發生OOM異常,以及下降程序因爲內存過大被LMK機制殺死的機率。另外一方面,不合理的內存使用會使GC大大增多,從而致使程序變卡。
  2. 優化ROM,即下降程序佔ROM的體積。這裏主要是爲了下降程序佔用的空間,防止因爲ROM空間不足致使程序沒法安裝。

本文的着重點爲第一點,總結概述下降應用運行內存的技巧。在這裏咱們再也不細述PSS、USS等概念與Android應用的內存管理,如對這部份內容感興趣,可自行閱讀文末的參考文章。php

內存泄露的檢測與修改

內存泄露:簡單來講對象因爲編碼錯誤或系統緣由,仍然存在着對其直接或間接的引用,致使系統沒法進行回收。內存泄露,容易留下邏輯隱患,同時增長了應用內存峯值與發生OOM的機率。它屬於bug issue,是咱們必定要修改的。html

下面是形成內存泄露的一些常見緣由,可是如何創建一套發現內存泄露、解決內存泄露的閉環方案,纔是咱們工做的重點。java

一. 內存泄露的監控方案android

Square的開源庫leakcanry是一個很是不錯的選擇,它經過弱引用方式偵查Activity或對象的生命週期,若發現內存泄露自動dump Hprof文件,經過HAHA庫獲得泄露的最短路徑,最後經過notification展現。git

內存泄露判斷與處理的流程以下圖 ,各自運行的進程空間(主進程經過idlehandler,HAHA分析使用的是單獨的進程):github

微信在leakcanry推出以前已經有了本身的內存泄露監控體系,與leakcanry大體有如下的區別:web

  1. 在微信中,對於4.0以上的機型也是採用經過註冊ActivityLifecycleCallbacks接口,對於4.0如下的機型咱們會嘗試反射ActivityThread中的mInstrumentation對象。固然,如今微信也改爲只支持android-15以上,美美噠。
  2. leakcanry儘管使用了idlehandler與分進程,可是dumphprof依然會形成應用明顯的卡頓(SuspendAll Thread)。而在三星等一些手機,系統會緩存最後一個Activity,因此在微信,咱們採起了更嚴格的檢測模式,即泄露三次確認以及通過5個新建的Activity,確保不是因爲系統緩存的緣由形成。
  3. 在微信中,當發現疑似內存泄露時會彈出對話框,當咱們主動點擊時纔會去作dumpHprof以及上傳Hprof快照的操做,而是否誤報、泄露鏈等分析工做也是放於服務器端。

事實上,經過對leakcanry作簡單的定製,咱們就能夠實現如下一個內存泄露監控閉環。 算法

二. 對系統內存泄露的Hack Fix緩存

AndroidExcludedRefs列出了一些因爲系統緣由致使引用沒法釋放的例子,同時對於大多數的例子,都會提供建議如何經過hack的建議去修復。在微信中,對TextLine、InputMethodManager、AudioManger、android.os.Message也採用了相似Hack的方式(詳細可看參考資料)。性能優化

三. 經過兜底回收內存

Activity泄漏會致使該Activity引用到的Bitmap、DrawingCache等沒法釋放,對內存形成大的壓力,兜底回收是指對於已泄漏Activity,嘗試回收其持有的資源,泄漏的僅僅是一個Activity空殼,從而下降對內存的壓力。

作法也很是簡單,在Activity onDestory時候從view的rootview開始,遞歸釋放全部子view涉及的圖片,背景,DrawingCache,監聽器等等資源,讓Activity成爲一個不佔資源的空殼,泄露了也不會致使圖片資源被持有。

總的來講,咱們不是隻懂得一些內存泄露解決方法就能夠,更重要的是經過平常測試與監控,獲得內存泄露檢測與修改的一整套閉環體系。

下降運行時內存的一些方法

當咱們能確保應用中不會出現內存泄露時,咱們須要一些其餘的方法來下降運行時的內存。更多的時候,咱們其實只但願下降應用發生OOM的機率。

Android OOM:

  • Android 2.x系統,當dalvik allocated + external allocated + 新分配的大小 >= dalvik heap 最大值時候就會發生OOM。其中bitmap是放於external中 。
  • Android 4.x系統,廢除了external的計數器,相似bitmap的分配改到dalvik的java heap中申請,只要allocated + 新分配的內存 >= dalvik heap 最大值的時候就會發生OOM(art運行環境的統計規則仍是和dalvik保持一致)

一. 減小bitmap佔用的內存

說到內存,bitmap必然是這裏的大頭。對於bitmap內存佔用,想說的有如下幾點:

  1. 防止bitmap佔用資源多大致使OOM
    Android 2.x 系統 BitmapFactory.Options 裏面隱藏的的inNativeAlloc反射打開後,申請的bitmap就不會算在external中。對於Android 4.x系統,可採用facebook的fresco庫,便可把圖片資源放於native中。
  2. 圖片按需加載
    即圖片的大小不該該超過view的大小。在把圖片載入內存以前,咱們須要先計算出一個合適的inSampleSize縮放比例,避免沒必要要的大圖載入。對此,咱們能夠重載drawable與ImageView,例如在Activity ondestroy時,檢測圖片大小與View的大小,若超過,能夠上報或提示。
  3. 統一的bitmap加載器
    Picasso、Fresco都是比較出名的加載庫,一樣微信也有本身的庫ImageLoader。加載庫的好處在於將版本差別、大小處理對使用者不感知。有了統一的bitmap加載器,咱們能夠在加載bitmap時,若發生OOM(try catch方式),能夠經過清除cache,下降bitmap format(ARGB8888/RBG565/ARGB4444/ALPHA8)等方式,從新嘗試。
  4. 圖片存在像素浪費
    對於.9圖,美工可能在出圖時在拉伸與非拉伸區域都有大量的像素重複。經過獲取圖片的像素ARGB值,計算連續相同的像素區域,自定義算法斷定這些區域是否能夠縮放。關鍵也是須要將這些工做作到系統化,可及時發現問題,解決問題。

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

二. 自身內存佔用監控

對於系統函數onLowMemory等函數是針對整個系統而已的,對於本進程來講,其dalvik內存距離OOM的差值並無體現,也沒有回調函數供咱們及時釋放內存。倘若能有那麼一套機制,能夠實時監控進程的堆內存使用率,達到設定值即關於通知相關模塊進行內存釋放,這會大大的下降OOM。

  • 實現原理
    這個其實比較簡單,經過Runtime得到maxMemory,而totalMemory-freeMemory即爲當前真正使用的dalvik內存。
  • 操做方式
    咱們能夠按期(前臺每隔3分鐘)去獲得這個值,當咱們這個值達到危險值時(例如80%),咱們應當主要去釋放咱們的各類cache資源(bitmap的cache爲大頭),同時顯示的去Trim應用的memory,加速內存收集。

三. 使用多進程

對於webview,圖庫等,因爲存在內存系統泄露或者佔用內存過多的問題,咱們能夠採用單獨的進程。微信當前也會把它們放在單獨的tools進程中

四. 上報OOM詳細信息

當系統發生OOM的crash時,咱們應當上傳更加詳細的內存相關信息,方便咱們定位當時內存的具體狀況。

其餘例如使用large heap、inBitmap、SparseArray、Protobuf等再也不一一細述,對代碼採用優化--埋坑--優化--埋坑的方式並不推薦。咱們應該着力於創建一套合理的框架與監控體系,能及時的發現諸如bitmap過大、像素浪費、內存佔用過大、應用OOM等問題。

GC優化

Java擁有GC的機制,不一樣的系統版本GC的實現可能有比較大的差別。可是不管哪一種版本,大量的GC操做則會顯著佔用幀間隔時間(16ms)。若是在幀間隔時間裏面作了過多的GC操做,那麼天然其餘相似計算,渲染等操做的可用時間就變得少了。

一. GC的類型

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

  1. GC_FOR_ALLOC
    當堆內存不夠的時候容易被觸發,尤爲是new一個對象的時候,很容易被觸發到,因此若是要加速啓動,能夠提升dalvik.vm.heapstartsize的值,這樣在啓動過程當中能夠減小GC_FOR_ALLOC的次數。注意這個觸發是以同步的方式進行的。若是GC後仍然沒有空間,則堆進行擴張
  2. GC_EXPLICIT
    這個gc是被能夠調用的,好比system.gc, 通常gc線程的優先級比較低,因此這個垃圾回收的過程不必定會立刻觸發, 千萬不要認爲調用了system.gc,內存的狀況就能有所好轉
  3. GC_CONCURRENT
    當分配的對象大小超過384K時觸發,注意這是以異步的方式進行回收的.若是發現大量反覆的Concurrent GC出現,說明系統中可能一直有大於384K的對象被分配,而這些每每是一些臨時對象,被反覆觸發了。給到咱們的暗示是:對象的複用不夠。
  4. GC_EXTERNAL_ALLOC (在3.0系統以後被廢了)
    Native層的內存分配失敗了,這類GC就會被觸發。若是GPU的紋理、bitmap、或者java.nio.ByteBuffers的使用沒有釋放,這種類型的GC每每會被頻繁觸發。

二. 內存抖動現象

Memory Churn內存抖動,內存抖動是由於在短期內大量的對象被建立又立刻被釋放。瞬間產生大量的對象會嚴重佔用內存區域,當達到閥值,剩餘空間不夠的時候,會觸發GC從而致使剛產生的對象又很快被回收。即便每次分配的對象佔用了不多的內存,可是他們疊加在一塊兒會增長Heap的壓力,從而觸發更多其餘類型的GC。這個操做有可能會影響到幀率,並使得用戶感知到性能問題。

經過Memory Monitor,咱們能夠跟蹤整個app的內存變化狀況。若短期發生了屢次內存的漲跌,這意味着頗有可能發生了內存抖動。

三. GC優化

經過Heap Viewer,咱們能夠查看當前內存快照,便於對比分析哪些對象有可能發生了泄漏。更重要的工具是Allocation Tracker,追蹤內存對象的類型、堆棧、大小等。手Q有作一個統計工具,對Allocation Tracker的原始數據,按照(類型&堆棧)的組合(堆棧取棧頂的5層)統計某一種對象分配的大小、次數。同時按照次數、大小的排序,從多/大到少/小結合代碼分析,並自頂向下的逐輪進行優化。

這樣,咱們就能夠快速知道發生內存抖動時,是由於哪些變量的建立形成頻繁GC。通常來講咱們須要注意如下幾個方面:

  1. 字符串拼接優化
    減小字符串使用加號拼接,改成使用StringBuilder。減小StringBuilder.enlarge,初始化時設置capacity;這裏須要注意的是,若打開Looper中Printer回調,也會存在較多的字符串拼接。
  1. 讀文件優化 讀文件使用ByteArrayPool,初始設置capacity,減小expand
  2. 資源重用
    創建全球緩存池,對頻繁申請、釋放的對象類型重用
  3. 減小沒必要要或不合理的對象
    例如在ondraw、getview中應減小對象申請,儘可能重用。更可能是一些邏輯上的東西,例如循環中不斷申請局部變量等
  4. 選用合理的數據格式 使用SparseArray, SparseBooleanArray, and LongSparseArray來代替Hashmap

總結

咱們並不能將內存優化中用到的全部技巧都一一說明,並且隨着Android版本的更替,可能不少方法都會變的過期。我在想更重要的是咱們能持續的發現問題,精細化的監控,而不是一直處於」哪一個有坑填哪裏的」的窘況。在這裏給你們的建議有:

  1. 率先考慮採用已有的工具;中國人喜歡重複造輪子,咱們更推薦花精力去優化已有工具,爲廣大碼農作貢獻。生活已不易,碼農何爲爲難碼農!
  2. 不拘泥於點,更重要在於如何創建合理的框架避免發生問題,或者是能及時的發現問題。

當前微信內存監控體系中也存在一些不盡人意的地方,在將來的日子裏也一樣須要努力去優化。

參考文章

  1. Android內存管理(http://developer.android.com/intl/zh-cn/training/articles/memory.html)
  2. leakcanary(https://github.com/square/leakcanary)
  3. AndroidExcludedRefs(https://github.com/square/leakcanary/blob/master/leakcanary-android/src/main/java/com/squareup/leakcanary/AndroidExcludedRefs.java)
  4. fresco(https://github.com/facebook/fresco)
  5. 優化安卓應用內存的神祕方法以及背後的原理(http://bugly.qq.com/blog/?p=621)
  6. Android性能優化以內存篇(http://hukai.me/android-performance-memory/)
相關文章
相關標籤/搜索