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;
}
synchronized锁是一个内置锁,也就是由系统控制锁的lock unlock时机的。这个锁,说明的是对所有调用同一个MessageQueue对象的线程来说,他们都是互斥的,然而在我们的Handler里 面,一个线程是对应着一个唯一的Looper对象,而Looper中又只有一个唯一的MessageQueue(这个在上文中也有介绍)。所以,我们主线程就只有一个MessageQueue对象,也就是说,所有的子线程向主线程发送消息的时候, 主线程一次都只会处理一个消息,其他的都需要等待,那么这个时候消息队列就不会出现混乱。
另外再看next函数
@UnsupportedAppUsage
Message next() {
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;
}
}
next函数很多同学会有疑问:
我从线程里面取消息,而且每次都是队列的头部取,那么它加锁是不是没有意义呢?答 案是否定的,我们必须要在next里面加锁,因为,这样由于synchronized(this)作用范围是所有 this正在访问的代 码块都会有保护作用,也就是它可以保证 next函数和 enqueueMessage函数能够实现互斥。这样才能真正的保证多 线程访问的时候messagequeue的有序进行。
小结: 这个地方是面试官经常问的点,而且他们会基于这个点来拓展问你多线程,所以,这个地方请大家重视。
5.消息机制之同步屏障
消息是根据执行时间进行先后排序,然后消息是保存在队列中,因而消息只能从队列的队头取出来。那么问题来了!需要紧急处理 的消息怎么办?
同步屏障,view绘制中用 juejin.im/post/684490… 同步屏障的概念,在Android开发中非常容易被人忽略,因为这个概念在我们普通的开发中太少见了,很容易被忽略。
大家经过上面的学习应该知道,线程的消息都是放到同一个MessageQueue里面,取消息的时候是互斥取消息,而 且只能从头部取消息,而添加消息是按照消息的执行的先后顺序进行的排序,那么问题来了,同一个时间范围内的消 息,如果它是需要立刻执行的,那我们怎么办,按照常规的办法,我们需要等到队列轮询到我自己的时候才能执行 哦,那岂不是黄花菜都凉了。所以,我们需要给紧急需要执行的消息一个绿色通道,这个绿色通道就是同步屏障的概念。
同步屏障是什么?
屏障的意思即为阻碍,顾名思义,同步屏障就是阻碍同步消息,只让异步消息通过。如何开启同步屏障呢?
//MessageQueue.postSyncBarrier()
@UnsupportedAppUsage
@TestApi
public int postSyncBarrier() {
return postSyncBarrier(SystemClock.uptimeMillis());
}
private int postSyncBarrier(long when) {
// Enqueue a new sync barrier token.
// We don’t need to wake the queue because the purpose of a barrier is to stall it.
synchronized (this) {
final int token = mNextBarrierToken++;
//从消息池中获取消息
final Message msg = Message.obtain();
msg.markInUse();
//就是这里!!!初始化Message对象的时候,并没有给target赋值,因此 target==null
msg.when = when;
msg.arg1 = token;
Message prev = null;
Message p = mMessages;
if (when != 0) {
while (p != null && p.when <= when) {
//如果开启同步屏障的时间(假设记为T)T不为0,且当前的同步消息里有时间小于T,则prev也不为null
prev = p;
p = p.next;
}
}
//根据prev是不是为null,将 msg 按照时间顺序插入到 消息队列(链表)的合适位置
if (prev != null) { // invariant: p == prev.next
msg.next = p;
prev.next = msg;
} else {
msg.next = p;
mMessages = msg;
}
return token;
}
}
可以看到Message对象初始化的时候并没有给 target
赋值,因此target == null
的来源就找到了。上面消息的插入也做了相应的注释。这样,一条 target == null
的消息就进入了消息队列。
那么开启同步屏障后,所谓的异步消息又是如何被处理的呢? 如果对消息机制有所了解的话,应该知道消息的最终处理是在消息轮询器 Looper.loop()
中,而 loop()
循环中会调用 MessageQueue.next()
从消息队列中进行取消息。
//MessageQueue.java
Message next() {
final long ptr = mPtr;
if (ptr == 0) {
return null;
}
int pendingIdleHandlerCount = -1; // -1 only during first iteration
// 1.如果nextPollTimeoutMillis=-1,一直阻塞不会超时。
// 2.如果nextPollTimeoutMillis=0,不会阻塞,立即返回。
// 3.如果nextPollTimeoutMillis>0,最长阻塞nextPollTimeoutMillis毫秒(超时)
// 如果期间有程序唤醒会立即返回
int nextPollTimeoutMillis = 0;
//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; //当前链表的头结点
//关键!!!如果target==null,那么它就是屏障,需要循环遍历,一直往后找到第一个异步的消息
if (msg != null && msg.target == null) {
do {
prevMsg = msg;
msg = msg.next;
} while (msg != null && !msg.isAsynchronous());
}
if (msg != null) {
// 如果有消息需要处理,先判断时间有没有到,如果没到的话设置一下阻塞时间
// 场景如常用的postDelay
if (now < msg.when) {
//计算出离执行时间还有多久赋值给nextPollTimeoutMillis,
//表示nativePollOnce方法要等待nextPollTimeoutMillis时长后返回
nextPollTimeoutMillis = (int) Math.min(msg.when - now, Integer.MAX_VALUE);
} else {
// 获取到消息
mBlocked = false;
//链表操作,获取msg并且删除该结点
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 {
// 没有消息,nextPollTimeoutMills复位
nextPollTimeoutMillis = -1;
}
… //省略
}
}
从上面可以看出,当消息队列开启同步屏障的时候(即标识为msg.target == null
),消息机制在处理消息的时候,优先处理异步消息。这样,同步屏障就起到了一种过滤和优先级的作用。
下面用示意图简单说明:
如上图所示,在消息队列中有同步消息和异步消息(黄色部分)以及一道墙----同步屏障(红色部分)。有了同步屏障的存在,msg_2 和 msg_M 这两个异步消息可以被优先处理,而后面的 msg_3 等同步消息则不会被处理。那么这些同步消息什么时候可以被处理呢?那就需要先移除这个同步屏障,即调用removeSyncBarrier()
。
同步屏障使用场景
似乎在日常的应用开发中,很少会用到同步屏障。那么同步屏障在系统源码中有哪些使用场景呢?Android 系统中的 UI 更新相关的消息即为异步消息,需要优先处理。
比如在View更新时,draw、requestLayout、invalidate 等很多地方都调用了ViewRootImpl#scheduleTraversals()
,如下:
//ViewRootImpl.java
void scheduleTraversals() {
if (!mTraversalScheduled) {
mTraversalScheduled = true;
//开启同步屏障
mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
//发送异步消息
mChoreographer.postCallback(
Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
if (!mUnbufferedInputDispatch) {
scheduleConsumeBatchedInput();
}
notifyRendererOfFramePending();
pokeDrawLockIfNeeded();
}
}
postCallback()
最终走到了ChoreographerpostCallbackDelayedInternal()
:
private void postCallbackDelayedInternal(int callbackType,
Object action, Object token, long delayMillis) {
if (DEBUG_FRAMES) {
Log.d(TAG, “PostCallback: type=” + callbackType
- “, action=” + action + “, token=” + token
- “, delayMillis=” + delayMillis);
}
synchronized (mLock) {
final long now = SystemClock.uptimeMillis();
final long dueTime = now + delayMillis;
mCallbackQueues[callbackType].addCallbackLocked(dueTime, action, token);
if (dueTime <= now) {
scheduleFrameLocked(now);
} else {
Message msg = mHandler.obtainMessage(MSG_DO_SCHEDULE_CALLBACK, action);
msg.arg1 = callbackType;
msg.setAsynchronous(true); //发送异步消息
mHandler.sendMessageAtTime(msg, dueTime);
}
}
}
这里就开启了同步屏障,并发送异步消息,由于 UI 更新相关的消息是优先级最高的,这样系统就会优先处理这些异步消息。
最后,当要 移除同步屏障 的时候需要调用ViewRootImpl#unscheduleTraversals()
。
void unscheduleTraversals() {
if (mTraversalScheduled) {
mTraversalScheduled = false;
//移除同步屏障
mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);
mChoreographer.removeCallbacks(
Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
}
}
同步屏障总结
同步屏障的设置可以方便地处理那些优先级较高的异步消息。当我们调用Handler.getLooper().getQueue().postSyncBarrier()
并设置消息的setAsynchronous(true)
时,target 即为 null ,也就开启了同步屏障。当在消息轮询器 Looper 在loop()
中循环处理消息时,如若开启了同步屏障,会优先处理其中的异步消息,而阻碍同步消息。
6.Handler源码面试问题
- 一个线程有几个 Handler?
一个线程可以有N个Handler
- 一个线程有几个 Looper?如何保证?
一个线程只有一个Looper,由ThreadLocal决定了。
- Handler内存泄漏原因? 为什么其他的内部类没有说过有这个问题?
在handler的handlerMessage的方法中执行外部方法,即执行Activity的方法,这时就持有里Activity对象。
那在 recycleVIew的adpater的ViweHolder的静态内部类为什么没有内存泄漏问题呢? 生命周期的问题。
因为在handler的messageQueue中的message的tag就是持有了handler,就持有了Activity。在MesageQueue的next一直在执行,生命周期比较长。
enqueueMessage{ msg.target = this; } GC:JVM 可达性算法
- 为何主线程可以new Handler?如果想要在子线程中new Handler 要做些什么准备?
在ActivityThread中创建了Looper,在子线程中需要looper.prepare和looper.loop()
- 子线程中维护的Looper,消息队列无消息的时候的处理方案是什么?有什么用?
Looper.quit()可以退出循环
quit: 唤醒线程-> messagequeue ->null -> 退出loop
无消息的时候进入阻塞状态
在handler中两个方面的阻塞? 1) message 不到时间 ,自动唤醒 2) messageQueue为空,无限等待,添加一个消息可以唤醒
- 既然可以存在多个 Handler 往 MessageQueue 中添加数据(发消息时各个 Handler 可能处于不同线程),那它内部是如何确保线程安全的?取消息呢?
利用锁,synchronized:是一个内置锁,由JVM决定。
synchronize(this):同一个messagequeue对象里面的所有的函数、代码块,都会受限。
1个线程只有一个可以操作MessageQueue 的地方:因为一个线程只有一个MessageQueue。
取消息、添加消息、退出都是利用加锁
- 我们使用 Message 时应该如何创建它?
Message.obtain()
- Looper死循环为什么不会导致应用卡死?
线程没有处理任何操作,已经释放CPU资源,在底层采用wait的方式。有消息的是时候底层再唤醒。
- 为什么是在子线程发送消息,而在主线程收消息?
子线程:发送消息的调用函数所在的线程 ,这个函数就在子线程里面
thread: handler.sendMessage(msg) -> MessageQueue.enequeMessage(msg)
主线程Loop(): 轮询 MessageQueue ->msg
static final ThreadLocal sThreadLocal = new ThreadLocal();
所有得线程 sThreadLocal 都是同一个。
7.HandlerThread是什么?为什么它会存在?
作为一个Android开发,Handler机制是一定要了解的。在我面试过程中,发现很多人对Handler和Looper机制非常了解,对答如流,但是却不知道何为HandlerThread。
HandlerThread是Thread的子类,严格意义上来说就是一个线程,只是它在自己的线程里面帮我们创建了Loope。
HandlerThread 存在的意义
方便使用
- 方便初始化
- 方便获取线程looper
保证了线程安全
我们一般在Thread里面线程Looper进行初始化的代码里面,必须要对Looper.prepare(),同时要调用Loop. loop();
@Override public void run() {
Looper.prepare();
Looper.loop();
}
而我们要使用子线程中的Looper的方式是怎样的呢?看下面的代码
Thread thread = new Thread(new Runnable() {
Looper looper;
@Override
public void run() {
// Log.d(TAG, "click2: " + Thread.currentThread().getName());
Looper.prepare();
looper =Looper.myLooper();
Looper.loop();
}
public Looper getLooper() {
return looper;
}
});
thread.start();
Handler handler = new Handler(thread.getLooper());
上面这段代码有没有问题呢?
- 在初始化子线程的handler的时候,我们无法将子线程的looper传值给Handler,解决办法有如下办法:
- a. 可以将Handler的初始化放到Thread里面进行
- b. 可以创建一个独立的类继承Thread,然后通过类的对象获取。
这两种办法都可以,但是这个工作HandlerThread帮我们完成了。
- 依据多线程的工作原理,我们在上面的代码中,调用 thread.getLooper()的时候,此时的looper可能还没有初 始化,此时是不是可能会挂掉呢?
上面的两个问题 HandlerThread 已经帮我们完美的解决了,这就是handlerThread存在的必要性了。
最后
这里附上上述的技术体系图相关的几十套腾讯、头条、阿里、美团等公司2021年的面试题,把技术点整理成了视频和PDF(实际上比预期多花了不少精力),包含知识脉络 + 诸多细节,由于篇幅有限,这里以图片的形式给大家展示一部分。
相信它会给大家带来很多收获:
上述高清技术脑图以及配套的架构技术PDF可以点击我的GitHub免费获取
当程序员容易,当一个优秀的程序员是需要不断学习的,从初级程序员到高级程序员,从初级架构师到资深架构师,或者走向管理,从技术经理到技术总监,每个阶段都需要掌握不同的能力。早早确定自己的职业方向,才能在工作和能力提升中甩开同龄人。
- 无论你现在水平怎么样一定要 持续学习 没有鸡汤,别人看起来的毫不费力,其实费了很大力,这四个字就是我的建议!!!
- 我希望每一个努力生活的IT工程师,都会得到自己想要的,因为我们很辛苦,我们应得的。
当我们在抱怨环境,抱怨怀才不遇的时候,没有别的原因,一定是你做的还不够好!
*以及配套的架构技术PDF可以点击我的GitHub免费获取
当程序员容易,当一个优秀的程序员是需要不断学习的,从初级程序员到高级程序员,从初级架构师到资深架构师,或者走向管理,从技术经理到技术总监,每个阶段都需要掌握不同的能力。早早确定自己的职业方向,才能在工作和能力提升中甩开同龄人。
- 无论你现在水平怎么样一定要 持续学习 没有鸡汤,别人看起来的毫不费力,其实费了很大力,这四个字就是我的建议!!!
- 我希望每一个努力生活的IT工程师,都会得到自己想要的,因为我们很辛苦,我们应得的。
当我们在抱怨环境,抱怨怀才不遇的时候,没有别的原因,一定是你做的还不够好!