1. 程式人生 > >轉載:Android 記憶體洩露分析實戰演練

轉載:Android 記憶體洩露分析實戰演練

版權宣告:本文為博主原創文章,未經博主允許不得轉載。 https://mp.csdn.net/postedit/82736058

轉載自任玉剛微信推文,非常全面所以記錄下來

1. 記憶體洩露簡介

記憶體洩露,即Memory Leak,指程式中不再使用到的物件因某種原因從而無法被GC正常回收。發生記憶體洩露,會導致一些不再使用到的物件沒有及時釋放,這些物件佔用了寶貴的記憶體空間,很容易導致後續需要分配記憶體的時候,記憶體空間不足而出現OOM(記憶體溢位)。無用物件佔據的記憶體空間越多,那麼可用的空閒空間也就越少,GC就會更容易被觸發,GC進行時會停止其他執行緒的工作,因此有可能會造成介面卡頓等情況。

為什麼不再使用到的物件無法被GC正常回收呢?這是因為還有其他物件持有無用物件的引用。為什麼其他物件會持有無用物件的引用呢?這通常是我們意外地引進了無用物件的引用。從而導致無用物件無法給正常回收。

常見的記憶體洩露點

  1. 靜態變數

  2. 非靜態內部類(匿名類)

  3. 集合類

  4. 使用資源物件後未關閉

後面會對這些記憶體洩露點逐一分析。

2. 常見記憶體洩露例子及解決方案

2.1 靜態變數記憶體洩露

說明:靜態變數的生命週期跟整個程式的生命週期一致。只要靜態變數沒有被銷燬也沒有置null,其物件就一直被保持引用,也就不會被垃圾回收,從而出現記憶體洩露。

static Context這種,lint直接就報警告了,所以建議就別使用了,如下圖:

靜態Context.png

 

來看個比較隱蔽的例子

sTest作為靜態變數,並且持有Activity的引用,sTest的生命週期肯定比Activity長。因此當Activity退出後,由於Activity仍然被sTest引用到,所以Activity就不能被回收,造成了記憶體洩露。

Activity這種佔用記憶體非常多的物件,記憶體洩露的話影響非常大。

解決方案

  • 針對靜態變數
    在不用靜態變數時置為空,如:

sTest = null;
  • 針對Context
    如果用到Context,儘量去使用ApplicaitonContext,避免直接傳遞Activity,比如:

sTest = new Test(getApplicationContext());
  • 針對Activity
    若一定要使用Activity,建議使用弱引用或者軟引入來代替強引用。如下:

//弱引用
WeakReference<Activity> weakReference = new WeakReference<Activity>(this);
Activity activity = weakReference.get();

//軟引用
SoftReference<Activity> softReference=new SoftReference<Activity>(this);
Activity activity1 = softReference.get();

注意:單例模式其生命週期跟應用一樣,所以使用單例模式時傳入的引數需要注意一下,避免傳入Activity等物件造成記憶體洩露。

2.2 非靜態內部類(匿名類)記憶體洩露

說明:非靜態內部類 (匿名類)預設就持有外部類的引用,當非靜態內部類(匿名類)物件的生命週期比外部類物件的生命週期長時,就會導致記憶體洩露。

2.2.1 Handler記憶體洩露

一般我們都是使用內部類來實現Handler,然後lint就直接飄黃警告了,如下:

非靜態內部類.png

這裡會涉及到Handler的原理,如果還不懂Handler原理的話,建議先去看下。

如果Handler中有延遲的任務或者是等待執行的任務佇列過長,都有可能因為Handler繼續執行而導致Activity發生洩漏。

1.首先,非靜態的Handler類會預設持有外部類的引用,包含Activity等。
2.然後,還未處理完的訊息(Message)中會持有Handler的引用。
3.還未處理完的訊息會處於訊息佇列中,即訊息佇列MessageQueue會持有Message的引用。
4.訊息佇列MessageQueue位於Looper中,Looper的生命週期跟應用一致。

因此,此時的引用關係鏈是Looper -> MessageQueue -> Message -> Handler -> Activity。所以,這時退出Activity的話,由於存在上述的引用關係,垃圾回收器將無法回收Activity,從而造成記憶體洩漏。

解決方法

  • 靜態內部類+弱引用
    靜態內部類預設不持有外部類的引用,所以改成靜態內部類即可。同時,這裡採用弱引用來持有Activity的引用。

    private static class MyHalder extends Handler {

        private WeakReference<Activity> mWeakReference;

        public MyHalder(Activity activity) {
            mWeakReference = new WeakReference<Activity>(activity);
        }

        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);
            //...
        }
    }
  • Activity退出時,移除所有資訊
    移除資訊後,Handler將會跟Activity生命週期同步。

    @Override
    protected void onDestroy() {
        super.onDestroy();
        mHandler.removeCallbacksAndMessages(null);
    }

2.2.2 多執行緒引起的記憶體洩露

我們一般使用匿名類等來啟動一個執行緒,如下:

        new Thread(new Runnable() {
            @Override
            public void run() {

            }
        }).start();

同樣,匿名Thread類裡持有了外部類的引用。當Activity退出時,Thread有可能還在後臺執行,這時就會發生了記憶體洩露。

解決方法

  • 靜態內部類
    靜態內部類不持有外部類的引用,如下:

private static class MyThread extends Thread{
        //...  
    }
  • Activity退出時,結束執行緒
    同樣,這裡也是讓執行緒的生命週期跟Activity一致。

其他非靜態內部類(匿名類),都可以按照這個套路來:一個是改成靜態內部類,另外一個就是內部類的生命週期不要超過外部類。

2.3 集合類記憶體洩露

說明:集合類新增元素後,將會持有元素物件的引用,導致該元素物件不能被垃圾回收,從而發生記憶體洩漏。

舉個例子:

        List<Object> objectList = new ArrayList<>();
        for (int i = 0; i < 10; i++) {
            Object o = new Object();
            objectList.add(o);
            o = null;
        }

雖然o已經被置空了,但是集合裡還是持有Object的引用。

解決方法

  • 清空集合物件
    如下:

    objectList.clear();
    objectList=null;

2.4 未關閉資源物件記憶體洩露

說明:一些資源物件需要在不再使用的時候主動去關閉或者登出掉,否則的話,他們不會被垃圾回收,從而造成記憶體洩露。

以下是一些常見的需要主動關閉的資源物件:

  • 1.登出廣播
    如果廣播在Activity銷燬後不取消註冊,那麼這個廣播會一直存在系統中,由於廣播持有了Activity的引用,因此會導致記憶體洩露。

    unregisterReceiver(receiver);
  • 2.關閉輸入輸出流等
    在使用IO、File流等資源時要及時關閉。這些資源在進行讀寫操作時通常都使用了緩衝,如果不及時關閉,這些緩衝物件就會一直被佔用而得不到釋放,以致發生記憶體洩露。因此我們在不需要使用它們的時候就應該及時關閉,以便緩衝能得到釋放,從而避免記憶體洩露。

    InputStream.close();
    OutputStream.close();
  • 3.回收Bitmap
    Bitmap物件比較佔記憶體,當它不再被使用的時候,最好呼叫Bitmap.recycle()方法主動進行回收。

    Bitmap.recycle();
    Bitmap = null;
  • 4.停止動畫
    屬性動畫中有一類無限動畫,如果Activity退出時不停止動畫的話,動畫會一直執行下去。因為動畫會持有View的引用,View又持有Activity,最終Activity就不能給回收掉。只要我們在Activity退出把動畫停掉即可。

    animation.cancel();
  • 5.銷燬WebView
    WebView在載入網頁後會長期佔用記憶體而不能被釋放,因此我們在Activity銷燬後要呼叫它的destory()方法來銷燬它以釋放記憶體。此外,WebView在Android  5.1上也會出現其他的記憶體洩露。具體可以看下這篇文章:WebView記憶體洩漏解決方法。
    所以,要防止WebView記憶體洩露還是比較複雜的。程式碼如下:

@Override
protected void onDestroy() {
    if( mWebView!=null) {
        ViewParent parent = mWebView.getParent();
        if (parent != null) {
            ((ViewGroup) parent).removeView(mWebView);
        }

        mWebView.stopLoading();
        // 退出時呼叫此方法,移除繫結的服務,否則某些特定系統會報錯
        mWebView.getSettings().setJavaScriptEnabled(false);
        mWebView.clearHistory();
        mWebView.clearView();
        mWebView.removeAllViews();
        mWebView.destroy();

    }
    super.on Destroy();
}

所以,總的來說,該關的物件一律都主動去關掉,留著也沒啥用。

3. 常用記憶體洩露檢測工具介紹

3.1  lint

lint是一個靜態程式碼分析工具,同樣也可以用來檢測部分會出現記憶體洩露的程式碼,平時寫碼注意lint飄出來的各種黃色警告即可。如:

非靜態內部類.png

3.2 leakcanary

leakcanary是square開源的一個庫,能夠自動檢測發現記憶體洩露,其使用也很簡單:
build.gradle中新增依賴:

dependencies {
  debugImplementation 'com.squareup.leakcanary:leakcanary-android:1.6.1'
  releaseImplementation 'com.squareup.leakcanary:leakcanary-android-no-op:1.6.1'

  //可選項,如果使用了support包中的fragments
  debugImplementation 'com.squareup.leakcanary:leakcanary-support-fragment:1.6.1'
}

如果遇到下面這個問題:

Failed to resolve: com.squareup.leakcanary:leakcanary-android

根目錄下的build.gradle新增mavenCentral()即可,如下:

allprojects {
    repositories {
        google()
        jcenter()
        mavenCentral()
    }
}

然後在自定義的Application中呼叫以下程式碼就可以了。

public class MyApplication extends Application {
    @Override
    public void onCreate() {
        super.onCreate();
        if (LeakCanary.isInAnalyzerProcess(this)) {
            return;
        }
        LeakCanary.install(this);

        //正常初始化程式碼
    }
}

好了,完事。然後我們寫一個記憶體洩露的例子,來測試一下:

public class MainActivity extends Activity {

    public static Context sContext;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        sContext = this;
    }
}

例子夠簡單了吧,執行起來,然後退出Activity

如果檢測到有記憶體洩漏,通知欄會有提示,如下圖;如果沒有記憶體洩漏,則沒有提示。

leakcanary-1.png


點選通知欄或者點選Leaks那個圖示,可以得到記憶體洩露的資訊,如下圖所示,然後就可以知道是哪裡出現了記憶體洩漏。

leakcanary-2.png

3.3 Memory Profiler

Memory Profiler 是 Android Profiler 中的一個元件,可以幫助你分析應用卡頓,崩潰和記憶體洩露等等問題。

開啟 Memory Profiler後即可看到一個類似下圖的檢視。

memory-profiler-callouts_2x.png


上面的紅色數字含義如下:

1.用於強制執行垃圾回收事件的按鈕。
2.用於捕獲堆轉儲的按鈕。
3.用於記錄記憶體分配情況的按鈕。 此按鈕僅在連線至執行 Android 7.1 或更低版本的裝置時才會顯示。
4.用於放大/縮小/還原時間線的按鈕。
5.用於跳轉至實時記憶體資料的按鈕。
6.Event 時間線,其顯示 Activity 狀態、使用者輸入 Event 和螢幕旋轉 Event。
7.記憶體使用量時間線,其包含以下內容:

  • 一個顯示每個記憶體類別使用多少記憶體的堆疊圖表,如左側的 y 軸以及頂部的彩色鍵所示。

  • 虛線表示分配的物件數,如右側的 y 軸所示。

  • 用於表示每個垃圾回收事件的圖示。

如何使用Memory Profiler分析記憶體洩露呢?按以下步驟來即可:
1.使用Memory Profiler監聽要分析的應用程序
2.旋轉幾次要分析的Activity。(這是因為旋轉Activity後會重新建立)
3.點選捕獲堆轉儲按鈕去捕獲堆轉儲
4.在捕獲結果中搜索要分析的類。(這裡是MainActivity
5.點選要分析的類,右邊會顯示這個類建立物件的數量。

如下圖所示:

使用Memory Profiler分析記憶體洩露.png


可以看到,這裡建立了多個MainActivity,毫無疑問,這裡記憶體洩露了。

關於Memory Profiler的更多使用細節,可以檢視官方文件:使用 Memory Profiler 檢視 Java 堆和記憶體分配。

使用Memory Profiler分析記憶體的技巧

使用 Memory Profiler 時,您應對應用程式碼施加壓力並嘗試強制記憶體洩漏。 在應用中引發記憶體洩漏的一種方式是,先讓其執行一段時間,然後再檢查堆。洩漏在堆中可能逐漸匯聚到分配頂部。 不過,洩漏越小,您越需要執行更長時間的應用才能看到洩漏。

您還可以通過以下方式之一觸發記憶體洩漏:

  1. 將裝置從縱向旋轉為橫向,然後在不同的 Activity 狀態下反覆操作多次。 旋轉裝置經常會導致應用洩漏 ActivityContext 或 View 物件,因為系統會重新建立Activity,而如果您的應用在其他地方保持對這些物件之一的引用,系統將無法對其進行垃圾回收。

  2. 處於不同的 Activity 狀態時,在您的應用與另一個應用之間切換(導航到主螢幕,然後返回到您的應用)。

 

3.4 MAT(Memory Analysis Tools)

一個Eclipse的Java Heap記憶體分析工具,使用Android Studio進行開發的需要另外單獨下載它。關於MAT的使用,可以檢視《Android開發藝術探索》上面的介紹,也可以網上檢視相關資料。這裡就不細說了。

3.5 Memory Monitor、Allocation Tracker和Heap Dump

Memory Monitor和Heap Dump可以用來觀察記憶體的使用情況,Allocation Tracker則可以用來來跟蹤記憶體分配的情況。

這三款工具都是位於Android Device Monitor。但是在Android Studio 3.0之後,Android Device Monitor已經不整合到Android Studio中了。雖然我們還可以單獨開啟Android Device Monitor來使用,但其實Android Studio 3.0之後提供的Memory Profiler等功能足於能夠替代它。所以,這裡也就不多說了,有興趣的可以自行查詢資料去了解。

 

 

— — — END — — —

以上就是剛哥所有的了,相信大家學習了,對記憶體這一塊一定會印象深刻

 

 

再次宣告:本文轉載自任玉剛得微信公眾號:玉剛說

大家想看原文或者其他的,可以去關注他的微信公眾號 玉剛說

 

 

gradle version和sdk的關係:

 

Android Plugin Version Gradle Version SDK BuildTool Version
1.0.0 - 1.1.3 2.2.1 - 2.3  
1.2.0 - 1.3.1 2.2.1 - 2.9  
1.5.0 2.2.1 - 2.13  
2.0.0 - 2.1.2 2.10 - 2.13  
2.1.3 - 2.2.3 2.14.1  
2.3.0+ 3.3+