1. 程式人生 > >談一談Android記憶體

談一談Android記憶體

Android記憶體及其優化

或許,因為開發週期的原因;因為自身知識水平的原因;因為經驗的原因;又或者是你接了個爛攤子。我們寫出了並不太理想的程式碼,這都是可以接受的,只要你會去持續優化,這些問題都會得到改善。而有些人是心有餘而力不足,“我也想優化,可是怎麼去優化呢?”。本篇文章將給你帶來一點啟示,讓你從力不從心到知道怎麼去入手優化。

一、 為什麼需要做記憶體優化?

儘管現在的手機硬體越來越好,手機的RAM也是越來越大。但是,分配給應用的每個程序的記憶體也是有限的。如果我們對開發的APP佔用手機的記憶體大小無動於衷,輕則頻繁的記憶體洩漏,重則引起使用者操作卡頓甚至引發OOM導致應用崩潰,導致使用者流失。也許我們沒辦法做到完美,但是很多時候我們只要持續的追求卓越,比競爭對手優秀一點,成功的或許就是你。優秀的人總是不滿足於現狀,總是精益求精,總是想把事情做到更好。保持謙遜!保持進步!

二、講記憶體之前不得不講虛擬機器

Android在4.4之前一直用的都是Dalvik虛擬機器(以下以DVM簡稱),在Android 4.4的時候推出可選擇的ART虛擬機器並且在5.0的時候全面拋棄DVM而完全使用ART代替之。在對比這兩者之間的區別之前,筆者想先給大家普及一些基礎知識。為什麼會有虛擬機器這東西?清楚的可以自行跳過這段。

  • 為什麼會出現虛擬機器?

我們都知道,我們的電腦或者其他硬體裝置只認識二進位制的機器碼(例如0101)的。當我們用一個高階語言(C/C++/Java等等)寫出的程式機器是沒有辦法識別的。所以才有了編譯器的作用,例如當你用C/C++寫了一段漂亮的程式,通過編譯器將這段程式碼翻譯成了機器能識別的機器碼(0101),然後機器便識別到0101代表了某一條指令就去執行了。那麼問題來了,當我們想讓機器去幹某件事的時候,例如顯示一個警告彈窗。在windows上的指令可能是010101(舉例,並非真實指令),而在Linux上定義警告彈窗是101111(同樣為舉例,以下所有涉及的指令僅為舉例需要)。所以我用C/C++寫出了彈一個警告窗的程式碼,在Windows的編譯器編譯下生成了010101的程式碼,當我們拿著010101的程式碼去在Linux系統中執行時,糟糕!可能在linux系統中010101代表關機指令,更有甚者根本就沒有這條指令。所以我們需要在Linux系統中重新編譯生成101111指令,這就非常繁雜。如果在不同的系統平臺上我都要分別去使用所在平臺的編譯器編譯生成它們對應的機器碼去執行(參考下圖)。這就給應用的移植帶來很大的困難。
**圖1:使用C語言時如何在不同平臺上執行**


聰明的人類總是能想到好辦法,虛擬機器的概念從空而降。以Java來講,當我們用Java寫出了一段Java程式碼,編譯器講Java編譯成Java虛擬機器(JVM)能識別的.class檔案。只要生成了.class檔案,我們無論放在Windows上還是Linux中,只要對應平臺安裝了Java虛擬機器,.class檔案都能夠愉快的被虛擬機器執行。我們前面不是講不同平臺機器指令不同的嘛!怎麼這裡同樣的.class檔案就能在不同的平臺上執行了呢?這就歸功於Java虛擬機器了,當我們在不同的平臺上安裝了虛擬機器,Java虛擬機器會將同樣的.class檔案,在不同的平臺上使用不同的指令去執行。也就是講,JVM幫我們去處理不同平臺的指令關係了,我們只需要寫出統一的JVM能識別的class檔案就可以了(見下圖)。這就極大的方便了我們開發跨平臺的應用了。
**圖2**

總結:所以虛擬機器的出現就是方便開發者更容易便捷的開發出跨平臺的應用

  • 為什麼Android拋棄了DVM而選擇ART

    我們都知道Android最終是將Java程式碼編譯成.dex檔案裝載到虛擬機器中去的,DVM是基於JIT(Just In Time),即在執行的時候實時的將部分dex檔案翻譯成機器碼進行執行的,這樣帶來的問題是執行比較耗時,慢!所以Android推出了基於AOT(Ahead Of Time)的ART。它是在應用被安裝的時候提前將.dex檔案翻譯成機器碼放入手機中,當程式被執行的時候無需在實時的翻譯,而是直接執行。速度較與DVM來說更快速。由於減少了在執行時的翻譯工作,減少了CPU的佔用,因為CPU的消耗減少從而間接的減少電量的消耗

  • Dalvik虛擬機器是如何管理記憶體的

    在談論如何管理記憶體的時候,我們通常都會在記憶體分配和回收兩個方面闡述。每當我們的一個應用程式啟動時,zygote程序就會folk一個程序作為應用程式的程序,並且與zygote程序共享分配記憶體的堆。當發生應用程式或者對對堆進行寫操作時,就會對當前的堆分別做拷貝應用程序和zygote程序。因為拷貝工作是一件費時的事情,所以Dalvik在第一次執行拷貝之前會將當前堆分為兩個部分:zygote堆(zygote程序已經使用的部分,主要是一些預載入的類、資源、和物件)和active堆(未使用的部分)。當應用程式程序需要分配物件的時候,會在active堆中分配。如下圖所示:
    **圖3**

    因為我們手機的儲存容量是有限的,Dalvik就會考慮如何更好的利用這有限的記憶體資源,所以Dalvik會使用一定的策略回收分配出去的記憶體以便再次分配。在Dalvik中是使用Mark-Sweep演算法進行記憶體的回收的。從大的步驟來說分為Mark和sweep兩個階段,而演算法的主要過程就是Mark階段了。當垃圾回收執行緒在執行的時候是不允許其它執行緒工作的,所以當啟動垃圾回收的時候必然會帶來其它執行緒的停滯而引發卡頓。所以Dalvik在Mark階段又分解成下面的小階段:

      ①(此階段不允許其它執行緒工作):標記根集物件,所謂根集指的是被全域性變數、棧變數和暫存器等引用的物件

      ②(此階段允許其它執行緒正常工作):
        a、標記被根集物件引用的物件;
        b、使用Card Table標記在執行當前階段的時候,有執行緒修改了的物件,被修改過就置為DIRTY,未被修改過的置為CLEAN

      ③(此階段不允許其它執行緒工作):對在第②階段標記為DIRTY的物件重新標記(因為引用關係可能發生了變化)
    Dalvik分別使用Live Heap BitmapMark Heap Bitmap分別進行標記。為什麼需要兩個物件來標記呢?舉個栗子:一個酒店的十間房子住了十位客人,我們用LiveHeapBitmap分別對十間房間標記為1,當有一位客人退房離開時,我們將房間重新打掃,並且將該房間標記0,表示房間為可用狀態。當某一天又有部分客人需要退房時,我們只會對剩下的9間房子重新檢視是哪幾位客人需要退房,並使用MarkHeapBitMap將剩下的未退房的標記為1,沒有被標記為1的預設都是0。顯然如果有3位客人退房,MarkHeapBitmap中標記為1的有6間房,0的有4間房,LiveHeapBitmap中標記為1的有九間房,顯然我們需要重新打掃房間的是LiveBit中的九間房減去MarkBit中的6間房,剩下的三間房才是需要去清理的。我們以圖來說明:
          首先,第一步。我們當前的狀況是九間住房,一間已經清潔過的房間。
    **圖4**
          然後,第二步。有三間房客退房了,我們需要判斷是哪三間需要做清理工作。
    **圖5**
    上圖表示,當前markHeapBitmap中掃描到有16號房有佔用,將其markBits標記為1,剩下4間標記為0;而LiveBits還是9間標記為1(只有執行清潔後的房間才能標記為0,如果退房了,但沒有清潔當然還是1)。所以,簡單點講。當前markBits告訴我們又4四間房是空的,但是我們顯然不需要都去清潔,因為有一間房是清潔過的,所以我們只要判斷liveBits為1,markBits為0的房(79號房)是我們需要去清潔的。上面退房的過程可以理解為從被引用到未被引用的過程,清潔的過程就是GC清理的過程。所以可以簡單理解是Dalvik這麼設計標記清理的思路了。

  • ART執行時是如何管理記憶體的

    與Dalvik不同的是ART的堆分成了四個空間,由ImageSpaceZygoteSpaceAllocationSpaceLargeObjectSpace,其中前三個空間是在連續記憶體空間上的,最後一個是由零散的空間組成的。首先,我們簡單介紹下ART是如何分配記憶體的,如下圖所示:
    **圖6**
    上圖的流程圖基本反應了ART分配記憶體的過程,這裡不對細節去做更詳細的剖析了。有興趣的讀者可以針對原始碼去一探究竟。

三、優化記憶體
  • ① 儘量減少記憶體的開銷

      如果是同樣的效果能夠使用更少的記憶體分配,更少的觸發GC的發生。這樣從源頭上解決了記憶體不夠使用的問題。

a、使用字串拼接的時候優先考慮StringBuffer。如果你使用String會都分配很多次記憶體,而使用StringBuffer只會分配一次記憶體,後面如果要追加也是在原有的地方進行追加,不會像String需要重新開闢新的記憶體存放。

b、變數在使用的時候才去去申明和例項化。區域性變數的生命週期比全域性變數的生命週期總會短,儘量不要過早或者不必要佔用記憶體。

c、儘量避免使用static,這個要儘量,有些必須為static的不要強求。

  • ② 記憶體洩漏

      當虛擬機器為你分配的一塊記憶體在你不需要的時候無法回收,這就是記憶體發生洩漏。發生記憶體洩漏不會立刻導致你的應用發生崩潰,但如果記憶體洩漏多了,勢必會造成記憶體不夠用導致OOM的崩潰發生了。那麼哪些情況下容易發生記憶體洩漏呢?

a.Context別亂傳
當你有一個單例類,構造方法裡面千萬別將Activity的Context作為引數傳進去,如果必須要Context,可以使用Application的Context代替。還有如果是普通的類,需要傳入Activity的Context,最好使用弱引用,以便記憶體的釋放。

b.非靜態匿名內部類
因為非靜態內部類自動獲得外部類的強引用,而且它的生命週期甚至比外部類更長,這就可能會出現記憶體洩露。如果一個 Activity 的非靜態內部類的生命週期比 Activity 更長,那麼 Activity 的記憶體便無法被回收導致洩漏,而且還有可能發生空指標問題。我們可以使用靜態的內部類代替,這樣就和外部類的例項斷絕了引用關係。

c.靜態集合要置空
集合會引用儲存的物件,靜態的集合生命週期與應用一樣,導致儲存的物件的記憶體無法釋放,所以在不用的時候一定要將集合置空。

d.註冊和反註冊
當我們註冊一些receiver或者EventBus等等,一定要在activity銷燬的時候反註冊,不然很容易導致activity還在被引用而無法釋放記憶體。

e.檔案流
使用檔案流操作時,結束的時候務必一定要關閉。

f.Bitmap
如果你的Activity大量的使用Bitmap時,記得一定要在Activity被銷燬前做釋放操作。

四、記憶體分析
  • ① leakcanary

我們可以藉助開源的記憶體檢查工具,當你的應用發生記憶體洩漏時,它能記錄記憶體洩漏的地方,以及整個引用棧。這個是非常棒的工具,簡單明瞭,筆者從15年的專案到現在的專案都有在用。

  • ② Android Profile

AndroidStudio 3.0推出了這個分析工具,其中的Memory Profile可以幫助我們分析記憶體的佔用情況。這裡不做詳細介紹了,讀者可以去官方的中文文件中學習下方法:
使用 Memory Profiler 檢視 Java 堆和記憶體分配

有任何疑問?歡迎關注微信公眾號:nj_android