Android消息机制(一) Handler looper messengerQueue

参考
Handler和他的小伙伴们(上)
Android:异步处理之Handler+Thread的应用(一)
Android:异步处理之Handler、Looper、MessageQueue之间的恩怨(三)
android的消息处理机制(图+源码分析)——Looper,Handler,Message
Android中为什么主线程不会因为Looper.loop()里的死循环卡死

主线程是指进程所拥有的线程,默认情况下一个进程只有一个线程就是主线程,主线程主要处理界面交互,又叫UI线程;除了主线程之外都是子线程,又叫工作线程。

一、为什么不能直接更新UI

不要在UI主线程中进行耗时操作,你可能会疑问什么是UI主线程,UI主线程主要运行的就是Activity、Service等里面的生命周期方法,所以不要在生命周期方法如onCreate()中进行下载这些大事件。对于耗时操作,我们应该新建一个子线程并交给他处理。但是还需要注意一点,不要在子线程中更新UI界面。
为什么不允许子线程访问UI呢?这是因为UI控件是线程不安全的,如果多线程并发访问会导致UI控件处于不可预期状态。那为什么不加锁机制来解决呢?缺点有两个:

  • 锁机制会让UI访问逻辑变复杂
  • 锁机制会阻塞某些线程的执行,降低UI访问效率
    所以最简单高效方式就是单线程处理UI操作。
二、Handler

现在我们需要进行耗时操作(例如下载文件)时不能在主线程执行,我们又需要在UI界面通知用户我们活干完了不能在子线程中执行。这似乎是一个棘手的热山芋呀,幸好谷歌给我们提供了一个救我们于危难之中的Handler,一个能让主线程监听子线程发送来消息的东东。
<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() {
        //为了不阻塞主线程,将下载任务通过子线程来执行
        for(int i = 0; i < 100; i++){
          try {
            Thread.sleep(200);//休眠0.2秒,模拟耗时操作
          } catch (InterruptedException e) {
            e.printStackTrace();
          }
          handler.sendEmptyMessage(0);//发送消息到handler,通知下载进度
        }
        handler.sendEmptyMessage(1);//发送消失到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("下载完成");
      }
    });//发送消失到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("下载完成");
  }
});//发送消失到handler,通知主线程下载完成
</pre>

关于sendMessage和post详细区别,可参考承香墨影Android--多线程之Handler

三、Handler looper messengerQueue

这里给大家写了一个向子线程发送消息并显示输出的例子,强调一下下哦,是向子线程哟。
<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);//向子线程发送消息
        }
    });
}

class HandlerThread extends Thread{
    @Override
    public void run() {
        //开始建立消息循环
        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();//启动消息循环
    }
}

}
</pre>

说一下消息发送的过程:

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()的线程就建立起一个消息循环的对象。

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)这个方法;我们发现mLooper = Looper.myLooper()把线程中的Looper绑定到了Handler上,通过mQueue = mLooper.mQueue获取了线程的消息队列(单链表,方便插入删除),我当然也可以换句话说:Handler已经绑定到了创建此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、执行Looper.loop()启动消息循环,子线程进入等待消息状态。

<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()的这个静态方法中,我们可以注意到for (;;)这个方法,这是死胡同死循环,所以我们将其称作为“消息循环”,说起来挺形象滴。在消息循环中会调用queue.next()来获取消息队列中排队等待处理的消息,并将其赋值到msg这个变量上;接下来就判断如果msg != null 就开始分发消息,也就是执行msg.target.dispatchMessage(msg)。在分发消息结束后,将会回收掉这个消息,体现在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>
不知道大家有没有一眼发现handleMessage()这个方法,这可不是我们在第三步重写Handler中的方法么。真相大白,当 msg.callback != null 并且 mCallback != null 时将会调用 handleMessage(msg) 来处理其他线程发送来的消息,我们通过覆盖这个方法来实现我们具体的消息处理过程;这也就是Handler消息处理机制的全部内容。
通读全文,我们可以知道消息循环机制的核心就是Looper,因为Looper持有了MessageQueue的对象,并且可以被一个线程设为该线程的一个局部变量,我们可以这么认为这个线程通过Looper拥有了一个消息队列。而Handler的用处就是封装了消息发送和消息处理的方法,在线程通信中,线程可以通过Handler发送消息给创建Handler的线程,通过Looper将消息放入进入消息接收线程的消息队列,等待Looper取出消息并在最后交给Handler处理具体消息。
我们会发现在Activity中实例化一个Handler并不需要Looper.prepare()来初始化一个Looper和Looper.loop()来启动消息循环,因为Activity在构造过程中已经对Looper进行了初始化并且建立了消息循环,参见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类,并且执行这个类的main函数,应用程序的消息循环过程就是在这个main函数里面实现的。

总结:一个线程只有一个Looper, 而一个Looper持有一个MessageQueue, 当调用Looper.prepare()时,Looper就与当前线程关联起来了(在Activity里没有显示调用Looper.prepare()是因为系统自动在主线程里帮我们调用了),而Handler是与Looper的线程是绑定的,查看Handler类的源码可以发现它几个构造函数,其中有接收一个Looper参数的,也有不接收Looper参数的,从上面的代码上看,我们没有为Handler指定Looper,那么Handler就默认更当前线程(即主线程)的Looper关联起来了,之所以啰嗦那么多就是因为这决定了Handler.handlerMessage(msg)方法体里的代码到底在哪个线程里执行,我们再梳理一下,Looper.prepare调用决定了Looper与哪个线程关联,间接决定了与这个Looper相关联的Handler.handlerMessage(msg)方法体里的代码执行的线程。(太啰嗦了)
现在回到上面的代码,我们的Handler是在主线程里的定义的,所以也默认跟主线程的Looper相关联,即handlerMessage方法的代码会在UI线程执行。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,029评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,395评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,570评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,535评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,650评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,850评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,006评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,747评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,207评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,536评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,683评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,342评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,964评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,772评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,004评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,401评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,566评论 2 349

推荐阅读更多精彩内容