編輯:Android資訊
Android編程中一個共同的困難就是協調Activity的生命周期和長時間運行的任務(task),並且要避免可能的內存洩露。思考下面Activity的代碼,在它啟動的時候開啟一個線程並循環執行任務。
/** * 一個展示線程如何在配置變化中存活下來的例子(配置變化會導致創 * 建線程的Activity被銷毀)。代碼中的Activity洩露了,因為線程被實 * 例為一個匿名類實例,它隱式地持有外部Activity實例,因此阻止Activity * 被回收。 */ 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被銷毀並重新創建,很容易假定Android將會為我們清理和回收跟Activity相關的內存及它運行中的線程。然而,這並非如此。這兩者都會導致內存洩露而且不會被回收, 後果是性能可能顯著地下降。
如果你讀過我前一篇關於Handler和內部類的文章,那麼第一種內存洩露應該很容易理解。在Java中,非靜態匿名類隱式地持有他們的外部類的引用。如果你不小心,保存這個引用可能導致Activity在可以被GC回收的時候被保存下來。Activity持有一個指向它們整個View繼承樹和它所持有的所有資源的引用,所以如果你洩露了一個,很多內存都會連帶著被洩露。
配置發生變化只加劇了這個問題,它發出一個信號讓Activity銷毀並重新創建。比如,基於上面的代碼進行10次橫豎屏變化後,我們可以看到(使用Eclipse Memory Analyzer)由於那些隱式的引用,每一個Activity對象其實都留存在內存中:
圖1.在10次配置發生變化後,存留在內存中的Activity實例
每一次配置發生變化後,Android系統都會創建一個新的Activity並讓舊的Activity可以被回收。然而,隱式持有舊Activity引用的線程,阻止他們被回收。所以每次洩露一個新的Activity,都會導致所有跟他們關聯的資源都沒有辦法被回收。
解決方法也很簡單,在我們確定了問題的根源,那麼只要將線程定義為private static內部類,如下所示:
/** * 這個例子通過將線程實例聲明為private static型的內部 類,從而避免導致Activity洩 * 露,但是這個線程依舊會跨越配置變化存活下來。DVM有一個指向所有運行中線程的 * 引用(無論這些線程是否 可以被垃圾回收),而線程能存活多長時間以及什麼時候可 * 以被回收跟Activity的生命周期沒有任何關系。 * 活動線程會一直運行下去,直到系統將你的應用程序銷毀。 */ 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在配置發生變化後都會變得可以被回收。
第二個問題是每當創建了一個新Activity,就會導致一個thread洩露並且不會被回收。在Java中,thread是GC Root也就是說在系統中的Dalvik Virtual Machine (DVM)保存對所有活動 中線程的強引用,這就導致了這些線程留存下來繼續運行並且不會達到可以被回收的條件。因此你必須要考慮怎樣停止後台線程。下面是一個例子:
/** * 跟例子2一樣,除了這次我們實現了取消線程的機制,從而保證它不會洩露。 * onDestroy()常常被用來在Activity推出前取消線程。 */ 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(); } /** * 靜態內部類不會隱式地持有他們外部類的引用,所以Activity實例不會在配置變化 * 中被洩露 */ 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()中關閉線程保證了線程不會意外洩露。如果你想要在配置變化的時候保存線程的狀態(而不是每次都要關閉並重新創建一個新的線程)。考慮使用可留存(在配置變化中不會被銷毀)、沒有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下載。
本文由碼農網 – 小峰原創,轉載請看清文末的轉載要求,歡迎參與我們的付費投稿計劃! Android開發是目前最熱門的移動開發技術之一,隨著開發者的不斷努力
1 android java.net.UnknownHostException: Unable to resolve host “…
1背景 前些日子需要在科室內做關於Android系統啟動流程的培訓。為此,我在幾年前的技術手記的基礎上,重新改了一份培訓文檔。在重新整理文檔期間,我也重讀了一下A
一 IntentService介紹 IntentService定義的三個基本點:是什麼?怎麼用?如何work? 官方解釋如下: //IntentService定義