天天看點

Android中常見的記憶體洩露及解決方法(持續更新)

記憶體洩露的原因

記憶體洩露是指不在需要的對象仍然被引用不能被GC回收釋放,這句話你可能看到過不止一遍了,下面我們來深入研究一下這句話。

首先了解一下兩個名詞:

  • GC: 垃圾回收器,會自動回收不在被引用的記憶體資料
  • GC Roots:不能被回收的對象(這裡的解釋不是很好,往下看就明白了)

下面我們來看一張圖:

Android中常見的記憶體洩露及解決方法(持續更新)

GC Roots持有的對象都不能被垃圾回收器回收,是以這裡的Object D不能被回收,Object G可以被回收,那麼那些對象可以作為GC Roots呢,有以下對象:

  • JavaStack(棧)中的引用的對象
  • 方法區中靜态引用指向的對象
  • 方法區中常量引用指向的對象
  • Native方法中JNI引用的對象
  • Thread——活着的線程

可能現在你對這些還不是很明白,不要着急,下面我們講具體的記憶體洩露的例子你就清楚了。

1.單例模式導緻的記憶體洩露

直接上代碼:

public class MyUtil {
    private volatile static MyUtil myUtil;
    private Context context;

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

    public static MyUtil getMyUtil(Context context) {
        if (myUtil == null) {
            synchronized (MyUtil.class){
                if (myUtil == null) {
                    myUtil = new MyUtil(context);
                }
            }
        }
        return myUtil;
    }
}
           
public class MainActivity extends AppCompatActivity {

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

可以看到上面的MyUtil是以一個很普遍的單例類并且在第一次建立的時候傳入context,然後在Activity中初始化這個單例,這樣單例對象級持有了這個Activity的引用,當退出Activity的時候由于被單例對象所持有就不能被GC回收,這就造成了記憶體洩漏。解決方法也很簡單,在application中初始化就可以了,這樣單例對象會持有application的引用。

注:其實這就是我們上面說的GC Roots中的靜态引用,靜态對象引用context導緻對象不能被回收。

2.匿名内部類導緻的記憶體洩露

其實内部類并不會直接導緻記憶體洩露,而是很多情況下我們使用不當導緻的。

我們知道内部類會持有外部類的引用,當内部類對象被GC Roots直接或間接持有導緻不能回收釋放的時候外部類也會被連帶着不能回收釋放,這就是導緻記憶體洩露的原因。

我們看一個非常常見的例子:

public class MainActivity 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_main);
        handler.postDelayed(new Runnable() {
            @Override
            public void run() {

            }
        },  * );
    }
}
           

這裡我們建立了一個匿名内部類并執行個體化了一個對象handler,然後在Activity中發送一個延時事件,這時退出目前Activity會發生記憶體洩露嗎,如果延時任務已經結束就不會記憶體洩漏,如果延時任務沒有結束就會發生記憶體洩露,因為當該 Activity 被 finish() 掉時,延遲執行任務的 Message 還會繼續存在于主線程中,它持有該 Activity 的 Handler 引用,然後又因 為 Handler 為匿名内部類,它會持有外部類的引用,是以此時 finish() 掉的 Activity 就不會被回收了,進而造成記憶體洩漏。

修複方法:在 Activity 中避免使用非靜态内部類或匿名内部類,比如将 Handler 聲明為靜态的,則其存活期跟 Activity 的生命周期就無關了。如果需要用到Activity,就通過弱引用的方式引入 Activity,避免直接将 Activity 作為 context 傳進去。另外, Looper 線程的消息隊列中還是可能會有待處理的消息,是以我們在 Activity 的 Destroy 時或者 Stop 時應該移除消息隊列 MessageQueue 中的消息。

public class MainActivity extends AppCompatActivity {

    private MyHandler handler

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

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

        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);
            MainActivity activity = activityWeakReference.get();
            if (activity != null) {
                //do something
            }
        }
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        handler = new MyHandler(this);
        handler.postDelayed(new Runnable() {
            @Override
            public void run() {

            }
        },  * );
    }

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

關于引用類型這裡提一下:

Java中的引用有四種,分别為強引用,軟引用,弱引用,虛引用。

  • 強引用(Strong References):強應用的對象永遠不會被回收,即使系統記憶體溢出也不會回收
  • 軟應用(Soft References):隻有當系統的記憶體不夠的情況下才會去回收
  • 弱引用(Weak References):弱引用會被jvm忽略,也就說在GC進行垃圾收集的時候,如果一個對象隻有弱引用指向它,那麼和沒有引用指向它是一樣的效果,jvm都會對它就行果斷的銷毀,釋放記憶體。
  • 虛引用(Phantom References):虛幻應用和弱引用的回收機制差不多,都是可以被随時回收的。但是不同的地方是,它的構造方法必須強制傳入ReferenceQueue,因為在jvm回收前(重點: 對,就是回收前,軟引用和弱引用都是回收後),會将PhantomReference對象加入ReferenceQueue中; 還有一點就是PhantomReference.get()方法永遠傳回空,不管對象有沒有被回收。

3.InputMethodManager導緻的記憶體洩露

當我們在代碼中擷取InputMethodManager之後再退出目前的頁面就會發生記憶體洩漏,這是Android的一個bug,直到Android6.0之後才修複,下面我們看一下解決辦法:

public static void fixSoftInputLeaks(final Context context) {
        if (context == null) return;
        InputMethodManager imm =
                (InputMethodManager) Utils.getApp().getSystemService(Context.INPUT_METHOD_SERVICE);
        if (imm == null) return;
        String[] strArr = new String[]{"mCurRootView", "mServedView", "mNextServedView"};
        for (int i = ; i < ; i++) {
            try {
                Field declaredField = imm.getClass().getDeclaredField(strArr[i]);
                if (declaredField == null) continue;
                if (!declaredField.isAccessible()) {
                    declaredField.setAccessible(true);
                }
                Object obj = declaredField.get(imm);
                if (obj == null || !(obj instanceof View)) continue;
                View view = (View) obj;
                if (view.getContext() == context) {
                    declaredField.set(imm, null);
                } else {
                    return;
                }
            } catch (Throwable th) {
                th.printStackTrace();
            }
        }
    }
           

這裡是通過反射将InputMethodManager中的”mCurRootView”, “mServedView”, “mNextServedView”三個變量緻空。

檢測記憶體洩漏的工具

LeakCanary是Android平台一個非常好用的記憶體洩漏檢測工具,我們隻需要将他放到我們的項目中,然後打包app運作到手機上就能自動幫我們找到記憶體洩漏的位置,但是他也不是百分比能檢測出來,而且有時可能還會出現檢測出錯的情況,但這絲毫不影響他好用的優點。

github位址

關于他的使用我這裡就不多說了,網上有很多教程。

我們的Android studio其實也能很好的分析記憶體洩漏,再3.0的版本裡叫做Android Profiler,關于他的使用可以參考這篇文章,講的很詳細。