三方庫源碼筆記(6)-LeakCanary 擴展閱讀

對於 Android Developer 來講,不少開源庫都是屬於開發必備的知識點,從使用方式到實現原理再到源碼解析,這些都須要咱們有必定程度的瞭解和運用能力。因此我打算來寫一系列關於開源庫源碼解析實戰演練的文章,初定的目標是 EventBus、ARouter、LeakCanary、Retrofit、Glide、OkHttp、Coil 等七個知名開源庫,但願對你有所幫助 😇😇java

公衆號:字節數組android

系列文章導航:數組

上篇文章對 LeakCanary 進行了一次比較全面的源碼解析,按流程來講本篇文章應該是屬於實戰篇的,但是因爲某些緣由就不打算寫實戰內容了(其實就是本身有點菜,寫不太出來),就仍是來寫一篇關於內存泄露相關的擴展閱讀吧 😂😂markdown

Java 的一個很顯著的優勢就在於內存自動回收機制,Java 經過垃圾收集器(Garbage Collection,GC)來自動管理內存的回收過程,而無需開發者來主動釋放內存。這種自動化行爲有效地節省了開發人員的開發成本,但也讓一些開發者誤覺得 Java 就不存在內存泄漏的問題了,或者是誤認爲內存泄露應該是 GC 或者 JVM 層面來關心和解決的問題。這種想法是不正確的,由於內存泄露大多時候是因爲程序自己存在缺陷而致使的,GC 和 JVM 並沒有法精準理解程序的實現初衷,因此仍是須要由開發人員來主動解決問題app

1、內存泄露和內存溢出

內存泄露(Memory Leak)內存溢出(Out Of Memory) 兩個概念常常會一塊兒被說起,二者有相互關聯的地方,但實質上仍是有着很大的區別:異步

  • 內存泄露。內存泄漏屬於代碼錯誤,這種錯誤會致使應用程序長久保留那些再也不被須要的對象的引用,從而致使分配給該對象的內存沒法被回收,形成程序的可用內存逐步下降,嚴重時甚至會致使 OOM。例如,當 Activity 的 onDestroy() 方法被調用後,正常來講 Activity 自己以及它涉及到的 View、Bitmap 等對象都應該被回收。但若是有一個後臺線程持續持有對這個 Activity 的引用的話,那麼 Activity 佔據的內存就沒法被回收,嚴重時將致使 OOM,最終 Crash
  • 內存溢出。指一個應用在申請內存時,系統沒有足夠的內存空間能夠供其使用

二者都會致使應用運行出現問題、性能降低或崩潰。不一樣點主要在於:async

  • 內存泄露是致使內存溢出的緣由之一,內存泄露嚴重時將致使內存溢出
  • 內存泄露是因爲代碼缺陷引發的,能夠經過完善代碼來避免;內存溢出能夠經過調整配置來減小發生頻率,但沒法完全避免

對於一個存在內存泄露的程序來講,即便每次僅會泄露少許內存,程序的可用內存也是會逐步下降,在長期運行事後,程序也是隱藏着崩潰的危險ide

2、內存管理

爲了判斷程序是否存在內存泄露的狀況,咱們首先必須先了解 Java 是如何管理內存的,Java 的內存管理就是對象的分配和釋放過程oop

在 Java 中,咱們都是經過關鍵字 new 來申請內存空間並建立對象的(基本類型除外),全部的對象都在堆 (Heap)中分配空間。總的來講,Java 的內存區域能夠分爲三類:源碼分析

  1. 靜態存儲區:在程序整個運行期間都存在,編譯時就分配好空間,主要用於存放靜態數據和常量
  2. 棧區:當一個方法被執行時會在棧區內存中建立方法體內部的局部變量,方法結束後自動釋放內存
  3. 堆區:一般存放 new 出來的對象

對象的釋放則由 GC 來完成。GC 負責監控每個對象的運行狀態,包括對象的申請、引用、被引用、賦值等行爲。當某個對象被 GC 判斷爲再也不被引用了時,GC 就會回收並釋放該對象對應的內存空間

一個對象的引用方式能夠分爲四種:

  1. 強引用(StrongReference):JVM 寧肯拋出 OOM 也不會讓 GC 回收具備強引用的對象
  2. 軟引用(SoftReference):若是一個對象只具備軟引用,那麼在內存空間不足時就會回收該對象
  3. 弱引用(WeakReference):若是一個對象只具備弱引用,那麼在 GC 時無論當前內存空間是否足夠,都會回收該對象
  4. 虛引用(PhantomReference):任什麼時候候均可以被 GC 回收,當垃圾回收器準備回收一個對象時,若是發現它還有虛引用,就會在回收對象的內存以前,把這個虛引用加入到與之關聯的引用隊列中。程序能夠經過判斷引用隊列中是否存在該對象的虛引用,來了解這個對象是否將要被回收

而一個對象再也不被引用的標記就是其再也不被強引用,JVM 會經過引用計數法或者是可達性分析等方法來判斷一個對象是否還被強引用着

在 Java 中,內存泄露的就意味着發生了這麼一種狀況:一個對象是可達的,存在其它對象強引用着該對象,但該對象是無用的,程序之後不會再使用這些對象。知足這種狀況的對象就意味着該對象已經泄露,該對象不會被 GC 所回收(由於該對象可達,還未達到 GC 的標準),然而卻一直持續佔用着內存。例如,因爲非靜態內部類會持有對外部類的隱式引用,因此當非靜態內部類在被回收以前,外部類也沒法被回收

3、常見的內存泄露

如下列舉九種常見的內存泄露場景及相應的解決方案,內容來自於國外的一篇文章:9 ways to avoid memory leaks in Android

一、Broadcast Receivers

若是在 Activity 中註冊了 BroadcastReceiver 而忘記了 unregister 的話,BroadcastReceiver 就將一直持有對 Activity 的引用,即便 Activity 已經執行了 onDestroy

public class BroadcastReceiverLeakActivity extends AppCompatActivity {

    private BroadcastReceiver broadcastReceiver;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_first);
    }

    private void registerBroadCastReceiver() {
        broadcastReceiver = new BroadcastReceiver() {
            @Override
            public void onReceive(Context context, Intent intent) {
                //your receiver code goes here!
            }
        };
        registerReceiver(broadcastReceiver, new IntentFilter("SmsMessage.intent.MAIN"));
    }
    
    
    @Override
    protected void onStart() {
        super.onStart();
        registerBroadCastReceiver();
    }    


    @Override
    protected void onStop() {
        super.onStop();

        /* * Uncomment this line in order to avoid memory leak. * You need to unregister the broadcast receiver since the broadcast receiver keeps a reference of the activity. * Now when its time for your Activity to die, the Android framework will call onDestroy() on it * but the garbage collector will not be able to remove the instance from memory because the broadcastReceiver * is still holding a strong reference to it. * */

        if(broadcastReceiver != null) {
            unregisterReceiver(broadcastReceiver);
        }
    }
}
複製代碼

開發者必須謹記在 Activity.onStop() 的時候調用 unregisterReceiver。但須要注意的是,若是 BroadcastReceiver 是在 onCreate() 中進行註冊的,那麼當應用進入後臺並再次切換回來時,BroadcastReceiver 將不會被再次註冊。因此,最好在 Activity 的 onStart() 或者 onResume() 方法中進行註冊,而後在 onStop() 時進行註銷

二、Static Activity or View Reference

看下面的示例代碼,將 TextView 聲明爲了靜態變量(不管出於什麼緣由)。無論是直接仍是間接經過靜態變量引用了 Activity 或者 View,在 Activity 被銷燬後都沒法對其進行垃圾回收

public class StaticReferenceLeakActivity extends AppCompatActivity {

    /* * This is a bad idea! */
    private static TextView textView;
    private static Activity activity;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_first);
        
        
        textView = findViewById(R.id.activity_text);
        textView.setText("Bad Idea!");
           
        activity = this;
    }
}
複製代碼

永遠不要經過靜態變量來引用 Activity、View 和 Context

三、Singleton Class Reference

看下面的例子,定義了一個 Singleton 類,該類須要傳遞 Context 以便從本地存儲中獲取一些文件

public class SingletonLeakExampleActivity extends AppCompatActivity {

    private SingletonSampleClass singletonSampleClass;
    
    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        
    /* * Option 1: Do not pass activity context to the Singleton class. Instead pass application Context */      
        singletonSampleClass = SingletonSampleClass.getInstance(this);
    }
  
    
   @Override
   protected void onDestroy() {
        super.onDestroy();

    /* * Option 2: Unregister the singleton class here i.e. if you pass activity context to the Singleton class, * then ensure that when the activity is destroyed, the context in the singleton class is set to null. */
     singletonSampleClass.onDestroy();
   }
}
複製代碼
public class SingletonSampleClass {
  
    private Context context;
    private static SingletonSampleClass instance;
  
    private SingletonSampleClass(Context context) {
        this.context = context;
    }

    public synchronized static SingletonSampleClass getInstance(Context context) {
        if (instance == null) instance = new SingletonSampleClass(context);
        return instance;
    }
  
    public void onDestroy() {
       if(context != null) {
          context = null; 
       }
    }
}
複製代碼

此時若是沒有主動將 SingletonSampleClass 包含的 context 置空的話,就將致使內存泄露。那如何解決這個問題?

  • 能夠傳遞 ApplicationContext,而不是將 ActivityContext 傳遞給 singleton 類
  • 若是真的必須使用 ActivityContext,那麼當 Activity 被銷燬的時候,須要確保傳遞將 singleton 類的 Context 設置爲 null

四、Inner Class Reference

看下面的例子,定義了一個 LeakyClass 類,你須要傳遞 Activity 才能重定向到新的 Activity

public class InnerClassReferenceLeakActivity extends AppCompatActivity {

  /* * Mistake Number 1: * Never create a static variable of an inner class * Fix I: * private LeakyClass leakyClass; */
  private static LeakyClass leakyClass;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_first);
        
        new LeakyClass(this).redirectToSecondScreen();

        /* * Inner class is defined here * */
         leakyClass = new LeakyClass(this);
         leakyClass.redirectToSecondScreen();
    }
    
  /* * Mistake Number 2: * 1. Never create a inner variable of an inner class * 2. Never pass an instance of the activity to the inner class */       
    private class LeakyClass {
        
        private Activity activity;
        public LeakyClass(Activity activity) {
            this.activity = activity;
        }
        
        public void redirectToSecondScreen() {
            this.activity.startActivity(new Intent(activity, SecondActivity.class));
        }
    }
}
複製代碼

如何解決這個問題?

  • 就如以前所述,不要建立內部類的靜態變量
  • LeakyClass 設置爲靜態類,靜態內部類不會持有對其外部類的隱式引用
  • 對任何 View/Activity 都使用 weakReference。若是隻有弱引用指向某個對象,那麼垃圾回收器就能夠回收該對象
public class InnerClassReferenceLeakActivity extends AppCompatActivity {

  /* * Mistake Number 1: * Never create a static variable of an inner class * Fix I: */
  private LeakyClass leakyClass;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_first);
        
        new LeakyClass(this).redirectToSecondScreen();

        /* * Inner class is defined here * */
         leakyClass = new LeakyClass(this);
         leakyClass.redirectToSecondScreen();
    }
  
  
    /* * How to fix the above class: * Fix memory leaks: * Option 1: The class should be set to static * Explanation: Instances of anonymous classes do not hold an implicit reference to their outer class * when they are "static". * * Option 2: Use a weakReference of the textview or any view/activity for that matter * Explanation: Weak References: Garbage collector can collect an object if only weak references * are pointing towards it. * */
    private static class LeakyClass {
        
        private final WeakReference<Activity> messageViewReference;
        public LeakyClass(Activity activity) {
            this.activity = new WeakReference<>(activity);
        }
        
        public void redirectToSecondScreen() {
            Activity activity = messageViewReference.get();
            if(activity != null) {
               activity.startActivity(new Intent(activity, SecondActivity.class));
            }
        }
    }  
}
複製代碼

五、Anonymous Class Reference

匿名內存類帶來的內存泄漏問題和上一節內容相同,解決辦法以下所示:

public class AnonymousClassReferenceLeakActivity extends AppCompatActivity {

    private TextView textView;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_first);


        textView = findViewById(R.id.activity_text);
        textView.setText(getString(R.string.text_inner_class_1));
        findViewById(R.id.activity_dialog_btn).setVisibility(View.INVISIBLE);

        /* * Runnable class is defined here * */
         new Thread(new LeakyRunnable(textView)).start();
    }



    private static class LeakyRunnable implements Runnable {

        private final WeakReference<TextView> messageViewReference;
        private LeakyRunnable(TextView textView) {
            this.messageViewReference = new WeakReference<>(textView);
        }

        @Override
        public void run() {
            try {
                Thread.sleep(5000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            TextView textView = messageViewReference.get();
            if(textView != null) {
                textView.setText("Runnable class has completed its work");
            }
        }
    }
}
複製代碼

六、AsyncTask Reference

看下面的示例,經過 AsyncTask 來獲取一個字符串值,該值用於在 onPostExecute() 方法中更新 textView

public class AsyncTaskReferenceLeakActivity extends AppCompatActivity {

    private TextView textView;
    private BackgroundTask backgroundTask;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_first);

        /* * Executing AsyncTask here! * */
        backgroundTask = new BackgroundTask(textView);
        backgroundTask.execute();
    }

    /* * Couple of things we should NEVER do here: * Mistake number 1. NEVER reference a class inside the activity. If we definitely need to, we should set the class as static as static inner classes don’t hold * any implicit reference to its parent activity class * Mistake number 2. We should always cancel the asyncTask when activity is destroyed. This is because the asyncTask will still be executing even if the activity * is destroyed. * Mistake number 3. Never use a direct reference of a view from acitivty inside an asynctask. * */
 private class BackgroundTask extends AsyncTask<Void, Void, String> {    
        @Override
        protected String doInBackground(Void... voids) {

            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            return "The task is completed!";
        }

        @Override
        protected void onPostExecute(String s) {
            super.onPostExecute(s);
            textView.setText(s);
        }
    }
}
複製代碼

如何解決這個問題?

  • 當 Activity 被銷燬時,咱們應該取消異步任務,這是由於即便 Activity 已經走向 Destoryed,未結束的 AsyncTask 仍將繼續執行
  • 永遠不要在 Activity 中引用內部類。若是確實須要,咱們應該將其設置爲靜態類,由於靜態內部類不會包含對其外部類的任何隱式引用
  • 經過 weakReference 來引用 textview
public class AsyncTaskReferenceLeakActivity extends AppCompatActivity {

    private TextView textView;
    private BackgroundTask backgroundTask;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_first);

        /* * Executing AsyncTask here! * */
        backgroundTask = new BackgroundTask(textView);
        backgroundTask.execute();
    }


    /* * Fix number 1 * */
    private static class BackgroundTask extends AsyncTask<Void, Void, String> {

        private final WeakReference<TextView> messageViewReference;
        private BackgroundTask(TextView textView) {
            this.messageViewReference = new WeakReference<>(textView);
        }


        @Override
        protected String doInBackground(Void... voids) {

            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            return "The task is completed!";
        }

        @Override
        protected void onPostExecute(String s) {
            super.onPostExecute(s);
          /* * Fix number 3 * */          
            TextView textView = messageViewReference.get();
            if(textView != null) {
                textView.setText(s);
            }
        }
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();

        /* * Fix number 2 * */
        if(backgroundTask != null) {
            backgroundTask.cancel(true);
        }
    }
}
複製代碼

七、Handler Reference

看下面的例子,經過 Handler 在五秒後更新 UI

public class HandlersReferenceLeakActivity extends AppCompatActivity {

    private TextView textView;

    /* * Mistake Number 1 * */
     private Handler leakyHandler = new Handler();


    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_first);

        /* * Mistake Number 2 * */
        leakyHandler.postDelayed(new Runnable() {
            @Override
            public void run() {
                textView.setText(getString(R.string.text_handler_1));
            }
        }, 5000);
    }
複製代碼

如何解決這個問題?

  • 永遠不要在 Activity 中引用內部類。若是確實須要,咱們應該將其設置爲靜態類。這是由於當 Handler 在主線程上實例化後,它將與 Looper 的 MessageQueue 相關聯,發送到 MessageQueue 的 Message 將持有對 Handler 的引用,以便當 Looper 最終處理消息時,framework 能夠調用 Handler#handleMessage(message) 方法
  • 經過 weakReference 來引用 Activity
public class HandlersReferenceLeakActivity extends AppCompatActivity {

    private TextView textView;

    /* * Fix number I * */
    private final LeakyHandler leakyHandler = new LeakyHandler(this);

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_first);

        leakyHandler.postDelayed(leakyRunnable, 5000);
    }

    /* * Fix number II - define as static * */
    private static class LeakyHandler extends Handler {
      
    /* * Fix number III - Use WeakReferences * */      
        private WeakReference<HandlersReferenceLeakActivity> weakReference;
        public LeakyHandler(HandlersReferenceLeakActivity activity) {
            weakReference = new WeakReference<>(activity);
        }

        @Override
        public void handleMessage(Message msg) {
            HandlersReferenceLeakActivity activity = weakReference.get();
            if (activity != null) {
                activity.textView.setText(activity.getString(R.string.text_handler_2));
            }
        }
    }

    private static final Runnable leakyRunnable = new Runnable() {
        @Override
        public void run() { /* ... */ }
    }
複製代碼

八、Threads Reference

Thread 和 TimerTask 也可能會致使內存泄露問題

public class ThreadReferenceLeakActivity extends AppCompatActivity {

    /* * Mistake Number 1: Do not use static variables * */    
    private static LeakyThread thread;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_first);

        createThread();
        redirectToNewScreen();
    }


    private void createThread() {
        thread = new LeakyThread();
        thread.start();
    }

    private void redirectToNewScreen() {
        startActivity(new Intent(this, SecondActivity.class));
    }


    /* * Mistake Number 2: Non-static anonymous classes hold an * implicit reference to their enclosing class. * */
    private class LeakyThread extends Thread {
        @Override
        public void run() {
            while (true) {
            }
        }
    }
複製代碼

如何解決這個問題?

  • 非靜態匿名類會包含對其外部類的隱式引用,將 LeakyThread 改成靜態內部類
  • 在 Activity 的 onDestroy() 方法中中止線程,以免線程泄漏
public class ThreadReferenceLeakActivity extends AppCompatActivity {

    /* * FIX I: make variable non static * */
    private LeakyThread leakyThread = new LeakyThread();

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_first);

        createThread();
        redirectToNewScreen();
    }


    private void createThread() {
        leakyThread.start();
    }

    private void redirectToNewScreen() {
        startActivity(new Intent(this, SecondActivity.class));
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        // FIX II: kill the thread
        leakyThread.interrupt();
    }


    /* * Fix III: Make thread static * */
    private static class LeakyThread extends Thread {
        @Override
        public void run() {
            while (!isInterrupted()) {
            }
        }
    }
}
複製代碼

九、TimerTask Reference

對於 TimerTask 也能夠遵循相同的原則,修復內存泄漏的示例以下所示:

public class TimerTaskReferenceLeakActivity extends Activity {

    private CountDownTimer countDownTimer;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_first);

        startTimer();
    }

    /* * Mistake 1: Cancel Timer is never called * even though activity might be completed * */
    public void cancelTimer() {
        if(countDownTimer != null) countDownTimer.cancel();
    }

    
    private void startTimer() {
        countDownTimer = new CountDownTimer(1000, 1000) {
            @Override
            public void onTick(long millisUntilFinished) {
                final int secondsRemaining = (int) (millisUntilFinished / 1000);
                //update UI
            }

            @Override
            public void onFinish() {
                //handle onFinish
            }
        };
        countDownTimer.start();
    }
}
複製代碼

如何解決這個問題?

  • 在 Activity 的 onDestroy() 方法中中止計時器,以免內存泄漏
public class TimerTaskReferenceLeakActivity extends Activity {

    private CountDownTimer countDownTimer;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_first);

        startTimer();
    }


    public void cancelTimer() {
        if(countDownTimer != null) countDownTimer.cancel();
    }


    private void startTimer() {
        countDownTimer = new CountDownTimer(1000, 1000) {
            @Override
            public void onTick(long millisUntilFinished) {
                final int secondsRemaining = (int) (millisUntilFinished / 1000);
                //update UI
            }

            @Override
            public void onFinish() {
                //handle onFinish
            }
        };
        countDownTimer.start();
    }
  
  
    /* * Fix 1: Cancel Timer when * activity might be completed * */  
   @Override
    protected void onDestroy() {
        super.onDestroy();
        cancelTimer();
    }
}
複製代碼

十、總結

最後再來簡單總結一下:

  1. 儘量使用 ApplicationContext 而不是 ActivityContext。若是真的必須使用 ActivityContext,那麼當 Activity 被銷燬時,請確保將傳遞的 Context 置爲 null
  2. 不要經過靜態變量來引用 View 和 Activity
  3. 不要在 Activity 中引用內部類,若是確實須要,那麼應該將它聲明爲靜態的,無論它是 Thread、Handler、Timer 仍是 AsyncTask
  4. 務必記住註銷 Activity 中的 BroadcastReceiver 和 Timer,在 onDestroy() 方法中取消任何異步任務和線程
  5. 經過 weakReference 來持有對 Activity 和 View 的引用
相關文章
相關標籤/搜索