在Android中可能會有多種狀況的內存泄露,其中比較常見的就是Context泄露,即上下文泄露。這個問題不容忽視,由於Activity、Service、Application都是Context的子類,因此一個Context對象有時會比較龐大,因此若是Context對象沒法釋放那麼很容易形成OOM。java
咱們都知道,形成內存泄露的根本緣由是某個已經無用的對象還被其餘對象引用着,因此GC沒法釋放。Context泄露也是這樣,那麼在Android中有什麼樣的場景會致使Context泄露呢?經筆者總結,有如下兩個場景(也許還有其餘的,筆者目前暫時沒有碰到,歡迎大神指正):android
1. Java語言的機制形成的泄露。ide
2. Android生命週期形成的泄露。工具
有時候兩者沒有明顯的區分,甚至有時候是兩種緣由一塊兒形成了Context泄露。本文咱們就從上面兩點進行闡述。性能
由於本文要討論內存泄露,因此咱們須要能夠檢測內存泄露的工具,筆者總結了三種方式:優化
1. Dump Java Heap生成hprof文件,用DDMS或者MAT工具分析。this
2. StrictMode嚴格模式:編碼
3. LeakCanary。spa
第一種較爲繁瑣,不適用於今天的場景。嚴格模式和LeakCanary見效比較快,因此本文就用第二種和第三種來檢測內存泄露。.net
嚴格模式的相關文章能夠參考《Android性能調優利器StrictMode》。
LeakCanary網上的教程不少,這裏就不一一列舉了。
說點題外話,嚴格模式和LeakCanary檢測Activity泄露,兩者的檢查時機是同樣的,都是在某個Activity onDestroy時檢測,但兩者的判斷方式是不同的。
嚴格模式會檢測應用中某一個Activity是否存在多個實例,默認是一個,若是存在多個就會給出提示。因此若是一個Activity存在內存泄露,第一次是檢測不出來的,由於第一次即便內存泄露也仍是有一個實例,除非再次進到這個Activity而後退出,那麼嚴格模式纔會給出提示。
LeakCanary則在Activity銷燬時檢查該Activity是否還存在,若是存在就發起一次GC再檢測,若是沒法銷燬就打印內存快照而後分析給出Notification提示。
這標題有點唬人,這裏不是說Java語言有bug。。。請聽我細細道來。讓咱們先來看一段代碼
package com.example; public class MyClass { public static void main(String[] args) throws Throwable { } public class A { public void methed1(){ } } public static class B { public void methed1(){ } } }新建一個類MyClass,其中有兩個內部類,非靜態內部類A和靜態內部類B。若是咱們把上面代碼編譯再反編譯,能夠看到非靜態內部類A自動生成的構造方法裏,默認的參數是外部類,所以使用內部類的時候會保存一個外部類的引用。而靜態內部類B呢,沒有生產默認的構造方法。這裏只是一個結論,具體探究的過程能夠參考《Java內部類的實現原理與可能的內存泄漏》。
再重述一遍結論,非靜態內部類會持有外部類的引用,而靜態內部類則不會持有外部類的應用。再加上一點,匿名內部類也會持有外部類的引用。
舉個Android的例子,看下面的代碼,
public class LeakActivity extends AppCompatActivity { private static final String TAG = "LeakActivity"; @Override protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_leak); MyThread myThread = new MyThread(); myThread.start(); } class MyThread extends Thread { @Override public void run() { while (true) { Log.e(TAG, "run: "); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } } } }在LeakActivity中聲明一個非靜態內部類MyThread每一個一秒打印log。由於這是一個非靜態內部類,持有了一個LeakActivity的引用。當咱們從這個Activity退出時,從理論上說,LeakActivity內存泄露了。讓咱們看看檢測工具,LeakCanary會給出提示
再次進入LeakActivity而後退出,StrictMode會給出提示(至於爲何要再次進入而後退出,請參考上文)
E/StrictMode: class com.gnepux.sdkusage.activity.LeakActivity; instances=2; limit=1 android.os.StrictMode$InstanceCountViolation: class com.gnepux.sdkusage.activity.LeakActivity; instances=2; limit=1 at android.os.StrictMode.setClassInstanceLimit(StrictMode.java:1)從以上能夠看出,MyThread這個類確實引用了LeakActivity致使了LeakActivity的內存泄露。
其實非靜態內部類會致使內存泄露,Google已經老早給出了提醒,若是咱們在Activity中建立一個非靜態內部類繼承Handler,Android Studio會給出這樣的提示
發現問題就要解決問題,那咱們該如何應對這種狀況的內存泄露呢。回頭再看看Google的那段話,咱們能夠找到解決方案:
1. 將非靜態內部類聲明爲靜態內部類;
2. 若是在靜態內部類中須要引用外部類,咱們須要用WeakReference進行引用。由於Android系統對WeakReference的回收是至關積極的,因此在使用前必定要記得判空!
如今讓咱們就修改一下LeakActivity的代碼來消滅內存泄露的問題吧
public class LeakActivity extends AppCompatActivity { private static final String TAG = "LeakActivity"; // 建立一個非靜態變量, 讓靜態內部類訪問 private String mStr = "LeakActivity還沒釋放..."; @Override protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_leak); MyThread myThread = new MyThread(this); myThread.start(); } static class MyThread extends Thread { // 使用WeakReference引用LeakActivity private WeakReference<LeakActivity> mWeakRef; public MyThread(LeakActivity leakActivity) { this.mWeakRef = new WeakReference<>(leakActivity); } @Override public void run() { while (true) { // WeakReference必定要記得判空 if (mWeakRef.get() != null) { Log.e(TAG, "run: " + mWeakRef.get().mStr); } else { Log.e(TAG, "run: LeakActivity已經釋放掉了..."); } try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } } } }這裏建立了一個非靜態全局變量mStr來模擬讓靜態內部類MyThread訪問。在MyThread的構造方法中加入參數傳入LeakActivity對象,內部使用WeakReference持有這個引用。當退出這個頁面後,若是系統發生GC,這個WeakReference就會馬上釋放掉,因此必定要記得判空。
有童鞋也許要問,若是發生GC後,那mStr變量不就訪問不了了麼,邏輯就不正確了啊。這裏有必要說一下,既然mStr定義爲LeakActivity的非靜態全局變量,那就默認這個變量的生命週期應該和Activity一致,在這個Activity銷燬以後,mStr變量從代碼設計的角度看就是不容許再被訪問的。若是出於某些業務上的需求,那就聲明爲LeakActivity的靜態全局變量或是MyThread的內部變量。
跑一下上面的代碼,數次進入退出LeakActivity,LeakCanary和StrictMode都沒有再給出內存泄露的提示了,這個小bug就這麼愉快地解決啦~ ^_^
跟上一節同樣,一樣是個很唬人的標題,就是這麼標題黨,哈哈。這裏不是說Android自身的生命週期會形成內存泄露,而是說某個持有了Context的對象的生命週期和Context的生命週期不一樣步致使了Context對象沒法釋放,從而形成了內存泄露。
這句話有點拗口,舉個例子就很容易理解了。下面的LeakObject是一個單例,構造是須要傳入一個Context對象。
public class LeakObject { private static final String TAG = "LeakObject"; private static LeakObject instance = null; private Context context; private LeakObject(Context context) { this.context = context; } public static LeakObject getInstance(Context context) { if (instance == null) { synchronized (LeakObject.class) { if (instance == null) { instance = new LeakObject(context); } } } return instance; } public void sayHi() { Log.e(TAG, "sayHi: "); } }咱們在LeakAcitivity中獲取一個LeakObject的單例對象並調用sayHi方法。
public class LeakActivity extends AppCompatActivity { private static final String TAG = "LeakActivity"; @Override protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_leak); LeakObject object = LeakObject.getInstance(this); object.sayHi(); } }這是很常見的單例模式的應用,咱們也常常這麼寫,這也會出現內存泄露的問題麼?咱們運行一遍,來回進入幾回LeakActivity,能夠看到StrictMode給出了下面的提示
E/StrictMode: class com.gnepux.sdkusage.activity.LeakActivity; instances=2; limit=1 android.os.StrictMode$InstanceCountViolation: class com.gnepux.sdkusage.activity.LeakActivity; instances=2; limit=1 at android.os.StrictMode.setClassInstanceLimit(StrictMode.java:1)這是爲何呢?由於咱們在申請單例的時候,傳入的是LeakActivity的Context,而咱們知道LeakObject中的單例靜態變量instance是常駐內存中的,它的生命週期跟應用同樣長,只要應用沒關閉,它就一直在。它持有了一個LeakActivity的引用,這樣致使LeakActivity的生命週期跟應用同樣長,沒法被釋放,形成呢內存泄露。
那麼該如何解決這個問題呢,咱們只要把在getInstance()中傳入getApplicationContext()就能夠了,這樣就確保了單例的持有的是Application的Context。由於ApplicationContext自己就是跟應用的生命週期同樣長的,這樣就不存在內存泄露了。完美~
經過上面的例子能夠看出,若是某個持有Context的對象的生命週期跟Context的生命週期不一致,就會致使Context的內存泄露。不光是例子中說的單例,咱們經常使用的AsyncTask、Handler等都會存在這個問題。
好比AsyncTask持有了Activity的Context,在Activity退出時AsyncTask的任務還沒作完,Activity就沒法被釋放。因此咱們須要在onDestroy時手動cancel AsyncTask,確保AsyncTask的生命週期跟Activity同步。
一樣,指向Activity等Context資源的靜態變量也會致使內存泄露,原理都同樣,就再也不贅述了。
下面作一個總結,本文從兩個角度介紹了Context上下文泄露的緣由。
1. Java語言的角度:非靜態內部類會持有外部類的引用,可能會致使內存泄露。解決方法是使用靜態內部類,同時用WeakReference持有Context引用。一樣匿名內部類也會持有外部類的引用,儘可能不要使用匿名內部類。
2. Android生命週期的角度:若是某個持有Context的對象的生命週期跟Context的生命週期不一致,就會致使Context的內存泄露。單例、指向Activity等Context資源的靜態變量、AsyncTask等都有可能由於這個緣由致使內存泄露。確保持有Context引用的對象跟Context的生命週期保持一致。可使用getApplicationContext或者手動控制。
兩者沒有嚴格的區分,有時是某一個的緣由形成了泄露,有時是兩者結合形成了泄露。咱們須要在編碼過程當中時刻長個心眼,有條件時也能夠利用工具來進行分析。