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

两件事,回调 onResume 和 添加 DecorView 到 WindowManager 。所以,在 onResume() 回调中获取 view 的宽高其实和 onCreate() 中没啥区别,都获取不到。

wm.addView(decor, l) 最终调用到 WindowManagerGlobal.addView()

public void addView(View view, ViewGroup.LayoutParams params,
Display display, Window parentWindow) {

// 1. 重点,初始化 ViewRootImpl
root = new ViewRootImpl(view.getContext(), display);
// 2. 重点,发起绘制并显示到屏幕上
root.setView(view, wparams, panelParentView);

这里两行代码都是重中之重。先来看看注释 1 处 ViewRootImpl 的构造函数。

public ViewRootImpl(Context context, Display display) {

// 1. IWindowSession 代理对象,与 WMS 进行 Binder 通信
mWindowSession = WindowManagerGlobal.getWindowSession();

// 2.
mWidth = -1;
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 就是 DecorView
public 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 赋值为 ViewRootImpl
view.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 置为 false
mTraversalScheduled = 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 已被测量)拿出来执行。先来看看 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开发中高级面试题收集起来,然后全网寻找最优的解答方案。每一道面试题都是百分百的大厂面经真题+最优解答。包知识脉络 + 诸多细节。
节省大家在网上搜索资料的时间来学习,也可以分享给身边好友一起学习。
给文章留个小赞,就可以免费领取啦~

戳我领取:Android对线暴打面试指南超硬核Android面试知识笔记3000页Android开发者架构师核心知识笔记

《960全网最全Android开发笔记》

《1307页Android开发面试宝典》

包含了腾讯、百度、小米、阿里、乐视、美团、58、猎豹、360、新浪、搜狐等一线互联网公司面试被问到的题目。熟悉本文中列出的知识点会大大增加通过前两轮技术面试的几率。

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数初中级Android工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则近万的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Android移动开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。

img

img

img

img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注:Android)

最后

简历首选内推方式,速度快,效率高啊!然后可以在拉钩,boss,脉脉,大街上看看。简历上写道熟悉什么技术就一定要去熟悉它,不然被问到不会很尴尬!做过什么项目,即使项目体量不大,但也一定要熟悉实现原理!不是你负责的部分,也可以看看同事是怎么实现的,换你来做你会怎么做?做过什么,会什么是广度问题,取决于项目内容。但做过什么,达到怎样一个境界,这是深度问题,和个人学习能力和解决问题的态度有关了。大公司看深度,小公司看广度。大公司面试你会的,小公司面试他们用到的你会不会,也就是岗位匹配度。

面试过程一定要有礼貌!即使你觉得面试官不尊重你,经常打断你的讲解,或者你觉得他不如你,问的问题缺乏专业水平,你也一定要尊重他,谁叫现在是他选择你,等你拿到offer后就是你选择他了。

另外,描述问题一定要慢!不要一下子讲一大堆,慢显得你沉稳、自信,而且你还有时间反应思路接下来怎么讲更好。现在开发过多依赖ide,所以会有个弊端,当我们在面试讲解很容易不知道某个方法怎么读,这是一个硬伤…所以一定要对常见的关键性的类名、方法名、关键字读准,有些面试官不耐烦会说“你到底说的是哪个?”这时我们会容易乱了阵脚。正确的发音+沉稳的描述+好听的嗓音决对是一个加分项!

最重要的是心态!心态!心态!重要事情说三遍!面试时间很短,在短时间内对方要摸清你的底子还是比较不现实的,所以,有时也是看眼缘,这还是个看脸的时代。

希望大家都能找到合适自己满意的工作!

进阶学习视频

附上:我们之前因为秋招收集的二十套一二线互联网公司Android面试真题 (含BAT、小米、华为、美团、滴滴)和我自己整理Android复习笔记(包含Android基础知识点、Android扩展知识点、Android源码解析、设计模式汇总、Gradle知识点、常见算法题汇总。)

《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!

工作!

进阶学习视频

[外链图片转存中…(img-lZ6kqThF-1713216737323)]

附上:我们之前因为秋招收集的二十套一二线互联网公司Android面试真题 (含BAT、小米、华为、美团、滴滴)和我自己整理Android复习笔记(包含Android基础知识点、Android扩展知识点、Android源码解析、设计模式汇总、Gradle知识点、常见算法题汇总。)

[外链图片转存中…(img-8Kh5EbLH-1713216737324)]

《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!

  • 30
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值