本文在簡書同步更新: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的原理。