Activitys中使用線程導致的內存泄露

在做Android開發過程中最長遇到的一個難點就是在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被銷燬及重新創建,我們總會簡單的認爲Android系統會在這之後清理並回收與Activity有關的內存和正在運行的線程。然而,事情並不是這樣的,所有提到的這些再也不會被回收,並且會導致內存泄露,從而很可能很大程度上影響到android的性能。

導致Acitvity內存泄露的根源

如果你讀過我之前發表的有關於Handlers跟內存類的博文的話,那我接下來要講的知識你肯定知道。在Java中,非靜態匿名類會隱式持有外部類的引用,如果你沒有注意這一點的話,存儲這些引用將導致Acitvity被保留而不是被垃圾回收機制回收。Activity對象持有其View層以及所有的資源,所以說一旦你出現Activity內存泄露,那麼你將會失去一大片的內存空間。

這樣的問題在Activity配置改變時更加嚴重,因爲Activity配置的改變表示Android系統將要銷燬一個Activity並且重新創建一個。例如執行10次橫屏豎屏的操作後,每次的操作都會執行前面的代碼,那麼我們會發現(使用Eclipse的內存分析工具可以看到)每一個Activity都因爲留有隱式引用而被保留下在內存中。

enter image description here

在每次配置改變時,Android系統會創建一個新的Activity,並且把改變前的Actvity交給垃圾回收機制回收。然而,線程隱式的持有了舊的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,線程的內存泄露將再也不能夠被回收,線程是JAVA垃圾回收機制的根源,由於在運行系統中DVM虛擬機一直持有着所有運行狀態的線程的引用,結果導致處於運行狀態的線程將永遠不會被回收。因此你必須要爲你的後臺進行實現銷燬的邏輯!下面的例子將展現如何完成這些銷燬邏輯的。

/**
 * 通例二一樣,除了這次我們爲線程實現了一個銷燬的邏輯,確保它再也不會出現內存泄露的問題。
 * 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() 方法中結束了線程,確保不會發生意外的線程的內存泄漏問題。如果你想要在配置改變後保留該線程(而不是每一次在關閉 Activity 後都要新建一個線程),那我建議你使用 Fragment 去完成該耗時任務。你可以翻我以前的博文,一名叫作“Handling Configuration Changes with Fragments”應該能滿足你的需求,在API demo中也提供了很好理解的例子來爲你闡述相關概念。

結論

Android 開發過程中,在 Activity 的生命週期裏協調耗時任務可能會很困難,你一不小心就會導致內存泄漏問題。下面是一些建議當你在Activity的生命週期中處理你的長時間後臺任務時:
- 儘可能的使用靜態內部類而不是非靜態內部類。 每一個非靜態內部類的實例都會持有外部Activity的實例引用,存儲這些引用將導致Acitvity被保留而不是被垃圾回收機制回收。如果你的靜態內部類需要引用相關的Activity以確保功能的正常使用,那麼你得確保你在對象中使用的是一個 Activity 的弱引用,否則你的 Activity 將會發生意外的內存泄漏。
- 不要總想着java會爲你清理你的正常運行的線程。在上面的例子中,我們很容易的認爲,當用戶退出Activity的時候,Activity的實例以及與他相關的正在運行的線程都會被垃圾回收機制回收,這個是不可能的,Java的線程將會一直存在直到他們都被顯式的關閉或者整個進程被Android系統結束掉。所以爲你的後臺線程實現回收邏輯是極其重要,此外,你在設計銷燬邏輯時要根據 Activity 的生命週期去設計,避免出現 Bug。
- 考慮你是否真的需要用到線程。Android應用的框架層爲我們提供了很多便於開發者執行後臺操作的類,例如:我們可以使用 Loader 代替在 Activity 的生命週期中用線程通過注入執行短暫的異步後臺查詢操作,考慮用 Service 將結構通知給 UI 的 BroadcastReceiver。最後,記住,這篇博文中對線程進行的討論同樣適用於 AsyncTask(因爲 AsyncTask 使用 ExecutorService 執行它的任務)。然而,雖說 ExecutorService 只能在短暫操作(文檔說最多幾秒)中被使用,那麼這些方法導致的 Activity 內存泄漏應該永遠不會發生。

這篇博文的源代碼都在Github ,你也可以從Google play下載到

enter image description here

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章