一、MainThread的Looper创建和准备
Android 的启动过程是: 创建init进程 --> Zygote进程 --> SystemServer进程 --> 各个应用进程
在SystemServer进程启动后(由Zygote进程fork出)在调用run方法时,调用了Looper.prepareMainLooper();,在老版本的则是在ActivityThread.main()中调用的prepareMainLooper
// SystemServer.java
package com.android.server;
public final class SystemServer {
...
public static void main(String[] args) {
new SystemServer().run();
}
private void run() {
...
Looper.prepareMainLooper();
...
startBootstrapService();
startCoreService();
startOtherService();
...
Looper.loop();
...
}
...
}
于是,主线程在启动后,Activity#onCreate(Bundle) 调用时候就已经准备好Looper了
而工作线程Looper是没有调用prepare()/loop()的,因此需要自己手动调用或者使用HandlerThread/IntentService
遇到一个问Looper存储在哪里的问题
我们来看下Looper.java
// Looper.java
...
public static void prepare() {
// 工作线程的prepare传入了true,表示允许消息队列退出
prepare(true);
}
private static void prepare(boolean quitAllowed) {
if (sThreadLocal.get() != null) {
// 在调用prepare的时候如果ThreadLocal中就已经存在Looper了
// 则抛出异常,避免重复prepare和同一个线程出现多个Looper对象
throw new RuntimeException("Only one Looper may be created per thread");
}
// 向ThreadLocal 中存入创建的Looper对象
sThreadLocal.set(new Looper(quitAllowed));
}
public static void prepareMainLooper() {
// 主线程的prepare传入了false,表示主线程的消息队列是不允许对出的!!
prepare(false);
synchronized (Looper.class) {
if (sMainLooper != null) {
throw new IllegalStateException("The main Looper has already been prepared.");
}
// 此处为Looper对象的成员变量sMainLooper初始化
// myLooper()方法实际还是调用ThreadLocal.get()
sMainLooper = myLooper();
}
}
所以我们有答案了,Looper是保存在ThreadLocal这个线程本地存储里的,每一个线程只有一个线程本地存储,所以确保了同一个线程只有一个Looper!
附:ThreadLocal是怎样的数据结构:
// Looper.java
// Looper类中有静态常量ThreadLocal对象,通过ThreadLocal.get()获取Looper实例(线程唯一)
static final ThreadLocal<Looper> sThreadLocal = new ThreadLocal<Looper>();
// ThreadLocal.java
// get方法获取了当前线程,通过线程取得线程的ThreadLocalMap引用,然后根据指定泛型对象获取实例
// 简单说就是从Map中获取了ThreadLocal的键值对对象Entry,最后返回Entry.value即Looper对象
// 而这个Map是一个哈希数组,key是ThreadLocal>对象,就是说只有唯一的一个ThreadLocal,
// 因为ThreadLocal与别的泛型的ThreadLocal对象的hash值不同,因此确保了一个线程只有一个Looper
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
// Thread.java
ThreadLocal.ThreadLocalMap threadLocals = null;
总结:
1. 主线程Looper在启动时就已经prepare/loop,子线程需要手动执行或使用HandlerThread/IntentService;
2. Looper被存储在相应Thread的ThreadLocal对象中,这个对象是一个哈希数组,保证了Looper线程唯一;
在loop方法中,实际是一个死循环,即不断的从消息队列中获取消息,并分发给target(Handler)来处;
那么主线程是如何不会被这个死循环卡死的?
简化loop方法如下:
//Looper.java
public static void loop() {
// 获取不到Looper 抛出异常
final Looper me = myLooper();
if (me == null) {
throw new RuntimeException(
"No Looper; Looper.prepare() wasn't called on this thread.");
}
// 获取Looper 持有的消息队列
final MessageQueue queue = me.mQueue;
...
for (;;) {
// 进入死循环 不断从消息队列中获取下一个消息
// 官方在这里注释了might block表示获取下一个消息有可能会阻塞
Message msg = queue.next(); // might block
if (msg == null) {// 拿到空消息退出循环 这时候loop也退出了
// No message indicates that the message queue is quitting.
return;
}
...
try {
// 把非空消息分发给目标Handler
msg.target.dispatchMessage(msg);
}
...
// 对消息进行回收
msg.recycleUnchecked();
}
}
通过loop方法可以看出端倪,死循环在 queue.next() 方法处阻塞
再看简化的MessageQueue.next方法
// MessageQueue.java
Message next() {
// MessageQueue持有Native消息队列的指针,如果已经退出了就会返回空消息
final long ptr = mPtr;
if (ptr == 0) {
return null;
}
int pendingIdleHandlerCount = -1; // -1 only during first iteration
int nextPollTimeoutMillis = 0;
for (;;) {
// 再进入一个死循环,调用本地方法去检测描述符有没有新的消息可以poll出来
// 参数nextPollTimeOutMillis表示的是没有检测到新消息时的超时阻塞时间,-1表示一直阻塞
nativePollOnce(ptr, nextPollTimeoutMillis);
// 休眠被唤醒后执行同步代码块
synchronized (this) {
// 获取系统时间 和 消息队列头部Message
final long now = SystemClock.uptimeMillis();
Message prevMsg = null;
Message msg = mMessages;
// 省略同步屏障有关处理
...
if (msg != null) {
if (now < msg.when) {
// 如果消息的时间没有到(比如延时消息),重新计算nextPollTimeoutMillis
// 下一次循环,nativePollOnce阻塞的时间就是确定的
nextPollTimeoutMillis = (int) Math.min(
msg.when - now, Integer.MAX_VALUE);
} else {
mBlocked = false;
if (prevMsg != null) {
// 有同步屏障才会走这里
prevMsg.next = msg.next;
} else {
// 更新消息队列对头
mMessages = msg.next;
}
// 修改use标记和从消息队列中剥离出msg,最后返回msg
msg.next = null;
if (DEBUG) Log.v(TAG, "Returning message: " + msg);
msg.markInUse();
return msg;
}
} else {
// 取到空消息,重置nextPollTimeoutMillis 继续循环
nextPollTimeoutMillis = -1;
}
...
}
...
}
至此,已经找到问题的关键部分了。
Looper.loop()方法的死循环是在MessageQueue.next()获取队列中下一个消息时,阻塞的,并且如果主线程消息队列一直没有新消息时,就会一直阻塞,是被一个nativePollOnce()的本地方法阻塞的。
回到问题,阻塞了为什么没有导致主线程卡死。由于 Linux pipe/epoll机制,主线程会在此时(没有新的消息和事件进入),释放CPU资源,进入休眠状态,等待新的消息或者事件唤醒。
Linux pipe/epoll 机制是一种IO多路复用的机制,基于事件驱动,监测了多个文件描述符。即使没有消息,如果有其他的操作(比如事件)唤醒了主线程,主线程就退出休眠的状态,继续工作,而消息Looper的继续阻塞不会导致主线程卡死。
而在消息机制中,来了新的消息,也会调用一个本地方法唤醒休眠中的主线程
// MessageQueue.java
private native static void nativeWake(long ptr);
引:
一个线程不仅只有一个Looper,并且也只有一个MessageQueue,这是如何保证的;
如果向队列中发送大量消息,消息又一直在不断被处理,那么为什么不会频繁GC Message导致内存抖动?
带着这两个问题来看源码
// Looper.java
private static void prepare(boolean quitAllowed) {
if (sThreadLocal.get() != null) {
throw new RuntimeException("Only one Looper may be created per thread");
}
// 这里创建并向ThreadLocal中存入了Looper实例
sThreadLocal.set(new Looper(quitAllowed));
}
// 构造方法中创建了MessageQueue实例
private Looper(boolean quitAllowed) {
mQueue = new MessageQueue(quitAllowed);
mThread = Thread.currentThread();
}
第一个问题已经有答案了:
因为Looper由线程私有存储ThreadLocal的哈希数组结构保证线程唯一,MessageQueue又是由Looper实例化的成员变量,那么也是线程唯一的。
在MessageQueue中有个接口IdleHandler,顾名思义是一个空闲时处理任务的处理器。
public static interface IdleHandler {
boolean queueIdle();
}
这个接口提供了一个返回boolean值的方法。
在MessageQueue中有一个ArrayList
此外,还提供了addIdle、removeIdle等方法用于向集合添加和移除IdleHandler。
那么这个IdleHandler有什么作用呢?在何时调用queueIdle方法?
在MessageQueue#next()中有了答案:
Message next() {
// 省略了nativePollOnce等逻辑
...
// 在阻塞结束(消息队列中的消息执行完了,而队列中下一个要执行的消息还没有达到可执行的时间)时
if (pendingIdleHandlerCount < 0
&& (mMessages == null || now < mMessages.when)) {
pendingIdleHandlerCount = mIdleHandlers.size();
} // 获取列表中IdleHandler个数
if (pendingIdleHandlerCount <= 0) {
// 没有IdleHandler直接进入下一次循环
mBlocked = true;
continue;
}
if (mPendingIdleHandlers == null) {
mPendingIdleHandlers = new IdleHandler[Math.max(pendingIdleHandlerCount, 4)];
}
mPendingIdleHandlers = mIdleHandlers.toArray(mPendingIdleHandlers);
}
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 {
// 在这里调用了queueIdle方法!!!
keep = idler.queueIdle();
} catch (Throwable t) {
Log.wtf(TAG, "IdleHandler threw exception", t);
}
if (!keep) {
synchronized (this) {
mIdleHandlers.remove(idler);
}
}
}
}
也就是当消息队列空闲时,可以执行IdleHandler的一些任务,这样可以提升性能。
比如在onResume方法中,使用主线程的消息队列的IdleHandler做一些准备工作,或者单纯的想获取UI绘制完成的回调,在绘制完成以后立即可以执行一些操作。
在使用Message时,通常建议使用Message.obtain()系列的方法,这是有原因的。
当然也可以通过new Message() 来创建消息,只是更推荐上面的方式,并且new的方式最终也会被做一些响应处理。
// Message.java
public final class Message implements Parcelable {
...
Message next;// 消息单向链表的下一个消息对象
/** @hide */
public static final Object sPoolSync = new Object();// 同步锁
private static Message sPool;// 私有静态成员,消息池
private static int sPoolSize = 0;// 消息池的大小,即消息对象个数
private static final int MAX_POOL_SIZE = 50;// 消息池最多可容纳50个消息对象
...
}
由此可以看出消息池最多可以服用的消息对象是50个,这个池是一个单向链表的数据结构;
// Message.java
public static Message obtain() {
synchronized (sPoolSync) {// 同步锁
if (sPool != null) {
// 当池可以复用时,取出单向链表队头的消息对象使用,
// 然后把队头重置为next,并维护池中可用的消息对象的个数
Message m = sPool;
sPool = m.next;
m.next = null;
m.flags = 0; // clear in-use flag
sPoolSize--;
return m;
}
}
// 消息池被清空了智能创建新的消息对象
return new Message();
}
答案就是,消息对象维护了一个消息池,由于消息本身是一种单向链表的结构,维护对头来复用消息对象,并且在Looper.loop()方法中通过recycleUnchecked()方法来回收消息对象。
Looper获取了消息队列中的消息后,通过Handler#dispathMessage()方法来分发给Handler,就会重发handleMessage方法,于是被重写的handleMessage中的业务逻辑就被执行了。
在日常使用中,经常使用sendMessageDelay/postDelay/sendMesageAtTime等方法,那么延时消息是怎么入队的呢?
// Handler.java
public final boolean sendMessageDelayed(@NonNull Message msg, long delayMillis) {
if (delayMillis < 0) {
delayMillis = 0;
}
return sendMessageAtTime(msg, SystemClock.uptimeMillis() + delayMillis);
}
首先,这些方法最终都是调用sendMessageAtTime实现的!
// Handler.java
public boolean sendMessageAtTime(@NonNull Message msg, long uptimeMillis) {
MessageQueue queue = mQueue;
...
return enqueueMessage(queue, msg, uptimeMillis);//
}
其次,直接调用了enqueueMessage进行入队!这个方法最终调用了MessageQueue#enqueueMessage()方法
那么是怎么实现延时呢?
下面是简化后的MessageQueue#enqueueMessage()方法
// MessageQueue.java
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;
}
// 标记入队的消息inUse 获取时间 获取消息队列对头
msg.markInUse();
msg.when = when;
Message p = mMessages;
boolean needWake;
if (p == null || when == 0 || when < p.when) {
// 当消息队列中没有消息或者新入队的消息时间小或为0,需要先执行入队的新消息
// 变更了对头
msg.next = p;
mMessages = msg;
needWake = mBlocked;
} else {
// 同步屏障有关省略
...
Message prev;
for (;;) {
prev = p;
p = p.next;
// 找到第一个时间比要入队新消息的消息然后放入,或者没有就放队尾
// 实质上是消息队列用when这个时间来排序了,时间小的在队头,从小到大的顺序排列
// 新消息找到正确的入队位置就退出此循环
if (p == null || when < p.when) {
break;
}
// 同步屏障有关省略
...
}
// 然后这一步就是真正的入队
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;
}
答案有了:延时消息也是在触发时就入队了,只是消息队列里面会依据when这个时间标志从小到大屏排序;
那怎样保证延时处理呢?答案就在Looper.loop()方法中,阻塞的过程中有一个nextPollTimeoutMillis参数,如果消息队列中队头就是一个延时消息,那么loop在取消息的时候,会计算一个新的nextPollTimeoutMillis阻塞超时时间,来在delay的时间向Handler交付Message,于是就实现了延时消息!!!
Handler在Activity中使用时常常要注意避免内存泄露,这里泄露的对象就是Activity,常常在使用延时消息时,需要注意如果延时消息还没有处理,Activity就销毁了,那么就会出问题。
MainThread --> ThreadLocal
--> Looper --> MessageQueue --> Message --> Handler --> Activity
匿名内部类Handler持有Activity的引用,Message.target.dispatchMessage()持有了target(Handler)的引用,就是延时消息持有了Handler的引用,延时消息又enqueue在MessageQueue中,MessageQueue由是由Looper创建的,最终存放在主线程ThreadLocal中了。
只要打破引用链的环节就解决了