Android消息機制(一) Handler looper messengerQueue

參考
Handler和他的小伙伴們(上)
Android:異步處理之Handler+Thread的應用(一)
Android:異步處理之Handler走贪、Looper抄淑、MessageQueue之間的恩怨(三)
android的消息處理機制(圖+源碼分析)——Looper,Handler,Message
Android中為什么主線程不會因為Looper.loop()里的死循環(huán)卡死

主線程是指進程所擁有的線程衩婚,默認情況下一個進程只有一個線程就是主線程剔应,主線程主要處理界面交互咒林,又叫UI線程熬拒;除了主線程之外都是子線程,又叫工作線程垫竞。

一澎粟、為什么不能直接更新UI

不要在UI主線程中進行耗時操作,你可能會疑問什么是UI主線程欢瞪,UI主線程主要運行的就是Activity活烙、Service等里面的生命周期方法,所以不要在生命周期方法如onCreate()中進行下載這些大事件遣鼓。對于耗時操作啸盏,我們應該新建一個子線程并交給他處理。但是還需要注意一點骑祟,不要在子線程中更新UI界面回懦。
為什么不允許子線程訪問UI呢?這是因為UI控件是線程不安全的次企,如果多線程并發(fā)訪問會導致UI控件處于不可預期狀態(tài)怯晕。那為什么不加鎖機制來解決呢?缺點有兩個:

  • 鎖機制會讓UI訪問邏輯變復雜
  • 鎖機制會阻塞某些線程的執(zhí)行缸棵,降低UI訪問效率
    所以最簡單高效方式就是單線程處理UI操作舟茶。
二、Handler

現在我們需要進行耗時操作(例如下載文件)時不能在主線程執(zhí)行堵第,我們又需要在UI界面通知用戶我們活干完了不能在子線程中執(zhí)行稚晚。這似乎是一個棘手的熱山芋呀,幸好谷歌給我們提供了一個救我們于危難之中的Handler型诚,一個能讓主線程監(jiān)聽子線程發(fā)送來消息的東東客燕。
<pre>
private Button btn;
private TextView text;

private Handler handler = new Handler(){
  private int process = 0;
  @Override
  public void handleMessage(Message msg) {
    switch(msg.what){
    case 0://更細下載進度
      process += 1;
      text.setText("下載" + process + "%");//在主線程中更新UI界面
      break;
    case 1://提示下載完成
      text.setText("下載完成");//在主線程中更新UI界面
      break;
    default:
      break;
    }
  }
};
//onCreate之類的生命周期的方法就是允許在UI主線程中
@Override
protected void onCreate(Bundle savedInstanceState) {
  super.onCreate(savedInstanceState);
  setContentView(R.layout.activity_main);

btn = (Button) findViewById(R.id.btn);
  text = (TextView) findViewById(R.id.text);

btn.setOnClickListener(new OnClickListener() {
  @Override
  public void onClick(View v) {
    new Thread(){
      @Override
      public void run() {
        //為了不阻塞主線程,將下載任務通過子線程來執(zhí)行
        for(int i = 0; i < 100; i++){
          try {
            Thread.sleep(200);//休眠0.2秒狰贯,模擬耗時操作
          } catch (InterruptedException e) {
            e.printStackTrace();
          }
          handler.sendEmptyMessage(0);//發(fā)送消息到handler也搓,通知下載進度
        }
        handler.sendEmptyMessage(1);//發(fā)送消失到handler,通知主線程下載完成
        }
      }.start();
    }
  });
}
</pre>
但是如果你覺得每次都要重寫handlerMessage()比較麻煩涵紊,我們完全可以用更加簡略的方法來解決我們的需求傍妒,就是用handler中的post方法。代碼如下
<pre>
new Thread(){
  @Override
  public void run() {
    //在子線程中進行下載操作
    try {
      Thread.sleep(1000);
    } catch (InterruptedException e) {
      e.printStackTrace();
    }
    handler.post(new Runnable() {
      @Override
      public void run() {
        text.setText("下載完成");
      }
    });//發(fā)送消失到handler摸柄,通知主線程下載完成
  }
}.start();
</pre>
這樣處理的話我們就可以不用重寫handlerMessage()方法了颤练,適合子線程與主線程進行較為單一的交流。但在這里我們要強調的一點的是驱负,post里面的Runnable還是在UI主線程中運行的嗦玖,而不會另外開啟線程運行患雇,千萬不要在Runnable的run()里面進行耗時任務
如果你有時候連handler都不想搞,還可以這樣寫代碼滴宇挫。我們只需要把handler換成View組件進行post苛吱,更新任務自然會加載到UI主線程中進行處理。
<pre>
text.post(new Runnable() {
  @Override
  public void run() {
    text.setText("下載完成");
  }
});//發(fā)送消失到handler器瘪,通知主線程下載完成
</pre>

關于sendMessage和post詳細區(qū)別翠储,可參考承香墨影Android--多線程之Handler

三、Handler looper messengerQueue

這里給大家寫了一個向子線程發(fā)送消息并顯示輸出的例子橡疼,強調一下下哦援所,是向子線程喲。
<pre>
public class MainActivity extends ActionBarActivity {

private Handler handler;
private Button btn;

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    
    btn = (Button) findViewById(R.id.sendmsg);
    
    new HandlerThread().start();//啟動子線程
    
    btn.setOnClickListener(new OnClickListener() {
        @Override
        public void onClick(View v) {
            handler.sendEmptyMessage(0);//向子線程發(fā)送消息
        }
    });
}

class HandlerThread extends Thread{
    @Override
    public void run() {
        //開始建立消息循環(huán)
        Looper.prepare();//初始化Looper
        handler = new Handler(){//默認綁定本線程的Looper
            @Override
            public void handleMessage(Message msg) {
                switch(msg.what){
                case 0:
                    Toast.makeText(MainActivity.this, "子線程收到消息", Toast.LENGTH_SHORT).show();
                }
            }
        };
        Looper.loop();//啟動消息循環(huán)
    }
}

}
</pre>

說一下消息發(fā)送的過程:

1欣除、啟動一個子線程任斋,并在子線程初始化一個Looper。

<pre>
public static void prepare() {
  prepare(true);
}
private static void prepare(boolean quitAllowed) {
  if (sThreadLocal.get() != null) {
    throw new RuntimeException("Only one Looper may be created per thread");
  }
  sThreadLocal.set(new Looper(quitAllowed));
}
private Looper(boolean quitAllowed) {
  mQueue = new MessageQueue(quitAllowed);
  mThread = Thread.currentThread();
}
</pre>
在Looper()中耻涛,實例化了一個消息隊列(MessageQueue)废酷!并且如我們所愿的綁定到了mQueue這個局部變量上,在這里我們可以得出這么一個結論:調用Looper. prepare()的線程就建立起一個消息循環(huán)的對象抹缕。

2澈蟆、在HandlerThread中實例化Handler,Handler自動綁定上當前線程的Looper。

<pre>
public Handler() {
  this(null, false);
}
public Handler(Callback callback, boolean async) {
  if (FIND_POTENTIAL_LEAKS) {
    final Class<? extends Handler> klass = getClass();
  if ((klass.isAnonymousClass() || klass.isMemberClass() || klass.isLocalClass()) &&
      (klass.getModifiers() & Modifier.STATIC) == 0) {
    Log.w(TAG, "The following Handler class should be static or leaks might occur: " +
      klass.getCanonicalName());
    }
  }
  mLooper = Looper.myLooper();
  if (mLooper == null) {
    throw new RuntimeException(
      "Can't create handler inside thread that has not called Looper.prepare()");
  }
  mQueue = mLooper.mQueue;
  mCallback = callback;
  mAsynchronous = async;
}
</pre>
在代碼中我們通過handler = new Handler() 調用到了Handler(Callback callback, boolean async)這個方法卓研;我們發(fā)現mLooper = Looper.myLooper()把線程中的Looper綁定到了Handler上趴俘,通過mQueue = mLooper.mQueue獲取了線程的消息隊列(單鏈表,方便插入刪除)奏赘,我當然也可以換句話說:Handler已經綁定到了創(chuàng)建此Handler對象的線程的消息隊列上了寥闪,所以咱們可以開始干壞事了。磨淌。疲憋。。

3梁只、重寫Handler里面的消息處理方法缚柳。

<pre>
@Override
public void handleMessage(Message msg) {
switch(msg.what){
case 0:
Toast.makeText(MainActivity.this, "子線程收到消息", Toast.LENGTH_SHORT).show();
}
}
</pre>

4、執(zhí)行Looper.loop()啟動消息循環(huán)搪锣,子線程進入等待消息狀態(tài)秋忙。

<pre>
public static void loop() {
  final Looper me = myLooper();
  if (me == null) {
    throw new RuntimeException("No Looper; Looper.prepare() wasn't called on this thread.");
  }
  final MessageQueue queue = me.mQueue;

Binder.clearCallingIdentity();
  final long ident = Binder.clearCallingIdentity();

for (;;) {
    Message msg = queue.next(); // might block
    if (msg == null) {
      return;
    }

Printer logging = me.mLogging;
    if (logging != null) {
      logging.println(">>>>> Dispatching to " + msg.target + " " +
        msg.callback + ": " + msg.what);
    }

msg.target.dispatchMessage(msg);

if (logging != null) {
      logging.println("<<<<< Finished to " + msg.target + " " + msg.callback);
    }

final long newIdent = Binder.clearCallingIdentity();
      if (ident != newIdent) {
        Log.wtf(TAG, "Thread identity changed from 0x"
              + Long.toHexString(ident) + " to 0x"
              + Long.toHexString(newIdent) + " while dispatching to "
              + msg.target.getClass().getName() + " "
              + msg.callback + " what=" + msg.what);
      }

msg.recycle();
  }
}
</pre>
在loop()的這個靜態(tài)方法中,我們可以注意到for (;;)這個方法构舟,這是死胡同死循環(huán)灰追,所以我們將其稱作為“消息循環(huán)”,說起來挺形象滴。在消息循環(huán)中會調用queue.next()來獲取消息隊列中排隊等待處理的消息弹澎,并將其賦值到msg這個變量上朴下;接下來就判斷如果msg != null 就開始分發(fā)消息裁奇,也就是執(zhí)行msg.target.dispatchMessage(msg)桐猬。在分發(fā)消息結束后麦撵,將會回收掉這個消息刽肠,體現在msg.recycle()這個函數上。
msg.target是一個handler對象免胃,表示需要處理這個消息的handler對象音五,所以我們回到Handler看看dispatchMessage()這個方法了:
<pre>
public void dispatchMessage(Message msg) {
  if (msg.callback != null) {
    handleCallback(msg);//post方法傳遞的Runnable參數
  } else {
    if (mCallback != null) {
      if (mCallback.handleMessage(msg)) {
        return;
      }
    }
    handleMessage(msg);
  }
}
</pre>
不知道大家有沒有一眼發(fā)現handleMessage()這個方法,這可不是我們在第三步重寫Handler中的方法么羔沙。真相大白躺涝,當 msg.callback != null 并且 mCallback != null 時將會調用 handleMessage(msg) 來處理其他線程發(fā)送來的消息,我們通過覆蓋這個方法來實現我們具體的消息處理過程扼雏;這也就是Handler消息處理機制的全部內容坚嗜。
通讀全文,我們可以知道消息循環(huán)機制的核心就是Looper诗充,因為Looper持有了MessageQueue的對象苍蔬,并且可以被一個線程設為該線程的一個局部變量,我們可以這么認為這個線程通過Looper擁有了一個消息隊列蝴蜓。而Handler的用處就是封裝了消息發(fā)送和消息處理的方法碟绑,在線程通信中,線程可以通過Handler發(fā)送消息給創(chuàng)建Handler的線程茎匠,通過Looper將消息放入進入消息接收線程的消息隊列格仲,等待Looper取出消息并在最后交給Handler處理具體消息。
我們會發(fā)現在Activity中實例化一個Handler并不需要Looper.prepare()來初始化一個Looper和Looper.loop()來啟動消息循環(huán)诵冒,因為Activity在構造過程中已經對Looper進行了初始化并且建立了消息循環(huán)凯肋,參見ActivityThread.java中的代碼:
<pre>
public final class ActivityThread {
public static final void main(String[] args) {
......
Looper.prepareMainLooper();
......
ActivityThread thread = new ActivityThread();
thread.attach(false);
......
Looper.loop();
......
thread.detach();
......
}
}
</pre>
Android應用程序進程在啟動的時候,會在進程中加載ActivityThread類汽馋,并且執(zhí)行這個類的main函數否过,應用程序的消息循環(huán)過程就是在這個main函數里面實現的。

總結:一個線程只有一個Looper, 而一個Looper持有一個MessageQueue, 當調用Looper.prepare()時惭蟋,Looper就與當前線程關聯起來了(在Activity里沒有顯示調用Looper.prepare()是因為系統自動在主線程里幫我們調用了)苗桂,而Handler是與Looper的線程是綁定的,查看Handler類的源碼可以發(fā)現它幾個構造函數告组,其中有接收一個Looper參數的煤伟,也有不接收Looper參數的,從上面的代碼上看,我們沒有為Handler指定Looper便锨,那么Handler就默認更當前線程(即主線程)的Looper關聯起來了围辙,之所以啰嗦那么多就是因為這決定了Handler.handlerMessage(msg)方法體里的代碼到底在哪個線程里執(zhí)行,我們再梳理一下放案,Looper.prepare調用決定了Looper與哪個線程關聯姚建,間接決定了與這個Looper相關聯的Handler.handlerMessage(msg)方法體里的代碼執(zhí)行的線程。(太啰嗦了)
現在回到上面的代碼吱殉,我們的Handler是在主線程里的定義的掸冤,所以也默認跟主線程的Looper相關聯,即handlerMessage方法的代碼會在UI線程執(zhí)行友雳。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末稿湿,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子押赊,更是在濱河造成了極大的恐慌饺藤,老刑警劉巖,帶你破解...
    沈念sama閱讀 219,270評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件流礁,死亡現場離奇詭異涕俗,居然都是意外死亡,警方通過查閱死者的電腦和手機神帅,發(fā)現死者居然都...
    沈念sama閱讀 93,489評論 3 395
  • 文/潘曉璐 我一進店門再姑,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人枕稀,你說我怎么就攤上這事询刹。” “怎么了萎坷?”我有些...
    開封第一講書人閱讀 165,630評論 0 356
  • 文/不壞的土叔 我叫張陵凹联,是天一觀的道長。 經常有香客問我哆档,道長蔽挠,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,906評論 1 295
  • 正文 為了忘掉前任瓜浸,我火速辦了婚禮澳淑,結果婚禮上,老公的妹妹穿的比我還像新娘插佛。我一直安慰自己杠巡,他們只是感情好,可當我...
    茶點故事閱讀 67,928評論 6 392
  • 文/花漫 我一把揭開白布雇寇。 她就那樣靜靜地躺著氢拥,像睡著了一般蚌铜。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上嫩海,一...
    開封第一講書人閱讀 51,718評論 1 305
  • 那天冬殃,我揣著相機與錄音,去河邊找鬼叁怪。 笑死审葬,一個胖子當著我的面吹牛,可吹牛的內容都是我干的奕谭。 我是一名探鬼主播涣觉,決...
    沈念sama閱讀 40,442評論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼展箱!你這毒婦竟也來了旨枯?” 一聲冷哼從身側響起蹬昌,我...
    開封第一講書人閱讀 39,345評論 0 276
  • 序言:老撾萬榮一對情侶失蹤混驰,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后皂贩,有當地人在樹林里發(fā)現了一具尸體栖榨,經...
    沈念sama閱讀 45,802評論 1 317
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 37,984評論 3 337
  • 正文 我和宋清朗相戀三年明刷,在試婚紗的時候發(fā)現自己被綠了婴栽。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,117評論 1 351
  • 序言:一個原本活蹦亂跳的男人離奇死亡辈末,死狀恐怖愚争,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情挤聘,我是刑警寧澤轰枝,帶...
    沈念sama閱讀 35,810評論 5 346
  • 正文 年R本政府宣布,位于F島的核電站组去,受9級特大地震影響鞍陨,放射性物質發(fā)生泄漏。R本人自食惡果不足惜从隆,卻給世界環(huán)境...
    茶點故事閱讀 41,462評論 3 331
  • 文/蒙蒙 一诚撵、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧键闺,春花似錦寿烟、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,011評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽盅藻。三九已至,卻和暖如春畅铭,著一層夾襖步出監(jiān)牢的瞬間氏淑,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,139評論 1 272
  • 我被黑心中介騙來泰國打工硕噩, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留假残,地道東北人。 一個月前我還...
    沈念sama閱讀 48,377評論 3 373
  • 正文 我出身青樓炉擅,卻偏偏與公主長得像辉懒,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子谍失,可洞房花燭夜當晚...
    茶點故事閱讀 45,060評論 2 355

推薦閱讀更多精彩內容