Android記憶體優化(二):一分鐘發現記憶體洩漏

cc復CC發表於2017-12-13

在上一篇文章Android記憶體優化(一):Java記憶體區域中已經大體上介紹了Java中的記憶體分佈情況,這一篇主要講一下記憶體洩漏的產生原因、記憶體洩漏的危害、記憶體洩漏一鍵分析與定位、以及程式碼中常見的記憶體洩漏。

#1記憶體洩漏的產生原因 前方高能,18歲以下請避讓!!! 驚天大咪咪:記憶體洩漏產生的原因是物件佔著茅坑不拉屎!!! 有必要講一下Android中的垃圾收集是怎麼進行的,Android中使用標記-清除(Mark-Sweep)演算法進行垃圾回收(garbage collection,簡稱GC),就是按照正常套路來說,在坑位(記憶體)不夠的情況下,垃圾收集器會遍歷全部物件,看哪些物件是可以被回收掉騰出記憶體的,這個過程稱為Mark(標記),Mark的時候要求除了垃圾收集執行緒之外,其它的執行緒都停止,這種吊炸天的現象在垃圾收集演算法中稱為Stop The World,世界圍著他轉,這就造成了我們的程式會卡頓,但是一般情況下這個時間就幾十毫秒,我根本就感受不到好嗎。Mark完之後,就是釋放記憶體空間啦,這個過程稱為Sweep(清除)。

這一切看起來很美好,但是就是有記憶體洩漏發生,所以得提一下,不是所有的物件都是特侖蘇,阿呸,不是所有的物件都能被回收的,比如下面的傲嬌賤貨。

  • 垃圾回收的原則:被全域性變數(static)、棧變數和暫存器等直接引用和間接引用的物件不能被回收。
    Android記憶體優化(二):一分鐘發現記憶體洩漏
    所以說,物件即使已經使用完,但卻一直被其它物件引用,就會導致這個物件無法被回收,造成記憶體的浪費,讓別的物件無屎可拉。物件無法被GC回收就是造成記憶體洩露的原因!

#2記憶體洩漏的可能會造成的創傷 如果不是利用工具去找的話,一般情況下記憶體洩漏是比較難發現的,因為Java中不會報記憶體洩漏這種異常,所以在輕微的記憶體洩漏表面上看是跟正常情況下沒有區別的。

  • 2.1 記憶體洩漏跟記憶體溢位(OOM)的區別就是:**量變和質變。**一個兩個記憶體洩漏表面看起來沒毛病,但是量變可以導致質變,記憶體洩漏多了會炸的,就是報OOM異常,應用直接崩潰,連解釋的機會都沒有。
  • 2.2 堆得記憶體大小是確定的,出現記憶體洩漏後可用的記憶體會減小,這又會造成垃圾回收的頻率加劇,上面提到過,垃圾回收的Mark階段會有一種吊炸天的現象,就是Stop The World,除了垃圾回收執行緒之外的執行緒會停止,頻繁的垃圾回收卡頓明顯的感受到。
  • 2.3 應用後臺執行的時候,記憶體佔用大,程式被系統殺死的概率就會大咯。

#3記憶體洩漏的發現 記憶體洩漏的分析的話,必須使用工具才行,慶幸的是,各路大神已經給我們提供了很多強大的記憶體分析工具,我這裡只會講最方便的。這裡提供幾個套餐供選擇 ####3.1 套餐一:Studio自帶Heap Viewer 想不想知道你的應用到底有沒有記憶體洩漏呢?說真,就一分鐘的事。

  • 3.1.1開啟Studio,連上你的應用,然後Android Monitor (1)->Monitors(2)->Memory,上面有四個圖示,暫停圖示是開啟記憶體使用狀態追蹤的開關,預設是開啟的,小車圖示就是手動GC(3),向下箭頭圖示(4)是檢視堆的分配情況,最後的圖示allocation tracker用來跟蹤記憶體分配情況。

  • 3.1.2我講一下我的使用方式,在應用中操作,從activity1跳轉到activity2,然後跳回到activity1介面,這樣是為了分析activity2是否會產生記憶體洩漏。接下來就是真刀真槍的幹了。

  • 3.1.3點選小車圖示(3),手動GC進行垃圾回收,這樣才能更準確的判斷activity2是否有記憶體洩漏發生,最後點選向下箭頭圖示(4),Studio會自動生成hprof檔案並自動展示在Studio介面中。

    Android記憶體優化(二):一分鐘發現記憶體洩漏

  • 3.1.4這個就是記憶體的分析檔案了,點選Analyzer Tasks(5),這是讓Studio幫我們自動分析是否出現記憶體洩漏。

    Android記憶體優化(二):一分鐘發現記憶體洩漏

  • 3.1.5勾上Detect Leaked Activities(6),最後執行(7)就出現分析結果了

    Android記憶體優化(二):一分鐘發現記憶體洩漏

  • 3.1.6看到沒,activity2出現記憶體洩漏了(8),左下角是引用樹(9),通過引用樹就可以定位到記憶體洩漏的具體資訊了。

    Android記憶體優化(二):一分鐘發現記憶體洩漏

####3.2套餐二:Heap Viewer + MAT 是啊,發現有記憶體洩漏了,然而還有其它的選擇,這裡就必須使用到其它的工具進行輔助了。

Android記憶體優化(二):一分鐘發現記憶體洩漏
MAT(Memory Analyzer)記憶體分析工具,這個工具的使用我只簡單講一下,因為我一般不用,不要問為什麼,因為用起來比較麻煩一些。

  • 3.2.1MAT下載,進入下載的官網,我電腦是64位的,所以選擇Windows(x86_64),整個下載安裝流程跟一般軟體沒啥區別,進入新頁面然後點選DOWNLOAD
    Android記憶體優化(二):一分鐘發現記憶體洩漏
    點選click here就可以下載使用了
    Android記憶體優化(二):一分鐘發現記憶體洩漏
  • 3.2.2 hprof檔案匯入,這個檔案的獲取流程跟記憶體洩漏的發現流程基本一樣,按上面說的通過Studio的Heap工具獲取的,但是檔案匯入前需要進行一下轉換,因為MAT工具不能直接使用,轉換也 不麻煩,Studio已經幫你簡化這個過程,一鍵匯出轉換檔案,請看過來
    Android記憶體優化(二):一分鐘發現記憶體洩漏
  • 3.2.3 用MAT開啟hprof的轉換檔案,其中Histogram和Dominator Tree比較常用,分析記憶體洩漏特別需要用到Histogram的兩份檔案對比分析,就是獲取兩份記憶體洩漏前後的hprof轉換檔案
    Android記憶體優化(二):一分鐘發現記憶體洩漏
  • 3.2.3 標題欄Window->Navigator History,開啟 Navigator History皮膚,然後點選開啟Histogram,![](http://upload-images.jianshu.io/upload_images/4821599-205d2c4bd313d070.png?imageMogr2/auto-orient/strip%7![Uploading 記憶體8_883765.png . . .] CimageView2/2/w/1240)
  • 3.2.4 右鍵histogram,將兩份分析檔案的 Histogram結果都新增到 Compare Basket中,點選右上角的!圖示就會生成對比檔案
    Android記憶體優化(二):一分鐘發現記憶體洩漏
  • 3.2.5 這就是最後生成的對比檔案,你還可以自己選擇對比的方式,紅圈裡面提供不同的對比方式,這樣就可以很直觀的看出差異,因為我對比的是同一份檔案,所以物件間木有差異。
    Android記憶體優化(二):一分鐘發現記憶體洩漏

####3.3套餐三:Leakcanary square的開源記憶體洩漏分析框架,好用得不得了,配置很簡單

  • 3.3.1建議在app的build.gradle檔案下新增下面的依賴
dependencies {
        debugCompile 'com.squareup.leakcanary:leakcanary-android:1.5'
        releaseCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.5'
        testCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.5'
 }```
- 3.3.2在你的```Application```中的```onCreate()```方法中進行初始化
複製程式碼

public class ExampleApplication extends Application {

@Override public void onCreate() { super.onCreate(); if (LeakCanary.isInAnalyzerProcess(this)) { // This process is dedicated to LeakCanary for heap analysis. // You should not init your app in this process. return; } LeakCanary.install(this); // Normal app init code... } }

- 3.3.3然後,就沒有然後了,編譯完後執行你的專案,會在專案安裝成功後出現附加的元件,裡面會展示具體的記憶體洩漏路徑。
![](http://upload-images.jianshu.io/upload_images/4821599-68cfb470f5cf3bb0.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
- 3.3.4通過這個洩漏路徑,就對應進行記憶體洩漏的原因進行分析了,你也可以通過輸出的日誌進行記憶體洩漏的定位。
![](http://upload-images.jianshu.io/upload_images/4821599-cd2b7b28aed9223e.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
>注:到這裡3個套餐已經講完了,關於MAT這個套餐我只是講一下基本的使用,其實已經夠用了,怎麼說呢,用起來比較麻煩,所以我自己本身也很少用,我就按自己的使用對比一下三者。
套餐三>套餐一>套餐二
1.套餐三使用最方便,一勞永逸,解析hprof的速度有點慢,但是因為後臺自動解析,所以基本上沒多大關係;
2.套餐一使用最快,切換一下頁面分分鐘就知道有沒有記憶體洩漏,但是需要你每一次都要手動操作;
3.套餐三最麻煩,耗時耗力,但是自動分析工具並不能保證找出所有的記憶體洩漏,這個時候就需要通過MAT輔助分析了。

#4程式碼裡頭記憶體洩漏的常見原因
程式碼中記憶體洩漏大多數產生的原因是不遵循activity的生命週期。
- 4.1單例模式(靜態activity):在你的Activity中定義了一個 static 變數引用了activity,因為static變數的生命週期和app一樣長,就算activity被銷燬,activity物件還是會被static變數持有,一直到app被銷燬,這也是單例模式最容易造成洩漏的原因,如果靜態的單例物件持有activity物件的引用,就會使得該物件不能被正常回收,從而導致了記憶體洩漏。解決辦法是使用Application的Context代替activity的context;
複製程式碼

/**

  • 單例模式 */ public class SingletonClass{     private static SingletonClass instance; private Context context;     public static SingletonClass getInstance(Context context){             synchronized(SingletonClass.class){                 if(instance==null){                     instance=new SingletonClass(Context context);                 }             }         return instance;     }     private SingletonClass(Context context){ this.context = context; //傳入activity的context就會造成記憶體洩露咯 } }
- 4.2靜態View:當一個view 被加入到介面中時,它就會持有 context 的強引用,也就是我們的 activity。如果我們通過一個static成員變數引用了這個 view,相當於直接引用了 activity,然後就洩漏了;
複製程式碼

private static View view; view = findViewById(R.id.sv_button);

- 4.3非靜態內部類:我們都知道,內部類能夠引用外部類的成員,這正是內部類的好處所在,但是恰恰是這個優勢會導致activity記憶體洩漏,因為非靜態內部類預設持有外部類的引用。如果我們建立了一個內部類的物件,並且通過靜態變數持有這個物件,就會導致記憶體洩漏;
複製程式碼
    private static InnerClass inner = new InnerClass();
    class InnerClass {
    }
複製程式碼
- 4.4匿名內部類:匿名類同樣會持有定義它們的物件的引用,如果在 activity 內定義了一個匿名的 AsyncTask 物件,就有可能發生記憶體洩漏了。因為在activity被銷燬之後AsyncTask可能仍然在執行,這樣只能等到AsyncTask執行結束才能回收activity;
複製程式碼

new AsyncTask<Void, Void, Void>() { @Override protected Void doInBackground(Void... params) { while(true); } }.execute();

- 4.5Handler+Runnable:定義一個匿名的 Runnable 物件並將其提交到 Handler 上也可能導致 activity 洩漏。Runnable物件引用了定義它的 activity 物件,而它會被提交到 Handler 的 MessageQueue 中,如果它在 activity 銷燬時還沒有被處理,那就會導致記憶體洩漏了。
複製程式碼

new Handler() { @Override public void handleMessage(Message message) { super.handleMessage(message); } }.postDelayed(new Runnable() { @Override public void run() { while(true); } }, 1000);

- 4.6Thread:原因類似4.5,儘管是在單獨的執行緒執行任務,但是執行緒還是會預設持有外部物件,任務沒有執行完成就不會釋放持有的引用;
複製程式碼

new Thread() { @Override public void run() { while(true); } }.start();

- 4.7資源未關閉:如果使用了BraodcastReceiver,ContentObserver,File,Cursor,Stream,Bitmap等資源,應該在Activity銷燬時及時關閉或者登出,否則這些資源將不會被回收,從而造成記憶體洩漏。
- 4.8集合容器:在我們做快取的時候會用一些資料結構來儲存一些資料,當我們不需要它時要及時清理,不然就會像滾雪球一樣會越來越大,想不洩露都難。

> 可以了,造成記憶體洩露還有很多原因,這就靠慢慢跳坑了,生活太艱難。再話癆一下,“千丈之堤,以螻蟻之穴潰;百尺之室,以突隙之煙焚。”,所以我推薦套餐三Leakcanary,讓你的整個開發過程伴隨著記憶體洩露的監控。
複製程式碼

相關文章