大厂面试秘籍—— 深入理解 Handler

概述

Android 的消息机制主要指的是 Handler 的运行机制,从开发者的角度来说 Handler 是 Android 消息机制的上层接口,而底层的逻辑则是由 MessageQueue、 Looper 来完成的。

Handler 的设计目的是为了解决不能在 Android 主线程中做耗时操作而又只有主线程才能访问 UI 的矛盾。通过 Handler 消息机制可以让开发者在子线程中完成耗时操作的同时在主线程中更新UI

Handler 机制是 Android 用于 UI 刷新的一套消息机制。开发者可以使用这套机制达到线程间通信、线程切换目的。

这里要思考一个问题:为什么 Android 非要规定只有主线程才能更新 UI 呢?

因为 Android 的所有 View 控件都不是线程安全的,如果在多线程中并发访问很可能造成意想不到的结果。对于加锁这种方案也不可取,首先加锁之后会让 UI 访问逻辑变的很复杂,开发者需要时刻考虑多线程并发将会带来的问题,其次锁机制太重了它会严重影响 UI 访问效率。介于这两个缺点,最简单且高效的方法就是采用单线程的方式访问 UI。Handler 机制应运而生。

那么 Handler 内部是如何完成线程切换的呢?答案就是神奇的 :ThreadLocal

ThreadLocal

ThreadLocal 并不是 Thread ,他的特点很有意思: 每一个线程存储的值是相互隔离的

public class TreadLocalDemo {
    // 就算设置为 static 结果也是一样的
    ThreadLocal<Boolean> mThreadLocal = new ThreadLocal<Boolean>();

    public void runDemo() {
        mThreadLocal.set(true);
        System.out.println(Thread.currentThread().getName() + "  " + mThreadLocal.get());
        new Thread("Thread#1") {
            @Override
            public void run() {
                super.run();
                mThreadLocal.set(false);
                System.out.println(Thread.currentThread().getName() + "  " + mThreadLocal.get());
            }
        }.start();

        new Thread("Thread#2") {
            @Override
            public void run() {
                super.run();
                System.out.println(Thread.currentThread().getName() + "  " + mThreadLocal.get());
            }
        }.start();
        System.out.println(Thread.currentThread().getName() + "  " + mThreadLocal.get());
    }
}

运行的结果很清晰的展示他的特点,虽然在主线程和线程1中都做了赋值操作,但并不能改变原来线程的赋值情况。

file

对于 ThreadLocal 的原理简单来说:每一线程都有一个专门用于保存 ThreadLocal 的成员变量 localValues。 尽管在不同线程中访问同一个 ThreadLocal 的 setget 方法,但所做的操作都仅限制于各自线程的内部。这就是 ThreadLocal 可以在多个线程中互不干扰的存储和读取数据的原因。正是这种特性让 Handler 做到了线程的切换。

Looper 正是借助 ThreadLocal 的特点在不同的线程创建不同的实例。至此 Handler 与 Looper 、线程达到了一一对应的绑定关系。所以无论此 Handler 的实例在什么线程调用,最终的回调都会分发到创建线程。

MessageQueue

MessageQueue 主要有两个操作:插入和读取。读取操作也会伴随着删除。插入和读取的方法分别对应的是:enquequeMessagenext,MessageQueue 并不是像名字一样使用队列作为数据结构,而是使用单链表来维护消息。单链表在插入和删除上比较有优势。

next()

首先来说说 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) {
            // 可见只有在调用 quit() 方法之后才会返回空
            return null;
        } 
        
   ......
          
        // 一个死循环 !
        for (;;) {
            if (nextPollTimeoutMillis != 0) {
                Binder.flushPendingCommands();
            }

            // 一个 native 方法,此方法在没有消息或者消息没有到执行时间的时候会让线程进入等待状态。
            // 有点类似于 Object.wait 但是 nativePollOnce 可以自定等待时间
            nativePollOnce(ptr, nextPollTimeoutMillis);

            synchronized (this) {
   ......
                     if (!keep) {
                    synchronized (this) {
                      // 获取消息后从列表中移除
                        mIdleHandlers.remove(idler);
                    }
                }
        }
    }

最关键的是三点内容

  1. 死循环
  2. nativePollOnce()
  3. 获取到消息之后从列表中移除

nativePollOnce 是一个 native 方法,如果单列表中没有消息或者等待的时间没有到,那么当前线程将会被设置为 **wait 等待状态 **,直到可以获取到下一个 Message更详细的内容可以参见 StackOverflow 上关于 nativePollOnce的回答而这个死循环的目的就是不让 next方法退出,等待 nativePollOnce 的响应。等到获取到消息之后再将这个消息从消息列表中移除。

enqueueMessage()

enqueueMessage 方法的主要工作就是向单链表中插入数据,当线程处于等待状态则调用 nativeWake 唤醒线程,让 next 方法处理消息。

    boolean enqueueMessage(Message msg, long when) {
    ......
            // We can assume mPtr != 0 because mQuitting is false.
            if (needWake) {
                nativeWake(mPtr);
            }
        }
        return true;
    }

如何处理延时消息

详情请参见Handler是怎么做到消息延时发送的 下面再抄一部分结论:

在 next 方法中如果头部的这个 Message 是有延迟而且延迟时间没到的(now < msg.when),会计算一下时间(保存为变量 nextPollTimeoutMillis), 然后在循环开始的时候判断如果这个 Message 有延迟,就调用nativePollOnce (ptr, nextPollTimeoutMillis)进行阻塞。nativePollOnce()的作用类似与 Object.wait(), 只不过是使用了 Native 的方法对这个线程精确时间的唤醒。

  1. postDelay()一个10秒钟的 Runnable A、消息进队,MessageQueue 调用nativePollOnce()阻塞,Looper 阻塞;
  2. 紧接着 post() 一个 Runnable B、消息进队,判断现在A时间还没到、正在阻塞,把B插入消息队列的头部(A的前面),然后调用 nativeWake()方法唤醒线程;
  3. MessageQueue.next() 方法被唤醒后,重新开始读取消息链表,第一个消息B无延时,直接返回给 Looper
  4. Looper 处理完这个消息再次调用 next() 方法,MessageQueue 继续读取消息链表,第二个消息A还没到时间,计算一下剩余时间(假如还剩 9秒)继续调用 nativePollOnce()阻塞;直到阻塞时间到或者下一次有Message 进队;
file

Looper

Looper 在 Android 消息机制中扮演着消息循环的角色。具体来说他的任务就是不停的从 MessageQueue 中获取消息,如果有新消息就立即处理,没有消息的时候,与 Looper 绑定的线程就会被 MessageQueue 的 next 的 nativePollOne 方法置于等待状态。

Looper 是如何创建的

/** Initialize the current thread as a looper.
  * This gives you a chance to create handlers that then reference
  * this looper, before actually starting the loop. Be sure to call
  * {@link #loop()} after calling this method, and end it by calling
  * {@link #quit()}.
  */
public static void prepare() {
    prepare(true);
}

private static void prepare(boolean quitAllowed) {
    if (sThreadLocal.get() != null) {
        throw new RuntimeException("Only one Looper may be created per thread");
    }
    // 创建 Looper 实例,将实例保存在 sThreadLocal 中与当前线程绑定。
    sThreadLocal.set(new Looper(quitAllowed));
}

在构造方法里面他会创建一个 MessageQueque,并保存当前线程。

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

getMainLooper 可以在任何地方获取到主线程的 Looper,那么主线程是如何创建 Looper 的呢?

主线程创建 Looper 的过程 —— AndroidThread

我们的目光来到了 AndroidThread 类, 在 AndroidThread 中我们看到了熟悉的方法 :main(String[] args。千万不要被 AndroidThread 的名字所迷惑,AndroidThread 并不是一个线程,它只是一个开启主线程的类。

public static void main(String[] args) {
        ....

        // 创建 Looper 和 MessageQueue 对象,用于处理主线程的消息
        Looper.prepareMainLooper();

        // 创建 ActivityThread 对象
        ActivityThread thread = new ActivityThread(); 

        // 建立 Binder 通道 (创建新线程)
        thread.attach(false);

        // 消息循环运行
        Looper.loop(); 
        throw new RuntimeException("Main thread loop unexpectedly exited");
    }

注意调用的是 prepare(false) 不允许退出,这是为什么呢?

    public static void prepareMainLooper() {
       // 不允许退出
        prepare(false);
        synchronized (Looper.class) {
            if (sMainLooper != null) {
                throw new IllegalStateException("The main Looper has already been prepared.");
            }
            sMainLooper = myLooper();
        }
    }

这是因为主线程的 Looper 伴随着一个 App 的整个生命周期,所有的 UI访问、View 刷新都是在 Looper 里面完成的,如果允许开发者手动退出,那么整个 App 都会变得不可控。

更多细节可以参见下面的一节「 Looper中的死循环为什么没有卡死线程」

Looper 是如何运行的

/**
 * 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();
  ......
    
    final MessageQueue queue = me.mQueue;
  ......

    // 死循环
    for (;;) {
       // 可能会被阻塞
        Message msg = queue.next();
        if (msg == null) {
            // msg 为 null 会立即退出循环,这也是退出循环的唯一方法。
            return;
        }
   ......
  
        try {
          // 开始分发消息
            msg.target.dispatchMessage(msg);
            dispatchEnd = needEndTime ? SystemClock.uptimeMillis() : 0;
        } finally {
            if (traceTag != 0) {
                Trace.traceEnd(traceTag);
            }
        }
   ......
    }
}

loop 方法是一个死循环,他的工作就是不断的检查 MessageQueue 是否有可以处理的消息,如果有这将消息分发给 Handler 处理。既然是死循环那么为什么没有卡死线程呢?更多细节可以参见下面的一节「 Looper中的死循环为什么没有卡死线程」

Looper 如何退出

Looper 内部提供了两种退出的方法,分别是 quit、quitSafely。从本质上来讲 quit 调用后会立即退出 Looper,而 quitSafely 只是设定一个退出标记,等待消息队列中的已有消息处理完毕后,再退出。

Looper 退出后,通过 Handler 发送的消息会失败,这个时候 Handler send 方法会返回 false。在子线程中,如果手动为其创建了 Looper,那么在所有的逻辑完成后理应手动调用 quit 方法终止 Looper 内部的循环,否则这个子线程会一直处于等待状态,而退出 Looper 之后,子线程也就会随之终止,因此在子线程中使用 Looper,==必须在恰当的时机终止它==。

/**
* Quits the looper.
*/
public void quit() {
    mQueue.quit(false);
}

/**
 * Quits the looper safely.
 */
public void quitSafely() {
    mQueue.quit(true);
}

如果是主线程开发者就退出不了,要是退出了,就麻烦大了。

public static void prepareMainLooper() {
  // fasle 不允许退出
    prepare(false);
 ....
}

退出的本质

Looper.quit 的源码中可以清晰看到,本质上调用的是 MessageQueue 的 quite 方法。而在调用 MessageQueue.quite 之后 再次调用 MessageQueue.next()会返回 null

 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) {
            // 可见只有在调用 quit() 方法之后才会返回空
            return null;
        } 
        
   ......

Looper.loop()在调用 queue.next()得的结果为 null 的时候会立即跳出死循环, 这也是退出死循环的唯一方式。

public static void loop() {
……
    for (;;) {
    Message msg = queue.next(); // might block
    if (msg == null) {
        // No message indicates that the message queue is quitting.
        return;
    }
……

Looper中的死循环为什么没有卡死线程

参考知乎:Android中为什么主线程不会因为Looper.loop()里的死循环卡死?

我们都知道:一个简单的死循环会消耗掉大量资源导致线程被卡死。但是 Looper.loop() 方法开启就是一个死循环,它为什么没有卡死线程呢?总结一下主要有3个疑惑:

  • Looper 为什么要使用死循环
  • Android 的主线程为什么没有被 Looper 中的死循环卡死
  • 唤醒主线程 Looper 的消息从何而来

Looper 为什么要使用死循环

首先要说说的是为什么在 Looper 中使用死循环。在 CPU 看来操作系统线程(这里的定义可以参见《Java基础》多线程和线程同步 —— 进程与线程一节 ) 只不过是一段可以执行的代码,CPU 会使用 CFS 调度算法,保证每一个 task 都尽可能公平的享用 CPU 时间片。既然操作系统线程是一段可以执行的代码,当可执行的代码结束之后,线程生命周期也就终止,线程将会退出。但是对于 Android 这类的 GUI 程序,我们绝对不希望代码执行一段时间之后主线程就自己停止掉,那么如何保证线程一直执行下去呢?简单的做法就是在线程中执行死循环,让线程一直工作下去不会停止退出。

总的来说,在线程中使用死循环想要解决的问题就是防止线程自己退出。所以对于 Looper 而言,他的死循环就是希望不断的从 MessageQueue 中获取消息,而不希望线程线性执行之后就退出。

Android 的主线程为什么没有被 Looper 中的死循环卡死

首先 Android 所有的 UI 刷新和生命周期的回调都是由 Handler消息机制完成的,就是说 UI 刷新和生命周期的回调都是依赖 Looper 里面的死循环完成的,这样设计的目的上文已经阐述清楚。这篇文章里面贴了 AndroidTread 对于 Handler 的实现类 H 的源码(进入文章后搜索:内部类H的部分源码) 源码太长,我就不贴了。

其次Looper 不是一直拼命干活的傻小子,而是一个有活就干没活睡觉的老司机,所以主线程的死循环并不是一直占据着 CPU 的资源不释放,不会造成过度消耗资源的问题。这里涉及到了Linux pipe/epoll机制,简单说就是在主线程的 MessageQueue 没有消息时,便在 loop 的 queue.next() 中的 nativePollOnce() 方法里让线程进入休眠状态,此时主线程会释放CPU资源,直到下个消息到达或者有事务发生才会再次被唤醒。所以 Looper 里的死循环,没有一直空轮询着瞎忙,也不是进入阻塞状态占据着 CPU 不释放,而是进入了会释放资源的等待状态,等待着被唤醒

经过上面的讨论可以得知:

  1. Looper 中的死循环是 Android 主线程刷新 UI 和生命周期回调的基石。
  2. Looper 中的死循环会根据消息分别进入等待和唤醒状态,并不会一直持有资源,所以就不会有卡死的问题。

那么唤醒 Looper 的消息是从哪里来的呢?

唤醒主线程 Looper 的消息从何而来

目光回到 AndroidThread 类中的这几行代码

public static void main(String[] args) {
        ....
          
        // 创建ActivityThread对象
        ActivityThread thread = new ActivityThread(); 
  
        //建立Binder通道 (创建新线程)
        thread.attach(false);

        Looper.loop(); //消息循环运行
    }

在创建 ActivityThread 后会通过thread.attach(false)方法在 ActivityThread 中创建 Binder 的服务端用于接收系统服务AMS发送来的事件,然后通过 ActivityThread 的内部类 ApplicationThread 中 sendMessage 方法

......

public final void scheduleStopActivity(IBinder token, boolean showWindow,
                int configChanges) {
           sendMessage(
                showWindow ? H.STOP_ACTIVITY_SHOW : H.STOP_ACTIVITY_HIDE,
                token, 0, configChanges);
        }

        public final void scheduleWindowVisibility(IBinder token, boolean showWindow) {
            sendMessage(
                showWindow ? H.SHOW_WINDOW : H.HIDE_WINDOW,
                token);
        }

        public final void scheduleSleeping(IBinder token, boolean sleeping) {
            sendMessage(H.SLEEPING, token, sleeping ? 1 : 0);
        }

        public final void scheduleResumeActivity(IBinder token, int processState,
                boolean isForward, Bundle resumeArgs) {
            updateProcessState(processState, false);
            sendMessage(H.RESUME_ACTIVITY, token, isForward ? 1 : 0);
......

将消息发送给 AndroidThread 的 Handler 实现内部类 H。从而完成了 Binder Thread 到 UI 线程即主线程的切换,唤醒 Looper 进行 dispatchMessage 的动作。

唤醒的具体操作参见上文「MessageQueue -> enqueueMessage -> nativeWake」

拓展:如何在非主线程中使用 Handler 消息机制

通过 ActivityThread 的源码可以清楚看到

public static void main(String[] args) {
        ....

        //创建Looper和MessageQueue对象,用于处理主线程的消息
        Looper.prepareMainLooper();
        ....

        Looper.loop(); //消息循环运行
        ....
    }

Android 在启动一个 App 的时候都会创建一个 Looper,而用户启动子线程的时候是没有这个操作的,所以需要开发者自己创建并调用 Looper.loop() 让 Looper 运行起来。

   new Thread("Thread#1") {
      @Override
      public void run() {
         // 手动生成为当前线程生成 Looper
         Looper.prepare();
         Handler handler = new Handler();
         Looper.loop();
      }

    }.start();

此处我们做个实验,既然 Looper 是个死循环那么在 loop() 之后的代码是不是永远没有机会执行呢?

/**
 * Android 消息机制 —— Handler
 * <p>
 * Created by im_dsd on 2019-09-07
 */
public class HandlerDemo {

    public static final String TAG = "HandlerDemo";
    private Handler mHandler;
    private Looper mLooper;

    /**
     * 如何在子线程中开启 Handler
     */
    public void startThreadHandler() {
        new Thread("Thread#1") {
            @Override
            public void run() {
                // 手动生成为当前线程生成 Looper
                Looper.prepare();
                mLooper = Looper.myLooper();
                mHandler = new Handler() {
                    @Override
                    public void handleMessage(Message msg) {
                        super.handleMessage(msg);
                        Log.d(TAG,Thread.currentThread().getName() + "  " + msg.what);
                    }
                };
                Log.d(TAG,Thread.currentThread().getName() + "loop 开始 会执行吗?  ");
                // 手动开启循环
                Looper.loop();
                Log.d(TAG,Thread.currentThread().getName() + "loop 结束 会执行吗?  ");
            }

        }.start();

        // 等待线程启动
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        Log.d(TAG,"start send message");
        mHandler.sendEmptyMessage(1);
        mHandler.post(() -> Log.d(TAG,Thread.currentThread().getName()));
    }
}

自启动到将 App 彻底杀死,输出结果也是如此:loop 后面的代码没有执行!

2964-3007/com.example.dsd.demo D/HandlerDemo: Thread#1loop 开始 会执行吗?  
2964-2964/com.example.dsd.demo D/HandlerDemo: start send message
2964-3007/com.example.dsd.demo D/HandlerDemo: Thread#1  1
2964-3007/com.example.dsd.demo D/HandlerDemo: Thread#1

这意味两个严重的问题:looper() 后面的代码一直都不会执行而且线程 Thread#1 将会一直运行下去!在 JVM 规范里面规定==处于运行中的线程会不被 GC==。在没有消息的时候 Looper 会处于等待状态。等待在 Thread 的生命周期里仍然属于运行状态,它永远不会被 GC

所以很多网上很多文章里都有一个致命的缺陷,根本就没有提及到要在使用完毕后即使退出 Looper。紧接上文的代码

      // 尝试 1秒 后停止
        try {
            Thread.sleep(1000);
            mLooper.quit();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

此时的结果

2964-3007/com.example.dsd.demo D/HandlerDemo: Thread#1loop 开始 会执行吗?  
2964-2964/com.example.dsd.demo D/HandlerDemo: start send message
2964-3007/com.example.dsd.demo D/HandlerDemo: Thread#1  1
2964-3007/com.example.dsd.demo D/HandlerDemo: Thread#1
2964-3007/com.example.dsd.demo D/HandlerDemo: Thread#1loop 结束 会执行吗?  

根据综上所述,Handler 机制完全可以在 Android 中用作线程间的消息同步,这里要强调一下,Handler 机制是 Android 独有的,笔者在写上面的 Demo 的时候竟然傻傻的将 Handler 的启动放在了 Java 中,直接抛出了 RuntimException Stub 的错误。

总结一下在子线程中使用 Handler 机制要注意两点问题:

  1. 必须调用 Looper.prepare();手动生成为当前线程生成 Looper,并调用Looper.looper()启动内部的死循环。
  2. 必须在使用结束后调用 Looper.myLooper().quit()退出当前线程。

Handler

Handler 的工作主要就是发送和接收消息。消息的发送可以通过 post 的一系列方法和 send 的一系类方法。在创建 Handler 的时候他会默认使用当前线程的 Looper ,如果当前线程没有创建过 Looper 会抛出如下异常。

file

当然也可以手动指定不同线程的 Looper。

Handler mHandler = new Handler(Looper.getMainLooper());

消息是如何发送到的呢?

    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 final boolean sendEmptyMessageAtTime(int what, long uptimeMillis) {
        Message msg = Message.obtain();
        msg.what = what;
        return sendMessageAtTime(msg, uptimeMillis);
    }

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

经过一系列的跟踪,最终的结果是调用了enqueueMessage(MessageQueue, Message, long)方法,目的就是为了向 MessageQueue 中插入一条消息。

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

而后 nativeWake 将会唤醒等待的线程,MessageQueue#next将会在Looper.loop()中将这条消息返回,Looper.loop()在收到这条消息之后最终会交由 Handler#dispatchMessage处理

/** 
 * Looper 的 loop 方法
 */
public static void loop() {
  ......

    // 死循环
    for (;;) {
   ......
      // 开始分发消息  msg.target 指的就是发送消息的 Handler
       msg.target.dispatchMessage(msg);
   ......
    }
}
   /**
     * Handle 的 dispatchMessage 方法
     */
    public void dispatchMessage(Message msg) {
       // 首先检查 msg 的 callback 是否为 null
        if (msg.callback != null) {
          // 不为 null 使用 msg 的 callback 处理消息
            handleCallback(msg);
        } else {
            // mCallback 是否为 null
            if (mCallback != null) {
                if (mCallback.handleMessage(msg)) {
                    return;
                }
            }
          // 都没有指定则交由开发者重写的 handleMessage 处理
            handleMessage(msg);
        }
    }

从上面的逻辑我们可以看出 callback 的优先级:msg#callback > new Handler(Callback) 中 指定的 Callback> 重写 Handler 的 callBack

mCallback指的是一个接口 , 可以使用 Handler handler = new Handler(Callback)的方式指定回调,这种方式可以由外部传递进来会回调方法,更加灵活。

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

至此对于 Android 的消息机制已经讲解完毕,你是否已经有了清晰的认识呢?对于开篇的问题:Handler 是如何完成线程切换的,你找到答案了吗?

常见问题分析

为什么不能在子线程中更新 UI ,根本原因是什么?

16c0641ad370a90a.jpeg
file

mThread 是主线程,这里会检查当前线程是否是主线程。

为什么 onCreate 里面没有进行上面的检查呢?

这个问题原因出现在 Activity 的生命周期中 , 在 onCreate 方法中, UI 处于创建过程,对用户来说界面还不可见,直到 onStart 方法后界面可见了,再到 onResume 方法后页面可以交互,从某种程度来讲, 在 onCreate 方法中不能算是更新 UI,只能说是配置 UI,或者是设置 UI 属性。 这个时候不会调用到 ViewRootImpl.checkThread () , 因为 ViewRootImpl 没有创建。 而在 onResume 方法后, ViewRootImpl 才被创建。 这个时候去交户界面才算是更新 UI。

setContentView 知识建立了 View 树,并没有进行渲染工作 (其实真正的渲染工作实在 onResume 之后)。也正是建立了 View 树,因此我们可以通过 findViewById() 来获取到 View 对象,但是由于并没有进行渲染视图的工作,也就是没有执行 ViewRootImpl.performTransversal。同样 View 中也不会执行 onMeasure (), 如果在 onResume() 方法里直接获取 View.getHeight() / View.getWidth () 得到的结果总是 0 解决方案是在 UI 真正可见的方法 onWindowFocusChanged() 里面获取。

为什么 Handler 构造方法里面的 Looper 不是直接 new ?

如果在 Handler 构造方法里面直接 new Looper(), 可能是无法保证 Looper 唯一,只有用 Looper.prepare() 才能保证唯一性,具体可以看 prepare 方法。

MessageQueue 为什么要放在 Looper 私有构造方法初始化?

因为一个线程只绑定一个 Looper ,所以在 Looper 构造方法里面初始化就可以保证 mQueue 也是唯一的 Thread 对应一个 Looper 对应一个 mQueue。

总结

  1. Android 的消息机制指的就是 Handler 消息机制,Handler 是面向开发者的上层接口,而底层的实现是 MessageQueue、Looper、ThreadLocal
  2. MessageQueue 使用单链表的数据结构承载消息,在 next 方法中 nativePollOne 方法会在消息为空的时候讲线程置为等待状态,直到有新的消息到来才会再次唤醒线程。所以 Looper.loop 虽然是死循环也不会卡死。
  3. Looper 的主要任务是不断尝试从 MessageQueue 中获取消息,为了不让线程线性执行完毕,loop 中开启了一个死循环。因为主线程的所有生命周期都是由 Handler 机制完成的,所以这个主线程中死循环不允许开发者手动退出,什么时候 App 退出了,这个死循环才会被退出。而子线程中没有这机制,所以在使用完毕后必须手动退出,否者线程会一直处于等待状态,不会被GC。
  4. Handler 是借助 ThreadLocal 机制完成线程切换的,Handler 在创建的时候就已经获取了和线程绑定的 Looper,所以无论 Handler 在什么线程调用,最终都会回到 Looper 绑定的线程,所以 Handler 很适合在 Android 中做线程间通信。

参考

  1. 《Android 开发艺术探究》第十章
  2. 知乎问答:《Android中为什么主线程不会因为Looper.loop()里的死循环卡死》
  3. StackOverflow: 《android - what is message queue native poll once in android?》
  4. 《Handler 是如何做到发送延时消息的》

Handler 的再理解与总结

MessageQueue#next()

这个方法里面是一个死循环,但是里面的方法 nativePollOnce 运用了 Linux 的 epoll 机制,在没有消息的时候回会将线程挂起,注意此时的挂起相当于 Object.wait() : 它会释放 CPU 资源,等待唤醒。有消息进入的时候回到用 MessageQueue#enqueueMessage() 加入数据,此时 MessageQueue#enqueueMessage() 内部的 nativeWeak 会重新唤醒线程,

可以发现

  1. 虽然是死循环但是他空闲时间并不消耗资源,死循环的目是为了防止获消息的逻辑退出
  2. **Loop#loop() 也是个死循环,但是没有 message 的时候同样会被 MessageQueue#next 挂起,不会控轮询消耗资源 **。
  3. **当有消息进入的时候 next 方法会被立即唤醒,但是是否将消息返回不一定,要看是不是延时消息 **。

延时消息

MessageQueue#next() 中会判断消息的时间,如果还没有到消息执行的时间,会将消息定时挂起(我们记录它 为 A。如果这个时候有新的消息到来(记录为 B), MessageQueue#enqueueMessage() 会按照消息的执行时间排序插入,然后唤醒 MessageQueue#next() 处理消息。如果 B 不是定时消息立即处理,如果是定时消息更新挂起时间继续阻塞,等到阻塞时间到的时候就会立即唤醒 next 方法处理。

为什么 MessageQueue#next() 需要死循环? loop 的死循环还不够用吗?

Loop#loop 里面的死循环是为了防止退出的,而 MessageQueue#next() 的死循环是为了确认到底有没有消息 参考:对于 MessageQueue 的解读

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

        //为-1时,说明是第一次循环,在当前消息队列中没有MSG的情况下,需要处理注册的Handler
        int pendingIdleHandlerCount = -1; // -1 only during first iteration
        // 超时时间。即等待xxx毫秒后,该函数返回。如果值为0,则无须等待立即返回。如果为-1,则进入无限等待,直到有事件发生为止
        int nextPollTimeoutMillis = 0;
        for (;;) {
            if (nextPollTimeoutMillis != 0) {//???
                Binder.flushPendingCommands();
            }

            // 该函数提供阻塞操作。如果nextPollTimeoutMillis为0,则该函数无须等待,立即返回。
            //如果为-1,则进入无限等待,直到有事件发生为止。
            //在第一次时,由于nextPollTimeoutMillis被初始化为0,所以该函数会立即返回
            //从消息链的头部获取消息
            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) {//message不为空,但没有执行者
                    // Stalled by a barrier.  Find the next asynchronous message in the queue.
                    do {//寻找Asynchronous的消息
                        prevMsg = msg;
                        msg = msg.next;
                    } while (msg != null && !msg.isAsynchronous());
                }
                if (msg != null) {
                    if (now < msg.when) {
                        //判断头节点所代表的message执行的时间是否小于当前时间
                        //如果小于,让loop()函数执行message分发过程。否则,需要让线程再次等待(when–now)毫秒
                        // 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;//将队列设置为非 blocked 状态
                        if (prevMsg != null) {
                            prevMsg.next = msg.next;
                        } else {
                            mMessages = msg.next;
                        }
                        msg.next = null;
                        if (false) Log.v("MessageQueue", "Returning message: " + msg);
                        msg.markInUse(); //将消息设置为 inuse
                        return msg;
                    }
                } else {
                //如果头节点为空,消息链中无消息,设置nextPollTimeoutMillis为-1,让线程阻塞住,
                //直到有消息投递(调用enqueueMessage方法),并利用nativeWake方法解除阻塞
                    // 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)) {
                    //第一次进入,当前无消息,或还需要等待一段时间消息才能分发,获得idle handler的数量
                    pendingIdleHandlerCount = mIdleHandlers.size();
                }
                if (pendingIdleHandlerCount <= 0) {
                    //如果没有idle handler需要执行,阻塞线程进入下次循环
                    // 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("MessageQueue", "IdleHandler threw exception", t);
                }

                //如果keep=false,表明此idler只执行一次,把它从列表中删除。如果返回true,则表示下次空闲时,会再次执行
                if (!keep) {
                    synchronized (this) {
                        mIdleHandlers.remove(idler);
                    }
                }
            }
        
            //pendingIdleHandlerCount=0,是为了避免第二次循环时,再一次通知listeners
            //如果想剩余的listeners再次被调用,只有等到下一次调用next()函数了
            // Reset the idle handler count to 0 so we do not run them again.
            pendingIdleHandlerCount = 0;

            // nextPollTimeoutMillis=0,是为了避免在循环执行idler.queueIdle()时,有消息投递。
            //所以nextPollTimeoutMillis=0后,第二次循环在执行nativePollOnce时,会立即返回
            //如果消息链中还是没有消息,那么将会在continue;处执行完第二次循环,进行第三次循环,然后进入无限等待状态
            // 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;
        }
    }

重点:

  1. nativePollOnce(ptr, nextPollTimeoutMillis); nextPollTimeoutMillis: 0 不阻塞,-1 无限阻塞等待唤醒。
  2. 死循环最多执行三次:
    1. 第一次循环,如果消息链中有合适的消息,就抛出 message 去处理。如果没有,则会通知各 listeners 线程空闲了。执行完后为了避免在 listners 执行的过程中有消息投递,那么此时重置 nextPollTimeoutMillis = 0。
    2. 然后进行第二次循环,由于此时 nextPollTimeoutMillis 为0,则 nativePollOnce 不会阻塞,立即返回,取出 message,如果此时消息链中还是没有 message,则会在将会在 continue 处结束第二次循环,此时 nextPollTimeoutMillis 已被设置为-1,
    3. 第三次循环时,nativePollOnce 发现 nextPollTimeoutMillis 为-1,则进入无限等待状态,直到有新的message 被投递到队列中来。当有新的 message 后,由于 enqueueMessage 中调用了 nativeWake 函数,nativePollOnce 会从等待中恢复回来并返回,继续执行,然后将新的 message 抛出处理,for 循环结束。

enqueueMessage

enqueueMessage 有排序功能,按照时间入队。

 boolean enqueueMessage(Message msg, long when) {
        if (msg.target == null) {
            throw new IllegalArgumentException("Message must have a target.");
        }
        if (msg.isInUse()) {
            throw new IllegalStateException(msg + " This message is already in use.");
        }

        synchronized (this) {
            if (mQuitting) {//如果正在退出,就不能插入消息
                IllegalStateException e = new IllegalStateException(
                        msg.target + " sending message to a Handler on a dead thread");
                Log.w("MessageQueue", e.getMessage(), e);
                msg.recycle();//把这个消息放回到消息池中
                //获得msg时,先去消息池中看看有没有被回收的msg,如果有,就不用创建新的msg了
                return false;
            }

            msg.markInUse();
            msg.when = when;//从消息队列中取出绝对时间戳
            Message p = mMessages;//指向队首
            boolean needWake;
            //如果当前的消息链为空,或者要插入的MSG为QUIT消息,或者要插入的MSG时间小于消息链的第一个消息
            //在队首插入
            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 {
                //否则,我们需要遍历该消息链,将该MSG插入到合适的位置
                // 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;
            }

//neekWake=mBlocked, 如果mBlocked为ture,表面当前线程处于阻塞状态,即nativePollOnce处于阻塞状态
//当通过enqueueMessage插入消息后,就要把状态改为非阻塞状态,所以通过执行nativeWake方法,触发nativePollOnce函数结束等待
            // We can assume mPtr != 0 because mQuitting is false.
            if (needWake) {
                nativeWake(mPtr);
            }
        }
        return true;
    }

本人最近在整理一套专属 Android 进阶笔记欢迎 start
Read More

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