Android 如何避免 Context 記憶體洩露

Bakumon發表於2017-03-17

Activity Context 的記憶體洩露

Android 中的 Activity Context 記憶體洩露,簡單說就是 Activity 呼叫 onDestroy() 方法銷燬後,此 Activity 還被其他物件強引用,導致此 Activity 不能被 GC(JAVA 垃圾回收器) 回收,最後出現記憶體洩露。

原因主要有兩個:

  1. 靜態的 Activity Context 或 任何包含 Activity Context 的物件(如,View)沒有在此 Activity Context 銷燬的時候置空引用。
  2. 非靜態內部類或匿名內部類擁有外部類的引用,在 Activity Context 銷燬後,依然執行耗時任務。

static Activity

問題程式碼:

static Activity activity;

void setStaticActivity() {
    activity = this;
}複製程式碼

這裡使用了 static 來修飾 Activity,靜態變數持有 Activity 物件很容易造成記憶體洩漏,因為靜態變數是和應用存活時間相同的,所以當 Activity 生命週期結束時,引用仍被持有。

解決方法:

1.去掉 static 關鍵字,使用別的方法來實現想要的功能。任何時候不建議 static 修飾 Activity,如果這樣做了,Android Studio 也會給出警告提示。

2.在 onDestroy 方法中置空 Activity 靜態引用

@Override
public void onDestroy() {
    super.onDestroy();
    if (activity != null) {
        activity = null;
    }
}複製程式碼

3.也可以使用到軟引用解決,確保在 Activity 銷燬時,垃圾回收機制可以將其回收。像下面這樣做:

private static WeakReference<MainActivity> activityReference;

private void setStaticActivity() {
    activityReference = new WeakReference<MainActivity>(this);
}
// 注意在使用時,必須判空
private void useActivityReference(){
    MainActivity activity = activityReference.get();
    if (activity != null) {
        // ...
    }
}複製程式碼

static 間接修飾 Activity Context

問題程式碼:

public class LoadingDialog extends Dialog {

    private static LoadingDialog mDialog;
    private TextView mText;
}複製程式碼

這裡 static 雖然沒有直接修飾 TextView(擁有 Context 引用),但是修飾了 mDialog 成員變數,mDialog 是 一個 LoadingDialog 物件, LoadingDialog 物件 包含一個 TextView 型別的成員變數,所以 mText 變數的生命週期也是全域性的,和應用一樣。這樣,mText 持有的 Context 物件銷燬時,沒有 GC 回收,導致記憶體洩露。

解決方法:

1.不使用 static 修飾

2.在適當的地方如,dismissLoadingDialog() 方法中置空 mDialog,這樣雖然可以解決,但是也存在風險,如果 dismissLoadingDialog() 沒有或忘記被呼叫,同樣也會導致記憶體洩漏。

public static void dismissLoadingDialog() {
    if (mDialog != null && mDialog.isShowing()) {
        mDialog.dismiss();
        mDialog = null;
    }
}複製程式碼

單例引用 Activity Context

問題程式碼:

public class AppManager {

    private static AppManager instance;
    private Context context;

    private AppManager(Context context) {
        this.context = context;
    }

    public static AppManager getInstance(Context context) {
        if (instance == null) {
            instance = new AppManager(context);
        }
        return instance;
    }
}複製程式碼

這是一段典型的單例模式,不同的是 AppManager 需要 Context 作為成員變數,和上面例子一樣,這裡延長了 Context 的存活時間,Context 如果是 Activity Context 的話,必然會引起記憶體洩漏。

解決方法:

1.使用 Applicaion Context 代替 Activity Context (推薦)

private AppManager(Context context) {
    this.context = context.getAppcalition();
}複製程式碼

或者在 App 中寫一個獲取 Applicaion Context 的方法。

private AppManager() {
    this.context = App.getAppcalitionContext();
}複製程式碼

2.在呼叫的地方使用弱引用

WeakReference<MainActivity> activityReference = new WeakReference<MainActivity>(this);
Context context = activityReference.get();
if(context != null){
    AppManager.getInstance(context);
    // ...
}複製程式碼

非靜態或匿名內部類執行耗時任務

問題程式碼:

public class MainActivity extends Activity {  
    @Override
    protected void onCreate(Bundle savedInstanceState) {    
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        test();
    }  

    public void test() {    
        new Thread(new Runnable() {     
            @Override
            public void run() {        
                while (true) {          
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }).start();
    }
}複製程式碼

由於非靜態內部類或匿名內部類都會擁有所在外部類的引用,上邊的程式碼,由於 new Thread 是匿名內部類,並且執行了長時間(一直)的任務,當 Activity 銷燬後,該匿名內部類還在執行任務,導致外部的 Activity 不能被回收,導致記憶體洩露。

解決方法:

1.靜態化匿名內部類

// static 修飾方法
public static void test() {
    new Thread(new Runnable() {     
            @Override
            public void run() {        
                while (true) {          
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }).start();
}複製程式碼

如果是內部類,這樣寫:

static class MyThread extends Thread {
    @Override
    public void run() {
        // ...
    }
}複製程式碼

靜態內部類持有外部類靜態成員變數

雖然靜態內部類的生命週期和外部類無關,但是如果在內部類中想要引入外部成員變數的話,這個成員變數必須是靜態的了,這也可能導致記憶體洩露。

問題程式碼:

public class MainActivity extends Activity {  

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

    private static class MyThread extends Thread {
        @Override
        public void run() {
           // 耗時操作
           mMainActivity...
        }
    }
}複製程式碼

解決方法:

使用弱引用。

public class MainActivity extends Activity {  

    private static WeakReference<MainActivity> activityReference;
    @Override
    protected void onCreate(Bundle savedInstanceState) {    
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        activityReference = new WeakReference<MainActivity>(this);;
    }  

    private static class MyThread extends Thread {
        @Override
        public void run() {
           // 耗時操作
           MainActivity activity = activityReference.get();
           if(activity != null){
               activity...
           }
        }
    }
}複製程式碼

Handler引起的記憶體洩漏

問題程式碼:

public class MainActivity extends Activity {

    private final Handler mLeakyHandler = new Handler() {
        @Override
        public void handleMessage(Message msg) {
            // ...
        }
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        mLeakyHandler.postDelayed(new Runnable() {
            @Override
            public void run() {
                // ...
            }
        }, 1000 * 60 * 10);

        finish();
    }
}複製程式碼

這種情況和 非靜態或匿名內部類執行耗時任務 的原因一樣。 在 MainActivity 中傳送了一個延遲10分鐘執行的訊息 Message,mLeakyHandler 將其 push 進了訊息佇列 MessageQueue 裡。當該 Activity 被 finish() 掉時,Message 還會繼續存在於主執行緒中,Handler 是非靜態內部類,會持有該 MainActivity 的引用,所以此時 finish() 掉的 Activity 就不會被回收了從而造成記憶體洩漏。

解決方法:

自定義靜態的 Handler

public class MainActivity extends Activity {

    private final MyHandler mHandler = new MyHandler(this);

    private static final Runnable mRunnable = new Runnable() {
        @Override
        public void run() { /* ... */ }
    };

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        mHandler.postDelayed(mRunnable, 1000 * 60 * 10);
        finish();
    }

    private static class MyHandler extends Handler {
        private final WeakReference<MainActivity> mActivityReference;

        public MyHandler(MainActivity activity) {
            mActivityReference = new WeakReference<MainActivity>(activity);
        }

        @Override
        public void handleMessage(Message msg) {
            MainActivity activity = mActivityReference.get();
            if (activity != null) {
                // ...
            }
        }
    }
}複製程式碼

首先定義一個靜態的 MyHandler 類,它將不會隱式的持有 MainActivity 的引用,並且內部利用弱引用獲取外部類的引用,這樣在 MainActivity 被 finish 掉後,弱引用不會影響 MainActivity 被回收,也就避免了記憶體洩露。另外,成員變數 mRunnable 也是靜態的,生命週期和應用一樣,並且不持有外部類的引用。

總結

Android 的 Context 記憶體洩露,其實就是因為 Activity 是有生命週期的,所以在 Activity 銷燬後,必須釋放掉所有對其的強引用,否則 GC 將不會及時回收已經不再使用的 Activity,導致記憶體洩露。所以,我們在使用 Activity Context 的時候,應該注意判斷下在 Activity 銷燬時此變數是否依然引用 Activity。

參考資料:
Android 記憶體洩漏總結
Android 記憶體洩漏分析心得

本文由 Bakumon 創作,採用 知識共享署名4.0 國際許可協議進行許可
本站文章除註明轉載/出處外,均為本站原創或翻譯,轉載前請務必署名

相關文章