//【1】拿到队列头部
Message p = mMessages;
boolean needWake;
//【2】如果消息不需要延时,或者消息的执行时间比头部消息早,插到队列头部
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 {
//【3】消息插到队列中间
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;
}
if (needWake) {
nativeWake(mPtr);
}
}
return true;
}
主要分为3个步骤(见以上代码标注)。
mMessages
是队列的第一消息,获取到它- 判断消息队列是不是空的,是则将当前的消息放到队列头部;如果当前消息不需要延时,或当前消息的执行时间比头部消息早,也是放到队列头部。
- 如果不是以上情况,说明当前队列不为空,并且队列的头部消息执行时间比当前消息早,需要将它插入到队列的中间位置。
如何判断这个位置呢?依然是通过消息被执行的时间。
通过遍历整个队列,当队列中的某个消息的执行时间比当前消息晚时,将消息插到这个消息的前面。
可以看到,消息队列是一个根据消息【执行时间先后】连接起来的单向链表。
想要获取可执行的消息,只需要遍历这个列表,对比当前时间与消息的执行时间,就知道消息是否需要执行了。
好了,MessageQueue
在Java
层的分析到这里就结束了。
等等,这就结束了?
没错,到这一步,消息已经添加到了这个名为队列实为单链表的队列中。
不对啊,我handleMessage
方法如何被调用呢?消息添加进去就完了?说好的线程切换呢?
其实到这一步真的就结束了,最起码在Java层是结束了。消息到这一步被添加到了队列中,Handler
和MessageQueue
在发送的过程中做的工作已经做完了。但是既然有队列,那么不可能说光添加不读取把。不然我添加了有什么用?
是的,接下来就是Looper
大展神威的时候到了。
Looper
在上面提到了,Handler
中的MessageQueue
对象其实就是Handler
中的Looper
它的MessageQueue
,Handler
往MessageQueue
中添加消息,其实就是往Handler
的Looper
所持有的MessageQueue
中添加对象。可能有点绕,但是需要明白的是这个MessageQueue
是Looper
的,不是Handler
的。明白了这一点,你就能很好的理解后面发生的事情。
可能有的小伙伴会说了,这个Looper
哪来的,我创建Handler
的时候从没看见过它出现啊。没错,在使用Handler
的时候它确实没出现过,但是大家还记得Handler
中两个参数的那个构造方法嘛?就是下面这个:
//Handler.java
//省略部分代码
public Handler(@Nullable Callback callback, boolean async) {
//敲黑板,划重点就是这一句!!!!
mLooper = Looper.myLooper();
if (mLooper == null) {
throw new RuntimeException(
"Can’t create handler inside thread " + Thread.currentThread()
- " that has not called Looper.prepare()");
}
mQueue = mLooper.mQueue;
mCallback = callback;
mAsynchronous = async;
}
在这一句中Handler
通过Looper.myLooper
方法获取到了Looper
对象,当然,也有可能没获取到。不过,你如果没获取到就惨了,就要抛异常了。
在职责分析中我们提到了, 这个Looper
对象作为消息循环的核心,不断从它的MessageQueue
中取出消息然后进行分发。
说人话可以不?
刚才说到MessageQueue
那个队列中那么多的消息没人拿,MessageQueue
的老板Looper
看不下去了,说你这也太浪费了,来我拿吧,然后它专门负责一个个拿,然后看这是谁发的,然后让谁去处理。
那我们看看这个Looper.myLooper()
方法做了什么事情呢。它是如何返回一个Looper
对象的呢?
public static @Nullable Looper myLooper() {
return sThreadLocal.get();
}
sThreadLocal
又是什么鬼?咱们看一下它的定义
//sThreadLocal.get() will return null unless you’ve called prepare().
@UnsupportedAppUsage
static final ThreadLocal sThreadLocal = new ThreadLocal();
可以看到这个sThreadLocal
是一个ThreadLocal
类,并且它的泛型是Looper
对象。ThreadLocal
提供了线程的局部变量,每个线程都可以通过set()
和get()
来对这个局部变量进行操作,但不会和其他线程的局部变量进行冲突,实现了线程的数据隔离。简要言之:往ThreadLocal
中填充的变量属于当前线程,该变量对其他线程而言是隔离的。
呀呵,源代码中还有行注释,这行注释的意思是除非您已调用prepare(),否则sThreadLocal.get()将返回null。这行注释就有趣了,刚才我还寻思这个ThreadLocal
的get方法得有数据才能返回,可这个数据是啥时候塞进去的呢?你这注释就告诉我了,只有我调用了prepare()
方法,才有值啊。那我们就去看看这个方法做了些什么。
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”);
}
sThreadLocal.set(new Looper(quitAllowed));
}
复制代码
可以看得出,最后调用了是prepare(boolean quitAllowed)
方法,而这个方法首先判断,如果sThreadLocal
有值,就抛异常,没有值才会塞进去一个值。其实很好理解,就是说prepare
方法必须调用但也只能调用一次,不调用没有值,抛异常,调用多次也还抛异常。我好难哦~不过大家还记得上面我们重点关注的一个内容吗,在Handler中的有参构造函数中有这么一行代码会报异常:
mLooper = Looper.myLooper();
if (mLooper == null) {
throw new RuntimeException(
"Can’t create handler inside thread " + Thread.currentThread()
- " that has not called Looper.prepare()");
}
mQueue = mLooper.mQueue;
如果Looper
为空就抛异常,现在我们知道了,什么时候Looper
为空呢?没有调用prepare
方法的时候会为null
,也就是说在构造Handler
之前,必须得有Looper
对象,换言之,**在构造Handler之前,必须调用Looper
的prepare
方法创建Looper
。**这句话非常重要,所以我又是下划线又是加粗的,一定要记住这句话。在后面自定义一个Looper的时候会用到。
接下来再看看这行sThreadLocal.set(new Looper(quitAllowed));
做了什么吧,它是如何塞进去的呢
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
set
方法首先获取到了当前的线程,然后获取一个map
。这个map
是以键值对形式存储内容的。如果获取的map
为空,就创建一个map
。如果不为空就塞进去值。要注意的是,这里面的key
是当前的线程,这里面的value
就是Looper
。也就是说,线程和Looper
是一一对应的。也就是很多人说的Looper
和线程绑定了,其实就是以键值对形式存进了一个map
中。没什么高大上的。你来你也行。
而这个Looper的构造方法我们也得去看一下:
private Looper(boolean quitAllowed) {
mQueue = new MessageQueue(quitAllowed);
mThread = Thread.currentThread();
}
在Looper
的构造方法中,可以看到它创建了一个MessageQueue
,没错就是那个被Handler
无耻使用的MessageQueue
。需要注意的一点,上面的分析中提到了prepare
方法必须调用但也只能调用一次,调用以后就会创建Looper对象,也就是说一个线程中只会创建一个Looper
对象,而一个Looper
对象也只会创建一个MessageQueue
对象。
现在我们来梳理一下这个流程哈~
首先创建一个无参数的Handler
,在这个Handler
的构造方法中又去获取Looper
对象,当然获取Looper
对象其实是为了它的MessageQueue
,Handler
巴结上了人家Looper
对象的MessageQueue
以后,发送消息的时候,把要发送的消息给了MessageQueue
,添加到了队列中。是不是感觉缺少了什么?没错,好像在这个里面Looper
的作用没体现出来,说好的分发消息呢?而且你刚刚说了得调用prepare()
方法才会创建Looper
,可我没调用过这个方法啊。那这个Looper
谁创建的?
刚才提到了,Looper
在创建的时候会被当成value
塞入到一个map
中去,这个map
是ThreadLocal
。而key
就是创建Looper
时所在的线程。也就是所谓的Looper
和线程绑定。我们一般在用的时候从没创建过Looper
,但是我们知道handle
中的回调handleMessage
方法是运行在主线程中的。Looper
的职责不就是分发消息么,也就是说Looper
对象在主线程中把消息分发给了Handler
。那么这下就明白了,在我们创建Looper
的时候,Looper
所在的线程是主线程,换言之,与这个Looper
绑定的线程就是主线程。
明白了,我这就去和面试官对线。
既然是主线程,那么大家应该知道,主线程是谁创建的?ActivityThread
类。ActivityThread
类也正是整个app的入口。以前我也很好奇,既然Android
是用Java
写的,按理说Java
不应该是有个什么main
方法么?怎么我写Android
没用过这个main
方法呢?其实呢,在ActivityThread
中就有这个main
方法,它是程序的入口,也就说当你点开app
以后,首先会进入到这个main
方法中,然后做了一大堆事情,这里就不分析了。你只需要知道,这个main
方法才是真正的入口。
那我们来看看这个main方法到底干了什么事情:
//ActivityThread.java
//省略部分代码
public static void main(String[] args) {
Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, “ActivityThreadMain”);
Process.setArgV0(“”);
//1 敲黑板,划重点,就是这一句!
Looper.prepareMainLooper();
ActivityThread thread = new ActivityThread();
thread.attach(false, startSeq);
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);
//2 敲黑板,划重点,这一句!
Looper.loop();
throw new RuntimeException(“Main thread loop unexpectedly exited”);
}
这段代码是不是很符合我们平常写的java程序呢?熟悉的main方法又回来了。main方法中可以看到,它调用了Looper的prepareMainLooper
方法:
public static void prepareMainLooper() {
//设置不允许退出的Looper
prepare(false);
synchronized (Looper.class) {
if (sMainLooper != null) {
throw new IllegalStateException(“The main Looper has already been prepared.”);
}
sMainLooper = myLooper();
}
}
可以看到注释1,这个方法最终还是调用了Looper
的prepare
方法,这个方法干嘛的?创建Looper
并且把它和当前线程一起塞进map
中的啊。当前线程是哪个线程?主线程啊!
一切到这里就真相大白了,在APP
启动的时候,入口方法中已经自动帮我们创建好了Looper
,并且也自动的帮我们和主线程绑定了。也就是说我们平常用的Handler
中的Looper
就是主线程中创建的这个Looper
。细心的小伙伴应该会发现,这个prepareMainLooper
方法你是不能调用的。为啥?因为这个方法在入口的时候执行了一次,所以里面的sMainLooper
不为Null了,如果你在调用一次,不就要抛异常了么~
现在Looper
也有了,Looper
的MessageQueue
也有了。接下来该分发消息了吧?我Handler
发送消息可是已经很久过去了,你这里分析一大通,我还干不干活了?
好,我们现在先假设一个场景。
你买了一个快递,你知道迟早会给你送到,但是不确定到底什么时候才会送到。你想早点拿到快递应该怎么做?
你会不停的问快递公司,我的快递到哪了,到哪了。当然,现实中一般都是等快递员打电话才去拿快递~问题在于,这是程序。
Looper
虽说要分发消息,但是它又不知道你什么时候会发送消息,只能开启一个死循环,不断的尝试从队列中拿数据。这个死循环在哪里开始的?没错就是注释2处,Looper.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;
// 省略一些代码…
//1 这里开启了死循环
for (;😉 {
Message msg = queue.next(); // might block
if (msg == null) {
// No message indicates that the message queue is quitting.
return;
}
try {
msg.target.dispatchMessage(msg);
//省略一些代码…
} catch (Exception exception) {
//省略一些代码…
throw exception;
} finally {
//省略一些代码…
}
//省略一些代码…
msg.recycleUnchecked();
}
}
在循环中Looper
不停的取出消息,拿到Message
对象以后,会去调用Message
的target
字段的dispatchMessage
方法,这个target
字段还有印象吗?没错,就是发送它的Handler
,message
在被发送出去的时候就已经暗暗记下了是谁发送出去的。现在轮到它报仇了~
我们可以跟进看一下这个dispatchMessage
方法:
//Handler.java
public void dispatchMessage(@NonNull Message msg) {
if (msg.callback != null) {
handleCallback(msg);
} else {
if (mCallback != null) {
if (mCallback.handleMessage(msg)) {
return;
}
}
handleMessage(msg);
}
}
可以看到,消息会先分发给Meesgae
的callback
,我们没有定义这个callback
,那我们接下来看,还有一个mCallback
。这个mCallback
是创建Handler
的时候可以选择传一个CallBack
回调,里面依然是handleMessage
方法。也就是说你可以自定义一个类继承Handler
,重写handleMessage
方法,也可以直接new
一个Handler
传一个回调。当然,这个都很简单,我就不再赘述了,大家可以自行尝试体验。
我们关注重点,当Looper
拿到Message
以后,并且根据Message
的target
字段找到了发送消息的Handler
,紧接着调用了Handler
的handleMessage
方法。重点来了,这个Looper
是在哪个线程运行的?主线程,它调用方法是在哪个线程运行的?依然是主线程!handleMessage
方法此时在哪个线程运行的?依然是主线程!不知不觉中,线程已经切换过来了,神奇不?其实并不神奇,其实就是主线程中的Looper
不断的尝试调用handleMessage
方法,如果有消息就调用成功了,此时handleMessage
方法就是在主线程中调用的。而handler
在哪个线程,Looper
并不关心,我反正只在主线程调用你的handleMessage
方法。这就是线程切换的本质。就是没有线程切换,主线程的Looper
不断的尝试调用而已。
可能有的小伙伴已经懵逼了,我们再次从头到尾梳理一下哈~
mainThread
中ActivityThread
首先创建了一个运行在主线程的Looper
,并且把它和主线程进行了绑定。Looper
又创建了一个MessageQueue
,然后调用Looper.loop
方法不断地在主线程中尝试取出Message
Looper
如果取到了Message
,那么就在主线程中调用发送这个Message
的Handler
的handleMessage
方法。- 我们在主线程或者子线程中通过
Looper
.getMainLooper
为参数创建了一个Handler
。 - 在子线程中发送了
Message
,主线程中的Looper
不断循环,终于收到了Message
,在主线程中调用了这个Handler
的handleMessage
方法。
这里需要注意的是,Looper.loop
方法中取到的Looper
对象并不一定就是主线程的,因为它是取出当前线程的Looper
对象。只不过在ActivityThread
这里是主线程,所以拿到的是主线程的Looper
对象。所以如果我们要在子线程中创建一个Looper
也是可以的,一会我们就实现一下。
到这里可能有的小伙伴还是懵逼,我还是不太明白怎么切换的线程。我们通过一个比喻很好的解释一下
首先有一个小学生小明,小明的任务是写作业。然后有一个老师,老师的任务是批改作业。这个班里还有一个学习委员,学习委员的任务就是负责收作业然后交给老师去批改。
一般情况下,老师是学校已经聘请好的,我们不需要自己去聘请老师。老师一般也就只在办公室批改作业,办公室我们可以理解为主线程。学校就是我们的app
。老师就是Looper
对象。而小明同学就是Handler
,学习委员就是MessageQueue
,作业就是Message。老师(Looper
)在办公室(主线程)不断的从学习委员(MessageQueue
)那里拿到下一本要批改的作业(Message
),老师突然发现作业里有错误,老师很生气,于是就从作业本上的姓名知道了是谁写的这个作业(对应Message
的target
字段),于是老师把小明(Handler
)叫到办公室(主线程),让小明在办公室(主线程)把作业改好(handleMessage
)。在这个例子中,小明作为Handler,他可以在任何地方写作业(sendMessage
),也就是说他可以在家里写作业,可以在教室写作业,也可以在小公园写作业,这里的各个地方就是不同的线程。但是写完作业以后一定要交给学习委员,学习委员手里有一摞作业,这一摞作业就是消息的队列,而学习委员就是MessageQueue
,他负责收集作业,也就是收集Message
。老师在办公室批改作业,发现出错了,就把小明叫到了办公室,让小明在办公室改错。在这里,办公室就是主线程,老师不会管小明是在哪里写的作业,老师只是关心作业出错了,需要小明在办公室里改错。小明在办公室里改错这就是handleMessage
方法运行在了主线程。但是也有个问题,不能说你小明在办公室改错改个没完没了,那岂不是影响了后面同学作业的批改?如果小明真的改错改的没完没了,也就是在主线程上作耗时操作很久,那么老师也无法进行下一个同学的作业批改,时间一长,教学就没法进行了。这就是著名的ANR问题。不知道这样比喻,小伙伴们能不能理解线程切换的意思和ANR的意思。如果还不能理解,那么你来砍我吧~
Looper和ANR
很多面试官喜欢问,Looper
的loop
方法是个死循环,而loop
方法又是运行在主线程的,主线程上有死循环为什么不会导致ANR存在呢?
其实这里面很有趣的一个点就是,很多小伙伴把Looper
的loop
方法当做一个普通方法来看待,所以才会有这样的疑问。但是这个loop
方法并不是一个普普通通的方法。
我们先思考一点,如果我们写一个app
,里面一行代码也不写的话,app会不会崩溃?
答案显而易见,是不会的。
可是在上面提到了,本质上App
就是一个Java
程序,Java
程序就有main
方法,在ActivityThread
类中也确实有这个main
方法。我们一般写java
程序的时候,是不是main
方法中的代码执行完,程序也就结束了。但是app
并没有,只要你不退出,它一直运行。那这是为什么呢?
很多小伙伴应该想到了,没错,让程序不退出的话,写一个死循环,那么main
方法中的代码永远不会执行完,这样程序就不会自己退出了。Android当然也是这么干的,而且不止Android,基本上所有的GUI程序都是这么干的。正是因为Looper.loop
方法这个死循环,它阻塞了主线程,所以我们的app才不会退出。那你可能有疑问了,那既然这里有死循环了,那我其他的代码怎么运行?界面交互怎么办?你问到点子上了。
本质上Android就是事件驱动的程序,界面刷新也好,交互也好,本质上都是事件,这些事件最后通通被作为了Message
发送到了MessageQueue
中。由Looper
来进行分发,然后在进行处理。用人话来说就是,我们的Android程序就是运行在这个死循环中的。一旦这个死循环结束,app也就结束了。
那么ANR
是什么呢?ANR
是Application Not Responding也就是Android程序无响应。为什么没响应呢?因为主线程做了耗时操作啊。可我还是不明白,明明Looper
的loop
方法就是阻塞了主线程,为什么不ANR
呢。那我们就来说道说道,什么是响应?响应就是界面的刷新,交互的处理等等对吧。那么这个响应是谁来响应的?没错,就是loop
方法中进行响应的。没响应什么意思?就是loop方法中被阻塞了,导致无法处理其他的Message
了。
所以结论就来了,主线程做耗时操作本质上不是阻塞了主线程,而是阻塞了Looper
的loop
方法。导致loop
方法无法处理其他事件,导致出现了ANR事件。对比小明这个比喻的话,就是因为小明在办公室里没完没了的改作业,占用了老师的时间,让老师没法批改下一个同学的作业,才导致了教学活动无法正常进行。而老师不断的批改作业,这本身就是正常的教学活动,也正是因为老师不断批改作业,同学们才有提高,教学才能继续。
Handler在Java层几个需要注意的点
如果要创建Handler,必须通过Looper.prepare()方法创建Looper,在主线程中ActivityThread已经帮我们创建好了,我们不需要自己去创建,但如果在子线程中创建Handler,要么使用Looper的mainLooper,要么自己调用Looper.prepare()方法创建属于这个线程的looper对象。如下是创建了一个子线程的Looper对象:
class LooperThread extends Thread {
public Handler mHandler;
public void run() {
Looper.prepare();
mHandler = new Handler() {
public void handleMessage(Message msg) {
//TODO 定义消息处理逻辑.
}
};
Looper.loop();
}
}
在生成消息的时候,最好是用 Message.obtain()
来获取一个消息,这是为什么呢?
// Message.java
public static Message obtain() {
synchronized (sPoolSync) {
if (sPool != null) {
Message m = sPool;
sPool = m.next;
m.next = null;
m.flags = 0; // clear in-use flag
sPoolSize–;
return m;
}
}
return new Message();
}
可以看到,obtain方法是将一个Message对象的所有数据清空,然后添加到链表头中。sPool就是个消息池,默认的缓存是50个。而且在Looper的loop方法中最后一行是这样的
msg.recycleUnchecked();
Looper在分发结束以后,会将用完的消息回收掉,并添加到回收池里。
什么是内存泄露?简而言之就是该回收的东西没有回收。在Handler中一般是这样使用:
class HandlerActivity: AppCompatActivity() {
private val mHandler = MyHandler()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// 在子线程中通过自定义的 Handler 发消息
thread {
mHandler.sendEmptyMessageDelayed(1, 1000)
}
}
// 自定义一个 Handler
class MyHandler: Handler() {
override fun handleMessage(msg: Message) {
Log.i(“HandlerActivity”, “主线程:handleMessage: ${msg.what}”)
}
}
}
乍一看没有问题,但是有没有想过一个问题,就是说再发送延时消息之前,app推出了,那么handleMessage方法还会执行吗?答案是会的。为什么?我明明退出了,为什么还会执行呢?其实这和java有关系
MyHandler
是HandlerActivity
的内部类,会持有HandlerActivity
的引用。在进入页面以后,发送了一个延时 1s 的消息,如果
HandlerActivity
在 1s 内退出了,由于Handler
会被Message
持有,保存在其target
变量中,而Message
又会被保存在消息队列中,这一系列关联,导致HandlerActivity
在退出的时候,依然会被持有,因此不能被GC
回收,这就是内存泄漏!当这个 1s 延时的消息被执行完以后,HandlerActivity
会被回收。
虽然最终结果还是会被回收,但是内存泄露问题我们也必须去解决,如何解决?
1.将
MyHandler
改为静态类,这样它将不再持有外部类的引用。可以将HandlerActivity
作为弱引用放到MyHandler
中使用,页面退出的时候可以被及时回收。2.页面退出的时候,在
onDestroy
中,调用Handler
的removeMessages
方法,将所有的消息 remove 掉,这样也能消除持有链。
什么是同步消息屏障?
在Looper的loop方法中通过Message msg = queue.next();
这么一行代码拿到Message进行分发,这个MessageQueue的next方法中有这么一行:
//MessageQueue.java
//省略部分代码
Message next() {
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;
//1 这一行很关键,同步消息屏障的关键点所在
if (msg != null && msg.target == null) {
do {
prevMsg = msg;
msg = msg.next;
} while (msg != null && !msg.isAsynchronous());
}
}
}
}
注释1下面的这一行代码,首先会判断msg
不为null,然后紧接着判断msg
的target
为null。我们知道message
的target
就是发送它的handler
,所有的message
都有一个handler
,这里怎么可能没有handller
呢?针对同步消息还真的是所有的message
都有handler
,而这里是异步消息。满足target == null
的消息就是异步消息。同步屏障
是用来阻挡同步消息执行的。说得好,那么同步屏障有什么用呢?
似乎在日常的应用开发中,很少会用到同步屏障。那么,同步屏障在系统源码中有哪些使用场景呢?Android 系统中的 UI 更新相关的消息即为异步消息,需要优先处理。简而言之,如果在启动绘制之前,用户(开发者)插入了一个非常耗时的消息到队列中,那就会导致 UI
不能按时绘制,导致卡顿掉帧。,同步消息屏障就可以用来保证 UI
绘制的优先性。
Handler在C++层
如果你的目标是理解Handler在Java层是如何实现的,下面就不需要看了。下面主要讲解Handler在C++层是如何工作并实现的。
首先,细心的小伙伴们可能会有疑问。Looper一直处于死循环中,就像老师一直不断的问学习委员要作业批改,老师也是人,不会累么?你问对了,老师当然不会一直不断的问学习委员要作业,正常情况下,是有人交了作业以后,学习委员送过来,老师才会去批改。没有作业的时候,老师可能在休息,可能在玩游戏**。Looper也是一样,在消息队列为空的时候,Looper实际上处于休眠状态,只要当有Handler发送消息的时候,Looper才会被唤醒,去进行分发消息**。那么是怎么实现的呢?
在整个消息机制中,MessageQueue
是连接Java
层和Native
层的纽带,换言之,Java
层可以向MessageQueue
消息队列中添加消息,Native
层也可以向MessageQueue
消息队列中添加消息。
这是MessageQueue
中的Native方法:
// MessageQueue.java
private native static long nativeInit();
private native static void nativeDestroy(long ptr);
private native void nativePollOnce(long ptr, int timeoutMillis);
private native static void nativeWake(long ptr);
private native static boolean nativeIsPolling(long ptr);
private native static void nativeSetFileDescriptorEvents(long ptr, int fd, int events);
在MessageQueue的构造方法中是这样的:
//MessageQu
eue.java
MessageQueue(boolean quitAllowed) {
mQuitAllowed = quitAllowed;
mPtr = nativeInit();
}
调用了nativeInit
方法,在native
层创建了native
层的MessageQueue
,mPtr
是保存了NativeMessageQueue
的指针,后续的线程挂起和线程的唤醒都要通过这个指针来完成,其实就是通过Native层的MessageQueue来完成。
//android_os_MessageQueue.cpp
static jlong android_os_MessageQueue_nativeInit(JNIEnv* env, jclass clazz) {
//初始化native消息队列
NativeMessageQueue* nativeMessageQueue = new NativeMessageQueue();
nativeMessageQueue->incStrong(env); //增加引用计数
return reinterpret_cast(nativeMessageQueue);
}
这个是NativeMessageQueue的构造方法:
//android_os_MessageQueue.cpp
NativeMessageQueue::NativeMessageQueue()
: mPollEnv(NULL), mPollObj(NULL), mExceptionObj(NULL) {
mLooper = Looper::getForThread(); //功能类比于Java层的Looper.myLooper();
if (mLooper == NULL) {
mLooper = new Looper(false); //创建native层的Looper
Looper::setForThread(mLooper); //保存native层的Looper到TLS,功能类比于Java层的ThreadLocal.set();
}
}
Looper的构造方法是这样的:
//Looper.cpp
Looper::Looper(bool allowNonCallbacks) :
mAllowNonCallbacks(allowNonCallbacks), mSendingMessage(false),
mPolling(false), mEpollFd(-1), mEpollRebuildRequired(false),
mNextRequestSeq(0), mResponseIndex(0), mNextMessageUptime(LLONG_MAX) {
mWakeEventFd = eventfd(0, EFD_NONBLOCK); //构造唤醒事件的fd
AutoMutex _l(mLock);
rebuildEpollLocked(); //重建Epoll事件
}
void Looper::rebuildEpollLocked() {
if (mEpollFd >= 0) {
close(mEpollFd); //关闭旧的epoll实例
}
mEpollFd = epoll_create(EPOLL_SIZE_HINT); //创建新的epoll实例,并注册wake管道
struct epoll_event eventItem;//新建唤醒监听事件
memset(& eventItem, 0, sizeof(epoll_event)); //把未使用的数据区域进行置0操作
eventItem.events = EPOLLIN; // 设置监听内容可读事件
eventItem.data.fd = mWakeEventFd;
//将唤醒事件(mWakeEventFd)添加到epoll实例(mEpollFd)
int result = epoll_ctl(mEpollFd, EPOLL_CTL_ADD, mWakeEventFd, & eventItem);
for (size_t i = 0; i < mRequests.size(); i++) {
const Request& request = mRequests.valueAt(i);
struct epoll_event eventItem;
request.initEventItem(&eventItem);
//将request队列的事件,分别添加到epoll实例
int epollResult = epoll_ctl(mEpollFd, EPOLL_CTL_ADD, request.fd, & eventItem);
}
}
等等,你上来给我这一大段C++代码,我怎么可能看得懂。还有这个epoll是什么?不是讲如何Looper怎么休眠和唤醒的么?
没错,就是讲的Looper
怎么休眠和唤醒的。Looper
的休眠和唤醒都是在Native层实现的,实现的原理是Linux上的epoll机制。
什么是epoll
机制呢?
epoll
你可以简单的理解为一个监听事件,在Linux
上通过epoll
机制监听一个事件,没什么事的时候我就让出CPU,进行休眠,当这个事件触发的时候我就从沉睡中唤醒开始处理。就像按钮的点击事件一样,点击了,监听到这个点击事件就会触发按钮的onClick
方法。不过在LInxu上是通过文件的读写来完成的。类比于
include <sys/epoll.h>
// 创建句柄 相当于初始化onClickListener
int epoll_create(int size);
// 添加/删除/修改 监听事件 相当于addOnClicklistener
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
// 进入等待 这就相当于onCLick方法了
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
epoll_wait
就类似于Java中的onCLick
方法,当监听的文件有变化的时候,它就会收到结果。其实更像是Kotlin协程中的suspend方法,就一直在等着,阻塞着,有结果才会进行下一步。onClick方法是使用接口回调的形式来实现的,是非阻塞的。而epoll_wait
方法是阻塞的。
在上面的Looper
构造方法中,调用了rebuildEpollLocked
方法,这个方法就是设置监听器的,可以理解为setOnClickListener
,不过它监听的是文件的可读事件。 即eventItem.events = EPOLLIN;
这行代码。什么是可读事件?就是说,文件里面有内容了是不是就可以读了,没错就是这样喵~
好了,事件也已经监听了,那么Looper是在哪沉睡的呢?
是在MessageQueue中的这行代码:
//MessageQueue.java
Message next() {
final long ptr = mPtr;
if (ptr == 0) {
return null;
}
for (;😉 {
…
nativePollOnce(ptr, nextPollTimeoutMillis); //阻塞操作
…
}
就是通过这行代码进行阻塞操作。
调用关系是这样的:
MessageQueue::nativePollOnce
->NativeMessageQueue::pollOnce()
->Looper::pollOnce()->Looper::pollInner
int Looper::pollInner(int timeoutMillis) {
struct epoll_event eventItems[EPOLL_MAX_EVENTS];
//1. 等待事件发生或者超时,如果nativeWake()方法中向管道写端写入字符,则该方法会返回;
int eventCount = epoll_wait(mEpollFd.get(), eventItems, EPOLL_MAX_EVENTS, timeoutMillis);
mPolling = false;
mLock.lock();
if (mEpollRebuildRequired) {
mEpollRebuildRequired = false;
rebuildEpollLocked();
goto Done;
}
if (eventCount < 0) {
if (errno == EINTR) {
goto Done;
}
result = POLL_ERROR;
goto Done;
}
if (eventCount == 0) {
result = POLL_TIMEOUT;
goto Done;
}
//循环遍历,处理所有的事件
for (int i = 0; i < eventCount; i++) {
int fd = eventItems[i].data.fd;
uint32_t epollEvents = eventItems[i].events;
//唤醒事件
if (fd == mWakeEventFd.get()) {
if (epollEvents & EPOLLIN) {
已经唤醒了,则读取并清空管道数据【7】
awoken();
} else {
ALOGW(“Ignoring unexpected epoll events 0x%x on wake event fd.”, epollEvents);
}
} else {
// 处理其他事件,Handler没有
// 省略一些代码…
}
}
Done: ;
//省略一些代码…
// Release lock.
mLock.unlock();
//省略一些代码…
return result;
}
代码到了注释1处就开始了阻塞,也就是所谓的休眠。那么什么时候才能唤醒它呢?超时了,或者文件发生了变化,可以读了就可以唤醒了。注意,这个超时就是在Java层设置的延时发送,也就是说Java的sendMessageDelayed
方法最后是通过epoll
设置超时的机制实现延迟发送的。
不知道大家注意到没有,在我们发送Message的时候有这么一行代码:
// MessageQueue.java
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 {
//消息插到队列中间
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;
}
if (needWake) {
// 敲黑板划重点:唤醒
nativeWake(mPtr);
}
}
return true;
}
在最后nativeWake(mPtr);
这行代码进行了唤醒。不过必须neekWake
为true
的时候才会唤醒,那么neekWake
什么时候才是True呢?
两种情况会唤醒线程:
- (队列为空 || 消息无需延时 || 或消息执行时间比队列头部消息早) && (线程处于挂起状态时(
mBlocked
=true
)) - 【线程挂起(
mBlocked
=true
)&& 消息循环处于同步屏障状态】,这时如果插入的是一个异步消息,则需要唤醒。
唤醒操作具体是如何去做的?
调用链是这样的:
MessageQueue::nativeWake
—>android_os_MessageQueue_nativeWake()
—>NativeMessageQueue::wake()
—>Looper::wake()
//Looper.cpp
void Looper::wake() {
uint64_t inc = 1;
// 向管道mWakeEventFd写入字符1
ssize_t nWrite = TEMP_FAILURE_RETRY(write(mWakeEventFd, &inc, sizeof(uint64_t)));
if (nWrite != sizeof(uint64_t)) {
if (errno != EAGAIN) {
ALOGW(“Could not write wake signal, errno=%d”, errno);
}
}
}
前面说了,epoll_wait
在监听文件的可读事件,那么现在有消息来了,我要触发这个事件只需要往文件里随便写点什么就可以,Looper里面只是写了一个字符1。成功的唤醒了线程。然后开始轮询取出消息分发。
总结
Handler
在C++层也有自己的一套消息轮询机制,和Java
的基本一样,这里就不做分析了。
Handler是构成整个Android系统的基础,正是Looper
的死循环才让Android
程序能够不退出。所有的类似于屏幕刷新,UI互动都是一种事件,通过Handler
发送给了Looper
来进行分发。整个Android
程序可以说就是运行在这个死循环中。
Looper
就是不断批改作业的老师,MessageQueue
就是催你交作业的学习委员,Message
就是作业,上面写了写作业人的名字,Handler
就是写作业的小明。
在一个线程中只能有一个Looper
,也只能有一个MessageQueue
,但是可以有多个Handler
,MessageQueue
也可以处理多个Handler发来的消息。
Looper
的唤醒与挂起是靠Linux
中的epoll
机制来实现的,通过对文件的可读事件的监听来实现唤醒。
整个过程中,MessageQueue
是实现Java层与C++层的互动的纽带,Native方法基本都是靠MessageQueue
来实现的。
Handler
与线程的绑定是依靠ThreadLocal
中的map
来实现的。另外,消息处理流程是先处理Native Message
,再处理Native Request
,最后处理Java Message
。理解了该流程,也就明白有时上层消息很少,但响应时间却较长的真正原因。
参考并且致谢:
最后
小编这些年深知大多数初中级Android工程师,想要提升自己,往往是自己摸索成长,自己不成体系的自学效果低效漫长且无助。
因此我收集整理了一份《2024年Android移动开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友。
[外链图片转存中…(img-X3TM4SE7-1718963867867)]
一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人
都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!
资料⬅专栏获取
,MessageQueue
就是催你交作业的学习委员,Message
就是作业,上面写了写作业人的名字,Handler
就是写作业的小明。
在一个线程中只能有一个Looper
,也只能有一个MessageQueue
,但是可以有多个Handler
,MessageQueue
也可以处理多个Handler发来的消息。
Looper
的唤醒与挂起是靠Linux
中的epoll
机制来实现的,通过对文件的可读事件的监听来实现唤醒。
整个过程中,MessageQueue
是实现Java层与C++层的互动的纽带,Native方法基本都是靠MessageQueue
来实现的。
Handler
与线程的绑定是依靠ThreadLocal
中的map
来实现的。另外,消息处理流程是先处理Native Message
,再处理Native Request
,最后处理Java Message
。理解了该流程,也就明白有时上层消息很少,但响应时间却较长的真正原因。
参考并且致谢:
最后
小编这些年深知大多数初中级Android工程师,想要提升自己,往往是自己摸索成长,自己不成体系的自学效果低效漫长且无助。
因此我收集整理了一份《2024年Android移动开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友。
[外链图片转存中…(img-X3TM4SE7-1718963867867)]
一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人
都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!
资料⬅专栏获取