Android消息机制详解

Android消息机制

*本篇文章已授权微信公众号 guolin_blog (郭霖)独家发布
这一定是一个被写烂了的专题吧。那本媛也来凑一个热闹吧。哈哈
这篇博客将会涉及以下内容:

  • 消息机制概述
  • UML图解消息机制相关类
  • 从在主线程更新UI的方法带你畅游消息机制的源码,更加方便自己理解
  • Handler
  • Looper
  • MessageQueue和Message
  • 消息机制的应用

消息机制概述

Android系统在设计的初期就已经考虑到了UI的更新问题,由于Android中的View是线程不安全的,然而程序中异步处理任务结束后更新UI元素也是必须的。这就造成了一个矛盾,最简单的解决方法肯定是给View加同步锁使其变成线程安全的。这样做不是不可以,只是会有两点坏处:

  1. 加锁会有导致效率底下
  2. 由于可以在多个地方更新UI,开发就必须很小心操作,开发起来就很麻烦,一不小心就出错了。

基于以上两个缺点,这种方式被抛弃。于是机智如我谷歌爸爸。。。设置一个线程专门处理UI控件的更新,如果其他线程也需要对UI进行更新,不好意思,您把您想做的告诉那个专门处理UI线程的家伙,让它帮你做。大家各有各的任务,井水不犯河水,各司其职,效率才会高,不仅仅对于软件如此,人也是如此,我只管写我的代码,有农民伯伯帮我种吃的、有电脑公司卖给我电脑、有建筑公司给我盖房子(当然了,房子我是万万买不起的... 哼!),这些事儿好像自己也能干,但是都自己干好像就回到了远古时代,人类的进步和发展和社会分工的明确也是离不开的,而且关系很大!
好像扯的有点远了。

那么您也看出来了,消息机制其实可以很简单的用一句话概括,就是:其他线程通过给特定线程发送消息,将某项专职的工作,交给这个特定的线程去做。比如说其他线程都把处理UI显示的工作通过发送消息交给UI线程去做。
实现的原理呢,我是这么理解的,现在要做的主要工作就是切换线程去操作,怎么切换呢?把两条线程看作是两条并行的公路,如果要从一条公路转到另一条公路上,要怎么做呢?是不是只要找到两条公路交叉或者共用某个资源的地方,如果说交叉路口,比如说加油站。当然了,线程是不存在交叉的地方的,那么可以考虑他们公用资源的地方,不同的进程享用不同的内存空间,但是同一个进程的不同线程享用的是同一片内存空间,那让其他线程把要处理的消息放到这个特定的内存空间上,处理消息的线程来这个内存空间上来取消息去处理不就可以了吗。事实正是如此,在Android的消息机制中,扮演这个特定内存空间的对象就是MessageQueue对象,发送和处理的消息就是Message对象。其他的Handler和Looper都是为了配合线程切换用的。
其实不仅仅是线程之间,不同进程之间进行消息传递(IPC机制),也是这个思路,找到公用的一个资源点,文件系统啊,共享内存啊等等,这个以后再讲吧。

这样理解起来是不是就是so easy了呢?

UML图解消息机制相关类

不知道上面的说法您是否可以对消息机制有了一个基本的认知呢?我曾经在想,怎么通过很简洁直观的方式去把消息机制讲明白(讲给自己,也讲给你)呢,后来我就在想,当初设计者的思路是什么样的呢?我想到了UML图,用类图来对消息机制中涉及到的几个类有一个概括的认识;通过时序图,可以很清晰的观察到整个消息机制的处理过程。
消息主要设计到下面几个类:

  • Handler:这是消息的发出的地方,也是消息处理的地方。
  • Looper:这是检测消息的地方。
  • MessageQueue:这是存放消息的地方,Handler把消息发到了这里,Looper从这里取出消息交给Handler进行处理
  • Message:呜呜呜...他们发的是我,处理的也是我。
  • Thread:我在这里专门指代的是,处理消息的线程。消息的发送是在别的线程。

话不多说,先来看一张图(UML忘的差不多了,刚补的,如果有错误,麻烦大家指出)

消息机制类图

畅游源码

图在这里了,怎么看呢,整个消息机制相关的类密密麻麻支撑了一张网,咋个看嘛,,,不急不急,咱们先来思考一下咱们常用的更新UI是怎么一个操作步骤。

  1. 在主线程新建一个Handler对象,在构造方法中传入一个实现Handler.Callback接口的匿名类的对象,实现接口中的handleMessage方法
  2. 在非UI线程使用Handler的sendMessage或者post方法发送一个消息
  3. 然后handleMessage方法会在不久的将来马上执行,实现更新UI的操作。

那咱们就跟着这个思路来看一看这张图,先看Handler类,你会发现,Handler真的是个相当关键的核心(当然,其他部分也是不可或缺的),他几乎拥有所有其他相关对象的引用。

  • Handler拥有Looper的引用,通过得到Looper对象获得Looper中保存的MessageQueue对象
  • Handler拥有MessageQueue的引用,使Handler得以拥有发送消息(将Message放入MessageQueue)的能力
  • Handler拥有Handler.Callback的引用,使得Handler可以方便的进行消息的处理。

来思考一个问题:为什么Handler在其他线程发送消息之后,就跑到了主线程的handleMessage方法中去更新UI?

这个问题暂时先放着,等下回过头再来看。
我们现在先跟着第1,2,3步看看系统都帮我们做了什么操作呢?这就是在看源码,不要觉得很高深
下面是鲜活的代码,为了方便您查看,我帮您摘出来了。如果有兴趣,您也可以在AS里点开看看


    //这是在主线程中
    Handler handler = new Handler(new Handler.Callback() {
        @Override
        public boolean handleMessage(Message msg) {

            switch (msg.what) {
                case 1:
                    Toast.makeText(mContext, "你真漂亮", Toast.LENGTH_SHORT).show();
                    break;
                case 2:
                    Toast.makeText(mContext, "你也很帅呢", Toast.LENGTH_SHORT).show();
                    break;
                default:
                    break;
            }
            return false;
        }
    });
    
    //这是我们在主线程中创建Handler时会使用的构造方法
    public Handler(Callback callback) {
        this(callback, 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());
            }
        }

          //在这里获取到Looper对象,怎么获取的,稍后再看
        mLooper = Looper.myLooper();
        //如果获取的mLooper为空,直接抛出异常,说你不能在一个没有调用Looper.prepare()方法
        //的线程里创建Handler
        //如此看来,Looper.prepare()方法重要的嘞
        if (mLooper == null) {
            throw new RuntimeException(
                "Can't create handler inside thread that has not called Looper.prepare()");
        }
        //通过mLooper对象获取MessageQueue这个消息队列(单链表)
        mQueue = mLooper.mQueue;
        mCallback = callback;
        mAsynchronous = async;
    }

到此为止,一个Handler就创建好了,(还有一个问题是Looper.prepare方法很重要,但是我们还没有去考虑他是干嘛的,不急不急,先顺着一条线看,不然看源代码的过程会把你搞死翘翘的)先面就该进行第二步,看看Handler.sendMessage干了啥.代码段又来喽

    //在一个新建的线程里使用创建好的Handler发送一个消息
    new Thread(new Runnable() {
        @Override
        public void run() {
            //在这儿干点你想干的吧,一些耗时的计算或者网络操作啥的
            Message message = new Message();
            message.what = 1;
            handler.sendMessage(message);
        }
    }).start();
    
    //直接调用的是这个函数
    public final boolean sendMessage(Message msg){
        return sendMessageDelayed(msg, 0);
    }
    
    //转而调用了这个函数
    public final boolean sendMessageDelayed(Message msg, long delayMillis)
    {
        if (delayMillis < 0) {
            delayMillis = 0;
        }
        return sendMessageAtTime(msg, SystemClock.uptimeMillis() + delayMillis);
    }
    
    //转而又来到了这里
    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) {
        //target就是Message绑定的Handler,看看类图,上面有这个细节
        msg.target = this;
        if (mAsynchronous) {
            msg.setAsynchronous(true);
        }
        //最后的最后,调用了MessageQueue的enqueueMessage方法
        return queue.enqueueMessage(msg, uptimeMillis);
    }
    
    //再看一下MessageQueue的enqueueMessage方法,
    //我把其他一些无关的细节给删掉了,只为了更加容易阅读
    boolean enqueueMessage(Message msg, long when) {
        synchronized (this) {
            Message p = mMessages;
            if (p == null) {
                // New head, wake up the event queue if blocked.
                msg.next = p;
                mMessages = msg;
            } else {
                //下面的错误就是遍历单链表,找到链表的尾部,这个没有难度的吧?
                Message prev;
                for (;;) {
                    prev = p;
                    p = p.next;
                    if (p == null || when < p.when) {
                        //找到了尾部,现在的结构是这样的。
                        //A->B->C...->pre(p)->null
                        break;
                    }
                }
                msg.next = p; // invariant: p == prev.next
                prev.next = msg;//把next插入链表的尾部
            }
        }
        return true;
    } 

到此为止,第二步就结束了,成功把一个消息插入到了MessageQueue的尾部。可是你很快就会发现,第三步好像从这条路探寻不下去了。接下来就等着别人来调用Handler中的方法了,可是是谁调用的,在哪儿调用的?我们现在好像毫无头绪了?怎么办?怎么办?我们刚才不是看到一个Looper.myLooper(),和Looper.prepare()方法,说是很重要但是一直没看吗,既然现在搜寻不下去了,是不是可以回头看看了?还有一点,Looper,看起来是在循环,循环什么玩意儿呢?我们去好好看看Looper类吧。
一共就三百来行代码,仔细看看,你会发现有一个核心方法:Looper.loop();
同样的,我把影响阅读的非主线代码剔除了,发现Looper.loop方法就长这样:

    /**
     * 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;
            }
            try {
                msg.target.dispatchMessage(msg);
            } finally {
                
            }
        }
    }

清晰可见的是,Looper.loop()方法一直遍历MessqgeQueue,阻塞线程,直到获取到一个Message,然后调用了Message的一个成员变量target(其实就是Handler)的dispatchMessage(msg)方法,嗨,还真的又跟Handler扯上关系了,既然这里扯上关系了,而且还是一个分发消息的方法,可以大胆猜测就是让Handler去处理这个消息的。
那么我们来看看这个方法:

    /**
     * Handle system messages here.
     * 如果Message中callback对象不为空(这是调用handler.post(Runnable)方法发送的消息),
     * 就调用callback的run方法
     * 否则如果创建Handler的时候如果设置了Callback就调用创建时候的传入的
     * 实现Handler.Callback接口的类的对象的handleMessage方法,看这就是回调方法被调用的地方。
     * 再如果没有mCallback对象,就调用自身的handleMessage方法,为了Handler的子类复写了该方法的时候,方便调用,如,IntentService里的ServiceHandler就是继承自Handler的,并且重写了handleMessage方法。
     */
    public void dispatchMessage(Message msg) {
        if (msg.callback != null) {
            handleCallback(msg);
        } else {
            if (mCallback != null) {
                if (mCallback.handleMessage(msg)) {
                    return;
                }
            }
            handleMessage(msg);
        }
    }
    
    private static void handleCallback(Message message) {
        message.callback.run();
    }
    
    //ServiceHandler继承自Service并且重写了handleMessage方法
    private final class ServiceHandler extends Handler {
        public ServiceHandler(Looper looper) {
            super(looper);
        }

        @Override
        public void handleMessage(Message msg) {
            onHandleIntent((Intent)msg.obj);
            stopSelf(msg.arg1);
        }
    }
    

到了这里,三步走已经看完了,我想消息机制在我们心里已经又清晰了一层,但是不用急,咱们前边提的一个问题不是还没有解决吗,先把他解决掉吧,一起继续来看源码。
咱们现在已知的是这样的,在主线程创建的Handler发送了一个消息,发送消息的代码运行在其他线程,将代码加入消息队列也是在其他线程(加了线程同步锁)。然后handleMessage发生在主线程,那么调用该方法的dispatchMessage方法也是运行在主线程的,dispatchMessage是在Looper.loop方法中调用的,也就是说loop方法也运行在主线程,那么问题就明朗了,可是loop方法是谁调用的,在哪里调用的呢?当然是系统启动的时候创建主线程之后再主线程的run方法中调用了Looper.prepare和Looper.loop方法,但是这点我还没看,留着以后再看吧。
然后通过上面的分析,我们是不是可以自己来试着建立这样一个模型:

  1. 创建一个线程A
  2. 在这个线程的run方法中调用Looper.prepare和Looper.loop方法使该线程阻塞,等待消息发过来,然后处理
  3. 在该线程中创建一个Hanlder,用来处理looper发送来的待处理的消息
  4. 创建一些其他的线程a、b、c,做一点操作之后,通过Handler把消息传递出去,让线程A去处理。
public class MyThread extends Thread {
    private static final String TAG = "MyThread";

    Handler handler = new Handler(new Handler.Callback() {
        @Override
        public boolean handleMessage(Message msg) {
            switch (msg.what) {
                case 1:
                    Log.i(TAG,"你真漂亮");
                    break;
                case 2:
                    Log.i(TAG,"你也很帅呢");
                    break;
                default:
                    break;
            }
            return false;
        }
    });


    public Handler getHandler() {
        return handler;
    }

    @Override
    public void run() {
        super.run();
        Looper.prepare();
        Looper.loop();
    }

    @Override
    public void destroy() {
        super.destroy();
        Looper.myLooper().quit();
    }
}

//
    private void testMyThread() {
        MyThread thread = new MyThread();
        thread.start();
        final Handler handler = thread.getHandler();
        
        Message message = new Message();
        message.what = 1;
        handler.sendMessage(message);

        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    sleep(400);
                    Message message = new Message();
                    message.what = 2;
                    handler.sendMessage(message);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }).start();
    }

试着想一想,如果把线程A看成主线程,在回调方法更新UI,那这不就是Android系统中更新UI使用的套路吗?不错,事实本就如此,工具是工具,用它来更新UI是可以的,那你当然也可以用来做一些其他的工作啊。

在这里,通过以上的分析,不难得到下面的这个整个消息机制运行过程的时序图:

[图片上传失败...(image-6a9973-1522052528840)]

ok啦,源码阅读到此为止。其他的细节,有兴趣的可以再细细研究一下。
下面我们来对涉及到的类进行一下总结。
先来说一下我的个人理解,

Handler

handler在消息机制中,扮演的是消息的发送方和处理方(通过回调函数)。消息在一个线程通过Handler发送到MessageQueue中。Looper获取到Message之后,根据Message中保存的handler对象调用handler对象的dispatch方法,进行消息的处理。

Looper

Looper在这里扮演的是一个轮询消息队列的角色,以为不停地去问MessageQueue要消息,得到消息之后,根据Message中保存的handler对象调用handler对象的dispatch方法,进行消息的处理。

MessageQueue和Message

MessageQueue实质上是一个单链表的结构,里面以链表的形式保存着Handler发送过来的消息,当有新消息发来时放在链表的尾部,Looper要取消息的时候从链表的头部取出消息返回给Looper处理。Message对象中保存在要处理的信息,同时也持有消息发送方(Handler)的引用,Looper在得到该Message的时候,可以从Message中拿到消息的发送方,调用发送方的回调方法将消息传递过去交给Handler进行处理。

消息机制的应用

在Android中有很多消息机制的应用,如:

  • UI的更新
  • HandlerThread
  • IntentService

UI的更新

UI线程持有一个Looper对象,Looper对象的loop方法在UI线程中一直不停的进行死循环,直到有新的消息发来的时候,交给特定的组件进行处理,当然了这个处理也是在主线程运行的(如我们设置的点击事件也是等着被UI线程调用的),正是由于这个原因,我们不能在主线程处理耗时操作。如果我们一个View的点击事件里做了大量耗时的操作,由于这个操作也在主线程中运行,主线程必须等着这个操作操作结束才能去处理其他的消息,这个时候表现的就是系统卡顿甚至报ANR的错误。

HandlerThread

HandlerThread继承自Thread,内部保存一个Looper对象。
这是一个系统帮我们包装好的Thread,这个线程的run方法已经调用了Looper.prepare和Looper.loop(即已经绑定了一个Looper对象,并且可以开始轮询消息),创建该对象之后可以通过获得对象获取到一个Looper对象,将Looper对象传递给Handler,完成Handler和Looper以及MessageQueue的绑定。最后再其他的线程中调用Handler的sendMessage或者post(Runable)方法发送消息,handler中的callback.handleMessage方法会在HandlerThread中运行。即,将消息发送到了特定的线程(此处是HandlerThread)处理。

IntentService

IntentService继承自Service,运行时优先级更高,内部使用了HandlerThread作为处理消息的线程。内部有一个私有内部类ServiceHandler继承自Handler,并且会创建一个ServiceHandler对象。
使用startService()方法启动IntentService时,不会重新创建一个服务,会调用ServiceHandler对象发送包含该Intent的Message对象,该对象通过HandlerThread处理后交给ServiceHandler重写的handleMessage方法进行处理,处理的方式是调用IntentService的onHandleIntent(Intent)方法,所以使用的方式就是创建一个继承自IntentService类的子类,并重写onHandleIntent方法,在该方法中处理startService时传递的Intent。Intent中包含有要交给Service处理的信息。

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

推荐阅读更多精彩内容