Android笔记之Handler异步消息处理机制

主要参考文献:深入理解Android,卷1
       Android 5.0 源码

学识尚浅,错误之处请指正。

为什么需要这种机制?

<p>在Android的应用开发中我们经常遇到这样的场景,为了避免在UI线程中的耗时操作产生ANR,我们会开启新的线程来处理好事操作,得到处理结果在对UI进行更新。
但是因为android本身设计时考虑到性能优先,所以UI操作并非线程安全的,为了避免多线程操作UI带来的线程安全问题,android设计者直接规定:只允许UI线程来操作UI组件。
为了解决这个问题我们需要考虑这么几个问题:

  1. 解决线程间的相互通信。
  2. 一方发送的消息另一方能够及时收到,并做出处理。

Handler异步消息处理机制就是Android中解决这个问题提供的一种方案。

基本的原理很简单:

<p>

  • 一个消息队列:可以往其中添加消息。
  • 一个消息循环:持续不断的从消息队列中取出消息,进行处理。
    <p>

他的四个组成部分Message、Handler、MessageQueue和Looper,形象的说明了各自的分工:

  • Message:封装需要传递的消息
  • Handler:发送和处理消息
  • MessageQueue:消息队列,采用先进先出的方式管理Message
  • Looper:他负责管理MessageQueue,不断的从中读取消息,并将读到的消息交给Handler处理。
图片来源:http://www.lxway.com/608450004.htm

举一个并不确切的例子说一下我的理解:

Handler好比一个要搬家的人,不同的线程就像是旧的住所和新的住所,你在旧的住所将自己的东西打包好装在行李箱(Message )中,并在上面贴上自己的标签,如姓名联系方式等(Message.target);MessageQueque好比是物流,你将打包好的行李箱交给他,他帮你运输和存储;looper就好比送货员,他将运送过来的消息取出来,查看主人(target),它将你的东西送到你的手里,任凭你的处理和摆放。

如何简单地使用它

<p>网上有很多这样的教程,但是我看了一些,觉得大多数单单使用这一点并没有讲清楚,让初次接触的人用起来非常复杂,其实事实并不是这样的。希望我能够尝试这说清楚。
首先它是一种线程间通信的通信的方式,因此至少涉及到了两个线程,我们将这两个线程按照一次消息处理的地位分为发送消息线程和接受消息线程。

对于接受线程:

  1. 创建Looper对象。
    Looper.prepare();使用Looper的prepare()方法即可创建一个Looper对象,其中也包含了MessageQueque对象的创建,当然每一个线程中只能允许拥有一个Looper对象。
  2. 初始化Handler对象,并重写其handleMessage()方法来处理接受的消息。
  3. 启动Looper。
    Looper.loop();使用Looper的loop()方法即可启动loop,这样Looper对象才能不断的管理MessageQueque。

对于发送线程:

  1. 创建携带信息的Message对象。
  2. 使用接受线程创建的Handler对象来发送消息。
    reThread.mHandler.sendMessage(msg);
简单的小程序
/**
 * 该例子是在子线程为发送消息的线程,主线程中接收消息
 */
public class HandlerTestActivity extends AppCompatActivity {

    //声明Handler变量
    private Handler mHandler ;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_handler_test);

        //开启一个接受线程
        Thread reThread = new Thread(){
          @Override
          public void run(){
              //1.使用prepare函数创建Looper对象
              Looper.prepare();
              //2.初始化Handler对象
              mHandler = new Handler(){
                  //3.重写handleMessage方法来处理接收到的消息
                  @Override
                  public void handleMessage(Message msg) {
                      //Message.what字段可以携带少量信息,这里作为身份识别信息
                      if(msg.what==0x123){
                          //接收到消息,弹出对话框提醒
                          Toast.makeText(HandlerTestActivity.this,"reThread接收到消息",Toast.LENGTH_LONG).show();
                      }
                  }
              };
              //3.使用loop函数开启Looper
              Looper.loop();
          }
        };
        //开启该接收消息的线程
        reThread.start();

    }

    /**
     * 添加一个按钮,为按钮添加点击事件
     * 在该回调函数中向接收线程发送消息
     */
    public void sendMessageFromSendThread(View view){
        //1.创建Message对象
        Message sendMsg = new Message();
        //在Message的what字段中携带少量信息,用于消息的身份识别
        sendMsg.what = 0x123;
        //使用接收线程创建的Handler对象的sendMessage函数向接收线程发送消息
        mHandler.sendMessage(sendMsg);
    }
}
   

这里有一个需要注意的问题:
我们经常使用的场景中是主线程即UI线程最为接收消息的线程,然后更新UI来相应数据的变化,在这种情况下,Looper对象的创建和启动这两个步骤可以省略,因为android本身已经默认为主线程完成了Looper相关的工作,用户只要实现Handler相关的即可。具体我们后面再做分析。

源码分析

<p>

1.Looper类

主要变量

 static final ThreadLocal<Looper> sThreadLocal = new ThreadLocal<Looper>();
 private static Looper sMainLooper;  // guarded by Looper.class

 final MessageQueue mQueue;
 final Thread mThread;

Looper构造器声明为private,不允许用户自己初始化Looper对象;自身携带了消息队列MessageQueue。

  private Looper(boolean quitAllowed) {
        mQueue = new MessageQueue(quitAllowed);
        mThread = Thread.currentThread();
  }

用户需要调用prepare函数来创建对象,可以看出prepare函数保证了一个线程中只有一个looper对象也就意味着只有一个MessageQueue消息队列。除此之外prepare函数还利用线程的ThreadLocal变量来存储该线程创建的Looper对象,使用这种机制来关联looper对象和他的调用线程。

    public static void prepare() {
        prepare(true);
    }

    private static void prepare(boolean quitAllowed) {
        //ThreadLocal为线程局部变量类
       //set方法设置线程的局部变量
        //get方法得到线程的局部变量
        if (sThreadLocal.get() != null) {
            throw new RuntimeException("Only one Looper may be created per thread");
        }
        sThreadLocal.set(new Looper(quitAllowed));
    }

loop函数开启了一个死循环,不断的从消息队列中读取消息,并将消息交给Handler对象去处理。

  /**
     * Run the message queue in this thread. Be sure to call
     * {@link #quit()} to end the loop.
     */
    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;
       //略去一部分
    
        for (;;) {
            Message msg = queue.next(); // might block
            if (msg == null) {
                // No message indicates that the message queue is quitting.
                return;
            }

            // This must be in a local variable, in case a UI event sets the logger
            Printer logging = me.mLogging;
            if (logging != null) {
                logging.println(">>>>> Dispatching to " + msg.target + " " +
                        msg.callback + ": " + msg.what);
            }
       //分发给message的target字段包含的目标handler中。
            msg.target.dispatchMessage(msg);

       //略去一部分
        }
    }

   public static Looper myLooper() {
        return sThreadLocal.get();
    }

总结Looper的作用:

  • 封装了一个消息队列
  • 将Looper对象和调用prepare函数的线程绑定
  • 保证了每一个线程只能有一个looper对象和消息队列
  • 及时取出消息队列中的消息将他交给绑定的线程中的handler进行处理。
2.Handler类

看源码中说明:

A Handler allows you to send and process Message and Runnable
objects associated with a thread's MessageQueue. Each Handler
instance is associated with a single thread and that thread's message
queue. When you create a new Handler, it is bound to the thread /
message queue of the thread that is creating it -- from that point on,
it will deliver messages and runnables to that message queue and execute
them as they come out of the message queue.

There are two main uses for a Handler: (1) to schedule messages and
runnables to be executed as some point in the future; and (2) to enqueue
an action to be performed on a different thread than your own.

Handler构造器有很多个,最终会调用到这两个。

   //没有传入looper对象时,使用Looper.myLooper()获得,前面提到过myLooper函数是从当前线程中取出绑定的looper对象。
   //然后获得该looper对象中的消息队列
   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;
    }
    public Handler(Looper looper, Callback callback, boolean async) {
        mLooper = looper;
        mQueue = looper.mQueue;
        mCallback = callback;
        mAsynchronous = async;
    }

sendMessage相关的几个函数,最终会调用sendMessageAtTime这个函数。他把得到的消息Message对象和消息队列MessageQueue交给enqueueMessage函数处理。
enqueueMessage负责在消息Message的target字段标记为自己,以便接收到的消息能够分发到自己。
接下来交给MessageQueque的enqueueMessage()方法将消息添加到消息队列中。

 public boolean sendMessageAtTime(Message msg, long uptimeMillis) {
        MessageQueue queue = mQueue;
        if (queue == null) {
            RuntimeException e = new RuntimeException(
                    this + " sendMessageAtTime() called with no mQueue");
            Log.w("Looper", e.getMessage(), e);
            return false;
        }
        return enqueueMessage(queue, msg, uptimeMillis);
    }

 private boolean enqueueMessage(MessageQueue queue, Message msg, long uptimeMillis) {
        msg.target = this;
        if (mAsynchronous) {
            msg.setAsynchronous(true);
        }
        return queue.enqueueMessage(msg, uptimeMillis);
    }

前面Looper对象取出消息队列中的消息后会根据msg.target调用该Handler的dispatchMessage函数。

/**
     * Handle system messages here.
     */
    public void dispatchMessage(Message msg) {
        //如果msg本身包含callback,交给消息的callback去处理
        if (msg.callback != null) {
            handleCallback(msg);
        } else {//如果没有,讲给Handler的callback去处理,也就是重写的handleMessage函数去处理。
            if (mCallback != null) {
                if (mCallback.handleMessage(msg)) {
                    return;
                }
            }
            handleMessage(msg);
        }
    }

总结Handler的作用:

  • 封装了将Message添加到MessageQueque的过程,即发送消息过程
  • 在发送的消息中打上自己的标签
  • 处理自己的发送的消息

MessageQueue和Message这两个组成部分比较简单,在此不做详解。看完了以上的代码部分的分析,知道这个机制原理很简单,这样再回去看看我那个不准确的小例子,你会发现很多的不合理的之处,但是用来初步理解还是可以的。

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

推荐阅读更多精彩内容