天天看點

【朝花夕拾】Android性能篇之(八)Android記憶體溢出/洩漏常見案例分析及優化方案最佳實踐總結

記憶體溢出是Android開發中一個老大難的問題,也是我們工作中不可回避的話題,本文将全面總結記憶體溢出、記憶體洩漏産生的原因,Android上常見的溢出或洩漏場景,對應的優化方案,以及常用的記憶體檢測工具的使用。

       轉載請申明,轉自【https://www.cnblogs.com/andy-songwei/p/15091806.html】,謝謝!

       記憶體溢出是Android開發中一個老大難的問題,相關的知識點比較繁雜,絕大部分的開發者都零零星星知道一些,但難以全面。本篇文檔會盡量從廣度和深度兩個方面進行整理,幫助大家梳理這方面的知識點(基于Java)。

一、Java記憶體的配置設定

  這裡先了解一下我們無比關心的記憶體,到底是指的哪一塊區域:

【朝花夕拾】Android性能篇之(八)Android記憶體溢出/洩漏常見案例分析及優化方案最佳實踐總結

       如上圖,整個程式執行過程中,JVM會用一段空間來存儲執行期間需要用到的資料和相關資訊,這段空間一般被稱作Runtime Data Area (運作時資料區),這就是咱們常說的JVM記憶體,我們常說到的記憶體管理就是針對這段空間進行管理。Java虛拟機在執行Java程式時會把記憶體劃分為若幹個不同的資料區域,根據《Java虛拟機規範(Java SE 7版)》的規定,Java虛拟機所管理的記憶體包含了5個區域:程式計數器,虛拟機棧,本地方法棧,GC堆,方法區。如下圖所示:

【朝花夕拾】Android性能篇之(八)Android記憶體溢出/洩漏常見案例分析及優化方案最佳實踐總結

各個區域的作用和包含的内容大緻為:

    (1)程式計數器:是一塊較小的記憶體空間,也有的稱為PC寄存器。它儲存的是程式目前執行的指令的位址,用于訓示執行哪條指令。這塊記憶體中存儲的資料所占空間的大小不會随程式的執行而發生改變,是以,此記憶體區域不會發生記憶體溢出(OutOfMemory)問題。

    (2)Java虛拟機棧:簡稱為Java棧,也就是我們常常說的棧記憶體,它是Java方法執行的記憶體模型。Java棧中存放的是一個個的棧幀,每個棧幀對應的是一個被調用的方法。每一個棧幀中包括了如下部分:局部變量表、操作數棧、方法傳回位址等資訊。每一個方法從調用直至執行完成的過程,就對應着一個棧幀在虛拟機棧中入棧到出棧的過程。在Java虛拟機規範中,對Java棧區域規定了兩種異常狀況:1)如果線程請求的棧深度大于虛拟機所允許的深度,将抛出棧記憶體溢出(StackOverflowError)異常,是以使用遞歸的時候需要注意這一點;2)如果虛拟機棧可以動态擴充,而且擴充時無法申請到足夠的記憶體,就會抛出OutOfMemoryError異常。

    (3)本地方法棧:本地方法棧與Java虛拟機棧的作用和原理非常相似,差別在與前者為執行Nativit方法服務的,而後者是為執行Java方法服務的。與Java虛拟機棧一樣,本地方法棧區域也會抛出StackOverflowError和OutOfMemoryError異常。

    (4)GC堆:也就是我們常說的堆記憶體,是記憶體中最大的一塊,被所有線程共享,此記憶體區域的唯一目的就是存放對象執行個體,幾乎所有的對象執行個體都在這裡配置設定。它是Java的垃圾收集器管理的主要區域,是以被稱為“GC堆”。當無法再擴充時,将會抛出OutOfMemoryError異常。

    (5)方法區:它與堆一樣,也是被線程共享的區域,一般用來存儲不容易改變的資料,是以一般也被稱為“永久代”。在方法區中,存儲了每個類的資訊(包括類名,方法資訊,字段資訊)、靜态變量、常量以及編譯器編譯後的代碼等内容。Java的垃圾收集器可以像管理堆區一樣管理這部分區域,當方法區無法滿足記憶體配置設定需求時,将抛出OutOfMemoryError異常。

       我這裡隻做了一些簡單的介紹,如果想詳細了解每個區域包含的内容及作用,可以閱讀這篇文章:【朝花夕拾】Android性能篇之(二)Java記憶體配置設定。

二、Java垃圾回收

       垃圾回收,即GC(Garbage Collection),回收無用記憶體空間,使其對未來執行個體可用的過程。由于裝置的記憶體空間是有限的,為了防止記憶體空間被占滿導緻應用程式無法運作,就需要對無用對象占用的記憶體進行回收,也稱垃圾回收。 垃圾回收過程中除了會清理廢棄的對象外,還會清理記憶體碎片,完成記憶體整理。

   1、判斷對象是否存活的方法

       GC堆記憶體中存放着幾乎所有的對象(方法區中也存儲着一部分),垃圾回收器在對該記憶體進行回收前,首先需要确定這些對象哪些是“活着”,哪些已經“死去”,記憶體回收就是要回收這些已經“死去”的對象。那麼如何其判斷一個對象是否還“活着”呢?方法主要由如下兩種:

    (1)引用計數法,該算法由于無法處理對象之間互相循環引用的問題,在Java中并未采用該算法,在此不做深入探究;

    (2)根搜尋算法(GC ROOT Tracing),Java中采用了該算法來判斷對象是否是存活的,這裡重點介紹一下。

       算法思想:通過一系列名為“GC Roots” 的對象作為起始點,從這些節點開始向下搜尋,搜尋所走過的路徑稱為引用鍊(Reference Chain),當一個對象到GC Roots沒有任何引用鍊相連(用圖論來說就是從GC Roots到這個對象不可達)時,則證明對象是不可用的,即該對象是“死去”的,同理,如果有引用鍊相連,則證明對象可以,是“活着”的。如下圖所示:      

【朝花夕拾】Android性能篇之(八)Android記憶體溢出/洩漏常見案例分析及優化方案最佳實踐總結

          那麼,哪些可以作為GC Roots的對象呢?Java 語言中包含了如下幾種:

          1)虛拟機棧(棧幀中的本地變量表)中的引用的對象。

          2)方法區中的類靜态屬性引用的對象。

          3)方法區中的常量引用的對象。

          4)本地方法棧中JNI(即一般說的Native方法)的引用的對象。

          5)運作中的線程

          6)由引導類加載器加載的對象

          7)GC控制的對象

          拓展閱讀:

           Java中什麼樣的對象才能作為gc root,gc roots有哪些呢?

2、對象回收的分代算法

       已經找到了需要回收的對象,那這些對象是如何被回收的呢?現代商用虛拟機基本都采用分代收集算法來進行垃圾回收,當然這裡的分代算法是一種混合算法,不同時期采用不同的算法來回收,具體算法我後面會推薦一篇文章較為詳細地介紹,這裡僅大緻介紹一下分代算法。

       由于不同的對象的生命周期不一樣,分代的垃圾回收政策正式基于這一點。是以,不同生命周期的對象可以采取不同的回收算法,以便提高回收效率。該算法包含三個區域:年輕代(Young Generation)、年老代(Old Generation)、持久代(Permanent Generation)。

【朝花夕拾】Android性能篇之(八)Android記憶體溢出/洩漏常見案例分析及優化方案最佳實踐總結

     1)年輕代(Young Generation)

所有新生成的對象首先都是放在年輕代中。年輕代的目标就是盡可能快速地回收哪些生命周期短的對象。

新生代記憶體按照8:1:1的比例分為一個Eden區和兩個survivor(survivor0,survivor1)區。Eden區,字面意思翻譯過來,就是伊甸區,人類生命開始的地方。當一個執行個體被建立了,首先會被存儲在該區域内,大部分對象在Eden區中生成。Survivor區,幸存者區,字面了解就是用于存儲幸存下來對象。回收時先将Eden區存活對象複制到一個Survivor0區,然後清空Eden區,當這個Survivor0區也存放滿了後,則将Eden和Survivor0區中存活對象複制到另外一個survivor1區,然後清空Eden和這個Survivor0區,此時的Survivor0區就也是空的了。然後将Survivor0區和Survivor1區交換,即保持Servivor1為空,如此往複。

當Survivor1區不足以存放Eden區和Survivor0的存活對象時,就将存活對象直接放到年老代。如果年老代也滿了,就會觸發一次Major GC(即Full GC),即新生代和年老代都進行回收。

新生代發生的GC也叫做Minor GC,MinorGC發生頻率比較高,不一定等Eden區滿了才會觸發。

      2)年老代(Old Generation)

在新生代中經曆了多次GC後仍然存活的對象,就會被放入到年老代中。是以,可以認為年老代中存放的都是一些生命周期較長的對象。

年老代比新生代記憶體大很多(大概比例2:1?),當年老代中存滿時觸發Major GC,即Full GC,Full GC發生頻率比較低,年老代對象存活時間較長,存活率比較高。

此處采用Compacting算法,由于該區域比較大,而且通常對象生命周期比較長,compaction需要一定的時間,是以這部分的GC時間比較長。

      3)持久代(Permanent Generation)

       持久代用于存放靜态檔案,如Java類、方法等,該區域比較穩定,對GC沒有顯著影響。這一部分也被稱為運作時常量,有的版本說JDK1.7後該部分從方法區中移到GC堆中,有的版本卻說,JDK1.7後該部分被移除,有待考證。

3、記憶體抖動

       不再使用的記憶體被回收是好事,但也會産生一定的負面影響。在 Android Android 2.2及更低版本上,當發生垃圾回收時,應用的線程會停止,這會導緻延遲,進而降低性能。在Android 2.3開始添加了并發垃圾回收功能,也就是有獨立的GC線程來完成垃圾回收工作,但即便如此,系統執行GC的過程中,仍然會占用一定的cpu資源。頻繁地配置設定和回收記憶體空間,可能會出現記憶體抖動現象。

      記憶體抖動是指在短時間内記憶體空間大量地被配置設定和回收,記憶體占用率馬上升高到較高水準,然後又馬上回收到較低水準,然後再次上升到較高水準...這樣循環往複的現象。展現在代碼中,就是短時間内大量建立和銷毀對象。記憶體抖動嚴重時會造成肉眼可見的卡頓,甚至造成記憶體溢出(記憶體回收不及時也會造成記憶體溢出),導緻app崩潰。

      那麼,如何在代碼層面避免記憶體抖動的發生呢?

       當調用Sytem.gc()時,程式隻會顯示地通知系統需要進行垃圾回收了,但系統并不一定會馬上執行gc,系統可能隻會在後續某個合适的時機再做gc操作。是以對于開發者來說,無法控制對象的回收,是以在做優化時可以從對象的建立上入手,這裡提幾點避免發生記憶體抖動的建議:

盡量避免在較大次數的循環中建立對象,應該把對象建立移到循環體外。

避免在繪制view時的onDraw方法中建立對象,實際上Android官方也是這樣建議的。

如果确實需要使用到大量某類對象,盡量做到複用,這一點可以考慮使用設計模式中的享元模式,建立對象池。

在網上看到一個我們平時很容易忽略的不良代碼示例,這裡摘抄下來加深大家的認識:

我們知道,String的底層實作是數組,不能進行擴容,拼裝字元串的時候會重新生成一個String對象,是以第4行代碼執行一次就會生成一個新的String對象,這段代碼執行完成後就會産生list.size()個對象。下面是優化後的代碼:

 StringBuilder執行append方法時,會在原有執行個體基礎上操作,不會生成新的對象,是以上述代碼執行完成後就隻會産生一個StringBuilder對象。當list的size比較大的時候,這段優化代碼的效果就會比較明顯了。    

       在文章:「記憶體抖動」?别再吓唬面試者們了行嗎 中對記憶體抖動講解得比較清晰,大家可以去讀一讀。

4、對象的四種引用方式

       為了便于對象被回收,常常需要根據實際需要與對象建立不同程度的引用,後文在介紹記憶體洩漏時,需要用到這方面的知識,這裡簡單介紹一下。Java中的對象引用方式有如下4種:

【朝花夕拾】Android性能篇之(八)Android記憶體溢出/洩漏常見案例分析及優化方案最佳實踐總結

       對于強引用的對象,即使是記憶體不夠用了,GC時也不會被JVM作為垃圾回收掉,隻會抛出OutOfMemmory異常,是以我們在解決記憶體洩漏的問題時,很多情況下需要處理強引用的問題。

        這一節對垃圾回收相關的知識做了簡單介紹,想更詳細了解的可以閱讀:【朝花夕拾】Android性能篇之(三)Java記憶體回收。

三、記憶體溢出

       記憶體溢出(Out Of Memory,簡稱OOM)是各個語言中均會出現的問題,也是軟體開發史一直存在的令開發者頭疼的現象。

       1、基本概念

       記憶體溢出是指應用系統中存在無法回收的記憶體或使用的記憶體過多,最終使得程式運作時需要用到的記憶體大于能提供的最大記憶體,此時程式就運作不了,系統會提示記憶體溢出,有時候會自動關閉軟體,重新開機電腦或者軟體後釋放掉一部分記憶體又可以正常運作該軟體,而由系統配置、資料流、使用者代碼等原因而導緻的記憶體溢出錯誤,即使使用者重新執行任務依然無法避免。(參考:百度百科:記憶體溢出)

       2、Android系統裝置中的記憶體

       在Android中,google原生OS虛拟機(Android 5.0之前是Dalvik,5.0及之後是ART)預設給每個app配置設定的記憶體大小為16M(?),不同的廠商在不同的裝置上會設定預設的上限值,可以通過在AndroidManifest的application節點中設定屬性Android:largeHeap=”true”來突破這個上限。我們可以在/system/build.prop檔案中查詢到這些資訊(需要有root權限,當然也可以通過代碼的方式擷取,這裡不做介紹了),以下以我手頭上的一台車機為例:

【朝花夕拾】Android性能篇之(八)Android記憶體溢出/洩漏常見案例分析及優化方案最佳實踐總結

主要字段含義如下(這裡說到的記憶體包括native和dalvik兩部分,dalvik就是我們普通的Java使用記憶體):

dalvik.vm.heapstartsize為app啟動時初始配置設定的記憶體

dalvik.vm.heapgrowthlimit就是一個普通應用的記憶體限制

dalvik.vm.heapsize是在manifest中設定了largeHeap=true 之後,可以使用的最大記憶體值

      我們知道,為了能夠使得Android應用程式安全且快速的運作,Android的每個應用程式都會使用一個專有的虛拟機執行個體來運作,它是由Zygote服務程序孵化出來的,也就是說每個應用程式都是在屬于自己的程序及記憶體區域中運作的,是以Android的一個應用程式的記憶體溢出對别的應用程式影響不大。

    3、 記憶體溢出産生的原因

       從記憶體溢出的定義中可以看出,導緻記憶體溢出的原因有兩個:

    (1)目前使用的對象過大或過多,這些對象所占用的記憶體超過了剩餘的可用空間。

    (2)記憶體洩漏;

    4、記憶體洩漏

       應用中長期保持對某些對象的引用,導緻垃圾收集器無法回收這些對象所占的記憶體,這種現象被稱為記憶體洩漏。準确地說,程式運作配置設定的對象回收不及時或者無法被回收都會導緻記憶體洩漏。記憶體洩漏不一定導緻記憶體溢出,隻有當這些回收不及時或者無法被回收的對象累積占用太多的記憶體,導緻app占用的記憶體超過了系統允許的範圍(也就是前面提到的記憶體限制)時,才會導緻記憶體溢出。

       分類:

四、目前使用記憶體過多導緻記憶體溢出的常見案例舉例及優化方案

    1、Bitmap對象太大造成的記憶體溢出

      Bitmap代表一張位圖檔案,它是非壓縮格式,顯示效果較好,但缺點就是需要占用大量的存儲空間。

    (1)Bitmap占用大量記憶體的原因

       Bitmap是windows标準格式圖形檔案,由點組成,每一個點代表一個像素。每個點可以由多種色彩表示,包括2、4、8、16、24和32位色彩,色彩越高,顯示效果越好,但所占用的位元組數也就越大。計算一張Bitmap所占記憶體大小的方式為:大小=圖像長度*圖檔寬度*機關像素占用的位元組數。機關像素占用位元組數其大小由BitmapFactory.Options的inPreferredConfig變量決定,inPreferredConfig為Bitmap.Config類型,是個枚舉類型,檢視Android系統源碼可以找到如下資訊:

       可見inPreferredConfig的預設值為ARGB_8888,對于一張1080*1920px的Bitmap,加載到Android記憶體中時占用的記憶體大小預設為:1080 * 1920 * 4 = 8294400B = 8100KB = 7.91MB。一張普通的bmp圖檔,就能夠占用7.91M的記憶體,可見Bitmap是非常耗記憶體的。是以,對于需要大量使用Bitmap的地方,需要特别注意其對記憶體的使用情況。

    (2)優化建議

       針對上述原因,這裡總結了一些使用Bitmap時的優化建議:

根據實際需要設定Bitmap的解碼格式,也就是上面提到的BitmapFactory.Options的inPreferredConfig變量,不能一味地使用預設的ARGB_8888。下面列舉了Android中Bitmap常見的4種解碼格式圖檔占用記憶體的大小的情況對比:

圖檔格式(Bitmap.Config)

含義說明

每個像素點所占位數

占用記憶體計算方法

一張100*100的圖檔所占記憶體大小

ALPHA_8

用8位表示透明度

8位(1位元組)

圖檔長度*圖檔寬度*1

100*100*1 = 10000位元組

ARGB_4444

用4位表示透明度,4位表示R,4位表示G,4位表示B

4+4+4+4=16位(2位元組)

圖檔長度*圖檔寬度*2

100*100*2 = 20000位元組

ARGB_8888

用4位表示透明度,8位表示R,8位表示G,8位表示B

8+8+8+8=32位(4位元組)

圖檔長度*圖檔寬度*4

100*100*4 = 40000位元組

RGB_565

用5位表示R,6位表示G,5位表示B

5+6+5=16位(2位元組)

 如果采用RGB_565的解碼格式,那麼占用的記憶體大小将會比預設的少一半。

當隻需要擷取圖檔的寬高等屬性值時,可以将BitmapFactory.Options的inJustDecodeBounds屬性設定為true,這樣可以使圖檔不用加載到記憶體中仍然可以擷取的其寬高等屬性。

對圖檔尺寸進行壓縮。如果一張圖檔大小為1080 * 1920px,但我們在裝置上需要顯示的區域大小隻有540 * 960px,此時無需将原圖加載到記憶體中,而是先計算出一個合适的縮放比例(這裡寬高均為原圖的一半,是以縮放比例為2),并指派給BitmapFactory.Options的inSampleSize屬性,也就是設定其采樣率,這樣可以使得占用的記憶體為原來的1/4。

建立Bitmap對象池,複用Bitmap對象。比如某個頁面需要顯示100張相同寬高及解碼格式的圖檔,但螢幕上最多隻能顯示10張,那麼就隻需要建立一個隻有10個Bitmap對象的對象池,在滑動過程中将剛剛隐藏的圖檔對應的bitmap對象複用,而無需建立100個Bitmap對象。這樣可以避免一次占用太多的記憶體以及避免記憶體抖動。

對圖檔品質進行壓縮,也就是降低圖檔的清晰度。代碼如下:

通過如上的幾種常見的方法後,同樣一張bitmap圖檔加載到記憶體後大小隻有原來的1/8不到了。

    3、代碼參考 

下面給出前三種方案的參考代碼:

除此之外還有如下一些Bitmap使用建議,比如使用已有的圖檔處理架構或工具,如Glide、LruCache等;直接使用我們所需尺寸的圖檔等。

       由于Bitmap比較占用記憶體,而且實際開發中Bitmap的使用頻率比較搞,Android官網中給了不少使用建議和規範用于管理記憶體,為了更好的了解這一節的内容以及更好地使用Bitmap,為了更好地使用Bitmap,建議閱讀如下的官方文檔: 

    處理位圖 

    高效加載大型位圖

    緩存位圖

    管理位圖記憶體

    2、使用ListView/GridView時Adapter沒有複用convertView

    (1)占用太多記憶體的原因

       在ListView/GridView中每個convertView對應展示一個資料項,如果不采用複用convertView的方案,當需要展示的資料非常多時,就需要建立大量的convertView對象,導緻對象太多,如果每個convertView上還需要展示bitmap這樣耗記憶體的資源時,就很容易一次性使用太多記憶體導緻記憶體溢出。

    (2)優化方案

         一般新手可能會犯這樣的錯,有一定工作經驗的開發者基本都知道需要複用convertView,  這裡就不貼代碼了。另外可以使用Recycleview替代ListView/GridView,自帶回收功能。

    3、從資料庫中取出大量資料造成的記憶體溢出

    (1)占用記憶體太多的原因

       當查詢資料庫時,會一次性傳回所有滿足條件的資料,加載到記憶體當中,如果資料太多,就會占用太多的記憶體。一般而言,如果一次取十萬條記錄到記憶體,就可能引起記憶體溢出。該問題比較隐蔽,在測試階段,資料庫中資料較少,通常運作正常,應用或者網站正式使用時,資料庫中資料增多,一次查詢即有可能引起記憶體溢出。

       是以,對于資料庫查詢,盡量采用分頁的方式查詢。

    4、應用中存在太多的對象導緻的記憶體溢出

        這個現象在大量使用者通路伺服器時容易出現,短時間内會出現非常多的對象,及程式中出現死循環或者次數很大的循環體中建立對象時,都可能導緻記憶體溢出。

       使用設計模式中的“享元模式”來建立對象池,重複使用對象,比如線程池、常量池等就是典型的例子。另外就是要避免“垃圾”代碼的出現。

五、常見的記憶體洩漏案例及優化方案

    1、Bitmap對象使用完成後不釋放資源

       幾乎所有講記憶體洩漏的文章中都提到,使用完Bitmap後需要調用recycle()方法回收資源,否則會發生記憶體洩漏。代碼樣例如下:

      那麼不調用recycle()方法真的會導緻記憶體溢出嗎?

如下是android-28(Android9.0)中recycle()方法的源碼:

從上述源碼的注釋中,我們可以得到如下資訊:

      1)該方法用于釋放與目前bitmap對象相關聯的native對象,并清理對像素資料的引用。這個方法不能同步地釋放像素資料,而是在沒有其它引用的時候,簡單地允許像素資料被作為垃圾回收掉。

      2)這是一個進階調用,一般情況下不需要調用它,因為在沒有其它對象引用該bitmap對象時,正常的垃圾回收程序将會釋放掉該部分記憶體。

       這裡我們需要先搞清楚,bitmap在記憶體中的存儲分兩部分 :一部分是bitmap對象,另一部分為對應的像素資料,前者占據的記憶體較小,而後者才是記憶體占用的大頭。在google官方開發者文檔:管理位圖記憶體 有如下的描述:

在 Android 2.3.3(API 級别 10)及更低版本上,位圖的後備像素資料存儲在本地記憶體中。它與存儲在 Dalvik 堆中的位圖本身是分開的。本地記憶體中的像素資料并不以可預測的方式釋放,可能會導緻應用短暫超出其記憶體限制并崩潰。從 Android 3.0(API 級别 11)到 Android 7.1(API 級别 25),像素資料會與關聯的位圖一起存儲在 Dalvik 堆上。在 Android 8.0(API 級别 26)及更高版本中,位圖像素資料存儲在原生堆中。

       Java的GC機制隻能回收dalvik記憶體中的垃圾,而對native層無效,native記憶體中的像素資料以不可預測的方式釋放。是以該文章中提到在Android2.3.3及之前的版本中需要調用recycle()方法,來回收native記憶體中的像素資料。

       這裡我有一個疑問,按照我的了解,Android8.0及以上的版本中,像素資料存儲在native堆中,應該也需要通過調用recycle()方法來回收像素資料才對,但這篇官方文檔中,提到Android3.0以上版本的記憶體管理辦法時,并沒有提到要調用recycle()方法,這一點我暫時還沒找到答案。

        總的來說,在所用的Android系統版本中,都調用recycle()應該都不會有問題,隻是是否能避免記憶體洩漏,就需要依不同系統版本而定了。

2、 單例模式中context使用不當産生的記憶體洩漏

    這種形式的記憶體洩漏在初級程式員的代碼中比較常見,如下是一種很常見的單例模式寫法:

當在Activity等短生命周期元件中采用如下代碼調用getInstance方法擷取對象時:

 如果這是第一次建立對象,Activity執行個體就會被對象sInstance中的mContext引用,我們知道static變量的生命周期和app程序生命周期一緻,是以即使目前Activity退出了,sInstance也會一直持有該activity對象而無法被回收,直達app程序消亡。

 解決辦法有兩種:一是調用context.getApplicationContext(),如

二是傳入application的執行個體,如:

實際上這兩種方法得到的context是一樣的,檢視系統源碼時會發現context.getApplicationContext()其實傳回的就是application的執行個體,系統源碼這裡就不深入分析了,讀者最好能自己去一探究竟,加深了解。

       如果目前Activity對象不大的話,該單例模式的context産生的記憶體洩漏影響也會很小,因為整個app生命周期中單例的context最多隻會持有一個該activity對象,而不會一直累加(個人了解)。

3、Handler使用不當産生的記憶體洩漏

這裡我們列舉一種比較常見導緻記憶體洩漏的代碼示例:

實際上對于上述代碼,Android Studio都會看不下去,會給出如下提示:

【朝花夕拾】Android性能篇之(八)Android記憶體溢出/洩漏常見案例分析及優化方案最佳實踐總結

    (1)handler工作機制

       首先我簡單介紹一下Handler的工作機制:這裡面主要包含了4個角色Handler、Message、Looper、MessageQueue,Handler通過sendMessage方法發送Message,Looper中持有MessageQueue,将Handler發送過來Message加入到MessageQueue當中,然後Looper調用looper()按順序處理Message。工作流程如下圖所示:

【朝花夕拾】Android性能篇之(八)Android記憶體溢出/洩漏常見案例分析及優化方案最佳實踐總結

 如果想詳細了解Handler的工作機制,可以閱讀:【朝花夕拾】Handler篇,從源碼的角度了解其工作流程。

    (2)示例代碼記憶體洩漏的原因

       示例中的handler預設持有的是主線程的looper,且處理message也是在主線程中完成的,但是是異步的。最終MyHandler執行個體所發送的Message如果還沒有被處理掉,就會一直持有對應MyHandler的執行個體,而非靜态内部類MyHandler又持有了外部類HandlerDemoActivity,這就導緻MyHandler執行個體發送完Message後,若此時HandlerDemoActivity也退出,由于Looper從MessageQueue中擷取Message并處理是異步的需要排隊,那麼該Activity執行個體是不會馬上被回收的,會一直延遲到消息被處理掉,這樣記憶體洩漏就産生了。如下圖所示:

【朝花夕拾】Android性能篇之(八)Android記憶體溢出/洩漏常見案例分析及優化方案最佳實踐總結

       如果想詳細了解原因,這裡推薦閱讀:Android Handler:詳解 Handler 記憶體洩露的原因

    (3)解決辦法

       這裡有兩種解決方式:

       1)當Activity退出時,如果不需要handler發送的Message繼續被處理(即終止任務),就在onDestroy()回調方法中清空消息隊列,具體代碼如下:

       2)當Activity退出時,如果仍然希望MessageQueue中的Message繼續被處理完,可以将MyHandler定義為靜态内部類。除此之外,還可以在此基礎上使用弱引用來持有外部類,當系統進行垃圾回收時,該弱引用對象就會被回收。具體代碼如下:

    4、子線程使用不當産生的記憶體洩漏

      在Android中使用子線程來執行耗時操作的方式比較多,如使用Thread,Runnable,AsyncTask(最新的Android sdk中已經去掉了)等,産生記憶體洩漏的原因和Handler基本相同,使用匿名内部類或者非靜态内部類時預設持有對外部類執行個體的引用,當外部類如Activity退出時,子線程中的任務還沒有執行完,該Activity執行個體就無法被gc回收,産生記憶體洩漏。

      解決方案也和Handler類似,也分兩種情況:

   (1)如果希望Activity退出後目前線程的任務仍然繼續執行完,可以将匿名内部類或非靜态内部類定義為靜态内部類,還可以結合弱引用來實作,如果耗時很長,可以啟動Service結合子線程來完成。

   (2)Activity退出時,該子線程終止執行,如下為示例代碼:

至于線程中斷方式的選擇和為什麼要用紅色字型的方式來實作線程中斷,這裡不做延伸,推薦閱讀:Java終止線程的三種方式。

    5、集合類長期存儲對象導緻的記憶體洩漏

       集合類使用不當導緻的記憶體洩漏,這裡分兩種情況來讨論:

       1)集合類添加對象後不移除的情況

        對于所有的集合類,如果存儲了對象,如果該集合類執行個體的生命周期比裡面存儲的元素還長,那麼該集合類将一直持有所存儲的短生命周期對象的引用,那麼就會産生記憶體洩漏,尤其是使用static修飾該集合類對象時,問題将更嚴重,我們知道static變量的生命周期和應用的生命周期是一緻的,如果添加對象後不移除,那麼其所存儲的對象将一直無法被gc回收。解決辦法就是根據實際使用情況,存儲的對象使用完後将其remove掉,或者使用完集合類後清空集合,原理和操作都比較簡單,這裡就不舉例了。

       2)根據hashCode的值來存儲資料的集合類使用不當造成的記憶體洩漏

       以HashSet為例子,當一個對象被存儲進HashSet集合中以後,就不能再修改該對象中參與計算hashCode的字段值了,否則,原本存儲的對象将無法再找到,導緻無法被單獨删除,除非清空集合,這樣記憶體洩漏就發生了。

這裡我們舉個例子:

如下為執行的結果:

name為參與計算hashCode的屬性,同一個對象修改name值前後的hashCode值已經不相同了,而HashSet中查找存儲對象就是通過hashCode來定位的,是以在第11行中删除s1對象失效了。

原因找到後,解決方法就容易了,對象存儲到HashSet後就不要再修改參與計算hashCode的字段值,或者在集合對象使用完後清空集合。

  HashMap也是我們經常使用的集合類,HashSet的底層實作就是對HashMap的封裝,也是一樣的原因導緻記憶體洩漏。

測試結果為:

和HashSet一樣,hashCode變了,最初存儲的對象就找不到了,也就沒法再單獨删除該項記錄了,解決辦法和HashSet一樣。另外,一般建議不要使用自定義類對象作為HashMap的key值,盡量使用final修飾的類對象,比如String、Integer等,以避免做為Key的對象被随意改動。

6、資源未關閉造成的洩漏

    (1)Bitmap用完後沒有調用recycle()

       這個前面有探讨過,這裡我們暫時先将這一點也歸納到記憶體洩漏中。

    (2)I/O流使用完後沒有close()

       I/O流使用完後沒有顯示地調用close()方法,一定會産生記憶體洩漏嗎? 

       參考:未關閉的檔案流會引起記憶體洩露麼?

    (3)Cursor使用完後沒有調用close()

        Cursor使用完後沒有顯示地調用close()方法,一定會産生記憶體洩漏嗎? 

        參考:(ANDROID 9.0)關于CURSOR的記憶體洩露問題總結

    (4)沒有停止動畫産生的記憶體洩漏

       在屬性動畫中有一類無限循環動畫,如果在Activity中播放這類動畫并且在onDestroy中去停止動畫,那麼這個動畫将會一直播放下去,這時候Activity會被View所持有,進而導緻Activity無法被釋放。解決此類問題則是需要早Activity中onDestroy去去調用objectAnimator.cancel()來停止動畫。 

 7、使用觀察者模式注冊監聽後沒有反注冊造成的記憶體洩漏

       (1)BroadcastReceiver沒有反注冊

       我們知道,當我們調用context.registerReceiver(BroadcastReceiver, IntentFilter) 的時候,會通過AMS的方式,将所傳入的參數BroadcastReceiver對象和IntentFilter對象通過Binder方式傳遞給系統架構程序中的AMS(ActivityManagerService),這樣AMS持有了BroadcastReceiver對象,BroadcastReceiver對象又持有了外部Activity對象(外部Activity對象也會傳遞到AMS中,在onReceive方法中會傳回該Context),如果沒有進行反注冊,外部Activity在退出後,Activity對象,BroadcastReceiver對象,IntentFilter對象均不能被釋放掉,這樣就産生了記憶體洩漏。這部分的源碼分析如果不清楚的話可以參考:【朝花夕拾】四大元件之(一)Broadcast篇 的第三節。

       我們看看context.unregisterReceiver(BroadcastReceiver)都做了些什麼工作:

從第8行可以看到,這個過程通過Binder的方式轉移到了AMS中,另外getOuterContext()這裡就是外部Acitivity對象了,被封裝到rd對象中一并傳遞給AMS了:

上述源碼中可以看到,在AMS中将BroadcastReceiver對象和IntentFilter對象都清理掉了,同時BroadcastReceiver對象所持有的外部Activity對象也清除了。

是以解決辦法就是在Activity退出時調用unregisterReceiver(BroadcastReceiver),其它元件如Service、Application中使用Broadcast也一樣,退出時要反注冊。

     (2)ContentObserver沒有反注冊導緻的記憶體洩漏

原因和BroadcastReceiver沒有反注冊類似,将ContentObserver對象通過Binder方式添加到了系統服務ContentService中,如果沒有執行反注冊,系統服務會一直持有ContentObserver對象,而ContentObserver對象如果使用匿名内部類或非靜态内部類的方式,那又會持有Activity的執行個體,Activity退出是無法被回收,産生記憶體洩漏。解決方法也是添加反注冊,将添加到系統中的ContentObserver對象清除掉。   

    (3)通用觀察者模式代碼沒有反注冊導緻的記憶體洩漏

       實際上BroadcastReceiver和ContentObserver都是觀察者模式的代表,我們平時在使用觀察者模式的時候,比如注冊監聽,使用回調等,也要特别注意,使用不當就容易産生記憶體洩漏,避免的辦法就是不再使用時執行反注冊。

  8、第三方庫使用不當造成的記憶體洩漏

      使用第三方庫的時候,務必要按照官方文檔指定的步驟來做,否則使用不當也可能産生記憶體洩漏,比如:

    (1)EventBus,也是使用觀察者模式實作的,同樣注冊和反注冊要成對出現。

    (2)Rxjava中,上下文銷毀時,Disposable沒有調用dispose()方法。

    (3)Glide中,在子線程中大量使用Glide.with(applicationContext),可能導緻記憶體溢出

  9、系統bug之InputMethodManager導緻記憶體洩漏

    這點可以閱讀文章:Android InputMethodManager記憶體洩漏 了解一下。

  10、ThreadLocal使用不當産生的記憶體洩漏

       ThreadLocal使用不當也容易産生記憶體洩漏,不過這個類平時大家基本不怎麼用,這裡就不多介紹了。 

六、Android記憶體管理最佳實踐

      Android裝置記憶體有限,為了适應有限的記憶體空間,Android SDK中引入了不少比JavaSE更省記憶體消耗的使用方案,這裡簡單介紹幾個。

    1、使用SparseArray存儲資料

    2、使用Parceable代替Serializable

    3、Android官方的記憶體使用建議

        以下是Android官方提供的記憶體管理文檔,可以參照來合理使用記憶體:

       記憶體管理概覽

       管理應用記憶體 

       程序間的記憶體配置設定

七、使用工具分析記憶體配置設定情況

    1、使用Android Studio自帶的Profiler工具

       官網文檔:使用記憶體性能分析器檢視應用的記憶體使用情況

    2、使用MAT工具

    3、使用Jdk自帶的Java VisualVM工具

    4、LeakCanary原理及使用

繼續閱讀