2011-10-30

Android Handler 筆記

每個 app 有一個 process,每個 process 可以有多個 thread,大部分的時候都只會有一個 thread 叫做 main thread。

Android app 的四大要角:activity、service、receiver、provider,都是由 main thread 負責執行。

因為 main thread 要做的事情太多,所以就有一個 message queue 叫做 main queue,紀錄了所有 main thread 要做的事情,或者反過來說,要交由 main thread 處理的事情就丟到 main queue 就好了。

由於 main thread 負責 UI 的互動,像是 user 按下一個按鈕這樣一個動作都會丟到 main queue 去,所以為了不讓 user 枯等,message 在 main queue 待超過五秒鐘或者 main thread 處理一個 message 超過五秒鐘,系統就會丟出 ANR(Application Not Resopnding),所以費時的工作就要另起 thread 來處理或者請 main thread 有空再回來處理。


除了 UI 的互動,sendBroadcast(...) 與 startService(...) 都一樣會被丟到 main queue 裡,等待 main thread 的執行,只有呼叫 local content provider 不會進到 main queue 裡,而是由 main thread 直接執行,猜測應該是因為 content provider 必須同步執行的關係,但如果是 remote content provider 則是由 thread pool 中取得 thread 來執行,外部 client 呼叫 service 也是如此。

除了 main thread,還有從 thread pool 裡出生的 worker thread,主要用來處理外部的呼叫,例如 App A 呼叫 App B 的 service 或者 content provider,就是由 worker thread 負責處理。

為了方便辨識哪個 thread 做了什麼事,可以用簡單的工具程式做紀錄。
public class Utils {

    private static final String TAG = "Utils";

    public static final void logThread() {
        Thread t = Thread.currentThread();
        Log.d(TAG,
                "<" + t.getName() + ">id: " + t.getId() + ", Priority: "
                        + t.getPriority() + ", Group: "
                        + t.getThreadGroup().getName());
    }
}
在需要的地方加上 Utils.logThread(),就可以得到類似以下的結果:
10-27 09:38:33.797: DEBUG/Utils(517): <main>id: 1, Priority: 5, Group: main
10-27 09:38:38.848: DEBUG/Utils(517): <Thread-10>id: 10, Priority: 5, Group: main
Handler 是什麼?

先看看怎麼使用 Handler,看完之後就有概念 Handler 是什麼了。
  • 先 new 一個 handler
  • 使用 handler 丟 message 到 queue 裡
  • thread 去 queue 取出 message
  • 從 message 可以得到 handler
  • thread 去執行 handler 裡叫做 handleMessage 的 callback method
簡單來說,Handler 就是可以叫 thread 做事情的工具。

但是上面的步驟有一個遺漏,就是 handler 丟 message 到「哪一個」queue?main queue 或者 worker queue?答案是,在哪一個 thread 建立的 handler 就丟到那一個 thread 的 queue。

也就是說,Handler 除了可以是同一個 thread 的溝通工具,也可以是不同 thread 間的溝通工具,只要可以拿到那一個 thread 的 handler。

舉例說明 Main thread, main queue, worker threads, worker queues, and handlers 的關係與互動
一位主人(user)會有一個管家(main thread)和多位女僕(worker threads),管家得隨侍在主人身邊,不能離開太久(五秒鐘),不然主人會失控(ANR),所以當主人交待事情給管家時,管家可以將事情交待給女僕們。
但是因為事情真的太多了,所以管家和女僕們每個人都會有一本紀錄待辦事項的記事本(main queue and worker queue),管家和女僕們當然希望工作愈少愈好,所以每個人都會把自己的筆記本上鎖,所以要打開筆試本加待辦事項就得有密碼(handler),每個人的密碼都不一樣,要向筆記本的主人要才能拿到密碼。
延遲處理

前面提過,費時的工作可以另起 thread 來處理或者稍後再處理,這邊先來看看後者的情況,假設要加總十個數字或更多,不想殺雞用牛刀起新的 thread,又擔心 main thread 執行超過五秒,就可以在 main thread 裡透過 handler 丟 message 到 main queue 裡,讓 main thread 有空再來處理,且一次處理一小部份,這次沒處理完再丟一次,遞迴到全部加總完為止。

HandlerActivity
public class HandlerActivity extends Activity {

 private static final String TAG = "HandlerActivity";

 @Override
 public void onCreate(Bundle savedInstanceState) {
  super.onCreate(savedInstanceState);
  setContentView(R.layout.main);
  Utils.logThread();
  // 建立供加總使用的數列
  List<Integer> intList = new ArrayList<Integer>();
  for (int i = 0; i < 10; i++) {
   intList.add((int) (Math.random() * 10));
  }
  this.deferWork(intList);
 }

 private void deferWork(List<Integer> intList) {
  // 將數列傳給 handler,一併傳入 acitivity 用來事後更新畫面
  CalculatorHandler h = new CalculatorHandler(intList, this);
  // 立即丟 message 到 main queue 裡
  h.sendEmptyMessage(0);
 }

 // 供 handler 計算完成後更新畫面
 public void show(String msg) {
  TextView tv = (TextView) this.findViewById(R.id.tv);
  tv.setText(tv.getText() + "\n" + msg);
 }
}
CalculatorHandler
public class CalculatorHandler extends Handler {

 private static final String TAG = "DelayHandler";
 private List<Integer> intList;
 private HandlerActivity activity;
 // 紀錄計算到第幾筆
 private int handled = 0;
 // 加總結果
 private int sum = 0;

 public CalculatorHandler(List<Integer> intList, HandlerActivity activity) {
  super();
  this.intList = intList;
  this.activity = activity;
 }

 @Override
 public void handleMessage(Message msg) {
  // 如果計算完成,就不再往下執行
  if (this.handled >= this.intList.size()) {
   // 在這裡可以將計算結果透過 activity 顯示在畫面上
   String m = "結果 : " + this.sum;
   Log.d(TAG, m);
   this.activity.show(m);
   return;
  }
  Utils.logThread();
  Integer cur = this.intList.get(this.handled);
  this.sum += cur;
  String m = "加上第 " + (this.handled + 1) + " 筆數字 " + cur + " 等於 "
    + this.sum;
  this.activity.show(m);
  Log.d(TAG, m);
  // 延遲一秒鐘送出下一個 message 到 main queue,形成 loop 效果
  this.sendMessageDelayed(this.obtainMessage(), 1000);
  this.handled++;
 }
}
可以從 log 看出來都是在 main thread 裡執行的:
10-28 13:59:41.663: DEBUG/Utils(2069): <main>id: 1, Priority: 5, Group: main
10-28 13:59:41.693: DEBUG/Utils(2069): <main>id: 1, Priority: 5, Group: main
10-28 13:59:41.693: DEBUG/DelayHandler(2069): 加上第 1 筆數字 0 等於 0
10-28 13:59:42.703: DEBUG/Utils(2069): <main>id: 1, Priority: 5, Group: main
10-28 13:59:42.703: DEBUG/DelayHandler(2069): 加上第 2 筆數字 4 等於 4
10-28 13:59:43.773: DEBUG/Utils(2069): <main>id: 1, Priority: 5, Group: main
10-28 13:59:43.773: DEBUG/DelayHandler(2069): 加上第 3 筆數字 9 等於 13
10-28 13:59:44.828: DEBUG/Utils(2069): <main>id: 1, Priority: 5, Group: main
10-28 13:59:44.833: DEBUG/DelayHandler(2069): 加上第 4 筆數字 3 等於 16
10-28 13:59:45.880: DEBUG/Utils(2069): <main>id: 1, Priority: 5, Group: main
10-28 13:59:45.903: DEBUG/DelayHandler(2069): 加上第 5 筆數字 5 等於 21
10-28 13:59:46.928: DEBUG/Utils(2069): <main>id: 1, Priority: 5, Group: main
10-28 13:59:46.943: DEBUG/DelayHandler(2069): 加上第 6 筆數字 2 等於 23
10-28 13:59:48.005: DEBUG/Utils(2069): <main>id: 1, Priority: 5, Group: main
10-28 13:59:48.023: DEBUG/DelayHandler(2069): 加上第 7 筆數字 0 等於 23
10-28 13:59:49.051: DEBUG/Utils(2069): <main>id: 1, Priority: 5, Group: main
10-28 13:59:49.073: DEBUG/DelayHandler(2069): 加上第 8 筆數字 6 等於 29
10-28 13:59:50.096: DEBUG/Utils(2069): <main>id: 1, Priority: 5, Group: main
10-28 13:59:50.124: DEBUG/DelayHandler(2069): 加上第 9 筆數字 1 等於 30
10-28 13:59:51.146: DEBUG/Utils(2069): <main>id: 1, Priority: 5, Group: main
10-28 13:59:51.174: DEBUG/DelayHandler(2069): 加上第 10 筆數字 1 等於 31
10-28 13:59:52.184: DEBUG/DelayHandler(2069): 結果 : 31

另起 thread 處理

同樣的假設狀況,在 main thread 裡起一個 worker thread 來執行運算,並傳給該 worker thread 由 main thread 產生的 handler,供 worker thread 與 main thread 進行溝通。

HandlerActivity
public class HandlerActivity extends Activity {

 private static final String TAG = "HandlerActivity";

 @Override
 public void onCreate(Bundle savedInstanceState) {
  super.onCreate(savedInstanceState);
  setContentView(R.layout.main);
  Utils.logThread();
  // 建立供加總使用的數列
  List<Integer> intList = new ArrayList<Integer>();
  for (int i = 0; i < 10; i++) {
   intList.add((int) (Math.random() * 10));
  }
  this.callWorker(intList);
 }

 private void callWorker(List<Integer> intList) {
  // 建立 handler 交由 worker thread 與 main thread 溝通
  WorkerHandler handler = new WorkerHandler(this);
  // 起一個 worker thread
  new Thread(new WorkerThread(intList, handler)).start();
 }

 // 供 handler 計算完成後更新畫面
 public void show(String msg) {
  TextView tv = (TextView) this.findViewById(R.id.tv);
  tv.setText(tv.getText() + "\n" + msg);
 }
}
WorkerHandler
public class WorkerHandler extends Handler {

 private static final String TAG = "WorkerHandler";
 public static final String MSG = "msg";
 private HandlerActivity activity;

 public WorkerHandler(HandlerActivity activity) {
  super();
  this.activity = activity;
 }

 @Override
 public void handleMessage(Message msg) {
  Utils.logThread();
  // 取出並輸出從 worker thread 傳進來的訊息
  String m = msg.getData().getString(WorkerHandler.MSG);
  Log.d(TAG, m);
  this.activity.show(m);
 }
}
WorkerThread
public class WorkerThread implements Runnable {

 private static final String TAG = "WorkerThread";
 private List<Integer> intList;
 private WorkerHandler handler;

 public WorkerThread(List<Integer> intList, WorkerHandler handler) {
  super();
  this.intList = intList;
  this.handler = handler;
 }

 @Override
 public void run() {
  Utils.logThread();
  // 直接進行計算,且一口氣完成
  int sum = 0;
  int cur;
  for (int i = 0; i < this.intList.size(); i++) {
   cur = this.intList.get(i);
   sum += cur;
   // 將過程透過 handler 傳給 main thread
   this.sendMessage("加上第 " + (i + 1) + " 筆數字 " + cur + " 等於 " + sum);
  }
  this.sendMessage("結果 : " + sum);
 }

 private void sendMessage(String m) {
  Message msg = this.handler.obtainMessage();
  msg.getData().putString(WorkerHandler.MSG, m);
  this.handler.sendMessage(msg);
 }
}
再從 log 看出差異:
10-28 13:57:39.724: DEBUG/Utils(1987): <main>id: 1, Priority: 5, Group: main
10-28 13:57:39.753: DEBUG/Utils(1987): <Thread-10>id: 10, Priority: 5, Group: main
10-28 13:57:39.844: DEBUG/Utils(1987): <main>id: 1, Priority: 5, Group: main
10-28 13:57:39.844: DEBUG/WorkerHandler(1987): 加上第 1 筆數字 5 等於 5
10-28 13:57:39.904: DEBUG/Utils(1987): <main>id: 1, Priority: 5, Group: main
10-28 13:57:39.904: DEBUG/WorkerHandler(1987): 加上第 2 筆數字 0 等於 5
10-28 13:57:39.904: DEBUG/Utils(1987): <main>id: 1, Priority: 5, Group: main
10-28 13:57:39.914: DEBUG/WorkerHandler(1987): 加上第 3 筆數字 4 等於 9
10-28 13:57:39.923: DEBUG/Utils(1987): <main>id: 1, Priority: 5, Group: main
10-28 13:57:39.923: DEBUG/WorkerHandler(1987): 加上第 4 筆數字 7 等於 16
10-28 13:57:39.934: DEBUG/Utils(1987): <main>id: 1, Priority: 5, Group: main
10-28 13:57:39.934: DEBUG/WorkerHandler(1987): 加上第 5 筆數字 4 等於 20
10-28 13:57:39.943: DEBUG/Utils(1987): <main>id: 1, Priority: 5, Group: main
10-28 13:57:39.943: DEBUG/WorkerHandler(1987): 加上第 6 筆數字 0 等於 20
10-28 13:57:39.963: DEBUG/Utils(1987): <main>id: 1, Priority: 5, Group: main
10-28 13:57:39.963: DEBUG/WorkerHandler(1987): 加上第 7 筆數字 6 等於 26
10-28 13:57:39.974: DEBUG/Utils(1987): <main>id: 1, Priority: 5, Group: main
10-28 13:57:39.974: DEBUG/WorkerHandler(1987): 加上第 8 筆數字 0 等於 26
10-28 13:57:39.984: DEBUG/Utils(1987): <main>id: 1, Priority: 5, Group: main
10-28 13:57:39.984: DEBUG/WorkerHandler(1987): 加上第 9 筆數字 4 等於 30
10-28 13:57:39.993: DEBUG/Utils(1987): <main>id: 1, Priority: 5, Group: main
10-28 13:57:40.004: DEBUG/WorkerHandler(1987): 加上第 10 筆數字 4 等於 34
10-28 13:57:40.014: DEBUG/Utils(1987): <main>id: 1, Priority: 5, Group: main
10-28 13:57:40.014: DEBUG/WorkerHandler(1987): 結果 : 34

從 log 還可以看出一個顯著的差異,執行時間,延遲執行用了十秒鐘左右,worker thread 不到一秒鐘就結束了,這是因為前者每計算一次就休息一秒鐘造成的差異。

Handler Lifecycle

關於 main queue 與 worker thread 還有一個很重要的特徵,就是當 app 不在前景執行時,包括畫面完全被覆蓋(onStop)或者部份被覆蓋(onPause),main thread 仍會繼續執行 main queue 裡的代辦事項,worker thread 也一樣,唯有在 onDestroy 之後才會停止,這可以從上面的範例看出來,不管是透過 main queue 或者 workder thread 執行計算,都是從 onCreate 就啟動的,不是在 onStart 或者 onResume,有一個測試的方法就是將計算的筆數加大為100筆,並在 worker thread 的 loop 裡呼叫 Thread.sleep(...) 以延長執行時間,一旦 app 啟動後,可以跳回 Home 或上一頁來離開該 app,這時先在 log 看到 main thread 與 worker thread 都仍在執行,再跳回該 app,可以從頁面的計算過程得到證明,但是如果離開太久,該 app 是有可以被強制關閉(onDestroy)的。

由上可知,不可以在 onStart 或者 onResume 啟動 worker thread,否則每次離開再回來或者甚至螢幕轉個方向,就可能產生一個新的 worker thread,是可以用 instance var 來追蹤 thread 以迴避這樣的問題,不過一般 worker thread 的啟動應該是透過 user 的事件驅動,比較不會有這樣的問題發生,但是如果是有記憶的狀況,例如這次沒做完下次要繼續做或者在 app 一起動便要建立 worker thread 的話,那就得要在 onCreate 裡進行了。

最後,如果要中途停止 worker thread,不建議呼叫 Thread.stop(...) ,因為這樣沒辦法控制停止的方式,建議在 thread 裡加一個 flag,當必須終止時,只要更改 flag,讓 thread 可以優雅的打卡下班。

相關文章

7 則留言:

  1. 建議在 thread 裡加一個 flag
    想不通怎麼做
    可以請您大概舉個例子嗎?

    回覆刪除
  2. 建議在 thread 裡加一個 flag
    可以拜託您大概舉個例子嗎
    目前想不到
    要怎麼做
    謝謝^^

    回覆刪除
  3. 建議在 thread 裡加一個 flag
    想不通怎麼做
    可以請您大概舉個例子嗎?

    回覆刪除
  4. 新增 boolean stop = false; 到 WorkerThread。

    // 保留住 wt 供稍後修改 stop
    WorkerThread wt = new WorkerThread(intList, handler);
    new Thread().start();

    // 在 WorkerThread.run() 的迴圈裡
    for (...) {
    if (this.stop) {
    break;
    }
    // ...
    }

    // 在某個地方,也許是使用者按下停止
    wt.stop = true;

    回覆刪除