先自我介绍一下,小编浙江大学毕业,去过华为、字节跳动等大厂,目前阿里P7
深知大多数程序员,想要提升技能,往往是自己摸索成长,但自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!
因此收集整理了一份《2024年最新HarmonyOS鸿蒙全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友。
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上鸿蒙开发知识点,真正体系化!
由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新
如果你需要这些资料,可以添加V获取:vip204888 (备注鸿蒙)
正文
(1)Handler唯一绑定一个线程,同时也绑定其MessageQueue、Looper,属于多对一的关系,即一个线程可以对应多个Handler
(2)主要有两个作用,一是调度Message/Runnable,二是进行线程间通信
针对前者,列举了一些调度Message/Runnable的具体方法,还提了一下延时任务
针对后者,说到主线程会专门维护一个MessageQueue,子线程可以通过Handler和主线程通信
总之,关于Handler,可以这样描述
Handler有两个作用,一是调度Message/Runnable(包括分发和处理),二是进行线程间通信。它和线程、MessageQueue、Looper是多对一的关系
3. MessageQueue
从源码中,我们可以了解到
(1)它持有一个会被Looper分发的Message的列表
(2)Message由Handler添加,而非MessageQueue直接添加
(3)可以通过Looper.myQueue()
来获取当前线程的MessageQueue
总之,关于MessageQueue,可以这样描述
MessgaeQueue是一个单链表构成的优先级队列,元素为Message,根据Message的when属性来确定优先级。由Handler来添加Message,由Looper来取出Message
4. Looper
源码依然告诉了我非常非常多的信息,让我试图归纳一下
(1)此类是用来开启,一个线程中,Message的循环的
(2)线程默认是没有消息循环的,需要手动创建,可以通过调用prepare
和loop
,来开启循环
(3)与Looper交互最多的是Handler
(4)给了一个典型的实现了消息循环的线程案例
总之,关于Looper,可以这样描述
可以理解为一个消息循环器,从MessageQueue中取出消息,交给Handler执行。创建方式是prepare,开启循环的方式是调用loop方法
5. ThreadLocal
这里的源码,略显难懂,但我也试图归纳一下
(1)ThreadLocal是与线程相关的,可以提供线程局部变量
(2)举了一个例子,说明ThreadLocal是如何提供线程局部变量的
只看这些,基本很难懂。因地制宜,对于理解ThreadLocal,我们或许可以不看源码的定义,转而看他的使用。也就是它的get
方法,和set
方法。
我们只看get
,因为看懂了get
方法,也就能看懂set
方法了
ThreadLocal的get方法
看get
方法就很清晰,比如我们调用ThreadLocal.get()
方法,它会这样执行:
首先拿到调用这个方法的线程,然后从这个线程中取出ThreadLocalMap对象,然后将ThreadLocal作为key,找到它对应的value,从而作为返回值返回。
那么这里,问题来了,ThreadLocalMap是什么呢?先不看源码,只看名字,大体能猜到,这是一个Map集合,那么既然是Map集合,key和value又分别是什么呢?我们进去看一下
可以看到,key就是ThreadLocal本身!
所以,我总结一下。
在每次调用ThreadLocal的get方法时,会首先拿到当前执行的线程,这个线程里面有一个变量,是ThreadLocalMap类型的,key是ThreadLocal,value就是我们想要得到的值
画图来看,就是这样
剖析ThreadLocal原理
又有一个问题,同一个ThreadLocal,在不同的线程,调用get方法,得到的value值是不一样的吗?
答案,是的。因为这就是ThreadLocal机制的作用呀,它的作用就是保存线程本地值,在不同的线程,需要映射为不同的值。它的原理,就是因为不同的线程,有不同的ThreadLocalMap,即虽然key一样,但是Map不一样,所以value可以不同
类比我们生活中的例子,就像是我们同一个人(同一个ThreadLocal),在不同的环境(不同的Thread),承担着不同的角色(映射出不同的value)。比如我们在家这个环境中,承担着孩子的角色,为人父母的角色,在职场这个环境中,我们就承担着职工,同事的角色。真可谓是“艺术来源于生活,高于生活”呀
所以,我说,不同的线程之间Looper可以不同,是通过ThreadLocal来保证的,这句话,相信你也能很快理解了。就是因为不同的线程,ThreadLocalMap不同而已。
ThreadLocal总结
综上,关于ThreadLocal,可以这样描述
它能够映射线程本地变量,映射的原理,就是不同的线程,ThreadLocalMap不同。
总结
ok,以上就是针对Handler机制几大角色的非常详细又通俗易懂的解释。这些都只是一个开胃菜,更详细,更有含金量的,还在后面。
接下来,让我们来介绍一下,关于Handler机制的运行原理。打起精神来!各位!!!
三. Handler消息机制的运行原理
在此部分,我会先捋源码,然后给出一个非常详细的图加深理解,以此来讲清楚Handler消息机制的运行原理
一图以蔽之
这张图,可以说非常详细、生动地展现了Handler机制整体的运行原理。再结合上文,对各个角色的解释,相信你很容易就能看懂了。那么,对自己要求高的人,肯定还是不满于此,是不是还想扒开源码看看?OK,我来满足你的需求。
Handler消息机制源码
看源码,有个问题,即我们看的线索,怎么找,我们从哪一截开始探入,会比较好理解,不会迷乱在纷乱的源码世界中?这个,不同的人有不同的理解,我认为,可以从Handler放消息,即我们调用sendXXX、postXXX
方法开始看起,从Looper交给Handler进行消息处理作为结束,正好按照时间顺序,同时也形成闭环。
所以,先从这里看起
Handler怎么放的消息
也就是图中红色区域
在前文,Handler源码的定义中,有介绍到Handler发送消息的几种方式,我再贴一下
一般来说呢,我们使用post、sendMessage
这两种方法会多一些,看方法名我们也可以猜出来,其他几种方法,基本都是这两种方式的变形体,所以呢,我们只关注post
方法,和sendMessage
方法。
sendMessage方法的执行过程
由简到繁,我们先看sendMessage方法的执行过程
这里调用了sendMessageDelayed()
方法,将msg传进去了,然后有一个delayMills
为0,这个delayMills
的意思就是延时任务的延时时间,前面也介绍了,Handler有一个功能是可以执行延时任务。那么延时的时间,就是sendMessageDelayed
的delayMills
参数。
那进去sendMessageDelayed
方法
这个方法又调用了一个sendMessageAtTime
。
传入了一个uptimeMills
参数,这个参数,是之前的delayMills
,加上启动后的时间。所以前面的delayMills
是一个相对时间,然后再加上SystemClock.uptimeMillis()
,组成了一个绝对时间uptimeMills
。(其实这在后面会被赋值为Message的when
属性)
然后调用了Handler的enqueueMessage
方法
最后,调用到了MessageQueue的enqueueMessage
方法。自此,就把Message放入消息队列了。
一图以蔽之
让我来总结一下,调用sendMessage
,需要传入一个Message对象,然后通过层层传递,最终调用到了MessageQueue的enqueueMessage
方法,将Message加入消息队列。
post方法的执行过程
看完sendMessage
,我们再看post
方法。
一般来说呢,我们是这样调用post
方法的
我们进去post方法,看一看
what???它居然又调用了sendMessageDelayed
方法!我们需要看下这个getPostMessage
做了啥
哦~,原来它把Runnable封装为了Message,并且将自身作为了Message的callback
属性!
后续流程都是一样的了,就不继续跟了。
一图以蔽之
好,以上我们就把Handler将Message传递给MessageQueue的过程,捋清楚了。
MessageQueue怎么调度的消息
接下来,我们看MessageQueue是怎么把Message加进去的,怎么把Message取出来的,即图中的这一部分
MessageQueue的放Message的方法,是enqueueMessage
,看名字就知道,它的意思是将Message放入队列,里面一定包含了一些策略。MessageQueue的取出Message方法,是next
。下面,我们还是按照时间顺序,先enqueueMessage
方法,然后再看next
方法。
enqueueMessage方法的执行过程
enqueueMessage
方法,有点大,截图截不开,我就直接贴代码了。别担心,我会在关键地方,留下注释
boolean enqueueMessage(Message msg, long when) {
if (msg.target == null) {
// 不允许发送target为null的消息,也就是说这个方法放不了同步屏障消息
throw new IllegalArgumentException(“Message must have a target.”);
}
synchronized (this) {
if (msg.isInUse()) {
throw new IllegalStateException(msg + " This message is already in use.");
}
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;
}
msg.markInUse();
msg.when = when; // 这里将when赋值给了message的when属性
Message p = mMessages;
boolean needWake;
if (p == null || when == 0 || when < p.when) {
// 把传入的message,作为新的头节点
msg.next = p;
mMessages = msg;
needWake = mBlocked; // 如果之前是阻塞状态,则唤醒
} else {
// 下面的英文注释说的非常详细,我可以再试图总结一下
// 只有队列的头节点为同步屏障消息,并且当前message,是最早加进来的异步消息时,才有可能需要唤醒
// 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;
}
}
// 把传进来的message插入了队列中
msg.next = p;
prev.next = msg;
}
if (needWake) {
nativeWake(mPtr);
}
}
return true;
}
建议你仔细看几遍以上代码,然后,看下面的流程图
一图以蔽之
此方法执行完后,如果一切正常的话,就可以将Message入队了。
next方法的执行过程
下面,我们来看,从MessageQueue中取出Message的部分,也就是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.
// 如果应用程序试图在退出后重新启动looper(这是不支持的),就会发生这种情况。
final long ptr = mPtr; // mPtr是native代码使用的
if (ptr == 0) {
return null;
}
int pendingIdleHandlerCount = -1; // -1 only during first iteration
int nextPollTimeoutMillis = 0; // 阻塞时间
for (;😉 { // 注意,这里是一个死循环
if (nextPollTimeoutMillis != 0) {
// 将当前线程中挂起的Binder命令刷新到内核驱动程序。
// 在执行可能会阻塞很长时间的操作之前调用此方法非常有用,
// 以确保所有挂起的对象引用已被释放,以防止进程持有对象的时间超过所需时间。
// 总之,这个方法在执行阻塞任务之前调用会好,又因为下面有可能会阻塞,所以在这里调用了这个方法。
Binder.flushPendingCommands();
}
// 阻塞,阻塞时长为nextPollTimeoutMillis
nativePollOnce(ptr, nextPollTimeoutMillis);
synchronized (this) {
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());
}
// 此时,msg要么为null,要么为队首消息,要么为第一个异步消息
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 {
// -1表示一直阻塞
nextPollTimeoutMillis = -1;
}
// 当处理完所有该处理的message时,才去处理要不要quit
if (mQuitting) {
dispose();
return null;
}
// idle handles 只能在MessageQueue为空,或者队首的Message的when不符合要求时,才会执行
// 下面的英文其实说的更详细
// 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();
}
// 如果第一次循环,处理了mIdleHandlers里面的任务,那么第二次循环时
// pendingIdleHandlerCount为0,所以直接continue,而不会重新处理mIdleHandlers
// 注意:这里的循环指的是某一次next方法执行时,里面for死循环中的一次循环。
// 而又因为next方法会执行多次,所以第二次next方法执行时,又会重新启动一次for死循环,
// 那么这时,还是会判断是否要处理mIdleHandlers里面的任务。
if (pendingIdleHandlerCount <= 0) {
// No idle Handlers to run. Loop and wait some more.
mBlocked = true;
continue; // 注意一下这个continue。
}
if (mPendingIdleHandlers == null) {
mPendingIdleHandlers = new IdleHandler[Math.max(pendingIdleHandlerCount, 4)];
}
mPendingIdleHandlers = mIdleHandlers.toArray(mPendingIdleHandlers);
}
// 运行IdleHandler里面的代码,只有第一次迭代的时候会执行到这里
// 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);
}
}
}
// 注意这里,不管上面的mIdleHandlers是否还有元素,都重置。因为queueIdle都已经执行过一次了
// Reset the idle Handler count to 0 so we do not run them again.
pendingIdleHandlerCount = 0;
// 设置阻塞时长为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;
}
}
一图以蔽之
出现了!!!你绝没见过的流程图!!! 这个就稍显复杂了,不过多看几遍代码,和我画的流程图做对照,相信你很快就能理解。
需要注意的点,是IdleHandler。在这里,可以看到,它执行的时机,是取不出合适的Message,在阻塞线程之前,执行。同时,执行完之后,会刷新阻塞时长。
所以在这里,它也有坑,
一是有可能永远都不会执行,因为有可能一直都能取出合适的Message。
二是有可能delay正常的Message。比如队首的Message,需要等待1ms就可以执行,然后在阻塞1ms之前,发现有idleHandler可以执行,那么就去执行idleHandler,如果执行idleHandler的耗时为10ms,那么就delay了队首的Message的执行时机,delay了9ms。
尽管它有坑,但还是会在性能优化的场景中用到,用于在线程空闲时执行一些优化任务。
好,以上,关于MessageQueue的拿到Message,和交付Message,就全部介绍完了。
Looper怎么调度的消息
下面介绍Looper的拿到Message,和交付Message的过程,也就是这一部分
如何拿到消息
拿到Message,是在开启消息循环,即Looper.loop
中实现的
可以看到这个方法的注释,谷歌官方特意提示,一定要调用!所以我们在开启子线程的消息循环的时候,一定要调用Looper.loop
方法。里面有一个核心逻辑,是死循环,每一次循环,会调用loopOnce
方法,让我们进入到这个loopOnce
方法中,看一看
private static boolean loopOnce(final Looper me,
final long ident, final int thresholdOverride) {
Message msg = me.mQueue.next(); // 这里有可能阻塞
if (msg == null) {
// msg为null,只有两种情况,即MessageQueue正在退出或者已经退出
return false;
}
…
msg.target.dispatchMessage(msg); // 把message交付给Handler
…
msg.recycleUnchecked(); // 回收message,缓存起来,以便下次复用
…
return true;
}
在每次loopOnce
中,一切正常的话,就可以取出一个消息。
如何交付给Handler的
把message
交付给Handler是怎么交付的呢?Message的target
属性就是Handler,所以在这里就是调用了Handler的dispatchMessage
方法,进去看下
这里就非常明朗了,在这里,进行最终关于message的处理,然后形成了闭环。
一图以蔽之(Looper的loop)
细看Handler对Message的处理
其实,这里值得再强调下,我们看到,这里有三种方式来处理message,分别是
我们一个一个地看。
处理方式一:handleCallback方法
第一个,handleCallback
,是Message的callback
属性不为null时,调用。在前面的内容中有过介绍,Message的callback
属性,就是调用Handler.post()
时,传入的Runnable。可以看下图
所以,如果此Message,是通过Handler.post
方式传进来的,那么就直接执行Runnable里面的run
方法。
这是第一种处理message的方式。
处理方式二:mCallback的handleMessage方法
第二种,是看Handler的mCallback
属性是否为null,如果不为null,则调用mCallback
的handleMessage
方法。这里的mCallback
和前面说的Message的callback
属性不同。这里的mCallback
是创建Handler的时候,通过构造方法得到的,
这个Callback和Runnable没有关系,而是一个自定义的接口,里面有一个方法,是handleMessage
所以第二种处理方式,是调用mCallback
的handleMessage
方法。如果返回了true,则处理结束,如果返回了false,那么就进入到第三种处理方式,即handleMessage
处理方式三:子类实现handleMessage方法
这里默认实现为空,所以此方式,一般是调用的子Handler的handleMessage
方法,来处理的Message。
一图以蔽之
为什么分了三种处理方式?
我个人认为,有两个原因。
(1)保证消息最终能够进行处理
Handler可以通过多种方式进行创建:比如可以直接new,或者new一个子类,同时呢,Handler的构造函数还有好几种。每一种创建方式,对应的消息处理方式,不一定是相同的。所以,分了三种,原因之一,是要把所有的消息处理方式全部覆盖到,尽可能防止有消息处理不到的情况。
(2)有些场景,可能需要对message多次处理,有第二种和第三种处理方式,可以满足此需求。
在代码上,关于第二种和第三种处理方式,分别是这样实现的
// 第二种处理方式,赋值给Handler的mCallback属性
val Handler1 = Handler(object : Handler.Callback {
override fun handleMessage(msg: Message): Boolean {
return true/false
}
})
// 第三种处理方式,派生子类,重写handleMessage方法
val Handler2 = object : Handler(){
override fun handleMessage(msg: Message) {
super.handleMessage(msg)
}
}
这两种处理方式有一种关系,就是如果第二种处理方式返回了false,那么第三种处理方式还要执行。即有可能两种处理都会执行,这在某些降级处理场景,可能会被用到。
但其实呢,谷歌提示我们尽量避免创建子类,进而避免调用子类的消息处理方法,而优先用前两种处理Message的方式。原因来自这个注释:
在Callback的类注释中,有“尽量避免不得不实现Handler的子类”这样的字眼,所以得出了上面那个结论。
所以,为什么分了三种处理方式,总结下,就是两个原因
- 保证消息最终能够进行处理
- 有些场景,可能需要对Message多次处理,有第二种和第三种处理方式,可以满足此需求。
以上,就把Handler机制的全貌,都捋了一遍了。
三. 加餐:Handler那些事儿
1. 为什么子线程不能访问UI,非要整一个Handler?
因为在Android中,关于UI操作,是没有加锁的,所以不是线程安全的,所以只能是单线程访问才可以。因此Android只能单线程修改UI,而且这个单线程只能是主线程。
那么,为什么不加锁呢?
我能想到的一个很重要的原因,就是流畅性,UI修改一定要是瞬时就要变化的,如果有多线程互相抢着锁,那UI的访问及修改效率都会低很多,手机就会表现的很卡。
而且像UI这种,非常基础的能力,没必要做的这么复杂,如果访问一个UI控件,修改一个UI控件都要加锁的话,那就太复杂了,完全没必要。
总结
Android只能允许单线程修改UI,然后设计了Handler机制来实现子线程修改UI的需求。
2. MessageQueue的阻塞唤醒机制是如何实现的?
其实就是:epoll、pipe机制
什么是pipe?
在类Unix-like操作系统中,一切皆文件,包括管道pipe。管道可以像文件一样在文件系统中存在,并且可以使用文件描述符来引用它们。
管道的作用之一,就是可以实现,线程间的通信。一个线程与另外一个线程之间发生的读、写操作,都可以通过管道,来实现。
什么是epoll?
epoll 是 Linux 中的高效的 I/O 多路复用机制,它会监听一个或多个文件描述符的多个事件类型,其中之一是文件描述符的写入事件。
epoll如何实现阻塞、唤醒?
epoll能够实现阻塞、唤醒的原理,就是能够监听管道这个文件描述符的读操作和写操作,实现阻塞和唤醒。
比如,一个MessageQueue中,没有了消息,那么就应该被阻塞了,那么怎么保持一直阻塞的?什么时候会被唤醒?答案就是:epoll可以监听,pipe的文件描述符的写操作。
当有新的消息写入时,epoll就可以监听到,从而通知线程,实现唤醒。没有写入时,就能保持一直阻塞。
因此,可以这么理解:在没有数据可读时,MessageQueue主要使用了 native 层的 epoll 机制来监听文件描述符(通常是pipe)的写入事件,以实现持续的阻塞和唤醒。
一个更好理解的例子
举个生活中的例子,当有人给我打过来电话时,我的手机会通过亮屏,响手机铃声或者震动等方式来通知我。
OK,这里面,包含了两个方面。一是可以打电话,有电话线这条“管道”。二是可以通知我,让我知道,有人打电话过来了。
在这里,电话线就像是pipe,通知就像是epoll。在没有电话打来时,且我当前没有正在接听电话时,属于“读阻塞”的状态,此时如果有写入操作,则会进行唤醒。这里的写入,就是有新的人给我打来了电话,然后epoll通过通知我,唤醒了我,然后我就可以接听电话,即对写入的这条消息进行处理。同时呢,手机不光是在收到电话的时候可以通知,在比如收到微信视频通话的通知时,来了一条短信时,都可以发起通知,这对应epoll的可以监听多个文件描述符的事件的特点。
在MessageQueue中,哪些地方进行了阻塞
在next
方法中,总共有两个地方,对mBlocked
进行了赋值。
- 一是消息正常取出时,将
mBlocked
赋值为false
,表示不阻塞。 - 二是当没有可取出的消息,且也没有可以处理的IdleHandler时,赋值为
true
,表示阻塞。
3. 同步屏障消息和异步消息?
同步屏障消息即Message的target
属性为null
异步消息即Message的isAsynchronous
返回true
两者之间是如何运转的?
在没有遇到同步屏障消息时,同步消息和异步消息都是正常在MessageQueue上面根据when
属性排列,依次取出,执行。然而,当遇到同步屏障消息时,就会向后遍历,找到第一个异步消息,然后根据when
是否满足要求,决定如何处理。
这里呢,这种机制,一般会用在优先级比较高的,需要尽快执行的任务,比如绘制UI(draw、invalidate、requestLayout
等方法)
4. 消息循环如何退出
网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。
需要这份系统化的资料的朋友,可以添加V获取:vip204888 (备注鸿蒙)
一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!
表示阻塞。
3. 同步屏障消息和异步消息?
同步屏障消息即Message的target
属性为null
异步消息即Message的isAsynchronous
返回true
两者之间是如何运转的?
在没有遇到同步屏障消息时,同步消息和异步消息都是正常在MessageQueue上面根据when
属性排列,依次取出,执行。然而,当遇到同步屏障消息时,就会向后遍历,找到第一个异步消息,然后根据when
是否满足要求,决定如何处理。
这里呢,这种机制,一般会用在优先级比较高的,需要尽快执行的任务,比如绘制UI(draw、invalidate、requestLayout
等方法)
4. 消息循环如何退出
网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。
需要这份系统化的资料的朋友,可以添加V获取:vip204888 (备注鸿蒙)
[外链图片转存中…(img-fyF2hBl2-1713620815263)]
一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!