写点什么

Android- 面试官:View-post()- 为什么能够获取到 -View- 的宽高 -?

用户头像
Android架构
关注
发布于: 2021 年 11 月 05 日

mHeight = -1;...// 3. 初始化 AttachInfo// 记住 mAttachInfo 是在这里被初始化的 mAttachInfo = new View.AttachInfo(mWindowSession, mWindow, display, this, mHandler, this,context);...// 4. 初始化 Choreographer,通过 Threadlocal 存储 mChoreographer = Choreographer.getInstance();}


  1. 初始化 mWindowSession,它可以 WMS 进行 Binder 通信

  2. 这里能看到宽高还未赋值

  3. 初始化 AttachInfo,这里着重记一下,后面会再提到

  4. 初始化 Choreographer,上篇文章 [面试官:如何监测应用的 FPS ?](


) 详细介绍过


再看注释 2 处的 ViewRootImpl.setView() 方法。


ViewRootImpl.java


// 参数 view 就是 DecorViewpublic void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {synchronized (this) {if (mView == null) {mView = view;


// 1. 发起首次绘制 requestLayout();


// 2. Binder 调用 Session.addToDisplay(),将 window 添加到屏幕 res = mWindowSession.addToDisplay(mWindow, mSeq, mWindowAttributes,getHostVisibility(), mDisplay.getDisplayId(), mWinFrame,mAttachInfo.mContentInsets, mAttachInfo.mStableInsets,mAttachInfo.mOutsets, mAttachInfo.mDisplayCutout, mInputChannel);


// 3. 将 decorView 的 parent 赋值为 ViewRootImplview.assignParent(this);}}}


requestLayout() 方法发起了首次绘制。


ViewRootImpl.java


public void requestLayout() {if (!mHandlingLayoutInLayoutRequest) {// 检查线程 checkThread();mLayoutRequested = true;// 重点 scheduleTraversals();}}


ViewRootImpl.scheduleTraversals() 方法在 [上篇文章](


) 中详细介绍过,这里大致总结一下:


  1. ViewRootImpl.scheduleTraversals() 方法中会建立同步屏障,优先处理异步消息。通过 Choreographer.postCallback() 方法提交了任务 mTraversalRunnable,这个任务就是负责 View 的测量,布局,绘制。

  2. Choreographer.postCallback() 方法通过 DisplayEventReceiver.nativeScheduleVsync() 方法向系统底层注册了下一次 vsync 信号的监听。当下一次 vsync 来临时,系统会回调其 dispatchVsync() 方法,最终回调 FrameDisplayEventReceiver.onVsync() 方法。

  3. FrameDisplayEventReceiver.onVsync() 方法中取出之前提交的 mTraversalRunnable 并执行。这样就完成了一次绘制流程。


mTraversalRunnable 中执行的是 doTraversal() 方法。


ViewRootImpl.java


void doTraversal() {if (mTraversalScheduled) {// 1. mTraversalScheduled 置为 falsemTraversalScheduled = false;// 2. 移除同步屏障 mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);


// 3. 开始布局,测量,绘制流程 performTraversals();......}


ViewRootImpl.java


private void performTraversals() {...// 1. 绑定 Window,重点记忆一下 host.dispatchAttachedToWindow(mAttachInfo, 0);mAttachInfo.mTreeObserver.dispatchOnWindowAttachedChange(true);


getRunQueue().executeActions(mAttachInfo.mHandler);


// 2. 请求 WMS 计算窗口大小 relayoutResult = relayoutWindow(params, viewVisibility, insetsPending);


// 3. 测量 performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);


// 4. 布局 performLayout(lp, mWidth, mHeight);


// 5. 绘制 performDraw();}


performTraversals() 方法的逻辑甚是复杂,这里精简出几个重要的方法调用。到这里,View 的整体绘制流程已经完成,毫无疑问,在这个时候肯定是可以获取到宽高的。


View 被测量的时机已经找到了。现在就来验证一下 View.post() 是不是在这个时机执行回调的。

探秘 View.post()

View.java


public boolean post(Runnable action) {final AttachInfo attachInfo = mAttachInfo;if (attachInfo != null) {// 1. attachInfo 不为空,通过 mHandler 发送 return attachInfo.mHandler.post(action);}// 2. attachInfo 为空,放入队列中 getRunQueue().post(action);return true;}


这里的关键是 attachInfo 是否为空。在上一节中介绍过,再来回顾一下:


  • attachInfo 是在 ViewRootImpl 的构造函数中初始化的,

  • ViewRootImpl 是在 WindowManagerGlobal.addView() 创建的

  • WindowManagerGlobal.addView() 是在 ActivityThread 的 handleResumeActivity() 中调用的,但是是在 Activity.onResume() 回调之后


所以,如果 attachInfo 不为空的话,至少已经处在进行视图绘制的这次消息处理当中。把 post() 方法要执行的 Runnable 利用 Handler 发送出去,当包含这个 Runnable 的 Message 被执行时,是一定可以获取到 View 的宽高的。


onCreate()onResume() 这两个回调中,attachInfo 肯定是空的,这时候就要依赖 getRunQueue().post(action) 。原理也很简单,把 post() 方法要执行的 Runnable 存储在一个队列中,在合适的时机(View 已被测量)


《Android学习笔记总结+最新移动架构视频+大厂安卓面试真题+项目实战源码讲义》
浏览器打开:qq.cn.hn/FTe 免费领取
复制代码


拿出来执行。先来看看 getRunQueue() 拿到的是一个什么队列。


View.java


private HandlerActionQueue getRunQueue() {if (mRunQueue == null) {mRunQueue = new HandlerActionQueue();}return mRunQueue;}


public class HandlerActionQueue {private HandlerAction[] mActions;private int mCount;


public void post(Runnable action) {postDelayed(action, 0);}


// 发送任务 public void postDelayed(Runnable action, long delayMillis) {final HandlerAction handlerAction = new HandlerAction(action, delayMillis);


synchronized (this) {if (mActions == null) {mActions = new HandlerAction[4];}mActions = GrowingArrayUtils.append(mActions, mCount, handlerAction);mCount++;}}


// 执行任务 public void executeActions(Handler handler) {synchronized (this) {final HandlerAction[] actions = mActions;for (int i = 0, count = mCount; i < count; i++) {final HandlerAction handlerAction = actions[i];handler.postDelayed(handlerAction.action, handlerAction.delay);}


mActions = null;mCount = 0;}}


...


private static class HandlerAction {final Runnable action;final long delay;


public HandlerAction(Runnable action, long delay) {this.action = action;this.delay = delay;}


public boolean matches(Runnable otherAction) {return otherAction == null && action == null|| action != null && action.equals(otherAction);}}}


队列 HandlerActionQueue 是一个初始容量是 4 的 HandlerAction 数组。HandlerAction 有两个成员变量,要执行的 Runnable 和延迟执行的时间。


队列的执行逻辑在 executeActions(handler) 方法中,通过传入的 handler 进行任务分发。现在我们只要找到 executeActions() 的调用时机就可以了。在 View.java 中就可以找到,在 dispatchAttachedToWindow() 方法中分发了任务。


void dispatchAttachedToWindow(AttachInfo info, int visibility) {...if (mRunQueue != null) {// 分发任务 mRunQueue.executeActions(info.mHandler);mRunQueue = null;}// 回调 onAttachedToWindow()onAttachedToWindow();}


关于 dispatchAttachedToWindow(),你不妨在本文中 Ctrl + F 全局搜索一下。上一节已经出现过,我也提示你重点记一下了,就在 performTraversals() 方法中。


ViewRootImpl.java


private void performTraversals() {...// 1. 看这里 host.dispatchAttachedToWindow(mAttachInfo, 0);mAttachInfo.mTreeObserver.dispatchOnWindowAttachedChange(true);


getRunQueue().executeActions(mAttachInfo.mHandler);


// 2. 请求 WMS 计算窗口大小 relayoutResult = relayoutWindow(params, viewVisibility, insetsPending);


// 3. 测量 performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);


// 4. 布局 performLayout(lp, mWidth, mHeight);


// 5. 绘制 performDraw();}


注意注释 1 处。看到这里你可能有那么一点疑惑。明明是先调用的 dispatchAttachedToWindow() ,再进行的测量流程,为什么 dispatchAttachedToWindow() 中可以获取到 View 的宽高呢?


首先,你要知道 performTraversals() 是在主线程消息队列的一次消息处理过程中执行的,而 dispatchAttachedToWindow() 间接调用的 mRunQueue.executeActions() 发送的任务也是通过 Handler 发送到主线程消息队列的,那么它的执行就一定在这次的 performTraversals() 方法执行完之后。所以,在这里获取 View 的宽高是完全没有问题的。


到这里,整个闭环就形成了,大致总结一下。


根据 ViewRootImpl 是否已经创建,View.post() 会执行不同的逻辑。如果 ViewRootImpl 已经创建,即 mAttachInfo 已经初始化,直接通过 Handler 发送消息来执行任务。如果 ViewRootImpl 未创建,即 View 尚未开始绘制,会将任务保存为 HandlerAction,暂存在队列 HandlerActionQueue 中,等到 View 开始绘制,执行 performTraversal() 方法时,在 dispatchAttachedToWindow() 方法中通过 Handler 分发 HandlerActionQueue 中暂存的任务。


另外要注意,View 绘制是发生在一次 Meesage 处理过程中的,View.post() 执行的任务也是发生在一次 Message 处理过程中的,它们一定是有先后顺序的。

还可以怎么获取视图宽高?

除了通过 View.post() 获取视图宽高之外,还有两种比较推荐的方式。


第一种,onWindowFocusChanged()


override fun onWindowFocusChanged(hasFocus: Boolean) {


super.onWindowFocusChanged(hasFocus)if (hasFocus){...}}


第二种,OnGlobalLayoutListener


binding.dialog.viewTreeObserver.addOnGlobalLayoutListener(object : ViewTreeObserver.OnGlobalLayoutListener{override fun onGlobalLayout() {binding.dialog.viewTreeObserver.removeOnGlobalLayoutListener(this)...}})


这两种方法都可能被调用多次。当 Activity 获取和失去焦点的时候,onWindowFocusChanged 都会调用。当 View 树发生状态变化时,OnGlobalLayoutListener 也会调用多次,可以根据需要移除监听。

用户头像

Android架构

关注

还未添加个人签名 2021.10.31 加入

还未添加个人简介

评论

发布
暂无评论
Android-面试官:View-post()-为什么能够获取到-View-的宽高-?