記憶體洩漏優化
阿新 • • 發佈:2019-06-27
目錄介紹:
- 01.什麼是記憶體洩漏
- 02.記憶體洩漏造成什麼影響
- 03.記憶體洩漏檢測的工具有哪些
- 04.關於Leakcanary使用介紹
- 05.錯誤使用單例造成的記憶體洩漏
- 06.Handler使用不當造成記憶體洩漏
- 07.Thread未關閉造成內容洩漏
- 08.錯誤使用靜態變數導致引用後無法銷燬
- 09.AsyncTask造成的記憶體洩漏
- 10.非靜態內部類建立靜態例項造成記憶體洩漏
- 11.不需要用的監聽未移除會發生記憶體洩露
- 12.資源未關閉造成的記憶體洩漏
- 13.廣播註冊之後沒有被銷燬
- 14.錯誤使用context上下文引起記憶體洩漏
- 15.靜態集合使用不當導致的記憶體洩漏
- 16.動畫資源未釋放導致記憶體洩漏
- 17.系統bug之InputMethodManager導致記憶體洩漏
好訊息
- 部落格筆記大彙總【16年3月到至今】,包括Java基礎及深入知識點,Android技術部落格,Python學習筆記等等,還包括平時開發中遇到的bug彙總,當然也在工作之餘收集了大量的面試題,長期更新維護並且修正,持續完善……開源的檔案是markdown格式的!同時也開源了生活部落格,從12年起,積累共計N篇[近100萬字,陸續搬到網上],轉載請註明出處,謝謝!
- 連結地址:https://github.com/yangchong211/YCBlogs
- 如果覺得好,可以star一下,謝謝!當然也歡迎提出建議,萬事起於忽微,量變引起質變!
01.什麼是記憶體洩漏
- 一些物件有著有限的宣告週期,當這些物件所要做的事情完成了,我們希望它們會被垃圾回收器回收掉。但是如果有一系列對這個物件的引用存在,那麼在我們期待這個物件生命週期結束時被垃圾回收器回收的時候,它是不會被回收的。它還會佔用記憶體,這就造成了記憶體洩露。持續累加,記憶體很快被耗盡。
- 比如:當Activity的onDestroy()方法被呼叫後,Activity以及它涉及到的View和相關的Bitmap都應該被回收掉。但是,如果有一個後臺執行緒持有這個Activity的引用,那麼該Activity所佔用的記憶體就不能被回收,這最終將會導致記憶體耗盡引發OOM而讓應用crash掉。
02.記憶體洩漏造成什麼影響
- 它是造成應用程式OOM的主要原因之一。由於android系統為每個應用程式分配的記憶體有限,當一個應用中產生的記憶體洩漏比較多時,就難免會導致應用所需要的記憶體超過這個系統分配的記憶體限額,這就
03.記憶體洩漏檢測的工具有哪些
- 最常見的是:Leakcanary
04.關於Leakcanary使用介紹
- leakCanary是Square開源框架,是一個Android和Java的記憶體洩露檢測庫,如果檢測到某個 activity 有記憶體洩露,LeakCanary 就是自動地顯示一個通知,所以可以把它理解為傻瓜式的記憶體洩露檢測工具。通過它可以大幅度減少開發中遇到的oom問題,大大提高APP的質量。
- 關於如何配置,這個就不說呢,網上有步驟
05.錯誤使用單例造成的記憶體洩漏
- 在平時開發中單例設計模式是我們經常使用的一種設計模式,而在開發中單例經常需要持有Context物件,如果持有的Context物件生命週期與單例生命週期更短時,或導致Context無法被釋放回收,則有可能造成記憶體洩漏,錯誤寫法如下:
- 問題引起記憶體洩漏程式碼
public class LoginManager { private static LoginManager mInstance; private Context mContext; private LoginManager(Context context) { this.mContext = 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() {} }
- 使用場景
- 在一個Activity中呼叫的,然後關閉該Activity則會出現記憶體洩漏。
LoginManager.getInstance(this).dealData();
- 看看報錯截圖
- 解決辦法:
- 要保證Context和AppLication的生命週期一樣,修改後程式碼如下:
- this.mContext = context.getApplicationContext();
- 1、如果此時傳入的是 Application 的 Context,因為 Application 的生命週期就是整個應用的生命週期,所以這將沒有任何問題。
- 2、如果此時傳入的是 Activity 的 Context,當這個 Context 所對應的 Activity 退出時,由於該 Context 的引用被單例物件所持有,其生命週期等於整個應用程式的生命週期,所以當前 Activity 退出時它的記憶體並不會被回收,這就造成洩漏了。
06.Handler使用不當造成記憶體洩漏
- handler是工作執行緒與UI執行緒之間通訊的橋樑,只是現在大量開源框架對其進行了封裝,我們這裡模擬一種常見使用方式來模擬記憶體洩漏情形。
- 解決Handler記憶體洩露主要2點
- 有延時訊息,要在Activity銷燬的時候移除Messages
- 匿名內部類導致的洩露改為匿名靜態內部類,並且對上下文或者Activity使用弱引用。
- 問題程式碼
public class MainActivity extends AppCompatActivity { private Handler mHandler = new Handler(); private TextView mTextView; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); mTextView = (TextView) findViewById(R.id.text); //模擬記憶體洩露 mHandler.postDelayed(new Runnable() { @Override public void run() { mTextView.setText("yangchong"); } }, 2000); } }
- 造成記憶體洩漏原因分析
- 上述程式碼通過內部類的方式建立mHandler物件,此時mHandler會隱式地持有一個外部類物件引用這裡就是MainActivity,當執行postDelayed方法時,該方法會將你的Handler裝入一個Message,並把這條Message推到MessageQueue中,MessageQueue是在一個Looper執行緒中不斷輪詢處理訊息,那麼當這個Activity退出時訊息佇列中還有未處理的訊息或者正在處理訊息,而訊息佇列中的Message持有mHandler例項的引用,mHandler又持有Activity的引用,所以導致該Activity的記憶體資源無法及時回收,引發記憶體洩漏。
- 檢視報錯結果如下:
- 解決方案
- 第一種解決辦法
- 要想避免Handler引起記憶體洩漏問題,需要我們在Activity關閉退出的時候的移除訊息佇列中所有訊息和所有的Runnable。
- 上述程式碼只需在onDestroy()函式中呼叫mHandler.removeCallbacksAndMessages(null);就行了。
@Override protected void onDestroy() { super.onDestroy(); if(handler!=null){ handler.removeCallbacksAndMessages(null); handler = null; } }
- 第二種解決方案
- 使用弱引用解決handler記憶體洩漏問題,關於程式碼案例,可以參考我的開源專案:https://github.com/yangchong211/YCAudioPlayer中的utils-share包下的ShareDialog程式碼
//自定義handler public static class HandlerHolder extends Handler { WeakReference<OnReceiveMessageListener> mListenerWeakReference; /** * @param listener 收到訊息回撥介面 */ HandlerHolder(OnReceiveMessageListener listener) { mListenerWeakReference = new WeakReference<>(listener); } @Override public void handleMessage(Message msg) { if (mListenerWeakReference!=null && mListenerWeakReference.get()!=null){ mListenerWeakReference.get().handlerMessage(msg); } } } //建立handler物件 private HandlerHolder handler = new HandlerHolder(new OnReceiveMessageListener() { @Override public void handlerMessage(Message msg) { switch (msg.what){ case 1: TextView textView1 = (TextView) msg.obj; showBottomInAnimation(textView1); break; case 2: TextView textView2 = (TextView) msg.obj; showBottomOutAnimation(textView2); break; } } }); //傳送訊息 Message message = new Message(); message.what = 1; message.obj = textView; handler.sendMessageDelayed(message,time); 即推薦使用靜態內部類 + WeakReference 這種方式。每次使用前注意判空。
- 第一種解決辦法
07.Thread未關閉造成內容洩漏
- 當在開啟一個子執行緒用於執行一個耗時操作後,此時如果改變配置(例如橫豎屏切換)導致了Activity重新建立,一般來說舊Activity就將交給GC進行回收。但如果建立的執行緒被宣告為非靜態內部類或者匿名類,那麼執行緒會保持有舊Activity的隱式引用。當執行緒的run()方法還沒有執行結束時,執行緒是不會被銷燬的,因此導致所引用的舊的Activity也不會被銷燬,並且與該Activity相關的所有資原始檔也不會被回收,因此造成嚴重的記憶體洩露。
- 因此總結來看, 執行緒產生記憶體洩露的主要原因有兩點:
- 1.執行緒生命週期的不可控。Activity中的Thread和AsyncTask並不會因為Activity銷燬而銷燬,Thread會一直等到run()執行結束才會停止,AsyncTask的doInBackground()方法同理
- 2.非靜態的內部類和匿名類會隱式地持有一個外部類的引用
- 例如如下程式碼,在onCreate()方法中啟動一個執行緒,並用一個靜態變數threadIndex標記當前建立的是第幾個執行緒
public class ThreadActivity extends AppCompatActivity { private final String TAG = "ThreadActivity"; private static int threadIndex; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_thread); threadIndex++; new Thread(new Runnable() { @Override public void run() { int j = threadIndex; while (true) { Log.e(TAG, "Hi--" + j); try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } } } }).start(); } }
- 旋轉幾次螢幕,可以看到輸出結果為:
04-04 08:15:16.373 23731-23911/com.yc.leakdemo E/ThreadActivity: Hi--2 04-04 08:15:16.374 23731-26132/com.yc.leakdemo E/ThreadActivity: Hi--4 04-04 08:15:16.374 23731-23970/com.yc.leakdemo E/ThreadActivity: Hi--3 04-04 08:15:16.374 23731-23820/com.yc.leakdemo E/ThreadActivity: Hi--1 04-04 08:15:16.852 23731-26202/com.yc.leakdemo E/ThreadActivity: Hi--5 04-04 08:15:18.374 23731-23911/com.yc.leakdemo E/ThreadActivity: Hi--2 04-04 08:15:18.374 23731-26132/com.yc.leakdemo E/ThreadActivity: Hi--4 04-04 08:15:18.376 23731-23970/com.yc.leakdemo E/ThreadActivity: Hi--3 04-04 08:15:18.376 23731-23820/com.yc.leakdemo E/ThreadActivity: Hi--1 04-04 08:15:18.852 23731-26202/com.yc.leakdemo E/ThreadActivity: Hi--5 ...
- 即使建立了新的Activity,舊的Activity中建立的執行緒依然還在執行,從而導致無法釋放Activity佔用的記憶體,從而造成嚴重的記憶體洩漏。想要避免因為 Thread 造成記憶體洩漏,可以在 Activity 退出後主動停止 Thread
- 例如,可以為 Thread 設定一個布林變數 threadSwitch 來控制執行緒的啟動與停止
public class ThreadActivity extends AppCompatActivity { private final String TAG = "ThreadActivity"; private int threadIndex; private boolean threadSwitch = true; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_thread); threadIndex++; new Thread(new Runnable() { @Override public void run() { int j = threadIndex; while (threadSwitch) { Log.e(TAG, "Hi--" + j); try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } } } }).start(); } @Override protected void onDestroy() { super.onDestroy(); threadSwitch = false; } }
- 如果想保持Thread繼續執行,可以按以下步驟來:
- 1.將執行緒改為靜態內部類,切斷Activity 對於Thread的強引用
- 2.線上程內部採用弱引用儲存Context引用,切斷Thread對於Activity 的強引用
public class ThreadActivity extends AppCompatActivity { private static final String TAG = "ThreadActivity"; private static int threadIndex; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_thread); threadIndex++; new MyThread(this).start(); } private static class MyThread extends Thread { private WeakReference<ThreadActivity> activityWeakReference; MyThread(ThreadActivity threadActivity) { activityWeakReference = new WeakReference<>(threadActivity); } @Override public void run() { if (activityWeakReference == null) { return; } if (activityWeakReference.get() != null) { int i = threadIndex; while (true) { Log.e(TAG, "Hi--" + i); try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } } } } } }
08.錯誤使用靜態變數導致引用後無法銷燬
- 在平時開發中,有時候我們建立了一個工具類。比如分享工具類,十分方便多處呼叫,因此使用靜態方法是十分方便的。但是建立的物件,建議不要全域性化,全域性化的變數必須加上static。這樣會引起記憶體洩漏!
- 問題程式碼
- 使用場景
- 在Activity中引用後,關閉該Activity會導致記憶體洩漏
DoShareUtil.showFullScreenShareView(PNewsContentActivity.this, title, title, shareurl, logo);
- 檢視報錯
- 解決辦法
- 靜態方法中,建立物件或變數,不要全域性化,全域性化後的變數或者物件會導致記憶體洩漏;popMenuView和popMenu都不要全域性化
- 知識延伸
非靜態內部類,靜態例項化 public class MyActivity extends AppCompatActivity { //靜態成員變數 public static InnerClass innerClass = null; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_my); innerClass = new InnerClass(); } class InnerClass { public void doSomeThing() {} } } 這裡內部類InnerClass隱式的持有外部類MyActivity的引用,而在MyActivity的onCreate方法中呼叫了。 這樣innerClass就會在MyActivity建立的時候是有了他的引用,而innerClass是靜態型別的不會被垃圾回收, MyActivity在執行onDestory方法的時候由於被innerClass持有了引用而無法被回收,所以這樣MyActivity就總是被innerClass持有而無法回收造成記憶體洩露。 靜態變數引用不當會導致記憶體洩漏 靜態變數Activity和View會導致記憶體洩漏,在下面這段程式碼中對Activity的Context和TextView設定為靜態物件,從而產生記憶體洩漏。 public class MainActivity extends AppCompatActivity { private static Context context; private static TextView textView; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); context = this; textView = new TextView(this); } }
09.AsyncTask造成的記憶體洩漏
- 早時期的時候處理耗時操作多數都是採用Thread+Handler的方式,後來逐步被AsyncTask取代,直到現在採用RxJava的方式來處理非同步。這裡以AsyncTask為例,可能大部分人都會這樣處理一個耗時操作然後通知UI更新結果:
- 問題程式碼
public class MainActivity extends AppCompatActivity { private AsyncTask<Void, Void, Integer> asyncTask; private TextView mTextView; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); mTextView = (TextView) findViewById(R.id.text); testAsyncTask(); finish(); } private void testAsyncTask() { asyncTask = new AsyncTask<Void, Void, Integer>() { @Override protected Integer doInBackground(Void... params) { int i = 0; //模擬耗時操作 while (!isCancelled()) { i++; if (i > 1000000000) { break; } Log.e("LeakCanary", "asyncTask---->" + i); } return i; } @Override protected void onPostExecute(Integer integer) { super.onPostExecute(integer); mTextView.setText(String.valueOf(integer)); } }; asyncTask.execute(); } }
- 造成記憶體洩漏原因分析
- 在處理一個比較耗時的操作時,可能還沒處理結束MainActivity就執行了退出操作,但是此時AsyncTask依然持有對MainActivity的引用就會導致MainActivity無法釋放回收引發記憶體洩漏
- 檢視報錯結果如下:
- 解決辦法
- 在使用AsyncTask時,在Activity銷燬時候也應該取消相應的任務AsyncTask.cancel()方法,避免任務在後臺執行浪費資源,進而避免記憶體洩漏的發生
private void destroyAsyncTask() { if (asyncTask != null && !asyncTask.isCancelled()) { asyncTask.cancel(true); } asyncTask = null; } @Override protected void onDestroy() { super.onDestroy(); destroyAsyncTask(); }
10.非靜態內部類建立靜態例項造成記憶體洩漏
- 有的時候我們可能會在啟動頻繁的Activity中,為了避免重複建立相同的資料資源,可能會出現這種寫法
- 問題程式碼
private static TestResource mResource = null; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); if(mResource == null){ mResource = new TestResource(); } } class TestResource { //裡面程式碼引用上下文,Activity.this會導致記憶體洩漏 }
- 解決辦法
- 將該內部類設為靜態內部類或將該內部類抽取出來封裝成一個單例,如果需要使用Context,請按照上面推薦的使用Application 的 Context。
- 分析問題
- 這樣就在Activity內部建立了一個非靜態內部類的單例,每次啟動Activity時都會使用該單例的資料,這樣雖然避免了資源的重複建立,不過這種寫法卻會造成記憶體洩漏,因為非靜態內部類預設會持有外部類的引用,而該非靜態內部類又建立了一個靜態的例項,該例項的生命週期和應用的一樣長,這就導致了該靜態例項一直會持有該Activity的引用,導致Activity的記憶體資源不能正常回收。
11.不需要用的監聽未移除會發生記憶體洩露
- 問題程式碼
//add監聽,放到集合裡面 tv.getViewTreeObserver().addOnWindowFocusChangeListener(new ViewTreeObserver.OnWindowFocusChangeListener() { @Override public void onWindowFocusChanged(boolean b) { //監聽view的載入,view加載出來的時候,計算他的寬高等。 } });
- 解決辦法
//計算完後,一定要移除這個監聽 tv.getViewTreeObserver().removeOnWindowFocusChangeListener(this);
- 注意事項:
tv.setOnClickListener();//監聽執行完回收物件,不用考慮記憶體洩漏 tv.getViewTreeObserver().addOnWindowFocusChangeListene,add監聽,放到集合裡面,需要考慮記憶體洩漏
12.資源未關閉造成的記憶體洩漏
- BroadcastReceiver,ContentObserver,FileObserver,Cursor,Callback等在 Activity onDestroy 或者某類生命週期結束之後一定要 unregister 或者 close 掉,否則這個 Activity 類會被 system 強引用,不會被記憶體回收。值得注意的是,關閉的語句必須在finally中進行關閉,否則有可能因為異常未關閉資源,致使activity洩漏。
13.廣播註冊之後沒有被銷燬
- 比如我們在Activity中註冊廣播,如果在Activity銷燬後不取消註冊,那麼這個廣播會一直存在系統中,同上面所說的非靜態內部類一樣持有Activity引用,導致記憶體洩露。因此註冊廣播後在Activity銷燬後一定要取消註冊。
- 在註冊觀察則模式的時候,如果不及時取消也會造成記憶體洩露。比如使用Retrofit+RxJava註冊網路請求的觀察者回調,同樣作為匿名內部類持有外部引用,所以需要記得在不用或者銷燬的時候取消註冊。
public class MeAboutActivity extends BaseActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); this.registerReceiver(mReceiver, new IntentFilter()); } private BroadcastReceiver mReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { // 接收到廣播需要做的邏輯 } }; @Override protected void onDestroy() { super.onDestroy(); this.unregisterReceiver(mReceiver); } }
14.錯誤使用context上下文引起記憶體洩漏
- 先來看看造成記憶體洩漏的程式碼
- 通過檢視Toast類的原始碼可以看到,Toast類內部的mContext指向傳入的Context。而ToastUtils中的toast變數是靜態型別的,其生命週期是與整個應用一樣長的,從而導致activity得不到釋放。因此,對Context的引用不能超過它本身的生命週期。
/** * 吐司工具類 避免點選多次導致吐司多次,最後導致Toast就長時間關閉不掉了 * @param context 注意:這裡如果傳入context會報記憶體洩漏;傳遞activity..getApplicationContext() * @param content 吐司內容 */ private static Toast toast; @SuppressLint("ShowToast") public static void showToast(Context context, String content) { if (toast == null) { toast = Toast.makeText(context , content, Toast.LENGTH_SHORT); } else { toast.setText(content); } toast.show(); }
- 解決辦法
- 是改為使用 ApplicationContext即可,因為ApplicationContext會隨著應用的存在而存在,而不依賴於Activity的生命週期
15.靜態集合使用不當導致的記憶體洩漏
- 有時候我們需要把一些物件加入到集合容器(例如ArrayList)中,當不再需要當中某些物件時,如果不把該物件的引用從集合中清理掉,也會使得GC無法回收該物件。如果集合是static型別的話,那記憶體洩漏情況就會更為嚴重。因此,當不再需要某物件時,需要主動將之從集合中移除。
16.動畫資源未釋放導致記憶體洩漏
- 問題程式碼
public class LeakActivity extends AppCompatActivity { private TextView textView; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_leak); textView = (TextView)findViewById(R.id.text_view); ObjectAnimator objectAnimator = ObjectAnimator.ofFloat(textView,"rotation",0,360); objectAnimator.setRepeatCount(ValueAnimator.INFINITE); objectAnimator.start(); } }
- 解決辦法
- 在屬性動畫中有一類無限迴圈動畫,如果在Activity中播放這類動畫並且在onDestroy中去停止動畫,那麼這個動畫將會一直播放下去,這時候Activity會被View所持有,從而導致Activity無法被釋放。解決此類問題則是需要早Activity中onDestroy去去呼叫objectAnimator.cancel()來停止動畫。
@Override protected void onDestroy() { super.onDestroy(); mAnimator.cancel(); }
17.系統bug之InputMethodManager導致記憶體洩漏
- 每次從MainActivity退出程式時總會報InputMethodManager記憶體洩漏,原因系統中的InputMethodManager持有當前MainActivity的引用,導致了MainActivity不能被系統回收,從而導致了MainActivity的記憶體洩漏。查了很多資料,發現這是 Android SDK中輸入法的一個Bug,在15<=API<=23中都存在,目前Google還沒有解決這個Bug。
其他介紹
01.關於部落格彙總連結
02.關於我的部落格
- github:https://github.com/yangchong211
- 知乎:https://www.zhihu.com/people/yczbj/activities
- 簡書:http://www.jianshu.com/u/b7b2c6ed9284
- csdn:http://my.csdn.net/m0_37700275
- 喜馬拉雅聽書:http://www.ximalaya.com/zhubo/71989305/
- 開源中國:https://my.oschina.net/zbj1618/blog
- 泡在網上的日子:http://www.jcodecraeer.com/member/content_list.php?channelid=1
- 郵箱:[email protected]
- 阿里雲部落格:https://yq.aliyun.com/users/article?spm=5176.100- 239.headeruserinfo.3.dT4bcV
- segmentfault頭條:https://segmentfault.com/u/xiangjianyu/articles
- 掘金:https://juejin.im/user/5939433efe88c2006afa0c6e