一篇技術好文之Android性能優(yōu)化內存泄漏無處可藏(圖文)

默認標題_官方公眾號首圖_2018.04.29 (1).png

每次來公司面試的人,一般都會問最基本的兩個問題,一個是自定義View的繪制流程及事件分發(fā),第二個就是性能優(yōu)化內存泄漏如何處理?第一個問題基本上都能說個大概,第二個問題其實很多工作好幾年的都不一定能回答的比較讓人滿意。這里整理下基本的內存泄漏及解決辦法。使用的是LeakCannary來進行檢測。

你能從本文了解到如下知識:1. 什么是內存泄漏 2. 內存泄漏的分類及影響 3.常見的內存泄漏及解決辦法 4.文章總結

[toc]

什么是內存泄漏?

內存泄漏也稱作"存儲滲漏",用動態(tài)存儲分配函數(shù)動態(tài)開辟的空間,在使用完畢后未釋放,結果導致一直占據(jù)該內存單元。直到程序結束。(其實說白了就是該內存空間使用完畢之后未回收)即所謂內存泄漏。再形象點比喻就像家里的水龍頭沒有擰緊,漏水了。

內存泄漏的分類及影響?

分類:常發(fā)性內存泄漏,偶發(fā)性內存泄漏,一次性內存泄漏,隱式內存泄漏。
危害:內存泄漏造成的影響其實是內存泄漏的堆積,這將會消耗系統(tǒng)所有的內存。所以一個內存泄漏危害并不大,因為不會堆積,而隱式內存泄漏危害性則非常大,因為較之于常發(fā)性和偶發(fā)性內存泄漏它更難被檢測到。

常見的內存泄漏及解決辦法:

1. 單例造成的內存泄漏:

第一種情況:

public class LoginActivity extends Activity {
    public static LoginActivity instance;
     @Override
    protected void onCreate(Bundle savedInstanceState) {
        ……
        instance = this;
    }
}

在其他地方引用LoginActivity.instance會造成檢測如下的:

image

這種情況我們可以通過使用弱引用的方法來優(yōu)化,修改如下:

public class LoginActivity extends Activity {
    public static WeakReference<LoginActivity> instance;
     @Override
    protected void onCreate(Bundle savedInstanceState) {
        ……
        instance = new WeakReference<LoginActivity>(this);
    }
}

單例造成的內存泄漏第二種情況(在網(wǎng)上找到的實例及圖片):

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;
}

檢測結果如下:


image

解決辦法,使用Application的context替代Activity的context,修改后的diam如下:

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. 接口實現(xiàn)引用造成的內存泄漏。

不知道這樣實現(xiàn)代碼的多不多?

public class MyApplication extends LitePalApplication{
    ……
     UnReadMsgListener unReadMsgListener;
     public void setUnReadMsgListener(UnReadMsgListener unReadMsgListener){
          this.unReadMsgListener = unReadMsgListener;//在其他頁面進行接口實現(xiàn)
     } 
     ……
}

造成的內存泄漏分析圖如下:

image

原因分析:在其他頁面進行setUnReadMsgListener操作,MyApplication將明顯持有對此接口的引用,此接口被Activity實現(xiàn),所以MyApplication一直持有Activity的引用。
在盡量不修改原代碼的情況下,解決辦法如下:

UnReadMsgListener unReadMsgListener;
    WeakReference<UnReadMsgListener> mListenerWeakReference;
    public void setUnReadMsgListener(UnReadMsgListener unReadMsgListener){
        mListenerWeakReference = new WeakReference<UnReadMsgListener>(unReadMsgListener);
        this.unReadMsgListener = mListenerWeakReference.get();
    }

3. 使用ViewVideo造成的內存泄漏(MediaPlayer.mSubtitleController):

有時候為了快速開發(fā),經常會在xml中使用VideoView去快速集成播放一個視頻,這樣做就會內存泄漏。檢測結果如下:

image

從LeakCanary分析結果得出,是由于VideoView持有對Activity的Context的引用造成的。因為我們將VideoView寫在XMl中,所以默認是應用當前頁面的Context的。

解決辦法:
第一種:將VideoView在代碼中實現(xiàn):

VideoView mVideoView = new VideoView(MyApplication.getContext());
//添加到父容器
……

第二種:重寫當前Activity頁面的attachBaseContext方法:

  @Override
    protected void attachBaseContext(Context newBase) {
        super.attachBaseContext(new ContextWrapper(newBase)
        {
            @Override
            public Object getSystemService(String name)
            {
                if (Context.AUDIO_SERVICE.equals(name))
                    return getApplicationContext().getSystemService(name);
                return super.getSystemService(name);
            }
        });
    }

4. MediaPlayer源碼存在的內存泄漏問題:

這個問題是緊接上一個內存泄漏,上面的處理方式基本能解決VideoView給我們帶來的內存泄漏問題。這里我們來深入了解下為什么使用VideoView會造成內存泄漏??碝ediaPlayer的源碼我們可以得知:在系統(tǒng)的MediaPlayer的release過程中就mSubtitleController 資源未做處理,幸運的是在reset中進行此資源的處理,所以我們在使用MeidPlayer播放視頻后進行資源釋放時再release時進行下MediaPlayer的reset操作。下面我們看下MediaPlayer的源碼:

    //MediaPlayer系統(tǒng)源碼
    ……
  public void release() {
        baseRelease();
        stayAwake(false);
        updateSurfaceScreenOn();
        mOnPreparedListener = null;
        mOnBufferingUpdateListener = null;
        mOnCompletionListener = null;
        mOnSeekCompleteListener = null;
        mOnErrorListener = null;
        mOnInfoListener = null;
        mOnVideoSizeChangedListener = null;
        mOnTimedTextListener = null;
        if (mTimeProvider != null) {
            mTimeProvider.close();
            mTimeProvider = null;
        }
        mOnSubtitleDataListener = null;
        _release();
    }
    ……
    public void reset() {
        mSelectedSubtitleTrackIndex = -1;
        synchronized(mOpenSubtitleSources) {
            for (final InputStream is: mOpenSubtitleSources) {
                try {
                    is.close();
                } catch (IOException e) {
                }
            }
            mOpenSubtitleSources.clear();
        }
        if (mSubtitleController != null) {//這里有對mSubtitleController進行處理操作
            mSubtitleController.reset();
        }
        if (mTimeProvider != null) {
            mTimeProvider.close();
            mTimeProvider = null;
        }

        stayAwake(false);
        _reset();
        // make sure none of the listeners get called anymore
        if (mEventHandler != null) {
            mEventHandler.removeCallbacksAndMessages(null);
        }

        synchronized (mIndexTrackPairs) {
            mIndexTrackPairs.clear();
            mInbandTrackIndices.clear();
        };
    }

如果上面的描述不夠詳細,你可以參考stackoverflow
解決辦法上面有提到過,如下:

image

沒錯,我就是截圖過來滴。

image

5. Handler使用造成的內存泄漏(MessageQueue.mMessage)

Handler 的使用造成的內存泄漏問題應該說是最為常見了,我們看一下下面代碼:

 public class BaseActivity extends AppCompatActivity {
     ......
     private Handler baseHandler = new Handler();
      @Override
    protected void onResume() {
         baseHandler.postDelayed(new Runnable() {
                @Override
                public void run() {
                    heartBeat();
                    if (!isPause) {
                        baseHandler.postDelayed(this, 60 * 1000);
                    }
                }
            }, 100);
    }
 }

檢測到泄漏結果如下:


image

原因分析:由于 Handler 屬于 TLS(Thread Local Storage) 變量, 生命周期和 Activity 是不一致的。因此這種實現(xiàn)方式一般很難保證跟 View 或者 Activity 的生命周期保持一致,故很容易導致無法正確釋放。

解決辦法:在 Activity 中避免使用非靜態(tài)內部類,比如上面我們將 Handler 聲明為靜態(tài)的,則其存活期跟 Activity 的生命周期就無關了。同時通過弱引用的方式引入 Activity,避免直接將 Activity 作為 context 傳進去,最后當我們Activity銷毀后,Looper線程的消息隊列中可能會存在待處理的消息,所以我們在Activity的OnDestroy中移除消息隊列 MessageQueue 中的消息。修改后代碼如下:

public class BaseActivity extends AppCompatActivity {
     ......
      private static class MyHandler extends Handler {
    private final WeakReference<SampleActivity> mActivity;
    public MyHandler(SampleActivity activity) {
      mActivity = new WeakReference<SampleActivity>(activity);
    }
    @Override
    public void handleMessage(Message msg) {
      SampleActivity activity = mActivity.get();
      if (activity != null) {//這里切記要判空
        // ...
      }
    }
  }
  private final MyHandler baseHandler= new MyHandler(this);
      @Override
    protected void onResume() {
         baseHandler.postDelayed(new Runnable() {
                @Override
                public void run() {
                    heartBeat();
                    if (!isPause) {
                        baseHandler.postDelayed(this, 60 * 1000);
                    }
                }
            }, 100);
    }
     @Override
    protected void onDestroy() {
         if (baseHandler != null) {
            baseHandler.removeCallbacks(null);
            baseHandler = null;
        }
    }
 }

當然這里簡單說一下軟引用和弱引用的使用:記住兩點即可:
第一點:如果只是想避免OutOfMemory異常的發(fā)生,則可以使用軟引用。如果對于應用的性能更在意,想盡快回收一些占用內存比較大的對象,則可以使用弱引用。
第二點:可以根據(jù)對象是否經常使用來判斷選擇軟引用還是弱引用。如果該對象可能會經常使用的,就盡量用軟引用。如果該對象不被使用的可能性更大些,就可以用弱引用。

6. 匿名內部類造成的內存泄漏

在異步操作過程中,我們經常會這樣做:

public class WelcomeActivity extends Activity {
    ......
    public void sel(){
         new Thread(new Runnable() {
                @Override
                public void run() {
                    SystemClock.sleep(10000);
                    //do something ... 里面持有對當前activity的引用
                }
            }).start();
    }
}

檢測到的內存泄漏結果如下:


image

原因分析:在Activity結束時,若線程內依舊還有任務未完成,則會發(fā)生內存泄漏。上面的Runnable是一個內部類,因此對當前的Activity存在一個隱式引用(文章開頭有提到,威脅最大的一種引用)。

解決思路:不使用匿名內部類,通過靜態(tài)內部類來實現(xiàn),使用弱應用來持有Activity的引用。

解決后的代碼:

public class WelcomeActivity extends Activity {
......
public void sel(){
new Thread(new splashhandler()).start();
}
static class splashhandler implements Runnable {
public void run() {
SystemClock.sleep(10000);
WelcomeActivity welcomeActivity = welcomeActivityWeakReference.get();
if(welcomeActivity != null) //注意判空
//do something ... 里面持有對當前activity的引用
}
}

匿名內部類被異步線程所持有的時候,我們一定要特別小心,如果么有進行任何處理措施,極容易出現(xiàn)內存泄漏的情況。下面我們再分析一種使用AsyncTask過程中造成的內存泄漏處理情況:

public class MainActivity extends Activity {
    public void sel(){
        new AsyncTask<void, void="">() {
            @Override
            protected Void doInBackground(Void... params) {
                SystemClock.sleep(10 * 1000);
                //do something ... 里面持有對當前activity的引用
                return null;
            }
        }.execute();
    }
}

原因分析和上面是一樣的,Activity結束了,異步任務還未處理完。

解決辦法:使用軟引用,并在Activity的onDestroy里調用AsyncTask.cancel()方法。

public class MainActivity extends Activity {
    private WeakReference<context> weakReference;
    AsyncTask asyncTask;
    public void sel(){
        asyncTask = new AsyncTask<>() {
            @Override
            protected Void doInBackground(Void... params) {
                SystemClock.sleep(10 * 1000);
                //do something ... 里面持有對當前activity的引用
                return null;
            }

            @Override
            protected void onPostExecute(Void aVoid) {
                super.onPostExecute(aVoid);
                MainActivity activity = (MainActivity) weakReference.get();
                if (activity != null) {
                    //...
                }
            }
        };
        asyncTask.execute();
    }
     @Override
    protected void onDestroy() {
        asyncTask.cancel();
    }
}

關于異步線程內存泄漏的原理,推薦看下這篇文章深入分析 ThreadLocal 內存泄漏問題

7. 集合的內存泄露問題

通常我們會添加一些對象的引用到集合中,當我們不需要用到該集合對象時,我們需要及時將該集合清空掉,如果不清空,將導致這個集合會越來越大。如果集合是靜態(tài)的話,那情況將會更嚴重,因為聲明為static的生命周期和整個app進程的生命周期一致。

下面代碼

public class MyApplication extends LitePalApplication {
     private static Map<String, Activity> destoryMap = new HashMap<>();
    public void registerActivity(String activityName,Activity act) {
        if (allActivities == null) {
             destoryMap.put(activityName, activity);
        }
    }

    public void unregisterActivity(String activityName) {
        destoryMap.remove(activityName);
    }
}

檢測結果如下圖:


image

HashMap的value對應的Activity對象未釋放,這里解決辦法我們可以使用上面多次提到過的使用弱應用去處理HashMap的Value值,當然這是在最小修改的前提下進行。如果需要對Activity進行管理,這里建議不要使用HashMap,可以使用HashSet去做,同樣最好存放的是弱應用對象,而且集合列表最好不要使用static修飾。修改后的代碼如下(使用HashMap或者HashSet存儲對象時,最好覆蓋hashCode()和equal()方法):

public class MyApplication extends LitePalApplication {
    ……
    private Set<WeakReference<Activity>> destoryMap ;
     public void registerActivity(Activity act) {
        if (allActivities == null) {
            allActivities = new HashSet<WeakReference<Activity>>();
        }
        allActivities.add(new WeakReference<Activity>(act));
    }

    public void unregisterActivity(Activity act) {
        if (allActivities != null) {
            allActivities.remove(new WeakReference<>(act));
        }
    }
}

當然,我們在使用集合時,應該注意不要使用staic去修飾。其次就是使用完集合之后需要將其致空。如果是如下寫法也會出現(xiàn)內存泄漏:

public void sel(){
    Vector vector = new Vector(10);
            for (int i = 0; i < 100; i++) {
                Object o = new Object();
                vector.add(o);
                o = null;
            }
}

我們將對象置空其集合還會持有對該對象的引用,為此我們應該在不使用Vector的時侯將vector 置null。這種情況比較常見的就是我們在Recyclerview的適配器中的運用,我們在當前活動頁面銷毀的時候應該將其對應的所有集合都清空。

8.資源對象沒關閉造成的內存泄漏

在開發(fā)過程中我們經常會使用到BraodcastReceiver,ContentObserver,InputStream,Cursor,Stream,Bitmap等資源。切記在資源不再使用的時候將其釋放,關閉掉。大多數(shù)頻發(fā)的OOM出現(xiàn)絕大部分是因為圖片資源未回收。在圖片資源使用完后可以通過recycler方法來進行處理:

if(!mBitmap.isRecycled){
    mBitmap.recycle();
    mBitmap = null;
}

當然,廣播的注銷,內容觀察者的注銷,輸入輸出流的關閉,cursor的關閉這些就不一一列舉了。只要在使用的時候多留意下這些都不是問題滴。

總結

對于內存泄漏問題,記住以下幾點:
1、對于生命周期比Activity長的對象如果需要應該使用ApplicationContext,在需要使用Context參數(shù)的時候先考慮Application.Context.
2、在引用組件Activity,F(xiàn)ragment時,優(yōu)先考慮使用弱引用。
3、在使用異步操作時注意Activity銷毀時,需要清空任務列表,如果有使用集合,將集合清空并置空,釋放相應的資源。
4、內部類持有外部類的引用盡量修改成靜態(tài)內部類中使用弱引用持有外部類的引用。
5、 留意活動的生命周期,在使用單例,靜態(tài)對象,全局性集合的時候應該特別注意置空。

文章中部分代碼純手打,可能有個別單詞誤差,如果有誤差,還請各位看官理解,如果能留言指出就十分感謝了。


image

等等,最后:盜用大牛的一句話,技術無罪,我是aserbao,微信公眾號aserbao,微博同名。隨時歡迎撩(學習交流)。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內容

友情鏈接更多精彩內容