Android中常見的內存泄露分析

前言

Android應用因爲本身可用內存的限制,需要特別重視內存泄露的問題,本文總結了Android中常見的一些內存泄露原因及避免方式。


一、單例造成的內存泄露


由於單例的靜態特性使得單例的生命週期和應用的生命週期一樣長,這說明,如果一個對象已經不需要使用了,而單例對象還持有該對象的引用,那麼該對象將不能被正常回收,這就導致了內存泄露。
例,如下的寫法是我們開發中非常常見的一種寫法,但是其實會存在一些問題:
public class MusicManager {
    private static MusicManager instance;
    private Context context;
    private MusicManager (Context context) {
        this.context = context;
    }
    public static MusicManager getInstance(Context context) {
        if (instance != null) {
            instance = new MusicManager (context);
        }
        return instance;
    }
}

當創建這個單例的時候,由於要傳入一個Context,所以這個Context的生命週期的長短十分重要
1、如果傳入的是application的context,那麼沒有任何問題,因爲單例的生命週期和Application一樣長
2、如果傳入的是一個Activity的Context,當這個Context對應的Activity退出的時候,因爲該context的引用被單例所持有,所以導致activity不會被回收
所以請避免單例模式傳入的是activity的context,或者說當你想要寫的單例需要持有一個activity時,請思考是否需要做成單例的。
上述代碼可以做以下修改
public class MusicManager {
    private static MusicManager instance;
    private Context context;
    private MusicManager(Context context) {
        this.context = context.getApplicationContext();
    }
    public static MusicManager getInstance(Context context) {
        if (instance != null) {
            instance = new MusicManager(context);
        }
        return instance;
    }
}
這樣就不管調用者傳入的是activity還是application的context都不會出現內存泄露

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


我們經常會在activity中,創建一個非靜態內部類的靜態對象,如下代碼
public class MainActivity extends AppCompatActivity {
    private static TestResource mResource = null;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        if(mManager == null){
            mManager = new TestResource();
        }
        //...
    }
    class TestResource {
        //...
    }
}

這樣些雖然避免了對象的重複創建,但是因爲static修飾的變量的生命週期和應用一樣長,然後非靜態內部類會默認持有外部類的引用,這樣導致了activity的實例一直被持有,導致activity銷燬了但是不會被回收。正常的做法應該是將該內部類修飾成靜態的,或者抽取出來封裝成一個單例。

三、Handler造成的內存泄露

Handler在應用中的使用非常普遍,但是很多人在使用的時候會不經意間造成內存泄露。
如下代碼,是我們經常會在應用中寫到的

public class MainActivity extends AppCompatActivity {
    private Handler mHandler = new Handler() {
        @Override
        public void handleMessage(Message msg) {
            //...處理消息
        }
    };
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        loadData();
    }
    private void loadData(){
        //發送消息
        Message message = Message.obtain();
        mHandler.sendMessage(message);
    }
}

很多人在使用handler時,會如上的寫法,但是mHandler是Handler的非靜態匿名內部類 的實例,所以它持有外部類Activity的引用,而消息隊列中的Message持有mHandler實例的引用,然後mHandler又持有Activity的引用,會出現,如果這個acitivity退出的時候,消息隊列中還有未處理或者正在處理的消息時,就會引發內存泄露,所以要避免這種情況的出現。
可以通過創建一個靜態Handler的內部類,然後對Handler持有的引用使用弱引用,這樣可以避免activity的內存泄露。但是消息隊列中可能還會有待處理的消息,所以在activity退出的時候,調用移除消息隊列中的消息。
在activity銷燬的時候,移除handler中的消息
 @Override
 protected void onDestroy() {
     super.onDestroy();
     mHandler.removeCallbacksAndMessages(null);//移除消息
 }
創建一個static的Handler內部類。
static class MyHandler extends Handler {
    WeakReference<Activity > mActivityReference;
    //創建一個靜態的handler的內部類,然後將activity的對象主動傳進去,
    MyHandler(Activity activity) {
        mActivityReference= new WeakReference<Activity>(activity);
       //用弱引用去持有該handler的對象
}

四、線程造成的內存泄露

線程造成的內存泄露主要是因爲線程的生命週期不可控

       new Thread(new Runnable() {
            @Override
            public void run() {
                SystemClock.sleep(10000);
            }
        }).start();

上述的代碼,很多人都會在項目中寫到,現在的Runnable對象是一個匿名內部類,他默認持有外部類的引用,如果在activity銷燬的時候,該線程還沒執行完成,此刻的activity的內存資源就無法被回收,正確的做法還是使用靜態內部類的方式,如下

    static class MyRunnable implements Runnable{
        @Override
        public void run() {
            SystemClock.sleep(10000);
        }
    }
    new Thread(new MyRunnable()).start();

五、資源未關閉造成的內存泄露

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


六、註冊監聽器沒有及時移除導致的內存泄露


android中可以通過Context.getSystemService(int name)來獲取到系統服務,這些服務工作在各自的進程中,幫助應用處理後臺任務,硬件交互等。如果需要使用這些服務,可以註冊監聽器,這會導致服務持有了Context的引用,如果傳入的是一個Activity的Context,那麼在Activity銷燬的時候沒有註銷這些監聽器,可能會導致內存泄露。
public class LeaksActivity extends Activity implements LocationListener {
    private LocationManager locationManager;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_leaks);
        locationManager = (LocationManager) getSystemService(LOCATION_SERVICE);//拿到LocationManager對象
        locationManager.requestLocationUpdates(LocationManager.NETWORK_PROVIDER,
                TimeUnit.MINUTES.toMillis(5), 100, this);//註冊一個監聽
    }
    // Listener implementation omitted
}
如上代碼,我們讓Android的LocationManager通知我們位置更新。我們需要傳入一個監聽器,在這裏我們讓Activity實現了位置監聽接口,這意味着LocationManager將持有該Activity的引用。現在如果出現了Activity銷燬的情況,而且沒有移除掉監聽的話,該Activity的內存資源將不會被回收,就會導致內存泄露

七、WebView導致的內存泄露

因爲WebView這個控件本身的一些缺陷,會出現內存泄露的情況,因爲,我們在xml中使用WebView控件的時候,在它初始化的時候,會將activity對象傳給WebView,在activity銷燬的時候,如果WebView沒有及時釋放該context,就會出現內存泄露。
避免出現這種情況,可以通過以下幾種方式去做:
A、避免在xml直接寫WebView的控件,而是在代碼中進行動態註冊,然後傳入一個applicationContext,而不是activity的Context
    WebView mWebView = new WebView(getApplicationgContext());
    LinearLayout mll = findViewById(R.id.xxx);
    mll.addView(mWebView);
然後在activity銷燬的時候,及時調用destory方法
protected void onDestroy(){
      super.onDestroy();
      mWebView.removeAllViews();
      mWebView.destroy()
}

這樣WebView就會持有applicationContext,而不是activity的Context,但是這樣做會有個問題,當在WebView中打開連接或者打開的頁面加載flash時,會出現類型轉換異常,導致頁面崩潰,因爲加載flash的時候,系統會把WebView作爲一個父控件,然後在該空間上繪製flash,它想找一個Activity的context來繪製他,而你傳入的是ApplicationContext。

B、如果你不能給我刪除引用,那麼我自己來刪除,核心思想是通過反射的方式,實現刪除引用
public void setConfigCallback(WindowManager windowManager) {
     try {
          Field field = WebView.class.getDeclaredField("mWebViewCore");
          field = field.getType().getDeclaredField("mBrowserFrame");
          field = field.getType().getDeclaredField("sConfigCallback");
          field.setAccessible(true);
          Object configCallback = field.get(null);
          if (null == configCallback) {
               return;
          }
          field = field.getType().getDeclaredField("mWindowManager");
          field.setAccessible(true);
          field.set(configCallback, windowManager);
      } catch(Exception e) {
      }
}
但是該方法是基於於android的webkit內核的,在android 4.4及以後版本中會失效,因爲4.4之後更換了WebView的內核爲chromium。

C、參考QQ的做法,讓使用WebView控件的頁面,單獨運行在一個進程中,在該頁面退出的時候,關閉該進程。安卓開啓多進程的方式,就是在四大組件的AndroidMenifest中指定android:process屬性。

D、Android 5.1系統中,因爲WebView源碼的一些問題,會導致內存泄露,關鍵方法在於onAttachedToWindow和onDetachedFromWindow。
onAttachedToWindow  添加一些監聽器,執行在onDraw方法之前,也就是view繪製之前
onDetachedFromWindow  移除掉這些監聽器,執行在view銷燬前
看上去這兩個方法沒有什麼問題,但是在5.1的源碼中可以看到
public void onAttachedToWindow() {
        if (isDestroyed()) return;//有是否destroyed的判斷
        if (mIsAttachedToWindow) {
            Log.w(TAG, "onAttachedToWindow called when already attached. Ignoring");
            return;
        }
        ......
 
        mComponentCallbacks = new AwComponentCallbacks();
        mContext.registerComponentCallbacks(mComponentCallbacks);
    }
 
    @Override
    public void onDetachedFromWindow() {
        if (isDestroyed()) return;
        if (!mIsAttachedToWindow) {
            Log.w(TAG, "onDetachedFromWindow called when already detached. Ignoring");
            return;
        }
        ......
 
        if (mComponentCallbacks != null) {
            mContext.unregisterComponentCallbacks(mComponentCallbacks);
            mComponentCallbacks = null;
        }
 
        ......
    } 

在執行移除監聽前進行了該頁面是否銷燬的判斷,也就是說,我們在activity的onDestory方法中調用WebView的destory方法,會銷燬webView,會導致anDetachedFromWindow方法在判斷是否WebView是否銷燬的時候,返回true,也就是說下面remove監聽的那些方法不會執行,會導致activty的引用一直被持有
我們可以通過提前觸發onDetachedFormWindow方法來避免這種問題
protected void onDestroy() {
      if (mWebView != null) {
          ((ViewGroup) mWebView.getParent()).removeView(mWebView);
          mWebView.destroy();
          mWebView = null;
      }
      super.onDestroy();
}
我們可以通過將WebView從父控件中移除來觸發WebView的onDetachedFromWindow方法,讓它提前執行onDetachedFromWindow方法。我在開發時,在mx4 pro的手機上遇到過這種問題,其系統就是android 5.1的,有一個頁面的對象一直回收不了,導致內存一直增加最後OOM,最後發現是因爲WebView這個控件導致的。


注:本文內容來自網絡他人共享以及自己平時的總結,因水平有限,難免出錯,歡迎大家指正。

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