https://github.com/bboyfeiyu/android-tech-frontier/tree/master/issue-7/%E5%9C%A8Activity%E4%B8%AD%E4%BD%BF%E7%94%A8Thread%E5%AF%BC%E8%87%B4%E7%9A%84%E5%86%85%E5%AD%98%E6%B3%84%E6%BC%8F
已測試
在Activity中使用Thread導致的內存泄漏
- 原文鏈接 : Activitys, Threads, & Memory Leaks
- 原文作者 : AlexLockwood
- 譯文出自 : 開發技術前線 www.devtf.cn
- 譯者 : chaossss
- 校對者: yinna317
- 狀態 : 完成
注:這篇博文涉及的源碼可以在 GitHub 上面下載哦
做 Android 開發最常遇到的問題就是在 Activity 的生命周期中協調耗時任務,避免執行任務導致不易察覺的內存泄漏。不妨先讀一讀下面的代碼,代碼寫了一個簡單的 Activity,Activity 在啟動后就會開啟一個線程,並循環執行該線程中的任務
/**
* 示例向我們展示了在 Activity 的配置改變時(配置改變會導致其下的 Activity 實例被銷 * 毀)存活。此外,Activity 的 context 也是內存泄漏的一部分,因為每一個線程都被初始 * 化為匿名內部類,使得每一個線程都持有一個外部 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 配置發生改變會使 Activity 被銷毀,並新建一個 Activity,我們總會覺得 Android 系統會將與被銷毀的 Activity 相關的一切清理干凈,例如回收與 Activity 關聯的內存,Activity 執行的線程等等……然而,現實總是很殘酷的,剛剛提到的這些東西都不會被回收,並導致內存泄漏,從而顯著地影響應用的性能表現。
Activity 內存泄漏的根源
如果你讀過我以前寫的一篇有關 Handler 和 內部類的博文,那我接下來要講的知識你肯定知道。在 Java 中,非靜態匿名內部類會持有其外部類的隱式引用,如果你沒有考慮過這一點,那么存儲該引用會導致 Activity 被保留,而不是被垃圾回收機制回收。Activity 對象持有其 View 層以及相關聯的所有資源文件的引用,換句話說,如果你的內存泄漏發生在 Activity 中,那么你將損失大量的內存空間。
而這樣的問題在 Activity 配置改變時會更加嚴重,因為 Activity 的配置改變表示 Android 系統將要銷毀當前 Activity 並新建一個 Activity。舉例來說吧,在使用應用的時候,你執行了10次橫屏/豎屏操作,每一次方向的改變都會執行下面的代碼,那么我們會發現(使用 Eclipse 的內存分析工具可以看到)每一個 Activity 對象都會因為留有一個隱式引用而被保留在內存中。
每一次配置的改變都會使 Android 系統新建一個 Activity 並把改變前的 Activity 交給垃圾回收機制回收。但因為線程持有舊 Activity 的隱式引用,使該 Activity 沒有被垃圾回收機制回收。這樣的問題會導致每一個新建的 Activity 都將發生內存泄漏,與 Activity 相關的所有資源文件也不會被回收,其中的內存泄漏有多嚴重可想而知。
看到這里可能你會很害怕,很惶恐,很無助,那我們該怎么辦……莫慌,解決辦法非常簡單,既然我們已經確定了問題的根源,那么對症下葯就可以了:我們把該線程類聲明為私有的靜態內部類就可以解決這個問題:
/**
* 示例通過將線程類聲明為私有的靜態內部類避免了 Activity context 的內存泄漏問題,但 * 在配置發生改變后,線程仍然會執行。原因在於,DVM 虛擬機持有所有運行線程的引用,無論 * 這些線程是否被回收,都與 Activity 的生命周期無關。運行中的線程只會繼續運行,直到 * Android 系統將整個應用進程殺死 */ 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,如果 Activity 中的線程發生發生內存泄漏。在Java中線程是垃圾回收機制的根源,也就是說,在運行系統中DVM虛擬機總會使硬件持有所有運行狀態的進程的引用,結果導致處於運行狀態的線程將永遠不會被回收。因此,你必須為你的后台線程實現銷毀邏輯!下面是一種解決辦法:
/**
* 除了我們需要實現銷毀邏輯以保證線程不會發生內存泄漏,其他代碼和示例2相同。在退出當前 * Activity 前使用 onDestroy() 方法結束你的運行中線程是個不錯的選擇 */ 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() 方法中結束了線程,確保不會發生意外的線程的內存泄漏問題。如果你想要在配置改變后保留該線程(而不是每一次在關閉 Activity 后都要新建一個線程),那我建議你使用 Fragment 去完成該耗時任務。你可以翻我以前的博文,一名叫作“Handling Configuration Changes with Fragments”應該能滿足你的需求,在API demo中也提供了很好理解的例子來為你闡述相關概念。
結論
Android 開發過程中,在 Activity 的生命周期里協調耗時任務可能會很困難,你一不小心就會導致內存泄漏問題。下面是一些小提示,能幫助你預防內存泄漏問題的發生:
-
盡可能使用靜態內部類而不是非靜態內部類。每一個非靜態內部類實例都會持有一個外部類的引用,若該引用是 Activity 的引用,那么該 Activity 在被銷毀時將無法被回收。如果你的靜態內部類需要一個相關 Activity 的引用以確保功能能夠正常運行,那么你得確保你在對象中使用的是一個 Activity 的弱引用,否則你的 Activity 將會發生意外的內存泄漏。
-
不要總想着 Java 的垃圾回收機制會幫你解決所有內存回收問題。就像上面的示例,我們以為垃圾回收機制會幫我們將不需要使用的內存回收,例如:我們需要結束一個 Activity,那么它的實例和相關的線程都該被回收。但現實並不會像我們劇本那樣走。Java 線程會一直存活,直到他們都被顯式關閉,抑或是其進程被 Android 系統殺死。所以,為你的后台線程實現銷毀邏輯是你在使用線程時必須時刻銘記的細節,此外,你在設計銷毀邏輯時要根據 Activity 的生命周期去設計,避免出現 Bug。
-
考慮你是否真的需要使用線程。Android 應用的框架層為我們提供了很多便於開發者執行后台操作的類。例如:我們可以使用 Loader 代替在 Activity 的生命周期中用線程通過注入執行短暫的異步后台查詢操作,考慮用 Service 將結構通知給 UI 的 BroadcastReceiver。最后,記住,這篇博文中對線程進行的討論同樣適用於 AsyncTask(因為 AsyncTask 使用 ExecutorService 執行它的任務)。然而,雖說 ExecutorService 只能在短暫操作(文檔說最多幾秒)中被使用,那么這些方法導致的 Activity 內存泄漏應該永遠不會發生。
這篇博文的源碼可以在 GitHub 中下載,你也可以在 Google Play 下載 APK 使用。