Android 關於內存泄露,你必須瞭解的東西

前言

內存管理的目的就是讓咱們在開發過程當中有效避免咱們的應用程序出現內存泄露的問題。內存泄露相信你們都不陌生,咱們能夠這樣理解:「沒有用的對象沒法回收的現象就是內存泄露」。android

若是程序發生了內存泄露,則會帶來如下這些問題git

  • 應用可用的內存減小,增長了堆內存的壓力github

  • 下降了應用的性能,好比會觸發更頻繁的 GC設計模式

  • 嚴重的時候可能會致使內存溢出錯誤,即 OOM Error性能優化

OOM 發生在,當咱們嘗試進行建立對象,可是堆內存沒法經過 GC 釋放足夠的空間,堆內存也沒法再繼續增加,從而完成對象建立請求的時候,OOM 發生頗有多是內存泄露致使的,但並不是全部的 OOM 都是由內存泄露引發的,內存泄露也並不必定引發 OOM。bash

1、基礎準備


若是真的想比較清楚的瞭解內存泄露的話,對於 Java 的內存管理以及引用類型有一個清晰的認識是必不可少的。網絡

  • 理解 Java 的內存管理能讓咱們更深一層地瞭解 Java 虛擬機是怎樣使用內存的,一旦出現內存泄露,咱們也能更加從容地排查問題。框架

  • 瞭解 Java 的引用類型,能讓咱們更加理解內存泄露出現的緣由,以及常見的解決方法。ide

具體的內容,能夠看下這篇文章 你真的懂 Java 的內存管理和引用類型嗎?函數

2、Android 中內存泄露的常見場景 & 解決方案


一、單例形成的內存泄露

單例模式是很是經常使用的設計模式,使用單例模式的類,只會產生一個對象,這個對象看起來像是一直佔用着內存,但這並不意味着就是浪費了內存,內存原本就是拿來裝東西的,只要這個對象一直都被高效的利用就不能叫作泄露。

可是過多的單例會讓內存佔用過多,並且單例模式因爲其 靜態特性,其生命週期 = 應用程序的生命週期,不正確地使用單例模式也會形成內存泄露。

舉個例子:

public class SingleInstanceTest {

    private static SingleInstanceTest sInstance;
    private Context mContext;

    private SingleInstanceTest(Context context){
        this.mContext = context;
    }

    public static SingleInstanceTest newInstance(Context context){
        if(sInstance == null){
            sInstance = new SingleInstanceTest(context);
        }
        return sInstance;
    }
}
複製代碼

上面是一個比較簡單的單例模式用法,須要外部傳入一個 Context 來獲取該類的實例,若是此時傳入的 Context 是 Activity 的話,此時單例就有持有該 Activity 的強引用(直到整個應用生命週期結束)。這樣的話,即便該 Activity 退出,該 Activity 的內存也不會被回收,這樣就形成了內存泄露,特別是一些比較大的 Activity,甚至還會致使 OOM(Out Of Memory)。

解決方法: 單例模式引用的對象的生命週期 = 應用生命週期

public class SingleInstanceTest {

    private static SingleInstanceTest sInstance;
    private Context mContext;

    private SingleInstanceTest(Context context){
        this.mContext = context.getApplicationContext();
    }

    public static SingleInstanceTest newInstance(Context context){
        if(sInstance == null){
            sInstance = new SingleInstanceTest(context);
        }
        return sInstance;
    }
}
複製代碼

能夠看到在 SingleInstanceTest 的構造函數中,將 context.getApplicationContext() 賦值給 mContext,此時單例引用的對象是 Application,而 Application 的生命週期原本就跟應用程序是同樣的,也就不存在內存泄露。

這裏再拓展一點,不少時候咱們在須要用到 Activity 或者 Context 的地方,會直接將 Activity 的實例做爲參數傳給對應的類,就像這樣:

public class Sample {
    
    private Context mContext;
    
    public Sample(Context context){
        this.mContext = context;
    }

    public Context getContext() {
        return mContext;
    }
}

// 外部調用
Sample sample = new Sample(MainActivity.this);
複製代碼

這種狀況若是不注意的話,很容易就會形成內存泄露,比較好的寫法是使用弱引用(WeakReference)來進行改進。

public class Sample {

    private WeakReference<Context> mWeakReference;

    public Sample(Context context){
        this.mWeakReference = new WeakReference<>(context);
    }

    public Context getContext() {
        if(mWeakReference.get() != null){
            return mWeakReference.get();
        }
        return null;
    }
}

// 外部調用
Sample sample = new Sample(MainActivity.this);
複製代碼

被弱引用關聯的對象只能存活到下一次垃圾回收以前,也就是說即便 Sample 持有 Activity 的引用,但因爲 GC 會幫咱們回收相關的引用,被銷燬的 Activity 也會被回收內存,這樣咱們就不用擔憂會發生內存泄露了。

二、非靜態內部類 / 匿名類

咱們先來看看非靜態內部類(non static inner class)和 靜態內部類(static inner class)之間的區別。

class 對比 static inner class non static inner class
與外部 class 引用關係 若是沒有傳入參數,就沒有引用關係 自動得到強引用
被調用時須要外部實例 不須要 須要
可否調用外部 class 中的變量和方法 不能
生命週期 自主的生命週期 依賴於外部類,甚至比外部類更長

能夠看到非靜態內部類自動得到外部類的強引用,並且它的生命週期甚至比外部類更長,這便埋下了內存泄露的隱患。若是一個 Activity 的非靜態內部類的生命週期比 Activity 更長,那麼 Activity 的內存便沒法被回收,也就是發生了內存泄露,並且還有可能發生難以預防的空指針問題。

舉個例子:

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        new MyAscnyTask().execute();
    }

    class MyAscnyTask extends AsyncTask<Void, Integer, String>{
        @Override
        protected String doInBackground(Void... params) {
            try {
                Thread.sleep(5000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            return "";
        }
    }
}
複製代碼

能夠看到咱們在 Activity 中繼承 AsyncTask 自定義了一個非靜態內部類,在 doInbackground() 方法中作了耗時的操做,而後在 onCreate() 中啓動 MyAsyncTask。若是在耗時操做結束以前,Activity 被銷燬了,這時候由於 MyAsyncTask 持有 Activity 的強引用,便會致使 Activity 的內存沒法被回收,這時候便會產生內存泄露。

解決方法: 將 MyAsyncTask 變成非靜態內部類

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        new MyAscnyTask().execute();
    }

    static class MyAscnyTask extends AsyncTask<Void, Integer, String>{
        @Override
        protected String doInBackground(Void... params) {
            try {
                Thread.sleep(50000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            return "";
        }
    }
}
複製代碼

這時候 MyAsyncTask 再也不持有 Activity 的強引用,即便 AsyncTask 的耗時操做還在繼續,Activity 的內存也能順利地被回收。

匿名類和非靜態內部類最大的共同點就是 都持有外部類的引用,所以,匿名類形成內存泄露的緣由也跟靜態內部類基本是同樣的,下面舉個幾個比較常見的例子:

public class MainActivity extends AppCompatActivity {

    private Handler mHandler = 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);
        // ① 匿名線程持有 Activity 的引用,進行耗時操做
        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    Thread.sleep(50000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }).start();

        // ② 使用匿名 Handler 發送耗時消息
        Message message = Message.obtain();
        mHandler.sendMessageDelayed(message, 60000);
    }
複製代碼

上面舉出了兩個比較常見的例子

  • new 出一個匿名的 Thread,進行耗時的操做,若是 MainActivity 被銷燬而 Thread 中的耗時操做沒有結束的話,便會產生內存泄露

  • new 出一個匿名的 Handler,這裏我採用了 sendMessageDelayed() 方法來發送消息,這時若是 MainActivity 被銷燬,而 Handler 裏面的消息還沒發送完畢的話,Activity 的內存也不會被回收

解決方法:

  • 繼承 Thread 實現靜態內部類

  • 繼承 Handler 實現靜態內部類,以及在 Activity 的 onDestroy() 方法中,移除全部的消息 mHandler.removeCallbacksAndMessages(null);

三、集合類

集合類添加元素後,仍引用着集合元素對象,致使該集合中的元素對象沒法被回收,從而致使內存泄露,舉個例子:

static List<Object> objectList = new ArrayList<>();
   for (int i = 0; i < 10; i++) {
       Object obj = new Object();
       objectList.add(obj);
       obj = null;
    }
複製代碼

在這個例子中,循環屢次將 new 出來的對象放入一個靜態的集合中,由於靜態變量的生命週期和應用程序一致,並且他們所引用的對象 Object 也不能釋放,這樣便形成了內存泄露。

解決方法: 在集合元素使用以後從集合中刪除,等全部元素都使用完以後,將集合置空。

objectList.clear();
    objectList = null;
複製代碼

四、其餘的狀況

除了上述 3 種常見狀況外,還有其餘的一些狀況

  • 一、須要手動關閉的對象沒有關閉

    • 網絡、文件等流忘記關閉
    • 手動註冊廣播時,退出時忘記 unregisterReceiver()
    • Service 執行完後忘記 stopSelf()
    • EventBus 等觀察者模式的框架忘記手動解除註冊
  • 二、static 關鍵字修飾的成員變量

  • 三、ListView 的 Item 泄露

3、利用工具進行內存泄露的排查


除了必須瞭解常見的內存泄露場景以及相應的解決方法以外,掌握一些好用的工具,能讓咱們更有效率地解決內存泄露的問題。

一、Android Lint

Lint 是 Android Studio 提供的 代碼掃描分析工具,它能夠幫助咱們發現代碼機構 / 質量問題,同時提供一些解決方案,檢測內存泄露固然也不在話下,使用也是很是的簡單,能夠參考下這篇文章:Android 性能優化:使用 Lint 優化代碼、去除多餘資源

二、leakcanary

LeakCanary 是 Square 公司開源的「Android 和 Java 的內存泄漏檢測庫」,Square 出品,必屬精品,功能很強大,使用也很簡單。建議直接看 Github 上的說明:leakcanary,也能夠參考這篇文章:Android內存優化(六)LeakCanary使用詳解


參考資料

相關文章
相關標籤/搜索