淺談Android記憶體優化

CarlWe發表於2019-03-24

淺談Android記憶體優化

今天我們來聊一聊Android 記憶體優化,這篇文章本來很早就應該寫了,但因為小遊戲開發太吸引人了,所以這個就拖到了現在才開始,不過我覺得也不晚?

這篇文章主要通過如下三個方面對Android記憶體優化進行介紹:

  1. Android記憶體分配與回收機制
  2. Android常用的記憶體優化方法
  3. Android記憶體分析與監控

文章不會涉及到native記憶體的優化,因為普通App開發中涉及的較少,如果想了解可以參考極客時間張紹文老師的Android開發高手課。

一、Android記憶體分配與回收機制

想要優化Android記憶體,一些必備的基礎知識是不能少的。所以在第一部分,我們先從Application Framework、Dalvik/Art、Linux核心三個部分由淺入深來講解關於Androd記憶體相關的知識。

Application Framework

首先來看下程式的優先順序:

淺談Android記憶體優化

前臺程式:使用者當前操作所必需的程式。

可見程式:沒有任何前臺元件、但仍會影響使用者在螢幕上所見內容的程式。

服務程式:正在執行已使用 startService() 方法啟動的服務。(後臺播放音樂,網路下載資料)

後臺程式:對使用者不可見的 Activity 的程式(已呼叫 Activity 的 onStop() 方法)

空程式:不含任何活動應用元件的程式。保留這種程式的的唯一目的是用作快取,以縮短下次在其中執行元件所需的啟動時間

程式生命週期:Android 系統將盡量長時間地保持應用程式,但為了新建程式或執行更重要的程式,最終需要移除舊程式來回收記憶體。 為了確定保留或終止哪些程式,系統會根據程式中正在執行的元件以及這些元件的狀態,將每個程式放入“重要性層次結構”中。 必要時,系統會首先消除重要性最低的程式,然後是重要性略高的程式,來回收系統資源。(一般情況下前臺程式就是與使用者互動的程式了,如果連前臺程式都需要回收那麼此時系統幾乎不可用了)。由此也衍生了很多程式保活的方法(提高優先順序,互相喚醒,native保活等等),出現各種殺不死的程式的APP。

最後我們需要知道:Android中由ActivityManagerService 類集中管理所有程式的記憶體資源分配,我們可以檢視其原始碼來具體分析實現過程。

Dalvik/Art 虛擬機器

Android Dalvik Heap

淺談Android記憶體優化

簡介:Android Dalvik Heap與原生Java一樣,將堆的記憶體空間分為三個區域,Young Generation新生代,Old Generation年老代, Permanent Generation持久代。

物件分配過程:最近分配的物件會存放在新生代區域,新生代區域分為eden區(伊甸園,聖經中指上帝為亞當夏娃創造的生活樂園)、so區和s1區,s1和s0區也被稱為from區和to區(合稱Survivor區),他們是兩塊大小相等並且可以互換角色的空間,絕大多數情況下,物件首先分配在eden區,在一次新生代回收後,如果物件還存活會進入s0或者s1區,之後每一次gc,存活的物件年齡都會相應增加,當達到一定年齡則會進入老年代,最後累積一定時間再移動到持久代區域。系統會根據記憶體中不同的記憶體資料型別分別執行不同的gc操作。

問題:GC發生的時候,所有的執行緒都是會被暫停的。執行GC所佔用的時間和它發生在哪一個Generation也有關係,新生代中的每次GC操作時間是最短的,年老代其次,持久代最長。GC時會導致執行緒暫停、介面卡頓的問題在Android Art中得到了優化。

Dalvik虛擬機器執行模式

淺談Android記憶體優化

Dalvik垃圾回收過程:GC會去標記和查詢所有可訪問到的活動物件,這個時候整個程式的執行緒就會掛起,並且虛擬機器內部的所有執行緒也會同時掛起(左下圖) 。之所以要掛起所有執行緒是確保:所有程式沒有進行任何變更,與此同時GC會隱藏所有處理過的物件,最終確保標記了所有需要回收的物件後,GC才會恢復所有執行緒,並釋放空間。

大記憶體物件分配:當發現需要給一個較大的物件(藍色方塊)分配空間時,發現可用空間還是夠的,但沒有這麼大的連續空間供新物件使用,這個時候就不得不進行一次GC回收(紅色方塊,右下圖),為大物件騰出較大並且連續的空間。這就是我們在分配一個較大物件的時候非常容易引起丟幀和卡頓的原因之一,所以Android5.0以前大家都認為Android卡頓是因為Darvik虛擬機器的效率低下導致的。

總結:Dalvik虛擬機器的三個問題

  1. GC時掛起所有執行緒
  2. 大而連續的空間緊張
  3. 記憶體碎片化嚴重

ART虛擬機器的優化

淺談Android記憶體優化

GC過程:在ART中GC會要求程式在分配空間的時候標記自身的堆疊,這個過程非常短,不需要掛起所有程式的執行緒.這樣就節約了很大一部分時間去查詢活動物件。

大記憶體物件分配:ART裡會有一個獨立的LOS供Bitmap使用,從而提高了GC的管理效率和整體效能.

記憶體碎片化在ART裡還會有一個moving collector來壓縮活動物件(綠色方塊),使得記憶體空間更加緊湊。

總結 :Google在ART裡對GC做了非常大的優化(更高效的回收演算法),使ART記憶體分配的效率提高了10倍,GC的效率提高了2-3倍(可見原來效率有多低),不過主要還是優化中斷和阻塞的時間,頻繁的GC還是會導致卡頓。

Linux核心

淺談Android記憶體優化

Lowmemorykiller:ActivityManagerService中trimApplications() 函式中會執行一個叫做 updateOomAdjLocked() 的函式,updateOomAdjLocked 將針對每一個程式更新一個名為 adj 的變數,(用來表示發生記憶體不足時殺死程式的優先順序順序)並將其告知 Linux 核心,核心同樣維護一個包含 adj 的資料結構(即程式表),並通過 lowmemorykiller 檢查系統記憶體的使用情況,在記憶體不足時,遍歷所有程式,選出低優先順序的程式殺死,最終由核心去完成真正的記憶體回收。

Oom_killer :如果上述各種方法都無法釋放出足夠的記憶體空間,那麼當為新的程式分配記憶體時將發生 Out of Memory 異常,OOM_killer 將盡最後的努力殺掉一些程式來釋放空間。Android 中的oom_killer同樣會遍歷程式,並計算所有程式的 badness 值,選擇 badness 最大的那個程式將其殺掉。

Oom的條件:只要allocated + 新分配的記憶體 >= dalvik heap(堆記憶體) 最大值的時候就會發生OOM(Art執行環境的統計規則還是和dalvik保持一致)

記憶體不優化會導致哪些問題?

淺談Android記憶體優化

上面介紹了Android記憶體分配從應用層到Linux層的一些知識,所以我總結出上圖記憶體會導致的一些問題,但是上圖只是列出了一些常見情況,前後並沒有絕對的因果關係,最後來說下記憶體抖動。

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

二、Android常用的記憶體優化方法

在Android中記憶體優化的方式實在是太多了,往細了說,到你寫的每一行程式碼其實都和記憶體優化相關。在這裡我從三個方面來說下Android記憶體優化的方法:

  1. 降低執行時記憶體
  2. 程式碼優化
  3. 記憶體洩漏優化

在實際開發中我們可以先考慮降低應用的執行時記憶體,然後針對程式碼寫的不好的地方著重優化,最後通過規避一些可能導致記憶體洩漏的編碼方式,去提前避免記憶體洩漏的問題。

降低執行時記憶體

淺談Android記憶體優化

降低執行時記憶體可以分為減小APK的體積和Bitmap優化兩部分:

  • 減小APK體積
  1. 去除無用的資源和程式碼,通過合理使用git,一些由於業務變更而基本不會用到的程式碼,該刪除的絕不能手軟。即使以後要用到,通過git也能找回。同時一些圖片資源未用到的也應該刪除,因為即使gradle配了sharkresource選項,釋出的時候這些沒有用到的圖片依然會被打包到你的apk。
  2. 儘量複用資源,其實這是一種比較好的編碼習慣。
  3. 對應用的啟動圖引導頁圖片進行壓縮,往往這些圖片佔據了大部分空間,壓縮後可以起到很好的效果。平時開發中對於解析度大雨100*100的圖片基本上都會進行壓縮,很多好的壓縮演算法經常可以減少一半的大小,而感官上基本看不出有任何改變。
  • Bitmap優化
  1. 統一的bitmap載入器,選擇Glide、Fresco、Picasso中的一個作為圖片載入框架。實際開發中載入到view的圖片的大小不應該超過view的大小,圖片載入框架預設會對圖片進行快取,按view實際大小載入。在開發中為了減少apk的大小,一般只放一套3X圖片,但是這些圖片在小解析度的手機上直接載入就會出現記憶體浪費。統一的bitmap載入器就可以很好的解決該問題。
  2. 圖片存在畫素浪費,對於.9圖,美工可能在出圖時在拉伸與非拉伸區域都有大量的畫素重複。而這些圖片是可以縮小,但並不影響顯示效果。
  3. inSampleSize:縮放比例,在把圖片載入記憶體之前,我們需要計算一個合適的縮放比例,避免不必要的大圖載入。
  4. 選擇ARGB_8888/RBG_565/ARGB_4444/ALPHA_8,存在很大差異。
  5. inBitmap:這個引數用來實現Bitmap記憶體的複用,但複用存在一些限制,具體體現在:在Android 4.4之前只能重用相同大小的Bitmap的記憶體,而Android 4.4及以後版本則只要後來的Bitmap比之前的小即可。使用inBitmap引數前,每建立一個Bitmap物件都會分配一塊記憶體供其使用,而使用了inBitmap引數後,多個Bitmap可以複用一塊記憶體,這樣可以提高效能。

參考:

Android 官網文件Managing Bitmap MemoryHandling bitmaps

程式碼優化

這裡介紹一些好的編碼習慣:

淺談Android記憶體優化

  1. 考慮使用ArrayMap/SpareseArray而不是傳統的HashMap等資料結構,Android系統為移動系統設計的容器ArrayMap更加高效,佔用記憶體更少,因為HashMap需要一個額外的例項物件來記錄Mapping的操作。而SparesArray高效的避免了key和value的自動裝箱,而且避免了裝箱後的解箱。詳細參考Android效能優化典範

  2. 在onDraw這種頻繁呼叫的方法要避免物件的建立操作,因為他會迅速增加記憶體的使用,引起頻繁的gc,甚至記憶體抖動。

  3. SoftReference(軟引用)、WeakReference(弱引用)、PhantomReference(虛引用)

    SoftReference:如果一個物件只具有軟引用,則記憶體空間足夠,垃圾回收器就不會回收它;如果記憶體空間不足了,就會回收這些物件的記憶體。只要垃圾回收器沒有回收它,該物件就可以被程式使用。軟引用可用來實現記憶體敏感的快取記憶體。

    WeakReference:與軟引用的區別在於:只具有弱引用的物件擁有更短暫的生命週期。在垃圾回收器執行緒掃描它所管轄的記憶體區域的過程中,一旦發現了只具有弱引用的物件,不管當前記憶體空間足夠與否,都會回收它的記憶體。不過,由於垃圾回收器是一個優先順序很低的執行緒,因此不一定會很快發現那些只具有弱引用的物件。

    PhantomReference:虛引用”顧名思義,就是形同虛設,與其他幾種引用都不同,虛引用並不會決定物件的生命週期。如果一個物件僅持有虛引用,那麼它就和沒有任何引用一樣,在任何時候都可能被垃圾回收器回收。虛引用主要用來跟蹤物件被垃圾回收器回收的活動。虛引用與軟引用和弱引用的一個區別在於:虛引用必須和引用佇列 (ReferenceQueue)聯合使用。當垃圾回收器準備回收一個物件時,如果發現它還有虛引用,就會在回收物件的記憶體之前,把這個虛引用加入到與之 關聯的引用佇列中。

  4. 謹慎使用large heap,android裝置由於軟硬體的差異,heap閥值不同,特殊情況下可以在manifest中使用largeheap=true宣告一個更大的heap空間,使用getLargeMemoryClass()來獲取到這個更大的空間。但是要謹慎使用,因為額外的空間會影響到系統整體的使用者體驗,切換任務時效能大打折扣,對於oom異常是治標不治本的一種做法。

  5. 謹慎使用多程式,使用多程式可以把應用中的部分元件執行在單獨的程式當中,這樣可以擴大應用的記憶體佔用範圍,但是這個技術必須謹慎使用,絕大多數應用都不應該貿然使用多程式,一方面是因為使用多程式會使得程式碼邏輯更加複雜,另外如果使用不當,它可能反而會導致顯著增加記憶體。當你的應用需要執行一個常駐後臺的任務,而且這個任務並不輕量,可以考慮使用這個技術,一個典型的例子是建立一個可以長時間後臺播放的Music Player。如果整個應用都執行在一個程式中,當後臺播放的時候,前臺的那些UI資源也沒有辦法得到釋放。類似這樣的應用可以切分成2個程式:一個用來操作UI,另外一個給後臺的Service。

  6. 考慮第三方庫的大小,如果會和現有的程式碼或其他庫的程式碼重複,考慮不要真個引入而是把庫的程式碼精簡之後再引入。

記憶體洩漏優化

記憶體洩漏的原因有很多,下面介紹一些常見的,我們需要在開發中多注意:

淺談Android記憶體優化

  1. Activity呼叫了finish,但是引用Activity的物件未被釋放(生命週期沒有結束),Activity Context被傳遞到其他例項中,可能導致自身被引用而發生洩露,建議使用weakReferce。

  2. 除必須使用Activity Context的情況(Dialog的context必須是Activity),我們可以使用Application Context來避免Activity洩露。

  3. 大多數情況下,我們對Bitmap物件增加快取機制,但是有時候部分bitmap需要及時回收。比如我們臨時建立的摸個相對大的bitmap物件,變換得到新的bitmap物件後,儘快回收原始的bitmap,及時釋放原來的空間。

  4. webview引起的記憶體洩漏主要是因為org.chromium.android_webview.AwContents 類中註冊了component callbacks,但是未正常反註冊而導致的。讓onDetachedFromWindow先走,在主動呼叫destroy()之前,把webview從它的parent上面移除掉(Basewebfragment onDestroy())

  5. 雖然單例模式簡單實用,提供了很多便利性,但是因為單例的生命週期和應用保持一致,使用不合理很容易出現持有物件的洩漏。

  6. 我們在對資料庫進行操作時,使用完cursor沒有及時關閉,cursor的洩露,會對記憶體管理帶來負面影響。

  7. 謹慎使用static物件,因為static的生命週期過長,和應用的程式保持一致,使用不當很可能導致物件洩漏。

總結:在實際的線上環境中發現,大部分記憶體洩漏是因為被呼叫的物件生命週期不同步導致,生命週期不同步不僅僅會導致記憶體洩漏,更會出現異常,崩潰等更嚴重的問題。

做好上面說的1、2、3就夠了嗎?

淺談Android記憶體優化

前面我們已經從系統級別了解了Android Framework、Darlvik/Art虛擬機器、Linux在記憶體分配上的原理,接著又在程式碼級別分別從減少記憶體佔用、避免記憶體洩漏和程式碼優化三個方面介紹瞭如何避免記憶體問題,再加上當前科技發展是如此迅速,4GB記憶體已經是很常見的手機配置。LPDDR4X的高速快閃記憶體也越來越被廣泛的使用。對於記憶體優化我們是不是就已經可以高枕無憂了,有上面這些就夠了嗎?

我想即使我們再瞭解記憶體,寫的程式碼再好,使用者的手機再先進,總還是有出錯的時候,那麼事後的記憶體分析和監控是必不可少的了!

三、Android記憶體分析與監控

Android記憶體分析和監控主要介紹如下四種方式:

  1. 檢視GC日誌
  2. 檢視記憶體使用情況
  3. 通過LeakCanary監控記憶體 洩漏
  4. 線上監控

檢視GC日誌

GC的型別:

淺談Android記憶體優化

Concurrent: 不會暫停應用執行緒的併發垃圾回收。此垃圾回收在後臺執行緒中執行,而且不會阻止分配。

Alloc: 您的應用在堆已滿時嘗試分配記憶體引起的垃圾回收。在這種情況下分配執行緒中發生了垃圾回收。

Explicit:由應用明確請求的垃圾回收,例如,通過呼叫system.gc()。與 Dalvik 相同,在 ART 中,最佳做法是您應信任垃圾回收並避免請求顯式垃圾回收(如果可能)。不建議使用顯式垃圾回收,因為它們會阻止分配執行緒並不必要地浪費 CPU 週期。如果顯式垃圾回收導致其他執行緒被搶佔,那麼它們也可能會導致卡頓(應用中出現間斷、抖動或暫停)

NativeAlloc:原生分配(如點陣圖或 RenderScript 分配物件)導致出現原生記憶體壓力,進而引起的回收。

檢視垃圾回收日誌

淺談Android記憶體優化

在AndroidStudio Logcat過濾GC,然後操作App一段時間後會出現上圖的GC內容:

垃圾回收原因+垃圾回收的名稱+釋放物件+釋放物件大小+釋放大型物件的大小+堆統計資料+暫停時間

LOS objects是前面所說到的Art虛擬機器新增的

著重關注最後面的暫停時間,超過16ms會影響介面,一般大於700ms會影響體驗,Android Vitals 將連續丟幀超過 700 毫秒定義為凍幀,也就是42幀

檢視記憶體使用情況

通過檢視記憶體使用情況來分析App的記憶體佔用是非常必要的,下面分別介紹如下兩種方式:

  1. adb shell
  2. Profiler

檢視記憶體使用情況

淺談Android記憶體優化

詳細的使用請參考AndroidDeveloper調查RAM使用情況

使用Profiler分析記憶體

AndroidStudio的Profiler功能越來越強大,不僅整合了記憶體分析,還有電量、CPU、網路等資料的分析。

淺談Android記憶體優化

如何通過Profiler進行記憶體的分析,如何找到記憶體洩漏請檢視

使用 Memory Profiler 檢視 Java 堆和記憶體分配

這裡要說下,Android官網的很多文章都被翻譯成了中文,這對國內的開發者來說越來越有好了,但要注意中文翻譯的文章會比較滯後,最新版一般都是英文。

使用LeakCanary監控記憶體洩漏

淺談Android記憶體優化

LeakCanary名字的由來:Canary是煤礦中金絲雀表達的參考,暗示了礦工將隨身攜帶進入礦井隧道的籠養金絲雀(鳥類)。如果在礦井中收集到一氧化碳等危險氣體,這些氣體會在殺死礦工之前殺死金絲雀,從而提供警告立即離開隧道。

原理:LeakCanary通過ApplicationContext統一註冊監聽的方式,通過application.registerActivityLifecycleCallbacks來繫結Activity生命週期的監聽,從而監控所有Activity; 在Activity執行onDestroy時,開始檢測當前頁面是否存在記憶體洩漏,並分析結果。KeyedWeakReference與ReferenceQueue聯合使用,在弱引用關聯的物件被回收後,會將引用新增到ReferenceQueue;清空後,可以根據是否繼續含有該引用來判定是否被回收;判定回收, 手動GC, 再次判定回收,採用雙重判定來確保當前引用是否被回收的狀態正確性;如果兩次都未回收,則確定為洩漏物件。

LeakCanary的問題:LeakCanary也有一定的不確定性,一般同一個地方反覆洩漏5次,算是一個洩漏,同時不建議用線上上環境。

詳細檢視 Github

線上監控

線上的記憶體監控一般都是一些大公司在做,例如美團的Probe還有微信最近開源的Matrix,個人覺得這個可以去了解下,大公司使用者數多時會用到,小公司App接入必要性不是很大,一般來說把上面的介紹的部分做好了就足夠了。

發表與 2019-01-11

原文連結

相關文章