Android的Handle消息机制|SquirrelNote

前言

Android的消息机制主要是指Handler的运行机制。Handler的运行需要底层的MessageQueue消息队列和Looper消息循环的支撑。简单来说,Android的消息机制就是:Handler给MessageQueue添加消息,然后Looper无限循环读取消息,再调用Handler处理消息。下面将从细节方面进行详细描述:

  1. 为什么会有消息机制
  2. 消息机制概述
  3. 消息机制源码分析

一、为什么会有消息机制

在Android的UI主线程中,不能执行超出5秒的耗时任务,并且所有View和ViewGroup都只能在UI主线程中运行。如果View或者ViewGroup在工作线程中运行,将会抛出【Only the original thread that created a view hierarchy can touch its views】异常,所以在android中会通过消息机制来解决线程和线程之间的通信问题。

二、消息机制概述

消息机制由以下四个部分组成:

  • Message消息(数据载体)
  • Handler消息处理器
    发送消息
    处理消息
  • MessageQueue消息队列(存储消息)
  • Looper轮循器
    去MessageQueue取消息
    分发给Handler处理

Message、MessageQueue、Looper、Handler之间的关系(即Handler机制原理)

Activity只要一创建Thread里面就多了一个looper(消息轮循器),在主线程里面要定义一个内部类handler,handler的初始化在主线程,有很多子线程要更新UI,Thread拿到主线程的handler,调用sendMessage方法,发送消息,把消息放到消息队列里面,只要消息进到消息队列,looper就会把消息取出来,调用里面的loop方法,取出来的消息就交给handler里面的handlerMessage方法,处理消息.
之所以这样做的原因是因为避免多线程并发更新UI线程所产生的问题的,如果我们允许其他子线程都可以更新界面,那么势必会造成界面的错乱(因为没有加锁机制),如果我们加锁,又会影响速度,而使用Handler机制,所有更新UI的操作,都是在主线程消息队列中轮询去处理的。

Message

Message消息,又叫task任务。封装了任务携带的信息和处理该任务的Handler。
Message的用法:

  • 可以通过Message.obtain()来从消息池中获得空消息对象
  • 如果message只需要携带简单的int信息,使用Message.arg1和Message.arg2来传递信息,这比用Bundle更省内存
  • 用message.what来标识信息,以便用不同方式处理message。
Handler

什么是Handler?Handler扮演了往MessageQueue中添加消息和处理消息的角色(只处理由自己发出的消息),即通知MessageQueue它要执行的任务(sendMessage),并在loop到自己的时候执行该任务(handMessage),整个过程是异步的。Handler创建时会关联一个looper,默认的构造方法会关联当前线程的looper。
handler必须关联一个looper才会起作用,Android UI主线程关联了一个Loop线程,但是我们自定义的线程必须开启Loop。

Handler发送消息
Handler能发送两种消息:一种是Runnable对象,一种是Message对象。但其实post发出的Runnable对象最后都被封装成message对象了。
具体:Handler创建完毕后,其内部的Looper以及MessageQueue就可以和Handler一起协同工作了,然后通过Handler的post方法将一个Runnable投递到Handler内部的Looper中去处理,也可以通过Handler的send方法发送一个消息,这个消息也会在Looper中去处理。其实post方法最终也是通过send方法来完成的。

  1. send方法发送消息(需要回调才能接收消息)
    sendMessage()立即发送Message到消息队列
    sendMessageAtFrontOfQueue() 立即发送Message到队列,而且是放在队列的最前面
    sendMessageAtTime() 设置时间,发送Message到队列
    sendMessageDelayed() 在延时若干毫秒后,发送Message到队列

send方法的工作过程:当Handler的send方法被调用时,它会调用MessageQueue的enqueueMessage方法将这个消息放到消息队列中,然后Looper发现有新消息到来时,就会处理这个消息,最终消息中的Runnable或者Handler的handleMessage方法就会被调用。注意:Looper是运行在创建Handler所在的线程中的,这样Handler中的业务逻辑就被切换到创建Handler所在的线程中去执行了。

  1. post方法发送消息(直接绑定handler当前线程执行,需要Runnable对象)
    post() 立即发送Message到消息队列
    postAtFrontOfQueue() 立即发送Message到队列,而且是放在队列的最前面
    postAtTime() 设置时间,发送Message到队列
    postDelayed() 在延时若干毫秒后,发送Message到队列

Activity中有一个方法runOnUiTheread()实际上就是Handler的post方法。

MessageQueue

消息队列,它的内部存储了一组消息,以队列的形式对外提供插入和删除的工作,但是它的内部存储结构并不是真正的队列,而是采用单链表的数据结构来存储消息列表。

Looper

消息循环。由于MessageQueue只是一个消息存储单元,它不能去处理消息,而Looper就填补了这个功能,Looper会以无限循环的形式去查找是否有新消息,如果有的话就处理消息,否则就一直等待着。

Looper中有一个特殊的概念:ThreadLocal,它的作用是可以在每个线程中存储数据。Handler创建的时候采用当前线程的Looper来构造消息循环系统,那么Handler内部如何获取到当前线程的Looper呢?这就要使用ThreadLoacal了,ThreadLocal可以在不同的线程中互不干扰地存储并提供数据,通过ThreadLoacal可以轻松获取每个线程的Looper。

注意:线程默认没有Looper的,如果需要使用Handler就必须为线程创建Looper。我们经常提到主线程,也叫UI线程,它就是ActivityThread,ActivityThread被创建时就会初始化Looper,所以在主线程中默认可以使用Handler。

三、消息机制源码分析

Message消息
两种创建方式:

Message msg1=Message.obtain();
Message msg2=new Message();

从源码上看:

/** 
*空的构造方法
*/
public Message() {
}

Handler
伪代码如下:

new Handler(){
  handleMessage(Message msg){
    //处理消息
  }
};

下面从源码角度看,new Handler做了哪些操作:
Handler的构造方法

public Handler() {
...
    //获取Looper(是在ActivityThread里面设置的Looper),只要一new Handler就会从当前主线程去拿Looper,在子线程去new会报错
    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 = null;
}

主线程设置Looper,在ActivityThread类里面

public static final void main(String[] args) {
...
    //第一步:主线程创建Looper
    //点进去,ActivityThread是运行在主线程的,调用了prepareMainLooper()这里面的方法,给当前线程set一个Looper,我们在
    //new Handler的时候,去Looper.myLooper,拿到的是主线程的Looper,这就是我们主线程的Looper何时去创建的。
    Looper.prepareMainLooper();
    if (sMainThreadHandler == null) {
        sMainThreadHandler = new Handler();
    }

    ActivityThread thread = new ActivityThread();
    thread.attach(false);

    if (false) {
        Looper.myLooper().setMessageLogging(new
                LogPrinter(Log.DEBUG, "ActivityThread"));
    }
    ////主线程创建Looper完成以后,就调用Looper的loop方法
    Looper.loop();
...

Looper

 /** 
  *既然知道它是从prepare方法里面放的,就要知道哪个地方调用prepare方法
  */
public static final void prepare() {
    if (sThreadLocal.get() != null) {
        throw new RuntimeException("Only one Looper may be created per thread");
    }
    //第三步:在主线程中设置Looper。new Looper()里面同时创建了一个MessageQueue,这样就知道消息队列在哪new出来的
    sThreadLocal.set(new Looper());
}

public static final void prepareMainLooper() {
    //第二步: 调用prepare()方法
    prepare();
    setMainLooper(myLooper());
    if (Process.supportsProcesses()) {
        myLooper().mQueue.mQuitAllowed = false;
    }
}

主线程调用Looper.loop()方法,主线程就会阻塞,是一个死循环,使用管道(Pipe),管道是Linux中的一种进程间通信的方式。管道的原理:使用了特殊的文件,文件里面有两个文件描述符(一个是读取,一个是写入)。
应用场景:当Linux下有两个进程需要通信时,主进程拿到读取的描述符等待读取,没有内容就阻塞,然后另一个进程拿到写入描述符去写内容,唤醒主进程,主进程拿到读取描述符读取到的内容,继续执行。

管道在Handler的应用场景:Handler在主线程中创建,Looper会在死循环里等待取消息,一种是没取到消息,就阻塞;一种是主线程一旦被子线程唤醒,取到消息,就把Message交给Handler去处理。子线程是用Handler去发送消息,拿写入描述符去写消息,写完之后就唤醒主线程。

public static final void loop() {
...
    //死循环
    while (true) {
        //每次从队列中去取下一个数据(消息),要看什么时候给队列赋值,(在Handler创建的时候,就已经给它设置了队列)
        Message msg = queue.next(); // might block,取消息,如果没有消息,会阻塞
        //if (!me.mRun) {
        //    break;
        //}
        if (msg != null) {
            if (msg.target == null) {
                // No target is a magic identifier for the quit message.
                return;
            }
            if (me.mLogging!= null) me.mLogging.println(
                    ">>>>> Dispatching to " + msg.target + " "
                    + msg.callback + ": " + msg.what
                    );
            //msg.target其实就是一个Handler,Handler就会调用dispatchMessage方法。
            msg.target.dispatchMessage(msg);
            ...
        }
    }
}

Handler发送消息代码:

public boolean sendMessageAtTime(Message msg, long uptimeMillis)
{
...
        if (queue != null) {
        //this是当前的Handler类,这样每个Message里面有一个target,target值就是当前的Handler
        //把message的target置为当前发送的Handler,以便Looper取到message后根据target把message分发给相对应的Handler
        msg.target = this;
        //往队列里面添加Message
        sent = queue.enqueueMessage(msg, uptimeMillis);
    }
...
    return sent;

MessageQueue.enqueueMessage代码

final boolean enqueueMessage(Message msg, long when) {
...
        //when代表发送的时间
        msg.when = when;
        //Log.d("MessageQueue", "Enqueing: " + msg);
        Message p = mMessages;
        //when=0,不延迟,当前发送的message消息需要马上处理(当前只有一个Message),needWake置为true
        if (p == null || when == 0 || when < p.when) {
            msg.next = p;
            mMessages = msg;
            //需要唤醒
            needWake = mBlocked; // new head, might need to wake up
        } else {//当前有很多Message,如果当前Message排在其他message后面,当前Message不用优先处理,不用唤醒主线程,needWake置为false
            Message prev = null;
            while (p != null && p.when <= when) {
                prev = p;
                p = p.next;
            }
            msg.next = prev.next;
            prev.next = msg;
            needWake = false; // still waiting on head, no need to wake up
        }
    }
    //是否唤醒主线程,如果要唤醒,调用底层的jni方法去唤醒
    if (needWake) {
        nativeWake(mPtr);
    }
    return true;
}

Handler.dispatchMessage()方法:

public void dispatchMessage(Message msg) {
    if (msg.callback != null) {
        handleCallback(msg);
    } else {
        //如果有runnable就调用这个
        if (mCallback != null) {
            if (mCallback.handleMessage(msg)) {
                return;
            }
        }
        //如果没有runnable就调用handleMessage方法,把Message交给Handle处理
        handleMessage(msg);
    }
}

提问:Handler与Looper的对应关系,是多对一。那么每个handler发送的消息,怎么保证不会处理别的handler发送的消息呢?

Handler底层用到了管道的通信方式,Handler在sendMessage的时候,会把Message的target置为Handler,在调用Looper的loop方法的时候,找到每个message,调用message的target,然后去dispatchMessage,这样Looper就会根据每个message找到需要相对应处理message的handler,因为在一个应用里面可能用到多个Handler,所以这个地方通过message的target去区分每个handler需要处理它自己发送的message。

小结:

大致流程:
在主线程创建Handler的时候,就可以拿到主线程的Looper,主线程创建完Looper之后,就会执行Looper中的loop方法,loop方法就会从MessageQueue消息队列中一个一个去拿Message消息,它是一个死循环。死循环里面使用了管道的通信方式,管道里面就会拿到文件描述符,一个是往里面存,一个是往里面写。存的时候,是通过上层的Handler,去sendMessage发送消息,调用MessageQueue的enqueueMessage方法,这样就拿到写入的描述符往里面写了,在loop方法里面有queue.next()方法,这个方法会拿到读取的描述符,当它没有读到消息时就阻塞,读到有消息的时候就调用dispatchMessage方法,dispatchMessage方法就会调用handleMessage方法处理消息

下面是Handler的流程图,描述了Handler消息机制:


image.png

最后

我们可以把Handler消息机制理解为生产消费模型:

  1. Handler是生产者,主要往MessageQueue中存放消息;
  2. Looper为消费者,主要从MessageQueue中消费消息;

以上是根据我的一些理解,做的总结分享,旨在抛砖引玉,希望有更多的志同道合的朋友一起讨论学习,共同进步!

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念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

推荐阅读更多精彩内容