android開發筆記之MAT定位內存泄漏

內存泄漏的代碼Demo

CommonUtils.java文件

public class CommonUtils {

    private static CommonUtils instance;
    private Context context;

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

    public static CommonUtils getInstance(Context context) {
        if (instance == null) {
            instance = new CommonUtils(context);
        }
        return instance;
    }

}

MainActivity類

public class MainActivity extends AppCompatActivity {

    private static final String TAG = "MainActivity";
    private static Link linkInstance;

    class Link {
        public void dosomething() {
            Log.i(TAG,"dosomething");
        }
    }

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

        if (linkInstance == null) {
            linkInstance = new Link();
            linkInstance.dosomething();
        }

        CommonUtils.getInstance(this);
        new MyThread().start();
    }

    class MyThread extends Thread {
        @Override
        public void run() {
            while (true) {
                try {
                    sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

}

生成hprof文件

在android profile中點擊 gc, 然後執行橫屏 ,豎屏切換,點擊dump java heap,再點擊Export capture to file,生成001.hprof.
在這裏插入圖片描述
再執行命令,生成001_mat.hprof給MAT來使用:

hprof-conv D:\001.hprof  D:\001_mat.hprof

MAT的使用

下載一個 Mat工具
http://www.eclipse.org/mat/downloads.php

下載完之後就可以直接使用,雙擊MemoryAnalyzer.exe,然後直接把001_mat.hprof文件拖到 Mat 工具中。也可以File–Open Heap Dump,找到我們要使用的001_mat.hprof.
在這裏插入圖片描述
然後點擊 histogram:
在這裏插入圖片描述
效果如下:
在這裏插入圖片描述
我們重點關注自己寫的代碼,所以在最上面輸入此應用的包名,然後回車:
在這裏插入圖片描述
可以看到都是我們自己寫的代碼了,然後進行挨個分析:
在這裏插入圖片描述
分析
選擇第一個右鍵,List objects -> with incoming references ->回車
with incoming references : 表示該類被哪些內部對象 引用了
with outgoing references : 表示 該類持有了哪些外部對象的引用
回車 之後我們發現有三個 類如下圖。
在這裏插入圖片描述
我們選擇去掉弱引用,軟引用 所 引用的對象(右鍵—Path To GC Roots----exclude all phantom/ewak/soft etc.references):
在這裏插入圖片描述
顯示爲:
在這裏插入圖片描述
一下子就可以定位到問題的地方了.
MainActivity被三個對象引用了.
1.MyThread
2.CommonUtils
3.Link linkInstance
在這裏插入圖片描述

內存泄漏的代碼修改

public class MainActivity extends AppCompatActivity {

    private static final String TAG = "MainActivity";
    
    //錯誤寫法
    //private static Link linkInstance;
    // 解決方法 不定義爲static類型
    private Link linkInstance;

    class Link {
        public void dosomething() {
            Log.i(TAG,"dosomething");
        }
    }

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

        if (linkInstance == null) {
            linkInstance = new Link();
            linkInstance.dosomething();
        }

        // 解決方法 傳入應用的context
        CommonUtils.getInstance(getApplicationContext());
        //錯誤寫法
        //CommonUtils.getInstance(this);

        new MyThread().start();
    }
    //錯誤寫法
    /*****
    class MyThread extends Thread {
        @Override
        public void run() {
            while (true) {
                try {
                    sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
     *****/
    // 解決方法   讓MyThread爲靜態內部類,靜態內部類就不會持有外部類的引用
    private static class MyThread extends Thread {
        @Override
        public void run() {
            while (true) {
                try {
                    sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
    
}

可能內存泄漏的代碼修改後,我們再來上面操作後的情況:
在這裏插入圖片描述
現在是不是沒有上面的幾個內存泄漏的問題了.

關於內存泄漏的一些知識點

爲什麼會有內存泄漏?

一個不會被使用的對象,因爲另一個正在使用的對象持有該對象的引用,導致它不能正常被回收,而停留在堆內存中,內存泄漏就產生了

常見的內存泄漏:

持有Context造成的內存泄漏

在Android中有兩種context對象:Activity和Application.當我們給一個類傳遞context的時候經常使用第一種,而這樣就導致了改類持有對Activity的全部引用,當Activity關閉的時候因爲被其他類持有,而導致無法正常被回收,而導致內存泄漏
解決方案:
在給其他給傳遞context的時候使用Application對象,這個對象的生命週期和共存亡,而不依賴activity的聲明週期. 而對context的引用不要超過他本身的生命週期,謹慎對context使用static關鍵字.

Handler造成的內存泄漏

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

這樣來使用Handler會造成嚴重的內存泄漏.
假設Hanlder中有延遲的任務或是等在執行的任務隊列過長,由於消息隊列持有對handler的引用,而handler又持有activity的隱式引用,這個引用會保持到消息得到處理,而導致activity無法被垃圾回收器進行回收,而導致內存泄漏

解決方案:
可以把Handler放到單獨的類中,或者使用靜態的內部類(靜態內部類不會引用activity)避免泄漏
如果想要在handler內部去調用Activity中的資源,可以在Handler中使用弱引用的方式指向所在的Activity,使用static+WeakReference的方式斷開handler與activity的關係

public static class MyHandler extends Handler {
    //聲明一個弱引用對象
    WeakReference<MainActivity> mReference;
 
    MyHandler(MainActivity activity) {
        //在構造器中傳入Activity,創建弱引用對象
        mReference = new WeakReference<MainActivity>(activity);
    }
 
    public void handleMessage(Message msg) {
        //在使用activity之前先判空處理
        if (mReference != null && mReference.get() != null) {
            mReference.get().text.setText("hello word");
        }
    }
}

使用單利模式造成的內存泄漏

在我們使用單利模式的時候如果使用不當也會造成內存泄漏.因爲單利模式的靜態特徵使得單利模式的生命週期和應用一樣的長,這說明了當一個對象不需要使用了,而單利對象還存在該對象的引用,那麼這個對象就不能正常的被回收,就造成了內存泄漏
解決方案:

XXUtils.getInstance(this);

這句代碼默認傳入的是Activity的Context,而Activity是間接繼承自Context的,當Activity退出之後,單利對象還持有他的引用,所以在爲了避免傳Activity的Context,在單利中通過傳入的context獲取到全局的上下文對象,而不使用Activity的Context就解決了這個問題.

public class XXUtils {
    private Context mContext;
    private XXUtils(Context context) {
        mContext = context.getApplicationContext();
    }
    private static XXUtils instance;
    public static XXUtils getInstance(Context context) {
        if (instance == null) {
            synchronized (XXUtils.class) {
                if (instance == null) {
                    instance = new XXUtils(context);
                }
            }
        }
        return instance;
    }
}

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

在項目中我們爲了避免多次的初始化資源,常常會使用靜態對象去保存這些對象,這種情況也很容易引發內存泄漏.
why?
非靜態的內部類默認會持有外部類的引用
而我們又使用非靜態內部類創建了一個靜態的實例
該靜態實例的聲明週期和應用一樣長,這就導致了該靜態實例一直會持有該Activity的引用,導致Activity不能正常回收
解決方案:
將內部類修改成靜態的,這樣它對外部類就沒有引用
將該對象抽取出來封裝成一個單例.

private static TestResource mTestResource;

protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
}
private void initData() {
    if (mTestResource == null) {
        mTestResource = new TestResource();
    }
}

//非靜態內部類默認會持有外部類的引用
//修改成就太之後正常被回收,是因爲靜態的內部類不會對Activity有引用
private static class TestResource {
}

線程造成的內存泄漏

當我們在使用線程的時候,一般都使用匿名內部類,而匿名內部類會對外部類持有默認的引用,當Acticity關閉之後如果現成中的任務還沒有執行完畢,就會導致Activity不能正常回收,造成內存泄漏

解決方案
創建一個靜態的類,實現Runnable方法,在使用的時候實例化他.

最終代碼:

private void loadData() {
    new Thread(new MyThread()).start();
}
private static class MyThread implements Runnable {
    public void run() {
        SystemClock.sleep(20000);
    }
}

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

對於使用了BraodcastReceiver,ContentObserver,File,Cursor,Stream,Bitmap等資源的代碼,應該在Activity銷燬時及時關閉或者註銷,否則這些資源將不會被回收,造成內存泄漏。

監聽器沒有註銷造成的內存泄漏

在Android程序裏面存在很多需要register與unregister的監聽器,我們需要確保及時unregister監聽器。

集合中的內存泄漏

我們通常把一些對象的引用加入到了集合容器(比如ArrayList)中,當我們不需要該對象時,
並沒有把它的引用從集合中清理掉,這樣這個集合就會越來越大。如果這個集合是static的話,那情況就更嚴重了。 所以要在退出程序之前,將集合裏的東西clear,然後置爲null,再退出程序。

參考資料

1.Android 性能優化 - 徹底解決內存泄漏
https://blog.csdn.net/wanghao200906/article/details/79305126
2.Android 內存泄露和性能檢測
https://blog.csdn.net/u012482178/article/details/78988176
3.Android Studio +MAT 分析內存泄漏實戰
https://blog.csdn.net/u012760183/article/details/52068490

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