Android性能優化之內存洩漏無處可藏(圖文講解)

Android性能優化之內存洩漏無處可藏(圖文講解)

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

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

什麼是內存洩漏?內存洩漏的分類及影響?常見的內存洩漏及解決辦法:1. 單例造成的內存洩漏:2. 接口實現引用造成的內存洩漏。3. 使用ViewVideo造成的內存洩漏(MediaPlayer.mSubtitleController):4. MediaPlayer源碼存在的內存洩漏問題:5. Handler使用造成的內存洩漏(MessageQueue.mMessage)6. 匿名內部類造成的內存洩漏7. 集合的內存洩露問題8.資源對象沒關閉造成的內存洩漏總結

什麼是內存洩漏?

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

內存洩漏的分類及影響?

分類:常發性內存洩漏,偶發性內存洩漏,一次性內存洩漏,隱式內存洩漏。

危害:內存洩漏造成的影響其實是內存洩漏的堆積,這將會消耗系統所有的內存。所以一個內存洩漏危害並不大,因為不會堆積,而隱式內存洩漏危害性則非常大,因為較之於常發性和偶發性內存洩漏它更難被檢測到。

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

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

第一種情況:

1public class LoginActivity extends Activity {2 public static LoginActivity instance;3 @Override4 protected void onCreate(Bundle savedInstanceState) {5 ……6 instance = this;7 }8}

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

Android性能優化之內存洩漏無處可藏(圖文講解)

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

1public class LoginActivity extends Activity {2 public static WeakReference instance;3 @Override4 protected void onCreate(Bundle savedInstanceState) {5 ……6 instance = new WeakReference(this);7 }8}

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

 1public class AppManager {2 private static AppManager instance;3 private Context context;4 private AppManager(Context context) {5 this.context = context;6 }7 public static AppManager getInstance(Context context) {8 if (instance == null) {9 instance = new AppManager(context);10 }11 return instance;12}

檢測結果如下:

Android性能優化之內存洩漏無處可藏(圖文講解)

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

 1public class LoginManager {2 private static LoginManager mInstance;3 private Context mContext;4 private LoginManager(Context context) {5 this.mContext = context.getApplicationContext();6 }7 public static LoginManager getInstance(Context context) {8 if (mInstance == null) {9 synchronized (LoginManager.class) {10 if (mInstance == null) {11 mInstance = new LoginManager(context);12 }13 }14 }15 return mInstance;16 }17 public void dealData() {18 }19}

2. 接口實現引用造成的內存洩漏。

不知道這樣實現代碼的多不多?

1public class MyApplication extends LitePalApplication{2 ……3 UnReadMsgListener unReadMsgListener;4 public void setUnReadMsgListener(UnReadMsgListener unReadMsgListener){5 this.unReadMsgListener = unReadMsgListener;//在其他頁面進行接口實現6 }7 ……8}

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

Android性能優化之內存洩漏無處可藏(圖文講解)

原因分析:在其他頁面進行setUnReadMsgListener操作,MyApplication將明顯持有對此接口的引用,此接口被Activity實現,所以MyApplication一直持有Activity的引用。

在儘量不修改原代碼的情況下,解決辦法如下:

1UnReadMsgListener unReadMsgListener;2 WeakReference mListenerWeakReference;3 public void setUnReadMsgListener(UnReadMsgListener unReadMsgListener){4 mListenerWeakReference = new WeakReference(unReadMsgListener);5 this.unReadMsgListener = mListenerWeakReference.get();6 }

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

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

Android性能優化之內存洩漏無處可藏(圖文講解)

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

解決辦法:

第一種:將VideoView在代碼中實現:

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

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

 1 @Override2 protected void attachBaseContext(Context newBase) {3 super.attachBaseContext(new ContextWrapper(newBase)4 {5 @Override6 public Object getSystemService(String name)7 {8 if (Context.AUDIO_SERVICE.equals(name))9 return getApplicationContext().getSystemService(name);10 return super.getSystemService(name);11 }12 });13 }

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

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

 1 //MediaPlayer系統源碼2 ……3 public void release() {4 baseRelease();5 stayAwake(false);6 updateSurfaceScreenOn();7 mOnPreparedListener = null;8 mOnBufferingUpdateListener = null;9 mOnCompletionListener = null;10 mOnSeekCompleteListener = null;11 mOnErrorListener = null;12 mOnInfoListener = null;13 mOnVideoSizeChangedListener = null;14 mOnTimedTextListener = null;15 if (mTimeProvider != null) {16 mTimeProvider.close();17 mTimeProvider = null;18 }19 mOnSubtitleDataListener = null;20 _release();21 }22 ……23 public void reset() {24 mSelectedSubtitleTrackIndex = -1;25 synchronized(mOpenSubtitleSources) {26 for (final InputStream is: mOpenSubtitleSources) {27 try {28 is.close();29 } catch (IOException e) {30 }31 }32 mOpenSubtitleSources.clear();33 }34 if (mSubtitleController != null) {//這裡有對mSubtitleController進行處理操作35 mSubtitleController.reset();36 }37 if (mTimeProvider != null) {38 mTimeProvider.close();39 mTimeProvider = null;40 }41 stayAwake(false);42 _reset();43 // make sure none of the listeners get called anymore44 if (mEventHandler != null) {45 mEventHandler.removeCallbacksAndMessages(null);46 }47 synchronized (mIndexTrackPairs) {48 mIndexTrackPairs.clear();49 mInbandTrackIndices.clear();50 };51 }

如果上面的描述不夠詳細,你可以參考stackoverflow

解決辦法上面有提到過,如下:

Android性能優化之內存洩漏無處可藏(圖文講解)

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

Android性能優化之內存洩漏無處可藏(圖文講解)

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

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

 1 public class BaseActivity extends AppCompatActivity {2 ......3 private Handler baseHandler = new Handler();4 @Override5 protected void onResume() {6 baseHandler.postDelayed(new Runnable() {7 @Override8 public void run() {9 heartBeat();10 if (!isPause) {11 baseHandler.postDelayed(this, 60 * 1000);12 }13 }14 }, 100);15 }16 }

檢測到洩漏結果如下:

Android性能優化之內存洩漏無處可藏(圖文講解)

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

 1public class BaseActivity extends AppCompatActivity {2 ......3 private static class MyHandler extends Handler {4 private final WeakReference mActivity;5 public MyHandler(SampleActivity activity) {6 mActivity = new WeakReference(activity);7 }8 @Override9 public void handleMessage(Message msg) {10 SampleActivity activity = mActivity.get();11 if (activity != null) {//這裡切記要判空12 // ...13 }14 }15 }16 private final MyHandler baseHandler= new MyHandler(this);17 @Override18 protected void onResume() {19 baseHandler.postDelayed(new Runnable() {20 @Override21 public void run() {22 heartBeat();23 if (!isPause) {24 baseHandler.postDelayed(this, 60 * 1000);25 }26 }27 }, 100);28 }29 @Override30 protected void onDestroy() {31 if (baseHandler != null) {32 baseHandler.removeCallbacks(null);33 baseHandler = null;34 }35 }36 }

當然這裡簡單說一下軟引用和弱引用的使用:記住兩點即可:

第一點:如果只是想避免OutOfMemory異常的發生,則可以使用軟引用。如果對於應用的性能更在意,想盡快回收一些佔用內存比較大的對象,則可以使用弱引用。

第二點:可以根據對象是否經常使用來判斷選擇軟引用還是弱引用。如果該對象可能會經常使用的,就儘量用軟引用。如果該對象不被使用的可能性更大些,就可以用弱引用。

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

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

 1public class WelcomeActivity extends Activity {2 ......3 public void sel(){4 new Thread(new Runnable() {5 @Override6 public void run() {7 SystemClock.sleep(10000);8 //do something ... 裡面持有對當前activity的引用9 }10 }).start();11 }12}

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

Android性能優化之內存洩漏無處可藏(圖文講解)

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

解決思路:不使用匿名內部類,通過靜態內部類來實現,使用弱應用來持有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的引用

}

}

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

 1public class MainActivity extends Activity {2 public void sel(){3 new AsyncTask() {4 @Override5 protected Void doInBackground(Void... params) {6 SystemClock.sleep(10 * 1000);7 //do something ... 裡面持有對當前activity的引用8 return null;9 }10 }.execute();11 }12}

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

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

 1public class MainActivity extends Activity {2 private WeakReference weakReference;3 AsyncTask asyncTask;4 public void sel(){5 asyncTask = new AsyncTask<>() {6 @Override7 protected Void doInBackground(Void... params) {8 SystemClock.sleep(10 * 1000);9 //do something ... 裡面持有對當前activity的引用10 return null;11 }12 @Override13 protected void onPostExecute(Void aVoid) {14 super.onPostExecute(aVoid);15 MainActivity activity = (MainActivity) weakReference.get();16 if (activity != null) {17 //...18 }19 }20 };21 asyncTask.execute();22 }23 @Override24 protected void onDestroy() {25 asyncTask.cancel();26 }27}

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

7. 集合的內存洩露問題

下面代碼

 1public class MyApplication extends LitePalApplication {2 private static Map destoryMap = new HashMap<>();3 public void registerActivity(String activityName,Activity act) {4 if (allActivities == null) {5 destoryMap.put(activityName, activity);6 }7 }8 public void unregisterActivity(String activityName) {9 destoryMap.remove(activityName);10 }11}

檢測結果如下圖:

Android性能優化之內存洩漏無處可藏(圖文講解)

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

 1public class MyApplication extends LitePalApplication {2 ……3 private Set> destoryMap ;4 public void registerActivity(Activity act) {5 if (allActivities == null) {6 allActivities = new HashSet>();7 }8 allActivities.add(new WeakReference(act));9 }10 public void unregisterActivity(Activity act) {11 if (allActivities != null) {12 allActivities.remove(new WeakReference<>(act));13 }14 }15}

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

1public void sel(){2 Vector vector = new Vector(10);3 for (int i = 0; i < 100; i++) {4 Object o = new Object();5 vector.add(o);6 o = null;7 }8}

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

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

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

1if(!mBitmap.isRecycled){2 mBitmap.recycle();3 mBitmap = null;4}

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

總結

對於內存洩漏問題,記住以下幾點:

1、對於生命週期比Activity長的對象如果需要應該使用ApplicationContext,在需要使用Context參數的時候先考慮Application.Context.

2、在引用組件Activity,Fragment時,優先考慮使用弱引用。

3、在使用異步操作時注意Activity銷燬時,需要清空任務列表,如果有使用集合,將集合清空並置空,釋放相應的資源。

4、內部類持有外部類的引用盡量修改成靜態內部類中使用弱引用持有外部類的引用。

5、 留意活動的生命週期,在使用單例,靜態對象,全局性集合的時候應該特別注意置空。

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

Android性能優化之內存洩漏無處可藏(圖文講解)


分享到:


相關文章: