Android中的Activitys, Threads和記憶體洩露

陳哈哈的部落格發表於2014-11-27

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相關的記憶體及它執行中的執行緒。然而,這並非如此。這兩者都會導致記憶體洩露而且不會被回收, 後果是效能可能顯著地下降。

怎麼樣讓一個Activity洩露

如果你讀過我前一篇關於Handler和內部類的文章,那麼第一種記憶體洩露應該很容易理解。在Java中,非靜態匿名類隱式地持有他們的外部類的引用。如果你不小心,儲存這個引用可能導致Activity在可以被GC回收的時候被儲存下來。Activity持有一個指向它們整個View繼承樹和它所持有的所有資源的引用,所以如果你洩露了一個,很多記憶體都會連帶著被洩露。

配置發生變化只加劇了這個問題,它發出一個訊號讓Activity銷燬並重新建立。比如,基於上面的程式碼進行10次橫豎屏變化後,我們可以看到(使用Eclipse Memory Analyzer)由於那些隱式的引用,每一個Activity物件其實都留存在記憶體中:

在10次配置發生變化後,存留在記憶體中的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在配置發生變化後都會變得可以被回收。

怎麼使一個Thread洩露

第二個問題是每當建立了一個新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下載。

相關文章