Android基礎之內存泄露

上一篇介紹了Android內存溢出,今篇我來繼續介紹一下關於Android內存優化的內存泄露。

內存泄露的基礎理解

  1. 一般內存泄露的原因是:由忘記釋放分配的內存導致的。(如Cursor忘記關閉等)
  2. 邏輯內存泄露的原因是:當一個對象已經不需要再使用了,本該被回收時,而有另外一個正在使用的對象持有它的引用從而導致它不能被回收,這導致本該被回收的對象不能被回收而停留在堆內存中。

這樣一方面佔用了寶貴的內存空間,這樣容易導致後續需要分配內存的時候,空閒空間不足而出現內存溢出OOM。所以我麼要理解好內存泄露,泄露形象理解成煤氣泄露,是我們不用煤氣的時候,應該是關掉閥口,但是在不正常的情況中,閥口沒有關好,導致煤氣泄露了。

Android中常見的內存泄漏彙總

我們關鍵需要做到的是:及時回收沒有使用的對象

  • 需手動關閉的對象沒有關閉,在try/catch/finally中的處理

    • HTTP
    • File
    • ContendProvider
    • Bitmap
    • Uri
    • Socket
    • Cursor
  • onDestory()或者onPause()中未及時關閉對象
    部分實例:

    • Handler泄露:我們的Handler是要求寫出static的,但是實際開發中我們並沒有寫到,因此我們要使用弱引用和在onDestory()中removeCallbacksAndMessages(null)手動關閉。
    • 廣播泄露:在手動註冊廣播時:需要退出的時候unregisterReceiver()。
    • Service泄露:使用Service進行某耗時操作結束後,需要手動stopself()。
    • WebView需要手動調用WebView.onPause()和WebView.destory()。
    • 常用第三方/開源框架泄露:ShareSDK、JPush、BaiduMap、ButterKnife等需要注意在對應的Activity的生命週期中關閉。

這些我就不一一說明了,大家或多或少都有接觸到,這些都是需要我們手動關閉,屬於一般內存泄露。

  • 然而我們可以藉助LeakCanary、MAT等工具來檢測應用程序是否存在以下幾種情況內存泄漏。

單例造成的內存泄漏

由於單例的靜態特性使得單例的生命週期和應用的生命週期一樣長,這就說明了如果一個對象已經不需要使用了,而單例對象還持有該對象的引用,那麼這個對象將不能被正常回收,這就導致了內存泄漏。如下這個典例:

public class AppManager {
  private static AppManager instance;
  private Context context;
  private AppManager(Context context) {
    this.context = context;
  }
  public static AppManager getInstance(Context context) {
    if (instance != null) {
      instance = new AppManager(context);
    }
    return instance;
  }
}

這是一個普通的單例模式,當創建這個單例的時候,由於需要傳入一個Context,所以這個Context的生命週期的長短至關重要:
1、傳入的是Application的Context:這將沒有任何問題,因爲單例的生命週期和Application的一樣長。
2、傳入的是Activity的Context:當這個Context所對應的Activity退出時,由於該Context和Activity的生命週期一樣長(Activity間接繼承於Context),所以當前Activity退出時它的內存並不會被回收,因爲單例對象持有該Activity的引用。

所以正確的單例應該修改爲下面這種方式:

public class AppManager {
  private static AppManager instance;
  private Context context;
  private AppManager(Context context) {
    this.context = context.getApplicationContext();
  }
  public static AppManager getInstance(Context context) {
    if (instance != null) {
      instance = new AppManager(context);
    }
    return instance;
  }
}

這樣不管傳入什麼Context最終將使用Application的Context,而單例的生命週期和應用的一樣長,這樣就防止了內存泄漏

靜態View

有時,當一個Activity經常啓動,但是對應的View讀取非常耗時,我們可以通過靜態View變量來保持對該Activity的rootView引用。這樣就可以不用每次啓動Activity都去讀取並渲染View了。這確實是一個提高Activity啓動速度的好方法!但是要注意,一旦View attach到我們的Window上,就會持有一個Context(即Activity)的引用。而我們的View有事一個靜態變量,所以導致Activity不被回收。當然了,也不是說不能使用靜態View,但是在使用靜態View時,需要確保在資源回收時,將靜態View detach掉。

內部類

我們知道,非靜態內部類持有外部類的一個引用。因此,如果我們在一個外部類中定義一個靜態變量,這個靜態變量是引用內部類對象。將會導致內存泄漏!因爲這相當於間接導致靜態引用外部類。

static InnerClass innerClass;

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main); 
    innerClass = this.new InnerClass(); 
}

class InnerClass { 
//
}

匿名類

與內部類一樣,匿名類也會持有外部類的引用。

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

    new AsyncTask<Void, Void, Void>() {
        @Override
        protected Void doInBackground(Void... params) {
            //另一個線程中持有Activity的引用,並且不釋放
            while (true) ;
        }
    }.execute();
}

Handler類引起內存泄漏

在Activity中定義一下Handler類:

    public class MainActivity extends Activity {
        private  Handler mHandler = new Handler() {
            @Override
            public void handleMessage(Message msg) {
                //TODO 
            }
        };
        @Override
        public void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
            mHandler.sendMessageDelayed(Message.obtain(), 60000);//延遲一分鐘執行
            finish();
        }
    }

我們知道非靜態內部類會持有外部類的引用,這裏Activity finish後,延時消息會繼續存在主線程消息隊列中1分鐘,然後處理消息。而該消息引用了Activity的Handler對象,然後這個Handler又引用了這個Activity。這些引用對象會保持到該消息被處理完,這樣就導致該Activity對象無法被回收,從而導致了上面說的 Activity泄露。

要修改這個問題,把Handler類定義爲靜態,然後通過WeakReference來持有外部的Activity對象。還在onDestory()中清掉Handler消息。

public class MainActivity extends Activity {

    private CustomHandler mHandler;

    private static class CustomHandler extends Handler {
        private WeakReference<MainActivity> mWeakReference;

        public CustomHandler(MainActivity activity) {
            mWeakReference = new WeakReference<MainActivity>(activity);
        }

        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);
            MainActivity activity = mWeakReference.get();
            if (null != activity)
                switch (msg.what) {
                  ///
                }
        }
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        mHandler = new CustomHandler(this);
    }

    @Override
    protected void onDestroy() {
        // TODO Auto-generated method stub
        super.onDestroy();
        mHandler.removeCallbacksAndMessages(null);
    }

Thread對象引起內存泄露

同Handler對象可能造成內存泄露的原理一樣,Thread的生命週期不一定是和Activity生命週期一致。
而且因爲Thread主要面向多任務,往往會造成大量的Thread實例。
據此,Thread對象有2個需要注意的泄漏點:

  1. 創建過多的Thread對象
  2. Thread對象在Activity退出後依然在後臺執行

解決方案是:

  1. 使用ThreadPoolExecutor,在同時做很多異步事件的時候是很常用的,這個不細說。
  2. 當Activity退出的時候,退出Thread。

TimerTask引起泄露

TimerTask對象在和Timer的schedule()方法配合使用的時候極容易造成內存泄露。要在合適的時候進行Cancel即可。

private void cancelTimer(){ 
        if (mTimer != null) { 
            mTimer.cancel(); 
            mTimer = null; 
        } 
        if (mTimerTask != null) { 
            mTimerTask.cancel(); 
            mTimerTask = null; 
        }
    }

監聽器管理內存泄露

當我們需要使用系統服務時,比如執行某些後臺任務、爲硬件訪問提供接口等等系統服務。我們需要把自己註冊到服務的監聽器中。然而,這會讓服務持有 activity 的引用,如果在Activity銷燬的時候沒有註銷這些監聽器,會導致內存泄漏。

void registerListener() {
       SensorManager sensorManager = (SensorManager) getSystemServic(SENSOR_SERVICE);
       Sensor sensor = sensorManager.getDefaultSensor(Sensor.TYPE_ALL);
       sensorManager.registerListene(this,sensor,SensorManager.SENSOR_DELAY_FASTEST);
}

然而對於中級Android工程師的面試過程中,如果你是一名老鳥,這個問題想必已經深入你的心。但是你是一個對內存模模糊糊的工程師,你的答案可能讓面試官並不滿意。下面的總結對你或許有點幫助。

  • 常見的內存泄露問題

    1. 單例造成的泄露。
    2. 靜態View造成的泄露。
    3. 內部類持有外部類實例的強引用。
    4. Handler、Thread耗時操作劫持Activity的外部類。
    5. TimerTask對象沒有及時回收和置空。
    6. 系統監聽器的引起的泄露。
  • 避免內存泄露的方法

    1. 注意Context上下文的使用。
    2. 謹慎使用static。
    3. 使用靜態內部類來代替內部類。
    4. 靜態內部類使用弱引用WeakReference來引用外部類。
    5. 在聲明週期結束的時候釋放資源。
    6. 優化佈局文件,減少層級關係。
    7. 謹慎使用第三方框架。
    8. 謹慎使用多線程。
    9. 橫豎屏切換引起的GC。
    10. 避免創建不必要的對象。

還有就是如何檢測內存泄露問題了。具體操作下次再詳細分析

  1. 善用Android Studio的標籤Memory。
  2. LeakCanary的使用。
  3. MAT的使用。

這裏寫圖片描述

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