Android的消息机制相关的几个类
Handler
MessageQueue
Looper
Message
ThreadLocal
问题大纲
从源码的角度来说为什么不能在主线程中更新ui?
为什么有时在 Activity 的 onCreate() 方法中创建一个子线程访问UI,程序还是正常能跑起来呢?
ThreadLocal 是干什么用的?
Handler 内部如何获取到当前线程的 Looper?
为什么在子线程弹出 Toast 会报错?
Android中为什么主线程不会因为Looper.loop()里的死循环卡死?
如何将 Message 插入到 MessageQueue 的队列头?
Question
Q:为什么不能在主线程中更新ui?
在 ViewRootImpl
中有一个 checkThread()
方法:
void checkThread() {
if (mThread != Thread.currentThread()) {
throw new CalledFromWrongThreadException(
"Only the original thread that created a view hierarchy can touch its views.");
}
}
mThread 表示主线程。
Q:为什么有时在 Activity 的 onCreate() 方法中创建一个子线程访问UI,程序还是正常能跑起来呢?
从上面可以看到,ViewRootImpl 的 checkThread()
方法的检查是非常严格的,Android 是运行在主线程中的,
mThread = Thread.currentThread();
从上面代码可以看出 mThread 肯定是主线程。
那就是只能说明 checkThread()
方法没有执行,执行onCreate方法的那个时候 ViewRootImpl 还没创建,无法去检查当前线程。
ViewRootImpl的实例对象是在什么时候创建的呢?
具体源码太长了,直接给出结论:ViewRootImpl的实例对象是在 onResume() 中创建!
Q:ThreadLocal 是干什么用的?
从JAVA官方对ThreadLocal类的说明定义(定义在示例代码中):ThreadLocal类用来提供线程内部的局部变量。这种变量在多线程环境下访问(通过get和set方法访问)时能保证各个线程的变量相对独立于其他线程内的变量。ThreadLocal实例通常来说都是private static类型的,用于关联线程和线程上下文。
JDK8以前:
每个ThreadLocal类都创建一个Map,然后用线程的ID threadID作为Map的key,要存储的局部变量作为Map的value,这样就能达到各个线程的值隔离的效果。这是最简单的设计方法,JDK最早期的ThreadLocal就是这样设计的。
JDK8以后:
每个Thread维护一个ThreadLocalMap哈希表,这个哈希表的key是ThreadLocal实例本身,value才是真正要存储的值Object。
Q:Handler 内部如何获取到当前线程的 Looper?
通过 ThreadLocal,在不同线程互不打扰的存储数据。
具体分析:
当我们 new Handler()
时,在构造里面会调用 Looper.myLooper()
去获取当前线程的 Looper 对象,这个 Looper 对象时通过 ThreadLocal 的 get() 方法取出来的,ThreadLocal 可以看做是一个以当前线程为 Key 的特殊 Map,当调用 Looper.prepare()
方法时,会创建一个 Looper 对象,并且通过 ThreadLocal 的 set() 方法把当前线程的 Looper 对象进行存储。
Java 的程序主入口是 main()
方法,Android 也是有 main()
方法的,在 ActivityThread 中,通过 Looper.prepareMainLooper();
将 ui 线程的 Looper 对象存入 ThreadLocal 中,这也是在ui线程中我们使用 Handler 不需要调用 Looper.prepare()
的原因。
上面是反向来看,我们再来顺着整理一遍:
Android 程序启动时,ActivityThread 类作为程序的入口类,在 ActivityThread 中的
main()
方法中调用Looper.prepareMainLooper();
,在Looper 的prepare()
方法中 new 一个 Looper 对象,并且通过 ThreadLocal 存储到当前线程中。然后在 ActivityThread 中的main()
方法中开启Looper.loop()
循环。
当我们在ui线程(主线程)new Handler()
时,通过 ThreadLocal 的get()
方法取出存储的 Looper 对象。如果是子线程中使用 Handler,因为没有通过 ThreadLocal 存储 Looper ,必须要手动通过Looper.prepare()
去创建一个 Looper,并且使用Looper.loop()
开启循环,但是Looper.loop()
里面是一个无线循环,会阻塞当前线程。
为什么在子线程弹出 Toast 会报错?
错误如下:
java.lang.RuntimeException: Can't create handler inside thread that has not called Looper.prepare()
顺着提示找到报错的源码如下:
if (looper == null) {
// Use Looper.myLooper() if looper is not specified.
looper = Looper.myLooper();
if (looper == null) {
throw new RuntimeException(
"Can't toast on a thread that has not called Looper.prepare()");
}
}
从上面知道 Looper.myLooper();
的源码是通过 sThreadLocal.get()
去获取当前线程的 Looper,由于是子线程,都没有通过 sThreadLocal.set()
去存储当前线程的 Looper,拿到的肯定是null,肯定会报错。
扩展一下:
Toast 创建时源码如下:
private final WindowManager.LayoutParams mParams = new WindowManager.LayoutParams();
从这可以看出,Toast本质上是一个 window,跟 activity 是平级的,checkThread() 只是Activity维护的 View树 的行为,不属于checkThread() 抛主线程不能更新UI异常的管理范畴。
Q:为什么主线程不会因为Looper.loop()里的死循环卡死?
Q:MessageQueue 没有消息时阻塞操作为什么不导致ANR?
Java 层跟着两个方法有关:
// MessageQueue.class
Message next() {
// ···
// 这是一个native方法,nextPollTimeoutMillis是阻塞的时长,阻塞操作是在native中执行的
nativePollOnce(ptr, nextPollTimeoutMillis)
// ···
}
boolean enqueueMessage(Message msg, long when) {
// ···
// 跟上面方法相对应,有新消息来,调用此方法从natvie中唤醒
nativeWake(mPtr);
// ···
}
这里就涉及到 Linux pipe/epoll 机制,简单说就是在主线程的
MessageQueue 没有消息时,便阻塞在 loop 的 queue.next()中的 nativePollOnce()方法里,此时主线程会释放 CPU 资源进入休眠状态,直到下个消息到达或者有事务发生,通过往 pipe 管道写端写入数据来唤醒主线程工作。
这里采用的 epoll机制,是一种 IO 多路复用机制,可以同时监控多个描述符,当某个描述符就绪(读或写就绪),则立刻通知相应程序进行读或写操作,本质是同步 I/O,即读写是阻塞的。所以说,主线程大多数时候都是处于休眠状态,并不会消耗大量 CPU 资 源。
https://www.zhihu.com/question/34652589
Q:如何将 Message 插入到 MessageQueue 的队列头?
调用 sendMessageAtFrontOfQueue()
,源码其实就是将延迟时间设置为 0;
public final boolean sendMessageAtFrontOfQueue(Message msg) {
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, 0);
}