Android 开发艺术探索读书笔记 10 -- Android 的消息机制

本篇文章主要介绍以下几个知识点:

  • Android 的消息机制概述
  • Android 的消息机制分析
  • 主线程的消息循环
hello,夏天 (图片来源于网络)

Android 的消息机制主要是指 Handler 的运行机制,Handler 的运行需要底层的 MessageQueueLooper 的支撑。

MessageQueue是消息队列,其内部存储了一组消息,以队列的形式对外提供插入和删除的工作。(采用单链表的数据结构来存储消息列表

Looper是消息循环,以无限循环的形式去查找是否有消息,若有消息就处理,否则一直等待。

10.1 Android 的消息机制概述

Handler 的主要作用是将一个任务切换到某个指定的线程中去执行,Android 提供这个功能主要是为了解决在子线程中无法访问 UI 的矛盾。


问题 1:Android 为什么要提供 Handler 这个功能?

答:Android 规定访问 UI 只能在主线程中进行,若在子线程中会抛出异常:

void checkThread(){
    if(mThread != Thread.currentThread()){
        throw new CalledFromWrongThreadException("Only the original thread that created a view hierarchy can touch its views.");
    }
}

但 Android 又不建议在主线程中进行耗时操作(会导致ANR),从而提供 Handler 解决上面的矛盾。

问题 2:不允许在子线程中访问 UI ?

答:Android 的 UI 控件不是线程安全的。


Handler 的工作原理:

1. Handler 创建时会采用当前线程的 Looper 来构建内部的消息循环系统。

2. 通过Handlerpost 方法将一个 Runnable 投递到 Handler 内部的 Looper 中去处理,或通过 Handlersend 方法发消息到 Looper 中去处理(post 方法最终也是通过 send 方法来完成)。

3. Handlersend 方法被调用时,它会调用 MessageQueueenqueueMessage 方法将消息放入消息队列中,Looper 发现新消息时会处理,最终消息中的 RunnableHandlerhandleMessage 就会被调用。

Handler 的工作过程

10.2 Android 的消息机制分析

10.2.1 ThreadLocal 的工作原理

ThreadLocal 是一个线程内部的数据存储类,通过它可在指定的线程中存储数据,数据存储后,只能在指定线程中可获取存储的数据。

ThreadLocal 的使用场景:当某些数据是以线程为作用域并且不同线程具有不同的数据副本时;复杂逻辑下的对象传递。


下面举个例子来演示 ThreadLocal 的含义。

首先定义一个 Boolean 类型的 ThreadLocal 对象如下:

private ThreadLocal<Boolean> mBooleanThreadLocal = new ThreadLocal<>();

然后分别在主线程、子线程1、子线程2 中设置和访问它的值:

       // 在主线程中设为 true
        mBooleanThreadLocal.set(true);
        LogUtils.e(TAG, "[Thread#main]mBooleanThreadLocal = " + mBooleanThreadLocal.get());

        new Thread("Thread#1"){
            @Override
            public void run() {
                // 在子线程1中设为 false
                mBooleanThreadLocal.set(false);
                LogUtils.e(TAG, "[Thread#1]mBooleanThreadLocal = " + mBooleanThreadLocal.get());
            }
        }.start();

        new Thread("Thread#2"){
            @Override
            public void run() {
                // 在子线程2中不设置
                LogUtils.e(TAG, "[Thread#2]mBooleanThreadLocal = " + mBooleanThreadLocal.get());
            }
        }.start();

运行效果如下:

运行打印的日志

从上面的日志可看出,虽然在不同线程访问的是同一个 ThreadLocal 对象,但获取到的值却是不一样的,这就是它的奇妙之处。

上面效果,是因为不同线程访问同一个 ThreadLocalget 方法,ThreadLocal 内部会从各自的线程中取出一个数组,再从数组中去查找出对应的 value 值。


下面分析 ThreadLocal 的内部实现。

ThreadLocal 是一个泛型类,其定义为public class ThreadLocal<T>,只要清楚它的 getset 方法就可明白其工作原理。

有兴趣的可以去看看其源码。。。

结论:在不同线程中访问同一个 ThreadLocalgetset 方法,其所做的读写操作仅限于各自线程的内部,从而使 ThreadLocal 可在多个线程中互不干扰的存储和修改数据。

10.2.2 消息队列的工作原理

MessageQueue 含两个操作:插入 enqueueMessage 和读取 next (读取操作本身伴随着删除操作)。

首先看其 enqueueMessage 实现:

    boolean enqueueMessage(Message msg, long when) {
        . . .
        synchronized (this) {
            . . .
            msg.markInUse();
            msg.when = when;
            Message p = mMessages;
            boolean needWake;
            if (p == null || when == 0 || when < p.when) {
                // New head, wake up the event queue if blocked.
                msg.next = p;
                mMessages = msg;
                needWake = mBlocked;
            } else {
                // Inserted within the middle of the queue.  Usually we don't have to wake
                // up the event queue unless there is a barrier at the head of the queue
                // and the message is the earliest asynchronous message in the queue.
                needWake = mBlocked && p.target == null && msg.isAsynchronous();
                Message prev;
                for (;;) {
                    prev = p;
                    p = p.next;
                    if (p == null || when < p.when) {
                        break;
                    }
                    if (needWake && p.isAsynchronous()) {
                        needWake = false;
                    }
                }
                msg.next = p; // invariant: p == prev.next
                prev.next = msg;
            }

            // We can assume mPtr != 0 because mQuitting is false.
            if (needWake) {
                nativeWake(mPtr);
            }
        }
        return true;
    }

enqueueMessage 实现来看,其主要操作是单链表的插入操作,下面看 next 的实现:

    Message next() {
        . . .
        int pendingIdleHandlerCount = -1; // -1 only during first iteration
        int nextPollTimeoutMillis = 0;
        for (;;) {
            if (nextPollTimeoutMillis != 0) {
                Binder.flushPendingCommands();
            }

            nativePollOnce(ptr, nextPollTimeoutMillis);

            synchronized (this) {
                // Try to retrieve the next message.  Return if found.
                final long now = SystemClock.uptimeMillis();
                Message prevMsg = null;
                Message msg = mMessages;
                if (msg != null && msg.target == null) {
                    // Stalled by a barrier.  Find the next asynchronous message in the queue.
                    do {
                        prevMsg = msg;
                        msg = msg.next;
                    } while (msg != null && !msg.isAsynchronous());
                }
                if (msg != null) {
                    if (now < msg.when) {
                        // Next message is not ready.  Set a timeout to wake up when it is ready.
                        nextPollTimeoutMillis = (int) Math.min(msg.when - now, Integer.MAX_VALUE);
                    } else {
                        // Got a message.
                        mBlocked = false;
                        if (prevMsg != null) {
                            prevMsg.next = msg.next;
                        } else {
                            mMessages = msg.next;
                        }
                        msg.next = null;
                        if (DEBUG) Log.v(TAG, "Returning message: " + msg);
                        msg.markInUse();
                        return msg;
                    }
                } else {
                    // No more messages.
                    nextPollTimeoutMillis = -1;
                }
               . . .
            }
            . . .
        }
    }

可以发现 next 是一个无限循环的方法,若消息队列中无消息, next 方法会一直阻塞在这里。当有新消息时,next 方法会返回这条消息并将其从单链表中移除。

10.2.3 Looper 的工作原理

Looper 扮演着消息循环的角色,会不停地从 MessageQueue 中查看是否有新消息,若有会立刻处理,否则一直阻塞在那里。

首先看一下其构造方法:

    private Looper(boolean quitAllowed) {
        // 创建一个消息队列 MessageQueue
        mQueue = new MessageQueue(quitAllowed);
        // 将当前线程的对象保存起来
        mThread = Thread.currentThread();
    }

接着看一下其最重要的一个方法 loop,只有调用了 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;

        // Make sure the identity of this thread is that of the local process,
        // and keep track of what that identity token actually is.
        Binder.clearCallingIdentity();
        final long ident = Binder.clearCallingIdentity();

        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
            final Printer logging = me.mLogging;
            if (logging != null) {
                logging.println(">>>>> Dispatching to " + msg.target + " " +
                        msg.callback + ": " + msg.what);
            }

            final long traceTag = me.mTraceTag;
            if (traceTag != 0 && Trace.isTagEnabled(traceTag)) {
                Trace.traceBegin(traceTag, msg.target.getTraceName(msg));
            }
            try {
                msg.target.dispatchMessage(msg);
            } finally {
                if (traceTag != 0) {
                    Trace.traceEnd(traceTag);
                }
            }

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

            // Make sure that during the course of dispatching the
            // identity of the thread wasn't corrupted.
            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.recycleUnchecked();
        }
    }

loop 方法是一个死循环,唯一跳出循环的方式是 MessageQueuenext 方法返回了 null。

MessageQueue 返回了新消息,Looper 就会处理这条消息:

msg.target.dispatchMessage(msg)

这样 Handler 发送的消息最终又交给它的 dispatchMessage 方法来处理了。


Handler 的工作需要 Looper,没有 Looper 的线程就会报错,那如何为一个线程创建 Looper 呢?

其实很简单,如下:

new Thread(){
    @Ovrttide
    public void run(){
        // 为当前线程创建一个 Looper
        Looper.prepare();
        Handler handler = new Handler();
        // 开启消息循环
        Looper.loop();
    }
}.start();

Looper 除了 prepare 外,还提供了 prepareMainLooper 方法给主线程创建 Looper,其本质也是通过 prepare 实现的。

Looper 提供了 getMainLooper,可以在任何地方获取到主线程的 Looper

Looper 提供两种方式退出:quit(直接退出)、quitSafely(设定一个退出标记,消息处理完毕后才安全退出)。

建议:不需要的时候终止 Looper


10.2.4 Handler 的工作原理

Handler 的工作主要包含消息的发送和接收过程。

消息发送可通过 post(最终也是通过 send 完成) 及 send 方法来实现。其过程如下:

    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) {
        msg.target = this;
        if (mAsynchronous) {
            msg.setAsynchronous(true);
        }
        return queue.enqueueMessage(msg, uptimeMillis);
    }

可以发现,Handler 发消息时向消息队列中插入一条消息,MessageQueuenext 方法返回这条消息给 LooperLooper 收到消息并处理再交由 HandlerdispatchMessage 方法进入处理消息阶段:

    public void dispatchMessage(Message msg) {
        // 1. 检查 Message 的 callback 是否为 null
        // 不为 null 就通过 handleCallback 来处理消息
        if (msg.callback != null) {
            handleCallback(msg);
        } else {
            // 2. 检查 mCallback 是否为 null
            // 不为 null 就调用其 handleMessage 来处理消息
            if (mCallback != null) {
                if (mCallback.handleMessage(msg)) {
                    return;
                }
            }
            // 3. 调用 Handler 的 handleMessage 来处理消息
            handleMessage(msg);
        }
    }

其中 handleCallback 逻辑如下:

private static void handleCallback(Message message){
    message.callback.run();
}

其中 mCallback 是个接口,定义如下:

/**
 *  可以用来创建一个 Handler 实例但不需要派生其子类
 */
public interface Callback{
    public boolean handleMessage(Message msg);
}

综上,Handler 处理消息过程可归纳成如下:

Handler 消息处理流程图

Handler 的构造方法很多,它的一个默认构造方法 public Handler() 会调用下面的构造方法:

    public Handler(Callback callback, boolean async) {
        . . .
        mLooper = Looper.myLooper();
        if (mLooper == null) {
            // 若当前线程无 Looper 的话就会抛出异常
            throw new RuntimeException(
                "Can't create handler inside thread that has not called Looper.prepare()");
        }
        mQueue = mLooper.mQueue;
        mCallback = callback;
        mAsynchronous = async;
    }

上面解释了在没有 Looper 的子线程中创建 Handler 会报错的原因。

10.3 主线程的消息循环

Android 的主线程就是 ActivityThread,其入口是在 main 方法中通过 Looper.prepareMainLooper() 来创建主线程的 Looper 以及 MessageQueue,并通过 Looper.loop() 来开启主线程的消息循环,如下:

public static void main(String[] args) {
        . . .
        Process.setArgV0("<pre-initialized>");

        Looper.prepareMainLooper();

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

        if (sMainThreadHandler == null) {
            sMainThreadHandler = thread.getHandler();
        }

        if (false) {
            Looper.myLooper().setMessageLogging(new
                    LogPrinter(Log.DEBUG, "ActivityThread"));
        }

        // End of event ActivityThreadMain.
        Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
        Looper.loop();

        throw new RuntimeException("Main thread loop unexpectedly exited");
    }

主线程的消息循环开始后,ActivityThread 还需要一个 ActivityThread.H (内部定义了一组消息类型) 来和消息队列进行交互。

ActivityThread 通过 ApplicationThreadAMS 进行线程间通信的方式完成 ActivityThread 的请求后会回调 ApplicationThread 中的 Binder 方法,然后ApplicationThread 会向 H 发送消息,H 收到消息后将 ApplicationThread 中的逻辑切换到 ActivityThread (主线程)中去执行。

以上便是主线程的消息循环模型。

本篇文章就介绍到这。

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

推荐阅读更多精彩内容

  • 前言:对于熟悉Android消息机制的小伙伴,可以跳到最后,看主线程的消息循环。 Android的消息机制主要是指...
    HuDP阅读 2,006评论 11 11
  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,468评论 25 707
  • 飘飘洒洒,樱花从枝叶间飘落;摇摇晃晃中,我们告别了懵懂的童年,恍惚之际,青春早已到来。可青春到底是什么呢?青春又...
    浪瘾F阅读 695评论 0 1
  • 所有可以称为道理的道理,不过是巫医开给将死之人的药方,或许救赎。可如果巫医也病了,那药方不过是灶下的一捧灰或天上落...
    四月的夏木阅读 159评论 0 0
  • 人生是一条漫长的路,充满荆棘与坎坷。如果说曲红比直线美,在于曲线是有转折的,富有流动的韵味。那么人生也就正是因为荆...
    D030鱼佛山阅读 148评论 0 0