天天看點

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檢測結果如下:

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檢測結果如下:

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檢測結果:

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)之間的差別

LeakCanary檢測記憶體洩漏及解決辦法

可以看到非靜态内部類自動獲得外部類的強引用,而且它的生命周期甚至比外部類更長,這便埋下了記憶體洩露的隐患。如果一個 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檢測記憶體洩漏結果如下:

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檢測到的結果如下:

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檢測記憶體洩漏及解決辦法