Android 內存泄漏分析

1、基礎知識

1.一、內存泄露、內存溢出:

  • 內存泄露(Memory Leak)指一個無用對象持續佔有內存或無用對象的內存得不到及時的釋放,從而形成內存空間的浪費 例如,當Activity的onDestroy()方法被調用之後,Activity 自己以及它涉及到的 View、Bitmap等都應該被回收。可是,若是有一個後臺線程持有對這個Activity的引用,那麼Activity佔據的內存就不能被回收,嚴重時將致使OOM,最終Crash。
  • 內存溢出(Out Of Memory)指一個應用在申請內存時,沒有足夠的內存空間供其使用

相同點:都會致使應用運行出現問題、性能降低或崩潰。 不一樣點:java

  1. 內存泄露是致使內存溢出的緣由之一,內存泄露嚴重時將致使內存溢出
  2. 內存泄露是因爲軟件設計缺陷引發的,能夠經過完善代碼來避免;內存溢出能夠經過調整配置來減小發生頻率,但沒法完全避免

1.二、Java 的內存分配:

  1. 靜態存儲區:在程序整個運行期間都存在,編譯時就分配好空間,主要用於存放靜態數據和常量
  2. 棧區:當一個方法被執行時會在棧區內存中建立方法體內部的局部變量,方法結束後自動釋放內存 堆區:一般存放 new 出來的對象,由 Java 垃圾回收器回收

1.三、四種引用類型:

  1. 強引用(StrongReference):Jvm寧肯拋出 OOM (內存溢出)也不會讓 GC(垃圾回收) 回收具備強引用的對象
  2. 軟引用(SoftReference):只有在內存空間不足時纔會被回收的對象
  3. 弱引用(WeakReference):在 GC 時,一旦發現了只具備弱引用的對象,無論當前內存空間足夠與否,都會回收它的內存
  4. 虛引用(PhantomReference):任什麼時候候均可以被GC回收,當垃圾回收器準備回收一個對象時,若是發現它還有虛引用,就會在回收對象的內存以前,把這個虛引用加入到與之關聯的引用隊列中。程序能夠經過判斷引用隊列中是否存在該對象的虛引用,來了解這個對象是否將要被回收。能夠用來做爲GC回收Object的標誌。

內存泄漏就是指new出來的Object(強引用)沒法被GC回收android

1.四、非靜態內部類和匿名類:

非靜態內部類和匿名類會隱式地持有一個外部類的引用git

1.五、靜態內部類:

外部類無論有多少個實例,都是共享同一個靜態內部類,所以靜態內部類不會持有外部類的引用github

2、內存泄漏狀況分析

2.一、資源未關閉

在使用Cursor,InputStream/OutputStream,File的過程當中每每都用到了緩衝,所以在不須要使用的時候就要及時關閉它們,以便及時回收內存。它們的緩衝不只存在於 java虛擬機內,也存在於java虛擬機外,若是隻是把引用設置爲null而不關閉它們,每每會形成內存泄漏。 此外,對於須要註冊的資源也要記得解除註冊,例如:BroadcastReceiver。動畫也要在界面再也不對用戶可見時中止。bash

2.二、Handler

在以下代碼中app

public class HandlerActivity extends AppCompatActivity {

    private Handler handler = new Handler() {
        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);
        }
    };

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_handler);
    }
    
}
複製代碼

在聲明Handler對象後,IDE會給開發者一個提示:ide

This Handler class should be static or leaks might occur.
複製代碼

意思是:Handler須要聲明爲static類型的,不然可能產生內存泄漏工具

這裏來進行具體緣由分析: 應用在第一次啓動時, 系統會在主線程建立Looper對象,Looper實現了一個簡單的消息隊列,用來循環處理Message。全部主要的應用層事件(例如Activity的生命週期方法回調、Button點擊事件等)都會包含在Message裏,系統會把Message添加到Looper中,而後Looper進行消息循環。主線程的Looper存在於整個應用的生命週期期間。 當主線程建立Handler對象時,會與Looepr對象綁定,被分發到消息隊列的Message會持有對Handler的引用,以便系統在Looper處理到該Message時能調用Handle的handlerMessage(Message)方法。 在上述代碼中,Handler不是靜態內部類,因此會持有外部類(HandlerActivity)的一個引用。當Handler中有延遲的的任務或者等待執行的任務隊列過長時,因爲消息持有對Handler的引用,而Handler又持有對其外部類的潛在引用,這條引用關係會一直保持到消息獲得處理爲止,致使了HandlerActivity沒法被垃圾回收器回收,從而致使了內存泄露。oop

好比,在以下代碼中,在onCreate()方法中令handler每隔一秒就輸出Log日記post

public class HandlerActivity extends AppCompatActivity {

    private final String TAG = "MainActivity";

    private Handler handler = new Handler();

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_handler);
        handler.postDelayed(new Runnable() {
            @Override
            public void run() {
                Log.e(TAG, "Hi");
                handler.postDelayed(this, 1000);
            }
        }, 6000);
    }

}
複製代碼

查看Handler的源碼能夠看到,postDelayed方法其實就是在發送一條延時的Message

public final boolean postDelayed(Runnable r, long delayMillis){
		return sendMessageDelayed(getPostMessage(r), delayMillis);
    }
複製代碼

首先要意識到,非靜態類和匿名內部類都會持有外部類的隱式引用。當HandlerActivity生命週期結束後,延時發送的Message持有Handler的引用,而Handler持有外部類(HandlerActivity)的隱式引用。該引用會繼續存在直到Message被處理完成,而此處並無能夠令Handler終止的條件語句,因此阻止了HandlerActivity的回收,最終致使內存泄漏。

此處使用 LeakCanary 來檢測內存泄露狀況(該工具下邊會有介紹) 先啓動HandlerActivity後退出,等個三四秒後,能夠看到LeakCanary提示咱們應用內存泄漏了

經過文字提示能夠看到問題就出在Handler身上

這裏寫圖片描述

解決辦法就是在HandlerActivity退出後,移除Handler的全部回調和消息

@Override
    protected void onDestroy() {
        super.onDestroy();
       handler.removeCallbacksAndMessages(null);
    }
複製代碼

2.三、Thread

當在開啓一個子線程用於執行一個耗時操做後,此時若是改變配置(例如橫豎屏切換)致使了Activity從新建立,通常來講舊Activity就將交給GC進行回收。但若是建立的線程被聲明爲非靜態內部類或者匿名類,那麼線程會保持有舊Activity的隱式引用。當線程的run()方法尚未執行結束時,線程是不會被銷燬的,所以致使所引用的舊的Activity也不會被銷燬,而且與該Activity相關的全部資源文件也不會被回收,所以形成嚴重的內存泄露。

所以總結來看, 線程產生內存泄露的主要緣由有兩點:

  1. 線程生命週期的不可控。Activity中的Thread和AsyncTask並不會由於Activity銷燬而銷燬,Thread會一直等到run()執行結束纔會中止,AsyncTask的doInBackground()方法同理
  2. 非靜態的內部類和匿名類會隱式地持有一個外部類的引用

例如以下代碼,在onCreate()方法中啓動一個線程,並用一個靜態變量threadIndex標記當前建立的是第幾個線程

public class ThreadActivity extends AppCompatActivity {

    private final String TAG = "ThreadActivity";

    private static int threadIndex;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_thread);
        threadIndex++;
        new Thread(new Runnable() {
            @Override
            public void run() {
                int j = threadIndex;
                while (true) {
                    Log.e(TAG, "Hi--" + j);
                    try {
                        Thread.sleep(2000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }).start();
    }

}
複製代碼

旋轉幾回屏幕,能夠看到輸出結果爲:

04-04 08:15:16.373 23731-23911/com.czy.leakdemo E/ThreadActivity: Hi--2
04-04 08:15:16.374 23731-26132/com.czy.leakdemo E/ThreadActivity: Hi--4
04-04 08:15:16.374 23731-23970/com.czy.leakdemo E/ThreadActivity: Hi--3
04-04 08:15:16.374 23731-23820/com.czy.leakdemo E/ThreadActivity: Hi--1
04-04 08:15:16.852 23731-26202/com.czy.leakdemo E/ThreadActivity: Hi--5
04-04 08:15:18.374 23731-23911/com.czy.leakdemo E/ThreadActivity: Hi--2
04-04 08:15:18.374 23731-26132/com.czy.leakdemo E/ThreadActivity: Hi--4
04-04 08:15:18.376 23731-23970/com.czy.leakdemo E/ThreadActivity: Hi--3
04-04 08:15:18.376 23731-23820/com.czy.leakdemo E/ThreadActivity: Hi--1
04-04 08:15:18.852 23731-26202/com.czy.leakdemo E/ThreadActivity: Hi--5
...
複製代碼

即便建立了新的Activity,舊的Activity中創建的線程依然還在執行,從而致使沒法釋放Activity佔用的內存,從而形成嚴重的內存泄漏

LeakCanary的檢測結果:

這裏寫圖片描述

想要避免由於Thread形成內存泄漏,能夠在Activity退出後主動中止Thread 例如,能夠爲Thread設置一個布爾變量threadSwitch來控制線程的啓動與中止

public class ThreadActivity extends AppCompatActivity {

    private final String TAG = "ThreadActivity";

    private int threadIndex;

    private boolean threadSwitch = true;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_thread);
        threadIndex++;
        new Thread(new Runnable() {
            @Override
            public void run() {
                int j = threadIndex;
                while (threadSwitch) {
                    Log.e(TAG, "Hi--" + j);
                    try {
                        Thread.sleep(2000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }).start();
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        threadSwitch = false;
    }

}
複製代碼

若是想保持Thread繼續運行,能夠按如下步驟來:

  1. 將線程改成靜態內部類,切斷Activity 對於Thread的強引用
  2. 在線程內部採用弱引用保存Context引用,切斷Thread對於Activity 的強引用
public class ThreadActivity extends AppCompatActivity {

    private static final String TAG = "ThreadActivity";

    private static int threadIndex;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_thread);
        threadIndex++;
        new MyThread(this).start();
    }

    private static class MyThread extends Thread {

        private WeakReference<ThreadActivity> activityWeakReference;

        MyThread(ThreadActivity threadActivity) {
            activityWeakReference = new WeakReference<>(threadActivity);
        }

        @Override
        public void run() {
            if (activityWeakReference == null) {
                return;
            }
            if (activityWeakReference.get() != null) {
                int i = threadIndex;
                while (true) {
                    Log.e(TAG, "Hi--" + i);
                    try {
                        Thread.sleep(2000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    }

}
複製代碼

2.四、Context

在使用Toast的過程當中,若是應用連續彈出多個Toast,那麼就會形成Toast重疊顯示的狀況 所以,可使用以下方法來保證當前應用任什麼時候候只會顯示一個Toast,且Toast的文本信息可以獲得當即更新

/**
 * 做者: 葉應是葉
 * 時間: 2017/4/4 14:05
 * 描述:
 */
public class ToastUtils {

    private static Toast toast;

    public static void showToast(Context context, String info) {
        if (toast == null) {
            toast = Toast.makeText(context, info, Toast.LENGTH_SHORT);
        }
        toast.setText(info);
        toast.show();
    }

}
複製代碼

而後,在Activity中使用

public class ToastActivity extends AppCompatActivity {

    private static int i = 0;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_toast);
    }

    public void showToast(View view) {
        ToastUtils.showToast(this, "顯示Toast:" + (i++));
    }

}
複製代碼

先點擊一次Button使Toast彈出後,退出ToastActivity,此時LeakCanary又會提示說形成內存泄漏了

這裏寫圖片描述

當中說起了 Toast.mContext,經過查看Toast類的源碼能夠看到,Toast類內部的mContext指向傳入的Context。而ToastUtils中的toast變量是靜態類型的,其生命週期是與整個應用同樣長的,從而致使 ToastActivity 得不到釋放。所以,對Context的引用不能超過它自己的生命週期。

public Toast(Context context) {
        mContext = context;
        mTN = new TN();
        mTN.mY = context.getResources().getDimensionPixelSize(
                com.android.internal.R.dimen.toast_y_offset);
        mTN.mGravity = context.getResources().getInteger(
                com.android.internal.R.integer.config_toastDefaultGravity);
    }
複製代碼

解決辦法是改成使用 ApplicationContext 便可,由於ApplicationContext會隨着應用的存在而存在,而不依賴於Activity的生命週期

/**
 * 做者: 葉應是葉
 * 時間: 2017/4/4 14:05
 * 描述:
 */
public class ToastUtils {

    private static Toast toast;

    public static void showToast(Context context, String info) {
        if (toast == null) {
            toast = Toast.makeText(context.getApplicationContext(), info, Toast.LENGTH_SHORT);
        }
        toast.setText(info);
        toast.show();
    }

}
複製代碼

2.五、集合

有時候咱們須要把一些對象加入到集合容器(例如ArrayList)中,當再也不須要當中某些對象時,若是不把該對象的引用從集合中清理掉,也會使得GC沒法回收該對象。若是集合是static類型的話,那內存泄漏狀況就會更爲嚴重。 所以,當再也不須要某對象時,須要主動將之從集合中移除

3、LeakCanary

LeakCanary是Square公司開發的一個用於檢測內存溢出問題的開源庫,能夠在 debug 包中輕鬆檢測內存泄露 GitHub地址:LeakCanary

要引入LeakCanary庫,只須要在項目的build.gradle文件添加

testCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.5'
    debugCompile 'com.squareup.leakcanary:leakcanary-android:1.5'
    releaseCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.5'
複製代碼

Gradle強大的可配置性,能夠確保只在編譯 debug 版本時纔會檢查內存泄露,而編譯 release 等版本的時候則會自動跳過檢查,避免影響性能

若是隻是想監測Activity的內存泄漏,在自定義的Application中進行以下初始化便可

/**
 * 做者: 葉應是葉
 * 時間: 2017/4/4 12:41
 * 描述:
 */
public class MyApplication extends Application {

    @Override
    public void onCreate() {
        super.onCreate();
        LeakCanary.install(this);
    }

}
複製代碼

若是還想監測Fragmnet的內存泄漏狀況,則在自定義的Application中進行以下初始化

/**
 * 做者: 葉應是葉
 * 時間: 2017/4/4 12:41
 * 描述:
 */
public class MyApplication extends Application {

    private RefWatcher refWatcher;

    @Override
    public void onCreate() {
        super.onCreate();
        refWatcher = LeakCanary.install(this);
    }

    public static RefWatcher getRefWatcher(Context context) {
        MyApplication application = (MyApplication) context.getApplicationContext();
        return application.refWatcher;
    }

}

複製代碼

而後在要監測的Fragment中的onDestroy()創建監聽

public class BaseFragment extends Fragment {
    
    @Override
    public void onDestroy() {
        super.onDestroy();
        RefWatcher refWatcher = MyApplication.getRefWatcher();
        refWatcher.watch(this);
    }
	
}
複製代碼

當在測試debug版本的過程當中出現內存泄露時,LeakCanary將會自動展現一個通知欄顯示檢測結果

這裏寫圖片描述

這裏提供上述示例代碼下載:Android 內存泄漏分析

相關文章
相關標籤/搜索