Android內存泄漏優化

目錄介紹:

  • 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/yangchong2...
  • 若是以爲好,能夠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沒法被釋放回收,則有可能形成內存泄漏,錯誤寫法以下:
  • 問題引發內存泄漏代碼php

    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() {}
    }
  • 使用場景java

    • 在一個Activity中調用的,而後關閉該Activity則會出現內存泄漏。
    LoginManager.getInstance(this).dealData();
  • 看看報錯截圖android

    • image
  • 解決辦法:git

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

06.Handler使用不當形成內存泄漏

  • handler是工做線程與UI線程之間通信的橋樑,只是如今大量開源框架對其進行了封裝,咱們這裏模擬一種常見使用方式來模擬內存泄漏情形。
  • 解決Handler內存泄露主要2點github

    • 有延時消息,要在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);
        }
    }
  • 形成內存泄漏緣由分析segmentfault

    • 上述代碼經過內部類的方式建立mHandler對象,此時mHandler會隱式地持有一個外部類對象引用這裏就是MainActivity,當執行postDelayed方法時,該方法會將你的Handler裝入一個Message,並把這條Message推到MessageQueue中,MessageQueue是在一個Looper線程中不斷輪詢處理消息,那麼當這個Activity退出時消息隊列中還有未處理的消息或者正在處理消息,而消息隊列中的Message持有mHandler實例的引用,mHandler又持有Activity的引用,因此致使該Activity的內存資源沒法及時回收,引起內存泄漏。
  • 查看報錯結果以下:設計模式

    • image
  • 解決方案markdown

    • 第一種解決辦法網絡

      • 要想避免Handler引發內存泄漏問題,須要咱們在Activity關閉退出的時候的移除消息隊列中全部消息和全部的Runnable。
      • 上述代碼只需在onDestroy()函數中調用mHandler.removeCallbacksAndMessages(null);就好了。
      @Override
      protected void onDestroy() {
          super.onDestroy();
          if(handler!=null){
              handler.removeCallbacksAndMessages(null);
              handler = null;
          }
      }
    • 第二種解決方案

      //自定義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。這樣會引發內存泄漏!
  • 問題代碼

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

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沒法釋放回收引起內存泄漏
  • 查看報錯結果以下:

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

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.關於個人博客

項目開源地址:https://github.com/yangchong2...

項目開源地址:https://github.com/yangchong2...

相關文章
相關標籤/搜索