Android开发Handler是如何确保UI刷新优先执行的源码解读
Android开发Handler是如何确保UI刷新优先执行的源码解读
-
问题分析
问题1:很久前被问到,requestLayout会立刻触发绘制界面吗?答案是不会立即的,需要等待下一个VSync信号到来(VSync信号即硬件垂直同步信号,比如1秒60帧即1000ms/60约为16.7ms一次)。
当然VSync信号到来也不一定就会触发绘制呀,起码dirty了也就是有变化了才绘制吧,那就bool值标记一下不就好了。。。比如ViewRootImpl里的mTraversalScheduled变量,Choreographer里的mFrameScheduled变量都是标记作用。。。
其实嘛本质是requestLayout方法(调用checkThread()检查是否为主线程)会调用scheduleTraversals()方法,发送一条message消息(特殊的message消息即屏障消息)给handler,接着使用mChoreographer.postCallback()发送一条异步消息给handler,具体实现在Choreographer.postCallbackDelayedInternal()方法里,怎么好像就一条屏障消息和一条异步消息就搞定了?具体下面会分析。
问题2:那我们给handler发送很多消息的话,不是会堵塞UI刷新消息的执行,然后导致UI不能及时刷新吗?答案是不会的,原因就是刷新UI的时候给handler发送的是一条屏障消息和一条异步消息,具体原因下面分析。
-
Handler、Looper、MessageQueue、Message四个之间的关联关系
//1.Looper的prepare静态方法里实例化了一个Looper对象,并将其放入sThreadLocal实现线程私有 //并且做了判断,也就是说每个线程最多可以有一个Looper,每个线程的Looper是私有的,互不干扰。 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)); } //1.Looper通过构造函数持有MessageQueue对象,而MessageQueue持有消息循环队列头指针Message mMessages private Looper(boolean quitAllowed) { mQueue = new MessageQueue(quitAllowed); mThread = Thread.currentThread(); }
//2.Hanndler通过构造函数持有Looper以及Looper的MessageQueue,也说明Looper持有MessageQueue public Handler(@NonNull Looper looper, @Nullable Callback callback, boolean async) { mLooper = looper; mQueue = looper.mQueue; mCallback = callback; mAsynchronous = async; }
handler.sendMessage(msg);msg.target = this;//除了屏障消息之外,同步消息和异步消息,有target并且target是Handler //target的作用(msg的target持有的是Handler对象) public void dispatchMessage(@NonNull Message msg) { if (msg.callback != null) {//优先回调msg自动的callback handleCallback(msg); } else { if (mCallback != null) {//再尝试回调handler带的callback if (mCallback.handleMessage(msg)) { return; } } handleMessage(msg);//最后才调用这个空实现,一般我们new一个handler重写这个方法处理发送消息的结果 } }
-
Handler从发送消息到处理消息的大致过程
//1. 第一阶段,就是通过handler发送消息到循环队列 public final boolean sendMessage(@NonNull Message msg) {} public boolean sendMessageAtTime(@NonNull Message msg, long uptimeMillis) {} private boolean enqueueMessage(MessageQueue queue,Message msg,long uptimeMillis) { msg.target = this;//这个target的值this指向的是Handler,用于后面队列循环到这个消息时回到Handler处理 ... } //2. 第二阶段,Looper.loop();之后,循环队列开始工作,系统早已调用主线程loop()方法了,所以主线程队列一直在工作或者等待 public static void loop() { for (;;) { Message msg = queue.next();//调用队列的next方法获取下一个符号条件的消息 ... try { msg.target.dispatchMessage(msg);//回到handler的dispatchMessage方法,和上面说的enqueueMessage对应 } catch (Exception exception) { } } msg.recycleUnchecked();//回收消息 } //2. 第二阶段,MessageQueue.next()方法取出下一个符号条件的消息 Message next() { int nextPollTimeoutMillis = 0;//等待多久时间,第一次等待时间是0,-1表示无限等待直到被nativeWakt唤醒 for (;;) { nativePollOnce(ptr, nextPollTimeoutMillis);//等待时间,第一次等待时间是0表示立刻返回,即查询一次 synchronized (this) { final long now = SystemClock.uptimeMillis(); Message prevMsg = null; Message msg = mMessages; if (msg != null && msg.target == null) {//本文重点:如果msg的target为空,表示这个msg是屏障消息 do {//如果是屏障消息,遍历找到下一个异步消息为止。别忘记UI刷新发送的就是异步消息哦,可能查找的就是UI刷新消息呢 prevMsg = msg; msg = msg.next; } while (msg != null && !msg.isAsynchronous());//循环找到下一个异步消息为止 } if (msg != null) { if (now < msg.when) {//时间还没到,计算下次等待时长 nextPollTimeoutMillis = (int) Math.min(msg.when - now, Integer.MAX_VALUE); } else {// Got a message.将消息从链表取出,并标记为已经使用,然后跳出循环返回到上一层Looper.loop(); mBlocked = false; if (prevMsg != null) { prevMsg.next = msg.next; } else { mMessages = msg.next; } msg.next = null; msg.markInUse(); return msg; } } else {// No more messages.没有消息,-1无限等待直到被唤醒 nextPollTimeoutMillis = -1; } } ...省略掉IdleHandler的处理,就是没有消息可处理时,查询是否添加了IdleHandler,就有处理IdleHandler回调 } }
-
到处已经分析完标题的疑问,可能还有人有疑问,总结一下
1.handler消息分为三类:
同步消息:就是我们平时使用的postMessage,sendMessage,Message的isAsynchronous()返回false,Message的target不能为空。
异步消息:同步消息通过Message的setAsynchronous(boolean async)方法可以变为异步消息,target同样不能为空。
屏障消息:同步消息的target为空时变成屏障消息。但是平时不能使用target为空的消息,因为使用完后需要对应的移除屏障消息,所以只能系统使用。
2.屏障消息的作用
屏障的作用自然是屏蔽遮挡作用,就是一个标记作用,当MessageQueue.next()方法取到的是屏障消息时,就是while循环只取异步消息,也就是遇到屏障消息时过滤不考虑同步消息,只处理异步消息,这样屏障消息就起到标记作用,提升异步消息处理优先级的作用。
3.UI刷新是异步消息,配合屏障消息的使用,异步消息优先处理,所以我们平时发送的普通消息是同步消息不会影响到UI刷新
mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier(); //插入屏障消息 mChoreographer.postCallback(Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);//然后再发送异步消息
4.屏障消息是target为空的消息,屏障消息使用完需要手动移除,没有暴露给开发者移除api,所以只能系统使用
void unscheduleTraversals() { if (mTraversalScheduled) { mTraversalScheduled = false; mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);//移除屏障消息 mChoreographer.removeCallbacks(Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null); } }
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· Linux系列:如何用 C#调用 C方法造成内存泄露
· AI与.NET技术实操系列(二):开始使用ML.NET
· 记一次.NET内存居高不下排查解决与启示
· 探究高空视频全景AR技术的实现原理
· 理解Rust引用及其生命周期标识(上)
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· 单线程的Redis速度为什么快?
· 展开说说关于C#中ORM框架的用法!
· Pantheons:用 TypeScript 打造主流大模型对话的一站式集成库