书海拾贝|开发艺术探索之 android 的消息机制

提到消息机制读者应该都不陌生……从开发角度来说, Handler 是 Android 消息机制的上层接口,这使得在开发过程中只需要和 Handler 交互即可。……通过它可以轻松将一个任务切换到 Handler 所在的线程中去执行。

正如开篇词所说,“主线程中不能进行网络通信等耗时操作,而子线程中不能进行 UI 更新”,是我在 android 开发入门遇到的第一个知识点(keng),但当时只是单纯记忆,本篇将顺着开发艺术探索的讲述,梳理 android 的消息机制有关知识。

开篇知识小点

Handler 是 Android 消息机制的上层接口,使用场景通常是更新 UI。
Android 消息机制主要指 Handler 的运行机制,Handler 的运行需要底层的 MessageQueue 和 Looper 的支持。

  • MessageQueue:消息队列,内部存储一组消息,以队列形式对外提供插入和删除的工作,但其内部实现并非队列,而是单链表的数据结构实现的,是一个消息的 存储单元,不能主动处理消息。
  • Looper:消息循环,以无限循环的形式查找是否有新消息,有的话就处理,否则等待。
  • ThreadLocal:Looper中的一个特殊概念,作用是可以在不同线程中互不干扰地存储数据。Handler 创建的时候需要采用当前进程的 Looper 来构造消息循环系统,此时通过 ThreadLocal 可以轻松获取每个线程的 Looper。

注意:线程是默认没有 Looper 的,如果需要使用 Handler 就必须为线程创建 Looper。主线程,即UI线程,是 ActivityThread ,ActivityThread 被创建时就会初始化Looper,所以主线程中默认可以使用 Handler。

概述

几乎所有的 Android 开发者都知道在 Android 中访问 UI 只能在主线程中进行。CheckThread() 方法会对线程调用 UI 操作的正确性做出验证,如果当前访问 UI 的线程并非主线程,则会抛出异常。
但, Android 建议不要在主线程使用耗时操作,以免导致程序无法响应,即ANR。在开发工作中,我们常常会遇到需要从服务端拉取信息,并在 UI 中进行显示。Handler 的存在就是为了解决在子线程中无法访问 UI 的矛盾。

  • 为什么在子线程中不允许访问 UI 呢?因为 Android 的 UI 控件并非线程安全的,如果多线程并发访问会导致 UI 控件处于不可预期的状态。
    *为什么不加锁?缺点有:1.加锁会导致 UI 访问逻辑变得复杂,其次锁机制会降低 UI 访问的效率,因为锁机制会阻塞某些线程的执行。

* Handler的简单使用

方法书里没有介绍,翻出萌新笔记贴一点:
简单应用:

private Handler handler = new Handler() {
 public void handleMessage(Message msg) {
            switch (msg.what) {
                case UPDATE_TEXT:
                    // 在这里可以进行UI操作
                   break;
                default:
                    break;
            }
        }

    };
//在需要耗时操作的地方,开子线程
new Thread(new Runnable() {
    @Override
    public void run() {
//可以进行耗时操作
        Message message = new Message();
        message.what = UPDATE_TEXT;
        handler.sendMessage(message); //将Message对象发送出去
        }
    }).start();

过程如下:

  1. 首先在主线程中创建一个 Handler 对象,并重写HandleMessage方法
  2. 在子线程中需要进行 UI 操作的时候创建一个 Message 对象,
  3. 通过 Handler 将信息发送出去,
  4. 该信息被添加到 MessageQueue 中等待被处理,Looper 则会一直尝试从 MessageQueue 中取出待处理信息,最后分配到 Handler 的handleMessage() 方法中。

注意:在Activity中,并没有显式调用 Looper.prepare() 和Looper.loop() 方法,因为在 Activity 的启动代码中,已经在当前 UI 线程调用了Looper.prepare() 和 Looper.loop() 方法,这就是前文提到 UI 线程默认可以使用 Handler 的原因。
runOnUiThread() 是一个异步消息处理机制的接口封装,用法简单但实际原理是一样的。

Handler 的工作原理

Handler 创建时会采用当前线程的 Looper 来构建内部消息循环系统,如果当前线程没有 Looper ,那么就会报错。
解决方法:为当前线程创建 Looper ,或者在一个有 Looper 的线程中创建 Handler
Handler 创建完毕之后,其内部的 Looper 以及 MessageQueue 就可以和 Handler 一起协同工作了,然后通过 Handler 的 post 方法将一个 Runnable 投递到 Handler 内部的 Looper 中处理,也可通过 Handler 中的 send 发送消息,同样在 Looper 内处理。post 的本质也是调用 send 。工作过程如图:


Handler 工作过程.png

当 send 方法被调用,它会调用 MessageQueue 的 enqureMessage 方法将这个消息放入消息队列,由 Looper 处理,最后消息中的 Runnable 或者 Handler 的 handlerMessage 方法会被调用。Looper 是运行在创建 Handler 所在的线程中的,这样一来, Handler 中的业务逻辑就会被切换到创建 Handler 所在的线程中执行,完成切换线程的目的。

Android 的消息机制全面分析

ThreadLocal

一个线程内部的数据存储类,通过它可以独立存储指定线程中的数据。日常开发中较少用到,但是 android 源码中有时会利用它实现一些看似复杂的问题。

  1. 一般来说,当某些数据是以线程为作用域并且不同线程有不同的数据父本的时候,就会使用 ThreadLocal。比如 Handler,需要获取当前线程的 Looper ,且 Looper 作用域为线程,不同线程间的 Looper 互相独立,这时候使用 ThreadLocal 则可以轻松实现 Looper 在线程中的存取。 否则,系统必须提供一个全局的哈希表供 Handler 查找指定线程的 Looper,就必须存在类似于 LooperManage 这样类,会使机制变得复杂。
  2. 可用于复杂逻辑下的对象传递,比如监听器传递。当函数调用栈比较深的时候,如果把监听器作为参数传递,会使程序设计变得糟糕;如果把监听器作为静态变量供线程访问,则基本不具备扩展性。而使用 ThreadLocal ,每个线程都将拥有自己的监听器,可以在线程内全局,一键 get 到。
    书上举了简单例子及源码,说明 ThreadLocal 在各个线程的数据存储独立性,因为例子较简单而源码部分比较繁琐,这里不再赘述。总之,不同线程访问 ThreadLocal 的 get 方法, ThreadLocal 将从各线程内部取出一个数组,按照当前线程的索引,查找相应的 value 值,所以线程不用,值不同。从源码分析可得, ThreadLocal 的 set 和 get 方法都仅能访问当前线程的 localValue 对象的 table 数组,因此在不同数组中访问同一个 ThreadLocal 的 set 和 get 方法可以互不干涉。

MessageQueue

前文已提过,消息队列实际上内部是用单链表实现的,包含两大重要方法, enqueueMessage 和 next 。

  • enqueueMessage 源码
// android SDK-27

    boolean enqueueMessage(Message msg, long when) {
      ...
        synchronized (this) {
            if (mQuitting) {
                IllegalStateException e = new IllegalStateException(
                        msg.target + " sending message to a Handler on a dead thread");
                Log.w(TAG, e.getMessage(), e);
                msg.recycle();
                return false;
            }

            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;
    }

实际上就是单链表插入操作。

  • next
    无限循环方法,如果消息队列中没有消息,会一直阻塞在这里,直到有消息到来。
 Message next() {
        // Return here if the message loop has already quit and been disposed.
        // This can happen if the application tries to restart a looper after quit
        // which is not supported.
        final long ptr = mPtr;
        if (ptr == 0) {
            return null;
        }

        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;
                }

                // Process the quit message now that all pending messages have been handled.
                if (mQuitting) {
                    dispose();
                    return null;
                }

                // If first time idle, then get the number of idlers to run.
                // Idle handles only run if the queue is empty or if the first message
                // in the queue (possibly a barrier) is due to be handled in the future.
                if (pendingIdleHandlerCount < 0
                        && (mMessages == null || now < mMessages.when)) {
                    pendingIdleHandlerCount = mIdleHandlers.size();
                }
                if (pendingIdleHandlerCount <= 0) {
                    // No idle handlers to run.  Loop and wait some more.
                    mBlocked = true;
                    continue;
                }

                if (mPendingIdleHandlers == null) {
                    mPendingIdleHandlers = new IdleHandler[Math.max(pendingIdleHandlerCount, 4)];
                }
                mPendingIdleHandlers = mIdleHandlers.toArray(mPendingIdleHandlers);
            }

            // Run the idle handlers.
            // We only ever reach this code block during the first iteration.
            for (int i = 0; i < pendingIdleHandlerCount; i++) {
                final IdleHandler idler = mPendingIdleHandlers[i];
                mPendingIdleHandlers[i] = null; // release the reference to the handler

                boolean keep = false;
                try {
                    keep = idler.queueIdle();
                } catch (Throwable t) {
                    Log.wtf(TAG, "IdleHandler threw exception", t);
                }

                if (!keep) {
                    synchronized (this) {
                        mIdleHandlers.remove(idler);
                    }
                }
            }

            // Reset the idle handler count to 0 so we do not run them again.
            pendingIdleHandlerCount = 0;

            // While calling an idle handler, a new message could have been delivered
            // so go back and look again for a pending message without waiting.
            nextPollTimeoutMillis = 0;
        }
    }

Looper源码解析

在构造方法中创建一个 MessageQueue ,然后将当前线程对象保存起来。

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

通过Looper.prepare() 即可手动当前线程创建一个 Looper, 接着通过 Looper.loop() 开启循环。
Looper 提供了 quit 和 quitSafely 两种方法退出 Looper,前者直接退出,后者设定一个安全标记,等消息队列内所有消息处理完毕之后才会安全退出。如果在子线程里手动创建了 Looper 在所有消息完成之后应该调用 quit 方法,否则这个子线程会一直处在等待状态。

  • 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 slowDispatchThresholdMs = me.mSlowDispatchThresholdMs;

            final long traceTag = me.mTraceTag;
            if (traceTag != 0 && Trace.isTagEnabled(traceTag)) {
                Trace.traceBegin(traceTag, msg.target.getTraceName(msg));
            }
            final long start = (slowDispatchThresholdMs == 0) ? 0 : SystemClock.uptimeMillis();
            final long end;
            try {
                msg.target.dispatchMessage(msg);
                end = (slowDispatchThresholdMs == 0) ? 0 : SystemClock.uptimeMillis();
            } finally {
                if (traceTag != 0) {
                    Trace.traceEnd(traceTag);
                }
            }
            if (slowDispatchThresholdMs > 0) {
                final long time = end - start;
                if (time > slowDispatchThresholdMs) {
                    Slog.w(TAG, "Dispatch took " + time + "ms on "
                            + Thread.currentThread().getName() + ", h=" +
                            msg.target + " cb=" + msg.callback + " msg=" + msg.what);
                }
            }

            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 方法是一个死循环,唯一跳出死循环的条件是 MessageQueue.next 方法返回 null 。当 Looper.quit 被调用,Looper 调用 MessageQueue.quit 或者 quitSafely 方法通知消息队列退出。next 是一个阻塞方法,如果未接到新消息将一直等待,如果接到新消息,则交给 dispatchMessage 处理,这个方法是在 handler 创建的 Looper 中执行的。

Handler 的工作原理

发送消息的典型过程

 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);
    }

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

也就是说,Handler 调用 sendMessage 方法,依次调用 sendMessageDelayed,sendMessageAtTime,enqueueMessage 方法后,调用 queue.enqueueMessage 向消息队列插入了一条消息,接下来,按上文分析, Looper 中调用 MessageQueue.next 方法得到消息,最后将消息交给 Handler.dispatchMessage 处理。
实现如下:

 public void dispatchMessage(Message msg) {
        if (msg.callback != null) {
            handleCallback(msg);
        } else {
            if (mCallback != null) {
                if (mCallback.handleMessage(msg)) {
                    return;
                }
            }
            handleMessage(msg);
        }
    }

Handler 处理消息过程如下:

  1. 检查 Message callback 是否为 null,不为 null 则通过 handleCallback 来处理消息,Message 的 callback 是一个 Runnable 对象,实际上就是 Handler 的 post 方法传递的 Runnable 参数。
private static void handleCallback(Message message) {
        message.callback.run();
    }

2.检查 mCallback 是否为 null ,不为 null 就调用 mCallback 的 handlerMessage 方法

 /**
     * Callback interface you can use when instantiating a Handler to avoid
     * having to implement your own subclass of Handler.
     *
     * @param msg A {@link android.os.Message Message} object
     * @return True if no further handling is desired
     */
    public interface Callback {
        public boolean handleMessage(Message msg);
    }

通过Callback 可以采用Handler handler = new handler(callback)的方式创建 Handler 对象。callback 的意义在于可以创建一个 Handler 实例但不需要派生 Handler 的子类。在日常开发中,最常见的方式就是派生一个 Handler 的子类并且重写 handlerMessage 方法,当不想用该方式的时候可以采用 callback 实现。

Handler 消息处理.png

主线程的消息循环

在主线程的入口方法中国,调用 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 还需要 Handler 来和消息队列进行交互,这个 Handler 就是 AcitivityThread.H。
ActivityThread 通过 Application Thread 和 AMS 进行进程间通信,AMS以进程间通信的方式完成 ActivityThread 的请求后会回调 ApplicationThread 中的 Binder 方法,然后 ApplicationThread 会向 H 发消息,H 收到消息后将 ApplicationThread 中的逻辑切换到 ActivityThread 中执行,即切换到主线程中去执行。

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

推荐阅读更多精彩内容