做 Android 開發(fā)最常遇到的問題就是在 Activity 的生命周期中協(xié)調(diào)耗時任務哨啃,避免執(zhí)行任務導致不易察覺的內(nèi)存泄漏。不妨先讀一讀下面的代碼熟掂,代碼寫了一個簡單的 Activity嗜侮,Activity 在啟動后就會開啟一個線程稽犁,并循環(huán)執(zhí)行該線程中的任務:
/**
* 示例向我們展示了在 Activity 的配置改變時(配置改變會導致其下的 Activity 實例被銷
* 毀)存活沮翔。此外陨帆,Activity 的 context 也是內(nèi)存泄漏的一部分,因為每一個線程都被初始
* 化為匿名內(nèi)部類采蚀,使得每一個線程都持有一個外部 Activity 實例的隱式引用疲牵,使得
* Activity 不會被 Java 的垃圾回收機制回收。
*/
public class MainActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
exampleOne();
}
private void exampleOne() {
new Thread() {
@Override
public void run() {
while (true) {
SystemClock.sleep(1000);
}
}
}.start();
}
}
Activity 配置發(fā)生改變會使 Activity 被銷毀榆鼠,并新建一個 Activity瑰步,我們總會覺得 Android 系統(tǒng)會將與被銷毀的 Activity 相關的一切清理干凈,例如回收與 Activity 關聯(lián)的內(nèi)存璧眠,Activity 執(zhí)行的線程等等……然而,現(xiàn)實總是很殘酷的,剛剛提到的這些東西都不會被回收责静,并導致內(nèi)存泄漏袁滥,從而顯著地影響應用的性能表現(xiàn)。
Activity 內(nèi)存泄漏的根源
在 Java 中灾螃,非靜態(tài)匿名內(nèi)部類會持有其外部類的隱式引用题翻,如果你沒有考慮過這一點,那么存儲該引用會導致 Activity 被保留腰鬼,而不是被垃圾回收機制回收嵌赠。Activity 對象持有其 View 層以及相關聯(lián)的所有資源文件的引用,換句話說熄赡,如果你的內(nèi)存泄漏發(fā)生在 Activity 中姜挺,那么你將損失大量的內(nèi)存空間。
而這樣的問題在 Activity 配置改變時會更加嚴重彼硫,因為 Activity 的配置改變表示 Android 系統(tǒng)將要銷毀當前 Activity 并新建一個 Activity炊豪。舉例來說吧,在使用應用的時候拧篮,你執(zhí)行了10次橫屏/豎屏操作词渤,每一次方向的改變都會執(zhí)行下面的代碼,那么我們會發(fā)現(xiàn)(使用 Eclipse 的內(nèi)存分析工具可以看到)每一個 Activity 對象都會因為留有一個隱式引用而被保留在內(nèi)存中串绩。
每一次配置的改變都會使 Android 系統(tǒng)新建一個 Activity 并把改變前的 Activity 交給垃圾回收機制回收缺虐。但因為線程持有舊 Activity 的隱式引用,使該 Activity 沒有被垃圾回收機制回收礁凡。這樣的問題會導致每一個新建的 Activity 都將發(fā)生內(nèi)存泄漏高氮,與 Activity 相關的所有資源文件也不會被回收,其中的內(nèi)存泄漏有多嚴重可想而知把篓。
看到這里可能你會很害怕纫溃,很惶恐,很無助韧掩,那我們該怎么辦……莫慌紊浩,解決辦法非常簡單,既然我們已經(jīng)確定了問題的根源疗锐,那么對癥下藥就可以了:我們把該線程類聲明為私有的靜態(tài)內(nèi)部類就可以解決這個問題:
/**
* 示例通過將線程類聲明為私有的靜態(tài)內(nèi)部類避免了 Activity context 的內(nèi)存泄漏問題坊谁,但
* 在配置發(fā)生改變后,線程仍然會執(zhí)行滑臊。原因在于口芍,DVM 虛擬機持有所有運行線程的引用,無論
* 這些線程是否被回收雇卷,都與 Activity 的生命周期無關鬓椭。運行中的線程只會繼續(xù)運行颠猴,直到
* Android 系統(tǒng)將整個應用進程殺死
*/
public class MainActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
exampleTwo();
}
private void exampleTwo() {
new MyThread().start();
}
private static class MyThread extends Thread {
@Override
public void run() {
while (true) {
SystemClock.sleep(1000);
}
}
}
}
通過上面的代碼,新線程再也不會持有一個外部 Activity 的隱式引用小染,而且該 Activity 也會在配置改變后被回收翘瓮。
線程內(nèi)存泄漏的根源
第二個問題是:對于每個新建 Activity,如果 Activity 中的線程發(fā)生發(fā)生內(nèi)存泄漏。在Java中線程是垃圾回收機制的根源裤翩,也就是說资盅,在運行系統(tǒng)中DVM虛擬機總會使硬件持有所有運行狀態(tài)的進程的引用,結(jié)果導致處于運行狀態(tài)的線程將永遠不會被回收踊赠。因此呵扛,你必須為你的后臺線程實現(xiàn)銷毀邏輯!下面是一種解決辦法:
/**
* 除了我們需要實現(xiàn)銷毀邏輯以保證線程不會發(fā)生內(nèi)存泄漏筐带,其他代碼和示例2相同今穿。在退出當前
* Activity 前使用 onDestroy() 方法結(jié)束你的運行中線程是個不錯的選擇
*/
public class MainActivity extends Activity {
private MyThread mThread;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
exampleThree();
}
private void exampleThree() {
mThread = new MyThread();
mThread.start();
}
/**
* 私有的靜態(tài)內(nèi)部類不會持有其外部類的引用,使得 Activity 實例不會在配置改變時發(fā)生內(nèi)
* 存泄漏
*/
private static class MyThread extends Thread {
private boolean mRunning = false;
@Override
public void run() {
mRunning = true;
while (mRunning) {
SystemClock.sleep(1000);
}
}
public void close() {
mRunning = false;
}
}
@Override
protected void onDestroy() {
super.onDestroy();
mThread.close();
}
}
通過上面的代碼烫堤,我們在 onDestroy() 方法中結(jié)束了線程荣赶,確保不會發(fā)生意外的線程的內(nèi)存泄漏問題。如果你想要在配置改變后保留該線程(而不是每一次在關閉 Activity 后都要新建一個線程)鸽斟,那我建議你使用 Fragment 去完成該耗時任務拔创。你可以翻我以前的博文,一名叫作“Handling Configuration Changes with Fragments”應該能滿足你的需求富蓄,在API demo中也提供了很好理解的例子來為你闡述相關概念剩燥。
結(jié)論
Android 開發(fā)過程中,在 Activity 的生命周期里協(xié)調(diào)耗時任務可能會很困難立倍,你一不小心就會導致內(nèi)存泄漏問題灭红。下面是一些小提示,能幫助你預防內(nèi)存泄漏問題的發(fā)生:
- 盡可能使用靜態(tài)內(nèi)部類而不是非靜態(tài)內(nèi)部類口注。每一個非靜態(tài)內(nèi)部類實例都會持有一個外部類的引用变擒,若該引用是 Activity 的引用,那么該 Activity 在被銷毀時將無法被回收寝志。如果你的靜態(tài)內(nèi)部類需要一個相關 Activity 的引用以確保功能能夠正常運行娇斑,那么你得確保你在對象中使用的是一個 Activity 的弱引用,否則你的 Activity 將會發(fā)生意外的內(nèi)存泄漏材部。
- 不要總想著 Java 的垃圾回收機制會幫你解決所有內(nèi)存回收問題毫缆。就像上面的示例,我們以為垃圾回收機制會幫我們將不需要使用的內(nèi)存回收乐导,例如:我們需要結(jié)束一個 Activity苦丁,那么它的實例和相關的線程都該被回收。但現(xiàn)實并不會像我們劇本那樣走物臂。Java 線程會一直存活旺拉,直到他們都被顯式關閉产上,抑或是其進程被 Android 系統(tǒng)殺死。所以蛾狗,為你的后臺線程實現(xiàn)銷毀邏輯是你在使用線程時必須時刻銘記的細節(jié)蒂秘,此外,你在設計銷毀邏輯時要根據(jù) Activity 的生命周期去設計淘太,避免出現(xiàn) Bug。
- 考慮你是否真的需要使用線程规丽。Android 應用的框架層為我們提供了很多便于開發(fā)者執(zhí)行后臺操作的類蒲牧。例如:我們可以使用 Loader 代替在 Activity 的生命周期中用線程通過注入執(zhí)行短暫的異步后臺查詢操作,考慮用 Service 將結(jié)構(gòu)通知給 UI 的 BroadcastReceiver赌莺。最后冰抢,記住,這篇博文中對線程進行的討論同樣適用于 AsyncTask(因為 AsyncTask 使用 ExecutorService 執(zhí)行它的任務)艘狭。然而挎扰,雖說 ExecutorService 只能在短暫操作(文檔說最多幾秒)中被使用,那么這些方法導致的 Activity 內(nèi)存泄漏應該永遠不會發(fā)生巢音。
這篇博文的源碼可以在GitHub 中下載遵倦。