LeakCanary檢測內存泄漏及解決辦法

目錄

         內存泄漏定義

內存泄漏造成的影響

LeakCanary工具

LeakCanary捕獲常見內存泄漏以及解決辦法

1、錯誤使用單例造成的內存泄漏

2、Handler造成的內存泄漏

3、Activity 內部類接口回調監聽

4、線程造成的內存泄漏

5、非靜態內部類創建靜態實例造成的內存泄漏

6、由WebView引起的內存泄漏

7、資源未關閉造成的內存泄漏

8、集合類


內存泄漏定義

內存泄露是指無用對象(不再使用的對象)持續佔有內存或無用對象的內存得不到及時釋放,從而造成的內存空間的浪費稱爲內存泄露。內存泄露有時不嚴重且不易察覺,這樣開發者就不知道存在內存泄露,但有時也會很嚴重,會提示你Out of memory。

內存泄漏的根本原因是長生命週期的對象持有短生命週期對象的引用,儘管短生命週期的對象已經不再需要,但由於長生命週期對象持有它的引用而導致不能被回收。

內存泄漏造成的影響

它是造成應用程序OOM的主要原因之一。由於android系統爲每個應用程序分配的內存有限,當一個應用中產生的內存泄漏比較多時,就難免會導致應用所需要的內存超過這個系統分配的內存限額,這就造成了內存溢出而導致應用Crash。

LeakCanary工具

leakCanary是Square開源框架,是一個Android和Java的內存泄露檢測庫,如果檢測到某個 activity 有內存泄露,LeakCanary 就是自動地顯示一個通知,所以可以把它理解爲傻瓜式的內存泄露檢測工具。通過它可以大幅度減少開發中遇到的oom問題,大大提高APP的質量。

LeakCanary捕獲常見內存泄漏以及解決辦法

1、錯誤使用單例造成的內存泄漏

在平時開發中單例設計模式是我們經常使用的一種設計模式,而在開發中單例經常需要持有Context對象,如果持有的Context對象生命週期與單例生命週期更短時,或導致Context無法被釋放回收,則有可能造成內存泄漏,錯誤寫法如下:

public class LoginManager {
    private static LoginManager mInstance;
    private Context mContext;

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


    public static LoginManager getInstance(Context context) {
        if (mInstance == null) {
            synchronized (LoginManager.class) {
                if (mInstance == null) {
                    mInstance = new LoginManager(context);
                }
            }
        }
        return mInstance;
    }

    public void dealData() {
    }

}

若我們在一個Activity中調用的,然後關閉該Activity則會出現內存泄漏。

LoginManager.getInstance(this).dealData();

LeakCanary檢測結果如下:

解決 辦法要保證Context和AppLication的生命週期一樣,修改後代碼如下:

public class LoginManager {
    private static LoginManager mInstance;
    private Context mContext;

    private LoginManager(Context context) {
    //生命週期
        this.mContext = context.getApplicationContext();
    }


    public static LoginManager getInstance(Context context) {
        if (mInstance == null) {
            synchronized (LoginManager.class) {
                if (mInstance == null) {
                    mInstance = new LoginManager(context);
                }
            }
        }
        return mInstance;
    }

    public void dealData() {
    }

}

2、Handler造成的內存泄漏

Handler的使用頻率還是蠻高的,它是工作線程與UI線程之間通訊的橋樑,只是現在大量開源框架對其進行了封裝,我們這裏模擬一種常見使用方式來模擬內存泄漏情形。

public class MainActivity extends AppCompatActivity {
    private Handler mHandler = new Handler();
    private TextView mTextView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        mTextView = (TextView) findViewById(R.id.text);//模擬內存泄露
        mHandler.postDelayed(new Runnable() {
            @Override
            public void run() {
                mTextView.setText("lcj");
            }
        }, 3 * 60 * 1000);
        finish();
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        LApplication.getRefWatcher().watch(this);
    }
}

上述代碼通過內部類的方式創建mHandler對象,此時mHandler會隱式地持有一個外部類對象引用這裏就是MainActivity,當執行postDelayed方法時,該方法會將你的Handler裝入一個Message,並把這條Message推到MessageQueue中,MessageQueue是在一個Looper線程中不斷輪詢處理消息,那麼當這個Activity退出時消息隊列中還有未處理的消息或者正在處理消息,而消息隊列中的Message持有mHandler實例的引用,mHandler又持有Activity的引用,所以導致該Activity的內存資源無法及時回收,引發內存泄漏。

LeakCanary檢測結果如下:

要想避免Handler引起內存泄漏問題,需要我們在Activity關閉退出的時候的移除消息隊列中所有消息和所有的Runnable。上述代碼只需在onDestroy()函數中調用mHandler.removeCallbacksAndMessages(null);就行了。

public class MainActivity1 extends AppCompatActivity {
    private Handler mHandler = new Handler();
    private TextView mTextView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        mTextView = (TextView) findViewById(R.id.text);
        //模擬內存泄露
        mHandler.postDelayed(new Runnable() {
            @Override
            public void run() {
                mTextView.setText("lcj"); //主線程更新ui
            }
        }, 3 * 60 * 1000);
        finish();
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        mHandler.removeCallbacksAndMessages(null);
        mHandler=null;
        LApplication.getRefWatcher().watch(this);
    }
}

handler基本使用 https://blog.csdn.net/ly502541243/article/details/52062179/


//handler內存泄露的解決方案
方法一:通過程序邏輯來進行保護。
1.在關閉Activity的時候停掉你的後臺線程。線程停掉了,就相當於切斷了Handler和外部連接的線,Activity自然會在合適的時候被回收。
2.如果你的Handler是被delay的Message持有了引用,那麼使用相應的Handler的removeCallbacks()方法,把消息對象從消息隊列移除就行了。

方法二:將Handler聲明爲靜態類。
PS:在Java 中,非靜態的內部類和匿名內部類都會隱式地持有其外部類的引用,靜態的內部類不會持有外部類的引用。
靜態類不持有外部類的對象,所以你的Activity可以隨意被回收。由於Handler不再持有外部類對象的引用,導致程序不允許你在Handler中操作Activity中的對象了。所以你需要在Handler中增加一個對Activity的弱引用(WeakReference)。

static class MyHandler extends Handler
    {
        WeakReference<Activity> mWeakReference;
        public MyHandler(Activity activity) 
        {
            mWeakReference=new WeakReference<Activity>(activity);
        }
        @Override
        public void handleMessage(Message msg)
        {
            final Activity activity=mWeakReference.get();
            if(activity!=null)
            {
                if (msg.what == 1)
                {
                    noteBookAdapter.notifyDataSetChanged();
                }
            }
        }
    }

說明:
WeakReference弱引用,與強引用(即我們常說的引用)相對,它的特點是,GC在回收時會忽略掉弱引用,即就算有弱引用指向某對象,但只要該對象沒有被強引用指向(實際上多數時候還要求沒有軟引用,但此處軟引用的概念可以忽略),該對象就會在被GC檢查到時回收掉。對於上面的代碼,用戶在關閉Activity之後,就算後臺線程還沒結束,但由於僅有一條來自Handler的弱引用指向Activity,所以GC仍然會在檢查的時候把Activity回收掉。這樣,內存泄露的問題就不會出現了。

3、Activity 內部類接口回調監聽

在編碼中常常會定義各種接口回調,類似有點擊時間監聽OnClickListener,這些回調監聽有時候就定義在Activity內部,或者直接用Activity對象去實現這個接口,到時候設置監聽的時候直接調用setListener(innerListener)或者setListener(this),innerListener是Activity內部定義的,this就是Activity對象,那麼問題來了,回調監聽並不一定馬上返回,只有在觸發條件滿足的時候纔會回調,這個時間是無法確定的,因此在Activity退出的時候應該顯示的把回調監聽都移除掉setListener(null),既釋放了回調監聽對象佔用的內存,也避免回調監聽繼續持有activity引用;對與內部類還有一種解決方式,和內部Handler相似,定義成static內部類,然後把Activity對象的弱引用傳遞進去,這樣也就萬無一失

4、線程造成的內存泄漏

最早時期的時候處理耗時操作多數都是採用Thread+Handler的方式,後來逐步被AsyncTask取代,直到現在採用RxJava的方式來處理異步。這裏以AsyncTask爲例,可能大部分人都會這樣處理一個耗時操作然後通知UI更新結果:

public class MainActivity extends AppCompatActivity {
    private AsyncTask<Void, Void, Integer> asyncTask;
    private TextView mTextView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        mTextView = (TextView) findViewById(R.id.text);
        testAsyncTask();
        finish();
    }

    private void testAsyncTask() {
        asyncTask = new AsyncTask<Void, Void, Integer>() {
            @Override
            protected Integer doInBackground(Void... params) {
                int i = 0;
                //模擬耗時操作
                while (!isCancelled()) {
                    i++;
                    if (i > 1000000000) {
                        break;
                    }
                    Log.e("LeakCanary", "asyncTask---->" + i);
                }
                return i;
            }

            @Override
            protected void onPostExecute(Integer integer) {
                super.onPostExecute(integer);
                mTextView.setText(String.valueOf(integer));
            }
        };
        asyncTask.execute();

    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        LApplication.getRefWatcher().watch(this);
    }

}

對於上面的例子來說,在處理一個比較耗時的操作時,可能還沒處理結束MainActivity就執行了退出操作,但是此時AsyncTask依然持有對MainActivity的引用就會導致MainActivity無法釋放回收引發內存泄漏。

LeakCanary檢測結果:

如何解決這種內存泄漏呢?在使用AsyncTask時,在Activity銷燬時候也應該取消相應的任務AsyncTask.cancel()方法,避免任務在後臺執行浪費資源,進而避免內存泄漏的發生。

public class MainActivity3 extends AppCompatActivity {
    private AsyncTask<Void, Void, Integer> asyncTask;
    private TextView mTextView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        mTextView = (TextView) findViewById(R.id.text);
        testAsyncTask();
        finish();
    }

    private void testAsyncTask() {
        asyncTask = new AsyncTask<Void, Void, Integer>() {
            @Override
            protected Integer doInBackground(Void... params) {
                int i = 0;
                //模擬耗時操作
                while (!isCancelled()) {
                    i++;
                    if (i > 1000000000) {
                        break;
                    }
                    Log.e("LeakCanary", "asyncTask---->" + i);
                }
                return i;
            }

            @Override
            protected void onPostExecute(Integer integer) {
                super.onPostExecute(integer);
                mTextView.setText(String.valueOf(integer));
            }
        };
        asyncTask.execute();

    }

    private void destroyAsyncTask() {
        if (asyncTask != null && !asyncTask.isCancelled()) {
            asyncTask.cancel(true);
        }
        asyncTask = null;
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        destroyAsyncTask();
        LApplication.getRefWatcher().watch(this);
    }

}

如果內部線程的生命週期比Activity的生命週期要長,那麼內部線程任然默認持有Activity的引用,導致Activity對象無法被回收,但是當這個線程執行完了之後,Activity對象就能被成功的回收了,這會造成一個崩潰風險,可能在線程裏面有調用到一些Activity的內部對象,但是在Activity退出後這些對象有可能有些已經被回收了,就變成null了,這時候要是不進行null的判斷就會報空指針異常,如果這個線程是一直跑的,那就會造成Activity對象一直不會被回收了,因此,在activity退出後一定要做相關的清理操作,中斷線程,取消網絡請求等等

5、非靜態內部類創建靜態實例造成的內存泄漏

注意:靜態變量的生命週期和應用程序一致%
我們先來看看非靜態內部類(non static inner class)和 靜態內部類(static inner class)之間的區別

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

有時我們需要一個可以隨着屏幕旋轉的Activity,比如視頻播放Activity,這時我們爲了防止多次調用onCreate方法導致某些參數重新初始化,我們一般會選擇創建一個內部類和一個靜態實例來保存這些參數,比如以下實現:

public class MainActivity extends AppCompatActivity {
    private static Config mConfig;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        //模擬內存泄露
        if (mConfig == null) {
            mConfig = new Config();
            mConfig.setSize(18);
            mConfig.setTitle("老九門");
        }
        finish();
    }


    @Override
    protected void onDestroy() {
        super.onDestroy();
        LApplication.getRefWatcher().watch(this);
    }

    class Config {
        private int size;
        private String title;

        public int getSize() {
            return size;
        }

        public void setSize(int size) {
            this.size = size;
        }

        public String getTitle() {
            return title;
        }

        public void setTitle(String title) {
            this.title = title;
        }
    }
}

上述代碼看着沒有任何問題,其實內部類都會持有一個外部類引用,這裏這個外部類就是MainActivity,然而內部類實例又是static靜態變量其生命週期與Application生命週期一樣,所以在MainActivity關閉的時候,內部類靜態實例依然持有對MainActivity的引用,導致MainActivity無法被回收釋放,引發內存泄漏。LeakCanary檢測內存泄漏結果如下:

對於這種泄漏的解決辦法就是將內部類改成靜態內部類,不再持有對MainActivity的引用即可,修改後的代碼如下:

public class MainActivity extends AppCompatActivity {
    private static Config mConfig;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        //模擬內存泄露
        if (mConfig == null) {
            mConfig = new Config();
            mConfig.setSize(18);
            mConfig.setTitle("老九門");
        }
        finish();
    }


    @Override
    protected void onDestroy() {
        super.onDestroy();
        LApplication.getRefWatcher().watch(this);
    }

    static class Config {
        private int size;
        private String title;

        public int getSize() {
            return size;
        }

        public void setSize(int size) {
            this.size = size;
        }

        public String getTitle() {
            return title;
        }

        public void setTitle(String title) {
            this.title = title;
        }
    }
}

6、由WebView引起的內存泄漏

在目前的開發中多多少少會用到Hybrid開發方式,這樣我們就會用WebView去承載Html網頁,就如下面這種方式:

java代碼:

public class MainActivity5 extends AppCompatActivity {
    private WebView mWebView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_web);
        mWebView = (WebView) findViewById(R.id.web);
        mWebView.loadUrl("http://www.cnblogs.com/whoislcj/p/5720202.html");
    }


    @Override
    protected void onDestroy() {
        super.onDestroy();
        LApplication.getRefWatcher().watch(this);
    }

}

WebView解析網頁時會申請Native堆內存用於保存頁面元素,當頁面較複雜時會有很大的內存佔用。如果頁面包含圖片,內存佔用會更嚴重。並且打開新頁面時,爲了能快速回退,之前頁面佔用的內存也不會釋放。有時瀏覽十幾個網頁,都會佔用幾百兆的內存。這樣加載網頁較多時,會導致系統不堪重負,最終強制關閉應用,也就是出現應用閃退或重啓。及時Activity關閉時在onDestroy中調用如下代碼也是沒有任何作用。

private void destroyWebView() {
        if (mWebView != null) {
            mLinearLayout.removeView(mWebView);
            mWebView.pauseTimers();
            mWebView.removeAllViews();
            mWebView.destroy();
            mWebView = null;
        }
    }

先看下LeakCanary檢測到的結果如下:

該如何解決呢?這個查了不少資料,其中一種就是使用getApplicationgContext作爲參數構建WebView,然後動態添加到一個ViewGroup中,最後退出的時候調用webView的銷燬的函數,雖然也達到了防止內存溢出的效果,但是在有些網頁彈出時候需要記住密碼的對話框的時候,會出現Unable to add window -- token null is not for an application 的錯誤,所以這裏採用的解決辦法是通過把使用了WebView的Activity(或者Service)放在單獨的進程裏。然後在檢測到應用佔用內存過大有可能被系統幹掉或者它所在的Activity(或者Service)結束後,調用android.os.Process.killProcess(android.os.Process.myPid());,主動Kill掉進程。由於系統的內存分配是以進程爲準的,進程關閉後,系統會自動回收所有內存。

修改後的代碼如下:

public class MainActivity5 extends AppCompatActivity {
    private WebView mWebView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_web);
        mWebView = (WebView) findViewById(R.id.web);
        mWebView.loadUrl("http://www.cnblogs.com/whoislcj/p/5720202.html");
    }

    @Override
    protected void onDestroy() {
        destroyWebView();
        android.os.Process.killProcess(android.os.Process.myPid());
        super.onDestroy();
        LApplication.getRefWatcher().watch(this);

    }

    private void destroyWebView() {
        if (mWebView != null) {
            mWebView.pauseTimers();
            mWebView.removeAllViews();
            mWebView.destroy();
            mWebView = null;
        }
    }

}

7、資源未關閉造成的內存泄漏

 對於使用了BraodcastReceiver,ContentObserver,File,Cursor,Stream,Bitmap等資源的使用,應該在Activity銷燬時及時關閉或者註銷,否則這些資源將不會被回收,造成內存泄漏。例如獲取媒體庫圖片地址代碼在查詢結束的時候一定要調用

Cursor 的關閉方法防止造成內存泄漏。

 String columns[] = new String[]{
                MediaStore.Images.Media.DATA, MediaStore.Images.Media._ID, MediaStore.Images.Media.TITLE, MediaStore.Images.Media.DISPLAY_NAME
        };
        Cursor cursor = this.getContentResolver().query(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, columns, null, null, null);
        if (cursor != null) {
            int photoIndex = cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DATA);
            //顯示每張圖片的地址,但是首先要判斷一下,Cursor是否有值
            while (cursor.moveToNext()) {
                String photoPath = cursor.getString(photoIndex); //這裏獲取到的就是圖片存儲的位置信息
                Log.e("LeakCanary", "photoPath---->" + photoPath);
            }
            cursor.close();
        }

8、集合類

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

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;


轉載鏈接:LeakCanary檢測內存泄漏及解決辦法

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章