Android編程中一個共同的困難就是協調Activity的生命周期和長時間運行的任務(task),並且要避免可能的內存泄露。思考下面Activity的代碼,在它啟動的時候開啟一個線程並循環執行任務。
1 /** 2 * 一個展示線程如何在配置變化中存活下來的例子(配置變化會導致創 3 * 建線程的Activity被銷毀)。代碼中的Activity泄露了,因為線程被實 4 * 例為一個匿名類實例,它隱式地持有外部Activity實例,因此阻止Activity 5 * 被回收。 6 */ 7 public class MainActivity extends Activity { 8 9 @Override 10 protected void onCreate(Bundle savedInstanceState) { 11 super.onCreate(savedInstanceState); 12 exampleOne(); 13 } 14 15 private void exampleOne() { 16 new Thread() { 17 @Override 18 public void run() { 19 while (true) { 20 SystemClock.sleep(1000); 21 } 22 } 23 }.start(); 24 } 25 }
當配置發生變化(如橫豎屏切換)時,會導致整個Activity被銷毀並重新創建,很容易假定Android將會為我們清理和回收跟Activity相關的內存及它運行中的線程。然而,這並非如此。這兩者都會導致內存泄露而且不會被回收, 后果是性能可能顯著地下降。
怎么樣讓一個Activity泄露
如果你讀過我前一篇關於Handler和內部類的文章,那么第一種內存泄露應該很容易理解。在Java中,非靜態匿名類隱式地持有他們的外部類的引用。如果你不小心,保存這個引用可能導致Activity在可以被GC回收的時候被保存下來。Activity持有一個指向它們整個View繼承樹和它所持有的所有資源的引用,所以如果你泄露了一個,很多內存都會連帶着被泄露。
配置發生變化只加劇了這個問題,它發出一個信號讓Activity銷毀並重新創建。比如,基於上面的代碼進行10次橫豎屏變化后,我們可以看到(使用Eclipse Memory Analyzer)由於那些隱式的引用,每一個Activity對象其實都留存在內存中:
圖1.在10次配置發生變化后,存留在內存中的Activity實例
每一次配置發生變化后,Android系統都會創建一個新的Activity並讓舊的Activity可以被回收。然而,隱式持有舊Activity引用的線程,阻止他們被回收。所以每次泄露一個新的Activity,都會導致所有跟他們關聯的資源都沒有辦法被回收。
解決方法也很簡單,在我們確定了問題的根源,那么只要將線程定義為private static內部類,如下所示:
1 /** 2 * 這個例子通過將線程實例聲明為private static型的內部 類,從而避免導致Activity泄 3 * 露,但是這個線程依舊會跨越配置變化存活下來。DVM有一個指向所有運行中線程的 4 * 引用(無論這些線程是否 可以被垃圾回收),而線程能存活多長時間以及什么時候可 5 * 以被回收跟Activity的生命周期沒有任何關系。 6 * 活動線程會一直運行下去,直到系統將你的應用程序銷毀。 7 */ 8 public class MainActivity extends Activity { 9 10 @Override 11 protected void onCreate(Bundle savedInstanceState) { 12 super.onCreate(savedInstanceState); 13 exampleTwo(); 14 } 15 16 private void exampleTwo() { 17 new MyThread().start(); 18 } 19 20 private static class MyThread extends Thread { 21 @Override 22 public void run() { 23 while (true) { 24 SystemClock.sleep(1000); 25 } 26 } 27 } 28 }
新的線程不會隱式地持有Activity的引用,並且Activity在配置發生變化后都會變得可以被回收。
怎么使一個Thread泄露
第二個問題是每當創建了一個新Activity,就會導致一個thread泄露並且不會被回收。在Java中,thread是GC Root也就是說在系統中的Dalvik Virtual Machine (DVM)保存對所有活動 中線程的強引用,這就導致了這些線程留存下來繼續運行並且不會達到可以被回收的條件。因此你必須要考慮怎樣停止后台線程。下面是一個例子:
1 /** 2 * 跟例子2一樣,除了這次我們實現了取消線程的機制,從而保證它不會泄露。 3 * onDestroy()常常被用來在Activity推出前取消線程。 4 */ 5 public class MainActivity extends Activity { 6 private MyThread mThread; 7 8 @Override 9 protected void onCreate(Bundle savedInstanceState) { 10 super.onCreate(savedInstanceState); 11 exampleThree(); 12 } 13 14 private void exampleThree() { 15 mThread = new MyThread(); 16 mThread.start(); 17 } 18 19 /** 20 * 靜態內部類不會隱式地持有他們外部類的引用,所以Activity實例不會在配置變化 21 * 中被泄露 22 */ 23 private static class MyThread extends Thread { 24 private boolean mRunning = false; 25 26 @Override 27 public void run() { 28 mRunning = true; 29 while (mRunning) { 30 SystemClock.sleep(1000); 31 } 32 } 33 34 public void close() { 35 mRunning = false; 36 } 37 } 38 39 @Override 40 protected void onDestroy() { 41 super.onDestroy(); 42 mThread.close(); 43 } 44 }
在上面的代碼中,我們在onDestroy()中關閉線程保證了線程不會意外泄露。如果你想要在配置變化的時候保存線程的狀態(而不是每次都要關閉並重新創建一個新的線程)。考慮使用可留存(在配置變化中不會被銷毀)、沒有UI的fragment來執行長時間任務。看看我的博客,叫做《用Fragment解決屏幕旋轉(狀態發生變化)狀態不能保持的問題》,里面有一個例子說明實現這點。API Demo中也一個全面的例子。
總結
在Android中處理Activity生命周期與長時間運行的任務的關系可能很困難並且可能導致內存泄露。下面有一些值得考慮的通用建議:
優先使用靜態內部類而不是非靜態的。非靜態內部類的每個實例都會有一個對它外部Activity實例的引用。當Activity可以被GC回收時,存儲在非靜態內部類中的外部Activity引用可能導致垃圾回收失敗。如果你的靜態內部類需要宿主Activity的引用來執行某些東西,你要將這個引用封裝在一個WeakReference
中,避免意外導致Activity泄露。
不要假定Java最后總會為你清理運行中的線程。在上面的例子中,很容易錯誤地認為用戶退出Activity后,Activity就會被回收,任何跟這個Activity關聯的線程也都將一並被回收。事實上不是這樣的。Java線程會繼續運行下去,直到他們被顯式地關閉或者整個process被Android系統殺掉。因此,一定要記得記得為后台線程實現對應的取消策略,並且在Activity生命周期事件發生的時候使用合理的措施。
考慮你是否真的應該使用線程。Android Framework提供了很多旨在為開發者簡化后台線程開發的類。比如,考慮使用Loader而不是線程當你需要配合Activity生命周期做一些短時間的異步后台任務查詢類任務。考慮使用使用Service,然后向使用BrocastReceiver向UI反饋進度、結果。最后,記住本篇文章中一切關於線程的討論也適用於AsyncTask(因為Asynctask類使用ExecutorService來執行它的任務)。然而,鑒於AsyncTask只應該用於短時間的操作(最多幾秒鍾,參照文檔),它倒不至於會導致像Activity或線程泄露那么大的問題。
這篇文章中的源代碼都可以從github下載。文章中的示例程序可以從Google play下載。