淺談Android內存優化

今天咱們來聊一聊Android 內存優化,這篇文章原本很早就應該寫了,但由於小遊戲開發太吸引人了,因此這個就拖到了如今纔開始,不過我以爲也不晚😁android

這篇文章主要經過以下三個方面對Android內存優化進行介紹:git

  1. Android內存分配與回收機制
  2. Android經常使用的內存優化方法
  3. Android內存分析與監控

文章不會涉及到native內存的優化,由於普通App開發中涉及的較少,若是想了解能夠參考極客時間張紹文老師的Android開發高手課。github

1、Android內存分配與回收機制

想要優化Android內存,一些必備的基礎知識是不能少的。因此在第一部分,咱們先從Application Framework、Dalvik/Art、Linux內核三個部分由淺入深來說解關於Androd內存相關的知識。web

Application Framework

首先來看下進程的優先級:算法

前臺進程:用戶當前操做所必需的進程。shell

可見進程:沒有任何前臺組件、但仍會影響用戶在屏幕上所見內容的進程。數據庫

服務進程:正在運行已使用 startService() 方法啓動的服務。(後臺播放音樂,網絡下載數據)緩存

後臺進程:對用戶不可見的 Activity 的進程(已調用 Activity 的 onStop() 方法)性能優化

空進程:不含任何活動應用組件的進程。保留這種進程的的惟一目的是用做緩存,以縮短下次在其中運行組件所需的啓動時間微信

進程生命週期:Android 系統將盡可能長時間地保持應用進程,但爲了新建進程或運行更重要的進程,最終須要移除舊進程來回收內存。 爲了肯定保留或終止哪些進程,系統會根據進程中正在運行的組件以及這些組件的狀態,將每一個進程放入「重要性層次結構」中。 必要時,系統會首先消除重要性最低的進程,而後是重要性略高的進程,來回收系統資源。(通常狀況下前臺進程就是與用戶交互的進程了,若是連前臺進程都須要回收那麼此時系統幾乎不可用了)。由此也衍生了不少進程保活的方法(提升優先級,互相喚醒,native保活等等),出現各類殺不死的進程的APP。

最後咱們須要知道:Android中由ActivityManagerService 類集中管理全部進程的內存資源分配,咱們能夠查看其源碼來具體分析實現過程。

Dalvik/Art 虛擬機

Android Dalvik Heap

簡介: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虛擬機執行模式

Dalvik垃圾回收過程:GC會去標記和查找全部可訪問到的活動對象,這個時候整個程序的線程就會掛起,而且虛擬機內部的全部線程也會同時掛起(左下圖) 。之因此要掛起全部線程是確保:全部程序沒有進行任何變動,與此同時GC會隱藏全部處理過的對象,最終確保標記了全部須要回收的對象後,GC纔會恢復全部線程,並釋放空間。

大內存對象分配:當發現須要給一個較大的對象(藍色方塊)分配空間時,發現可用空間仍是夠的,但沒有這麼大的連續空間供新對象使用,這個時候就不得不進行一次GC回收(紅色方塊,右下圖),爲大對象騰出較大而且連續的空間。這就是咱們在分配一個較大對象的時候很是容易引發丟幀和卡頓的緣由之一,因此Android5.0之前你們都認爲Android卡頓是由於Darvik虛擬機的效率低下致使的。

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

  1. GC時掛起全部線程
  2. 大而連續的空間緊張
  3. 內存碎片化嚴重

ART虛擬機的優化

GC過程:在ART中GC會要求程序在分配空間的時候標記自身的堆棧,這個過程很是短,不須要掛起全部程序的線程.這樣就節約了很大一部分時間去查找活動對象。

大內存對象分配:ART裏會有一個獨立的LOS供Bitmap使用,從而提升了GC的管理效率和總體性能.

內存碎片化在ART裏還會有一個moving collector來壓縮活動對象(綠色方塊),使得內存空間更加緊湊。

總結 :Google在ART裏對GC作了很是大的優化(更高效的回收算法),使ART內存分配的效率提升了10倍,GC的效率提升了2-3倍(可見原來效率有多低),不過主要仍是優化中斷和阻塞的時間,頻繁的GC仍是會致使卡頓。

Linux內核

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內存分配從應用層到Linux層的一些知識,因此我總結出上圖內存會致使的一些問題,可是上圖只是列出了一些常見狀況,先後並無絕對的因果關係,最後來講下內存抖動。

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

2、Android經常使用的內存優化方法

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

  1. 下降運行時內存
  2. 代碼優化
  3. 內存泄漏優化

在實際開發中咱們能夠先考慮下降應用的運行時內存,而後針對代碼寫的很差的地方着重優化,最後經過規避一些可能致使內存泄漏的編碼方式,去提早避免內存泄漏的問題。

下降運行時內存

下降運行時內存能夠分爲減少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

代碼優化

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

  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. 考慮第三方庫的大小,若是會和現有的代碼或其餘庫的代碼重複,考慮不要真個引入而是把庫的代碼精簡以後再引入。

內存泄漏優化

內存泄漏的緣由有不少,下面介紹一些常見的,咱們須要在開發中多注意:

  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的生命週期過長,和應用的進程保持一致,使用不當極可能致使對象泄漏。

總結:在實際的線上環境中發現,大部份內存泄漏是由於被調用的對象生命週期不一樣步致使,生命週期不一樣步不只僅會致使內存泄漏,更會出現異常,崩潰等更嚴重的問題。

作好上面說的一、二、3就夠了嗎?

前面咱們已經從系統級別了解了Android Framework、Darlvik/Art虛擬機、Linux在內存分配上的原理,接着又在代碼級別分別從減小內存佔用、避免內存泄漏和代碼優化三個方面介紹瞭如何避免內存問題,再加上當前科技發展是如此迅速,4GB內存已是很常見的手機配置。LPDDR4X的高速閃存也愈來愈被普遍的使用。對於內存優化咱們是否是就已經能夠高枕無憂了,有上面這些就夠了嗎?

我想即便咱們再瞭解內存,寫的代碼再好,用戶的手機再先進,總仍是有出錯的時候,那麼過後的內存分析和監控是必不可少的了!

3、Android內存分析與監控

Android內存分析和監控主要介紹以下四種方式:

  1. 查看GC日誌
  2. 查看內存使用狀況
  3. 經過LeakCanary監控內存 泄漏
  4. 線上監控

查看GC日誌

GC的類型:

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

Alloc: 您的應用在堆已滿時嘗試分配內存引發的垃圾回收。在這種狀況下分配線程中發生了垃圾回收。

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

NativeAlloc:原生分配(如位圖或 RenderScript 分配對象)致使出現原生內存壓力,進而引發的回收。

查看垃圾回收日誌

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

垃圾回收緣由+垃圾回收的名稱+釋放對象+釋放對象大小+釋放大型對象的大小+堆統計數據+暫停時間

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

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

查看內存使用狀況

經過查看內存使用狀況來分析App的內存佔用是很是必要的,下面分別介紹以下兩種方式:

  1. adb shell
  2. Profiler

查看內存使用狀況

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

使用Profiler分析內存

AndroidStudio的Profiler功能愈來愈強大,不只集成了內存分析,還有電量、CPU、網絡等數據的分析。

如何經過Profiler進行內存的分析,如何找到內存泄漏請查看

使用 Memory Profiler 查看 Java 堆和內存分配

這裏要說下,Android官網的不少文章都被翻譯成了中文,這對國內的開發者來講愈來愈有好了,但要注意中文翻譯的文章會比較滯後,最新版通常都是英文。

使用LeakCanary監控內存泄漏

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

原理:LeakCanary經過ApplicationContext統一註冊監聽的方式,經過application.registerActivityLifecycleCallbacks來綁定Activity生命週期的監聽,從而監控全部Activity; 在Activity執行onDestroy時,開始檢測當前頁面是否存在內存泄漏,並分析結果。KeyedWeakReference與ReferenceQueue聯合使用,在弱引用關聯的對象被回收後,會將引用添加到ReferenceQueue;清空後,能夠根據是否繼續含有該引用來斷定是否被回收;斷定回收, 手動GC, 再次斷定回收,採用雙重斷定來確保當前引用是否被回收的狀態正確性;若是兩次都未回收,則肯定爲泄漏對象。

LeakCanary的問題:LeakCanary也有必定的不肯定性,通常同一個地方反覆泄漏5次,算是一個泄漏,同時不建議用在線上環境。

詳細查看 Github

線上監控

線上的內存監控通常都是一些大公司在作,例如美團的Probe還有微信最近開源的Matrix,我的以爲這個能夠去了解下,大公司用戶數多時會用到,小公司App接入必要性不是很大,通常來講把上面的介紹的部分作好了就足夠了。

發表與 2019-01-11

原文連接

相關文章
相關標籤/搜索