內存泄漏優化

目錄介紹:

  • 1.什麼是內存泄漏
  • 2.內存泄漏造成什麼影響
  • 3.內存泄漏檢測的工具有哪些
  • 4.關於Leakcanary使用介紹
  • 5.Leakcanary捕捉常見的內存泄漏及解決辦法

    • 5.0.1 錯誤使用單例造成的內存泄漏
    • 5.0.2 錯誤使用靜態變量,導致引用後無法銷燬
    • 5.0.3 [常見]Handler使用不當造成的內存泄漏
    • 5.0.4 線程造成的內存泄漏[比較少見]
    • 5.0.5 非靜態內部類創建靜態實例造成的內存泄漏
    • 5.0.6 不需要用的監聽未移除會發生內存泄露
    • 5.0.7 [常見]資源未關閉造成的內存泄漏
    • 5.0.8 未註銷EventBus導致的內存泄漏
    • 5.0.9 [常見]持有activity引用未被釋放導致內存泄漏
    • 5.1.0 靜態集合使用不當導致的內存泄漏
    • 5.1.1 動畫資源未釋放導致內存泄漏
    • 5.1.2 系統bug之InputMethodManager導致內存泄漏
  • 6.其他建議

    • 6.0.1 儘量避免使用 static 成員變量
  • 7.版本更新

    • v1.0.0 更新於2016年3月19日
    • v1.1.0 更新於2017年7月8日
    • v1.2.0 更新於2018年5月3日
    • v1.3.0 更新於2018年9月18日

1.什麼是內存泄漏?

  • 一些對象有着有限的聲明週期,當這些對象所要做的事情完成了,我們希望它們會被垃圾回收器回收掉。但是如果有一系列對這個對象的引用存在,那麼在我們期待這個對象生命週期結束時被垃圾回收器回收的時候,它是不會被回收的。它還會佔用內存,這就造成了內存泄露。持續累加,內存很快被耗盡。
  • 比如:當Activity的onDestroy()方法被調用後,Activity以及它涉及到的View和相關的Bitmap都應該被回收掉。但是,如果有一個後臺線程持有這個Activity的引用,那麼該Activity所佔用的內存就不能被回收,這最終將會導致內存耗盡引發OOM而讓應用crash掉。

2.內存泄漏會造成什麼影響?

  • 它是造成應用程序OOM的主要原因之一。由於android系統爲每個應用程序分配的內存有限,當一個應用中產生的內存泄漏比較多時,就難免會導致應用所需要的內存超過這個系統分配的內存限額,這就

3.內存泄漏檢測的工具有哪些

  • 最常見的是:Leakcanary

4.關於Leakcanary使用介紹

  • leakCanary是Square開源框架,是一個Android和Java的內存泄露檢測庫,如果檢測到某個 activity 有內存泄露,LeakCanary 就是自動地顯示一個通知,所以可以把它理解爲傻瓜式的內存泄露檢測工具。通過它可以大幅度減少開發中遇到的oom問題,大大提高APP的質量。
  • 關於如何配置,這個就不說呢,網上有步驟

5.Leakcanary捕捉常見的內存泄漏及解決辦法

5.0.1 錯誤使用單例造成的內存泄漏

  • 在平時開發中單例設計模式是我們經常使用的一種設計模式,而在開發中單例經常需要持有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();
  • 看看報錯截圖

    • image
  • 解決辦法:

    • 要保證Context和AppLication的生命週期一樣,修改後代碼如下:
    • this.mContext = context.getApplicationContext();
    • 1、如果此時傳入的是 Application 的 Context,因爲 Application 的生命週期就是整個應用的生命週期,所以這將沒有任何問題。
    • 2、如果此時傳入的是 Activity 的 Context,當這個 Context 所對應的 Activity 退出時,由於該 Context 的引用被單例對象所持有,其生命週期等於整個應用程序的生命週期,所以當前 Activity 退出時它的內存並不會被回收,這就造成泄漏了。

5.0.2 錯誤使用靜態變量,導致引用後無法銷燬

  • 在平時開發中,有時候我們創建了一個工具類。比如分享工具類,十分方便多處調用,因此使用靜態方法是十分方便的。但是創建的對象,建議不要全局化,全局化的變量必須加上static。這樣會引起內存泄漏!
  • 問題代碼

    • image
  • 使用場景

    • 在Activity中引用後,關閉該Activity會導致內存泄漏
    DoShareUtil.showFullScreenShareView(PNewsContentActivity.this, title, title, shareurl, logo);
  • 查看報錯

    • image
  • 解決辦法

    • 靜態方法中,創建對象或變量,不要全局化,全局化後的變量或者對象會導致內存泄漏;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);
        }
    }

5.0.3 Handler使用不當造成的內存泄漏

  • handler是工作線程與UI線程之間通訊的橋樑,只是現在大量開源框架對其進行了封裝,我們這裏模擬一種常見使用方式來模擬內存泄漏情形。
  • 問題代碼

    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的內存資源無法及時回收,引發內存泄漏。
  • 查看報錯結果如下:

    • image
  • 解決方案

    • 第一種解決辦法

      • 要想避免Handler引起內存泄漏問題,需要我們在Activity關閉退出的時候的移除消息隊列中所有消息和所有的Runnable。
      • 上述代碼只需在onDestroy()函數中調用mHandler.removeCallbacksAndMessages(null);就行了。
@Override
protected void onDestroy() {
    super.onDestroy();
    if(handler!=null){
        handler.removeCallbacksAndMessages(null);
        handler = null;
    }
}
* 第二種解決方案
    - 使用弱引用解決handler內存泄漏問題
```
public class SampleActivity extends Activity {

    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 mHandler = new MyHandler(this);
    private static final Runnable sRunnable = new Runnable() {
        @Override
        public void run() { /* ... */ }
    };

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        mHandler.postDelayed(sRunnable, 1000 * 60 * 10);
        finish();
    }
}

即推薦使用靜態內部類 + WeakReference 這種方式。每次使用前注意判空。
```


5.0.4 線程造成的內存泄漏

  • 早時期的時候處理耗時操作多數都是採用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無法釋放回收引發內存泄漏
  • 查看報錯結果如下:

    • image
  • 解決辦法

    • 在使用AsyncTask時,在Activity銷燬時候也應該取消相應的任務AsyncTask.cancel()方法,避免任務在後臺執行浪費資源,進而避免內存泄漏的發生
    private void destroyAsyncTask() {
        if (asyncTask != null && !asyncTask.isCancelled()) {
            asyncTask.cancel(true);
        }
        asyncTask = null;
    }
    
    @Override
    protected void onDestroy() {
        super.onDestroy();
        destroyAsyncTask();
    }

5.0.5 非靜態內部類創建靜態實例造成的內存泄漏

  • 有的時候我們可能會在啓動頻繁的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的內存資源不能正常回收。

5.0.6 不需要用的監聽未移除會發生內存泄露

  • 問題代碼

    //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監聽,放到集合裏面,需要考慮內存泄漏

5.0.7 [常見]資源未關閉造成的內存泄漏

  • BroadcastReceiver,ContentObserver,FileObserver,Cursor,Callback等在 Activity onDestroy 或者某類生命週期結束之後一定要 unregister 或者 close 掉,否則這個 Activity 類會被 system 強引用,不會被內存回收。值得注意的是,關閉的語句必須在finally中進行關閉,否則有可能因爲異常未關閉資源,致使activity泄漏。
5.0.7.1 廣播註冊之後沒有被銷燬導致內存泄漏
- 比如我們在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);
    }
}
```
5.0.7.2 資源未關閉導致資源被佔用而內存泄漏
  • 在使用IO、File流或者Sqlite、Cursor等資源時要及時關閉。這些資源在進行讀寫操作時通常都使用了緩衝,如果及時不關閉,這些緩衝對象就會一直被佔用而得不到釋放,以致發生內存泄露。因此我們在不需要使用它們的時候就及時關閉,以便緩衝能及時得到釋放,從而避免內存泄露。

5.0.8 未註銷EventBus導致的內存泄漏

  • 直接展示代碼

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_common);
        EventBus.getDefault().register(this);
    }
    
    @Subscribe
    public void onEvent(MessageEvent msg) {
    
    }
    
    @Override
    protected void onDestroy() {
        super.onDestroy();
        //未移除註冊的EventBus
        //EventBus.getDefault().unregister(this);
    }

5.0.9 持有activity引用未被釋放導致內存泄漏

5.0.9.1 第一種場景
  • 先看看問題代碼

    • 這個是在開發中經常會犯的錯誤,NastyManager.getInstance() 是一個單例,當我們通過 addListener(this) 將 Activity 作爲 Listener 和 NastyManager 綁定起來的時候,由於單例和Activity生命週期不同,因此銷燬時就會導致內存泄漏。
    public class LeakActivity extends Activity {
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            NastyManager.getInstance().addListener(this);
        }
    }
  • 解決辦法:

    • 在你的 Acitivity 被銷燬的時候,將他和 NastyManager 取消掉綁定就好
    public class LeakActivity extends Activity {
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            NastyManager.getInstance().addListener(this);
        }
    
        @Override
        protected void onDestroy() {
            super.onDestroy();
            NastyManager.getInstance().removeListener(this);
        }
    }
5.0.9.2 第二種場景
  • 先來看看造成內存泄漏的代碼

    • 通過查看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的生命週期

5.1.0 靜態集合使用不當導致的內存泄漏

  • 有時候我們需要把一些對象加入到集合容器(例如ArrayList)中,當不再需要當中某些對象時,如果不把該對象的引用從集合中清理掉,也會使得GC無法回收該對象。如果集合是static類型的話,那內存泄漏情況就會更爲嚴重。因此,當不再需要某對象時,需要主動將之從集合中移除。

5.1.1 動畫資源未釋放導致內存泄漏

  • 問題代碼

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

5.1.2 系統bug之InputMethodManager導致內存泄漏

  • 每次從MainActivity退出程序時總會報InputMethodManager內存泄漏,原因系統中的InputMethodManager持有當前MainActivity的引用,導致了MainActivity不能被系統回收,從而導致了MainActivity的內存泄漏。查了很多資料,發現這是 Android SDK中輸入法的一個Bug,在15<=API<=23中都存在,目前Google還沒有解決這個Bug。

6.其他建議

6.0.1 儘量避免使用 static 成員變量

  • 儘量避免使用 static 成員變量

    • 如果成員變量被聲明爲 static,那我們都知道其生命週期將與整個app進程生命週期一樣。這會導致一系列問題,如果你的app進程設計上是長駐內存的,那即使app切到後臺,這部分內存也不會被釋放。按照現在手機app內存管理機制,佔內存較大的後臺進程將優先回收,如果此app做過進程互保保活,那會造成app在後臺頻繁重啓。當手機安裝了你參與開發的app以後一夜時間手機被消耗空了電量、流量,你的app不得不被用戶卸載或者靜默。
    • 架構設計上要思考是否真的有必要這樣做,儘量避免。如果架構需要這麼設計,那麼此對象的生命週期你有責任管理起來。

關於其他內容介紹

01.關於博客彙總鏈接

02.關於我的博客

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