從JVM GC角度分析AsyncTask內存泄露

本文在簡書同步更新:https://www.jianshu.com/p/6887b3b7c5c7
轉載請註明出處

前言

對於AsynTask大家也不陌生,是Android爲我們提供的執行異步任務的類。
常用的操作就是後臺運行線程,然後切換到主線程去更新UI。但是,在使用的過程中我們常常會發現AsynTask或多或少會出現內存泄露問題。

一、匿名內部類持有外部類的引用

網上大部分都提到造成AsynTask內存泄露的原因是因爲匿名內部類持有外部類的引用造成的,當Activity被關閉退到後臺時,由於AsynTask還持有Activity的引用導致Activity不能正常回收。其實這個說法並不完全正確,爲什麼呢。

在這裏要糾正一個誤區:匿名內部類持有外部類的引用導致內存泄露
代碼演示:

public class DemoActivity extends AppCompatActivity {
    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        new EggHurt().innerPrint();
    }

    public void outPrint(){
        System.out.println("DemoActivity print");
    }

    class EggHurt {
        public void innerPrint() {
            Field[] fields = this.getClass().getDeclaredFields();
            for (int i = 0;i<fields.length;i++){
                System.out.println(fields[i]);
            }
            outPrint();
        }
    }

}

打印結果:


final com.example.administrator.myapplication.DemoActivity(手動換行 - -)
com.example.administrator.myapplication.DemoActivity$EggHurt.this$0
DemoActivity print

getClass().getDeclaredFields()獲取的是該類的所有字段或者變量,因爲EggHurt並沒有定義任何字段,所以輸出的是兩個變量,一個是外部類DemoActivity,另外一個是本身DemoActivity$EggHurt.this$0,這兩個變量是內部幫我們自動定義好的。
DemoActivity print 打印出來,代表內部類持有外部類的引用,可以直接訪問到外部類非private的成員方法。

通過ActivityManager$getRunningTasks方法可知,當DemoActivity關閉時,內存也就釋放了。所以不單指AsynTask,任何的內部類都會持有外部類的引用的,所以匿名內部類持有外部類的引用不是導致內存泄露的根本原因。

二、內存泄露原因

在Android中內存泄露是指本來要被回收的資源卻沒有被回收,造成內存資源的浪費。

AsynTask內存泄露原因:
在Activity裏面AsynTask中執行耗時操作,這個操作並沒有因Activity的關閉而停止,當AsynTask持有外部Activity的引用時,AsynTask又沒有及時的停止就會導致該Activity不能被回收。

爲什麼會這樣,這就要理解Java的內存回收機制,這裏簡單說一下。
判斷一個對象是否能夠被回收,要看這個對象是否可以通過直接或者間接的引用到達GC Root,如果在這條引用鏈上面沒有GC Root,就代表這個對象不再被使用,等待回收。我從網上找了一張圖,有圖理解比較清晰:

GC會收集那些不是GC Root且沒有被GC Root引用的對象。

GC root,是叫做Garbage Collector root,指的是垃圾回收器的根對象,這種對象——GC Roots不止一個,可以這樣理解GC root機制:

  1.通過創建一組 GCRoot 指針來管理當前被引用的對象,被外界引用的對象 A 就掛在 GCRoot 指針上,如果 A 的屬性中引用了對象 B,就將 B 掛在 A 的後面,以此類推,形成樹形結構,我們叫做 GCRoot 樹(以 GCRoot 爲樹根的樹結構)。
  2.如果一個被外部引用的對象跟其他任何一個 GCRoot 樹中的節點都沒關係,就創建一個新的 GCRoot 指針,組成一棵新的樹。
  3.如果外界引用減少了一個,就從對應的 GCRoot 樹中撤去一條樹枝。
  4.當一棵樹的樹根不是 GCRoot 對象的時候,那麼就不存在外界的任何一個引用,因此該樹上的所有對象都爲死亡狀態。

可以作爲GC Root的對象:

1.Class - 由系統類加載器(system class loader)加載的對象,這些類是不能夠被回收的,他們可以以靜態字段的方式保存持有其它對象。我們需要注意的一點就是,通過用戶自定義的類加載器加載的類,除非相應的 實例以其它的某種(或多種)方式成爲roots,否則它們並不是roots

2.Thread - 活着的線程

3.Stack Local - Java方法的local變量或參數

4.JNI Local - JNI方法的local變量或參數

5.JNI Global - 全局JNI引用

6.Monitor Used - 用於同步的監控對象

7.Held by JVM - 用於JVM特殊目的由GC保留的對象,但實際上這個與JVM的實現是有關的。可能已知的一些類型是:系統類加載器、一些JVM知道的重要的異常類、一些用於處理異常的預分配對象以及一些自定義的類加載器等。然而,JVM並沒有爲這些對象提供其它的信息,因此就只有留給分析分員去確定哪些是屬於”JVM持有”的了。

根據上面第二點可知,AsyncTask裏面如果有耗時的線程,這個線程就是GC Root,如果這個線程不被停止,那麼AsyncTask所持有的Activit就算已經關閉,頁面變得不可見,但還是存在於內存中的。

三、AsyncTask題外話

網上有關於很多防止AsyncTask內存泄露方法,這裏不一一多說。

這裏要說的是關閉AsyncTask,AsyncTask爲我們提供了一個cancel方法,但是我們調用之後發覺並沒有起作用,AsyncTask還是在運行,似乎這個cancel方法是Android拿出來糊弄我們的,其實並不是,這跟Android的設計有關。AsyncTask和Thread一樣,並不能被interrupt或者cancel掉。

Thread調用了interrupt後,還需要在裏面添加isInterrupted()判斷才能退出:

class MyThread extends Thread{
        @Override
        public void run() {

            while(!isInterrupted()){
                //代碼...
            }
        }
    }

同理AsyncTask調用cancel後,doInBackground也要有isCancelled()判斷:

public class MyTask extends AsyncTask<Integer, Integer, Integer> {

        @Override
        protected void onPreExecute() {
            super.onPreExecute();
        }

        @Override
        protected Integer doInBackground(Integer... params) {

            while (isCancelled()){
                //代碼...
            }
            return 0;
        }
    }

當AsyncTask裏面沒有任務在執行,GC會在合適的時候就會把AsyncTask和AsyncTask所引用的Activity回收。

四、總結

曾經風光無限的AsyncTask現在已經被很多更好的框架所替代,緬懷一下,同時加深理解匿名內部類,內存回收機制GC Root的原理。

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