Android 深入理解View.post() 、Window加载View原理
创始人
2024-02-04 11:56:29
0

文章目录

      • 背景:如何在onCreate()中获取View的宽高?
      • View.post()原理
      • Window加载View流程
        • setContentView()
        • ActivityThread#handleResumeActivity()
        • 总结
      • 扩展
        • Window、Activity及View三者之间的关系
        • 是否可以在子线程中更新UI
      • 资料

背景:如何在onCreate()中获取View的宽高?

在某些场景下,需要我们在ActivityonCreate()中获取View的宽高,如果直接通过getMeasuredHeight()、getMeasuredWidth()去获取,得到的值都是0

2022-11-14 16:56:42.604  E/TTT: onCreate: width->0, height->0

为什么是这样呢?因为onCreate()回调执行时,View还没有经过onMeasure()、onLayout()、onDraw(),所以此时肯定是获取不到View的宽高的。通过下面几种方式可以在onCreate()中获取到View的宽高:

  • ViewTreeObserver
  • View.post()
  • 通过MeasureSpec自行测量宽高

具体可以参见:ViewTreeObserver使用总结及获得View高度的几种方法。有的同学可能会说:我用postDelay()也能获取View的宽高呀,确实,延迟一段时间再去获取宽高也是能得到的,但这种方式不够优雅,且具体延迟多长时间是不知道的,因此postDelay()这种方式先不考虑,本文重点来讨论下面几个问题:

  • View.post()是如何拿到宽高的?
  • 一个Activity对应一个Window,那么Window加载View的流程又是怎样的?

View.post()原理

先把结论贴出来,后面再详细分析:

  • View.post(Runnable)执行时,会根据View当前状态执行不同的逻辑:当View还没有执行测量、布局、绘制时,View.post()会将Runnable任务放入一个任务队列中以待后续执行;反之,当View已经执行了测量、绘制后,Runnable任务会直接通过AttachInfo中的Handler执行
  • View.post()能够保证提交的任务是在View测量、绘制之后执行,所以可以得到正确的宽高
  • 只有View关联到ViewRootImpl,或者说View必须依附到View树之后,View.post()中的任务才会生效,否则View.post()中的任务永远都不会执行

下面分析下View.post()的源码,文中的源码基于API 30~

 // View.javapublic boolean post(Runnable action) {//1final AttachInfo attachInfo = mAttachInfo;if (attachInfo != null) {return attachInfo.mHandler.post(action);}//2、 Postpone the runnable until we know on which thread it needs to run.// Assume that the runnable will be successfully placed after attach.//推迟runnable执行,确保View attach到Window之后才会执行getRunQueue().post(action);return true;}

可以看到post()方法中,主要是两块逻辑,1里面,如果mAttachInfo不为空,直接调用其内部的Handler执行Runnable任务;否则执行2中的getRunQueue().post(action)。我们逐步分析,各个击破。

1先来看AttachInfo赋值的地方,按mAttachInfo关键字搜索,一共有2个地方赋值

//View.java
void dispatchAttachedToWindow(AttachInfo info, int visibility) {//1、mAttachInfo赋值mAttachInfo = info;//2、 执行之前挂起的所有任务,这里的任务是通过 getRunQueue().post(action)挂起的任务。if (mRunQueue != null) {mRunQueue.executeActions(info.mHandler);mRunQueue = null;}//3、回调View的onAttachedToWindow方法,该方法在onResume之后,View绘制之前执行onAttachedToWindow();//......其他......
}void dispatchDetachedFromWindow() {//4、mAttachInfo在Window detach View的时候置为空mAttachInfo = null;//......其他......
}

其中给mAttachInfo赋值的地方是在View#dispatchAttachedToWindow()中,这里我们先记住该方法是在View执行测量、绘制时调用,下一节会详细介绍;同时2处会把之前View.post()中挂起的任务取出并通过AttachInfo.Handler执行,因为Android是基于消息模型运行的,所以这些任务能够保证都是在经过测量、绘制之后执行,即能正确的获取各自View的宽高。

2、回到View.post()的2处,来看getRunQueue().post(action)里的流程,

// View.java
private HandlerActionQueue getRunQueue() {if (mRunQueue == null) {mRunQueue = new HandlerActionQueue();}return mRunQueue;
}

getRunQueue()中初始化了HandlerActionQueue

//HandlerActionQueue.java
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++;}}// 将Runnable、delay时间合并到HandlerAction中private static class HandlerAction {final Runnable action;final long delay;public HandlerAction(Runnable action, long delay) {this.action = action;this.delay = delay;}}...
}

HandlerAction内部保存了要执行的Runnable任务及其delay时间

HandlerActionQueue#post()又调用了内部的postDelay()方法将Runnable任务保存在了HandlerAction数组中,getRunQueue().post(action)只是将Runnable任务进行保存,并不会执行

Window加载View流程

Window添加View

setContentView()

ActivityonCreate()里调用setContentView()之后,实际上是将操作委托给了PhoneWindow,如上面UML类图所示,我们在setContentView()里通过layoutId生成的View被添加到了树的顶层根部DecorView中,而此时DecorView还没有添加到PhoneWindow中。

ActivityThread#handleResumeActivity()

真正页面可见是在onResume()之后。具体来说,是在ActivityThread#handleResumeActivity()中,调用了WindowManager#addView()方法将DecorView添加到了WMS中:

 public void handleResumeActivity(IBinder token, boolean finalStateRequest, boolean isForward,String reason) {......final Activity a = r.activity;if (r.window == null && !a.mFinished && willBeVisible) {r.window = r.activity.getWindow();View decor = r.window.getDecorView();decor.setVisibility(View.INVISIBLE);ViewManager wm = a.getWindowManager();WindowManager.LayoutParams l = r.window.getAttributes();a.mDecor = decor;l.type = WindowManager.LayoutParams.TYPE_BASE_APPLICATION;if (a.mVisibleFromClient) {if (!a.mWindowAdded) {a.mWindowAdded = true;//重点看这里wm.addView(decor, l);} else {a.onWindowAttributesChanged(l);}}}

重点是调用了WindowManager.addView(decor, l)WindowManager是一个接口类型,其父类ViewManager也是一个接口类型,ViewManager描述了View的添加、删除、更新等操作(ViewGroup也实现了此接口)。

WindowManager的真正实现者是WindowManagerImpl,其内部通过委托调用了WindowManagerGlobaladdView()WindowMangerGlobal是一个单例类,一个进程中只有一个WindowMangerGlobal实例对象。来看WindowMangerGlobal#addView()的实现:

//WindowMangerGlobal.javapublic void addView(View view, ViewGroup.LayoutParams params,Display display, Window parentWindow, int userId) {ViewRootImpl root;//1、创建ViewRootImplroot = new ViewRootImpl(view.getContext(), display);mViews.add(view);mRoots.add(root);mParams.add(wparams);// do this last because it fires off messages to start doing thingstry {//2、调用了ViewRootImpl的setView()root.setView(view, wparams, panelParentView, userId);} catch (RuntimeException e) {// BadTokenException or InvalidDisplayException, clean up.if (index >= 0) {removeViewLocked(index, true);}throw e;}}

WindowMangerGlobal#addView()中主要有两步操作:在1处创建了ViewRootImpl,这里额外看一下ViewRootImpl的构造方法:

 public ViewRootImpl(Context context, Display display, IWindowSession session,boolean useSfChoreographer) {mContext = context;mWindowSession = session;mDisplay = display;...mWindow = new W(this);mLeashToken = new Binder();//初始化了AttachInfomAttachInfo = new View.AttachInfo(mWindowSession, mWindow, display, this, mHandler, this,context);}

可以看到在ViewRootImpl的构造方法中也初始化了AttachInfo。回到WindowMangerGlobal#addView()的2处,这里继续调用了ViewRootImpl#setView()

 public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView,int userId) {// 1、DecorView中关联的View会执行measure、layout、draw流程requestLayout();InputChannel inputChannel = null;if ((mWindowAttributes.inputFeatures& WindowManager.LayoutParams.INPUT_FEATURE_NO_INPUT_CHANNEL) == 0) {//2、创建InputChannel用于接收触摸事件inputChannel = new InputChannel();}try {// 3、通过Binder将View添加到WMS中res = mWindowSession.addToDisplayAsUser(mWindow, mSeq, mWindowAttributes,getHostVisibility(), mDisplay.getDisplayId(), userId, mTmpFrame,mAttachInfo.mContentInsets, mAttachInfo.mStableInsets,mAttachInfo.mDisplayCutout, inputChannel,mTempInsets, mTempControls);setFrame(mTmpFrame);} catch (RemoteException e) {...}}

setView()中,1处最终会执行到Viewmeasure、layout、draw流程,2处创建了InputChannel用于接收触摸事件,最终在3处通过BinderView添加到了WMS

再细看来下1处的requestLayout(),其内部会依次执行 scheduleTraversals() -> doTraversal() -> performTraversals()

//ViewRootImpl.java
private void performTraversals() {final View host = mView; //mView对应的是DecorView//1、host.dispatchAttachedToWindow(mAttachInfo, 0);mAttachInfo.mTreeObserver.dispatchOnWindowAttachedChange(true);//2、执行View的onMeasure()performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);//......其他代码......//3、执行View的onLayout(),可能会执行多次performLayout(lp, mWidth, mHeight);//......其他代码......//4、执行View的onDraw(),可能会执行多次performDraw();
}

performTraversals()中2、3、4处分别对应View的测量、布局、绘制流程,不再多说;1处hostDecorView(DecorView继承自FrameLayout),最终调用到了ViewGroupdispatchAttachedToWindow()方法:

    // ViewGroup.java@Overridevoid dispatchAttachedToWindow(AttachInfo info, int visibility) {...super.dispatchAttachedToWindow(info, visibility);final int count = mChildrenCount;final View[] children = mChildren;for (int i = 0; i < count; i++) {final View child = children[i];//遍历调用子View的dispatchAttachedToWindow()共享AttachInfochild.dispatchAttachedToWindow(info,combineVisibility(visibility, child.getVisibility()));}}

方法内部又会通过循环遍历调用了各个子ViewdispatchAttachedToWindow()方法,从而AttachInfo会通过遍历传递到各个子View中去,换句话说:经过dispatchAttachedToWindow(AttachInfo info, int visibility),ViewRootImpl中关联的所有View共享了AttachInfo

分析到这里,我们再回顾一下上一节的View.post()内部实现,View.post()提交的任务必须在AttachInfo != null时,通过AttachInfo内部的Handler发送及执行,此时View已经经过了测量、布局、绘制流程,所以肯定能正确的得到View的宽高;而如果AttachInfo == null时,View.post()中提交的任务会进入任务队列中,直到View#dispatchAttachedToWindow()执行过后才会将任务取出来执行。

总结

  • WindowManager继承自ViewManager接口,提供了添加、删除、更新View的APIWindowManager可以看作是WMS在客户端的代理类。
  • ViewRootImpl实现了ViewParent接口,其是整个View树的根部,View的测量、布局、绘制以及输入事件的处理都由ViewRootImpl触发;另外,它还是WindowManagerGlobal的实际工作者,负责与WMS交互通信以及处理WMS传过来的事件(窗口尺寸改变等)。ViewRootImpl的生命从setView()开始,到die()结束,ViewRootImpl起到了承上启下的作用

扩展

Window、Activity及View三者之间的关系

  • 一个 Activity 对应一个 Window(PhoneWindow)PhoneWindow 中有一个 DecorView,在 setContentView 中会将 layoutId生成的View 填充到此 DecorView 中。
  • Activity看上去像是一个被代理类,内部添加View的操作是通过Window操作的。可以将Activity理解成是WindowView之间的桥梁。

是否可以在子线程中更新UI

回看下ViewRootImpl中的方法:

  //ViewRootImpl.javapublic ViewRootImpl(Context context, Display display, IWindowSession session,boolean useSfChoreographer) {...mThread = Thread.currentThread();}@Overridepublic void requestLayout() {if (!mHandlingLayoutInLayoutRequest) {//检查线程的正确性checkThread();mLayoutRequested = true;scheduleTraversals();}}void checkThread() {if (mThread != Thread.currentThread()) {throw new CalledFromWrongThreadException("Only the original thread that created a view hierarchy can touch its views.");}}

可以看到在requestLayout()中,如果当前调用的线程不是 ViewRootImpl 的构造方法中初始化的线程就会在checkThread()中抛出异常

通过上一节的学习,我们知道ViewRootImpl是在ActivityThread#handleResumeActivity()中初始化的,那么如果在onCreate()里新起子线程去更新UI,就不会抛异常了,因为此时还没有执行checkThread()去检查线程的合法性。如:

//Activity.java
override fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)//子线程中更新UI成功thread { mTvDelay.text = "子线程中更新UI" }}

此时子线程中更新UI成功,结论:只要在ActivityThread#handleResumeActivity()之前的流程中(如onCreate())新起一个子线程更新UI,也是会生效的,不过一般不建议这么操作

资料

【1】WindowManger实现桌面悬浮窗
【2】深入理解WindowManager
【3】直面底层:你真的了解 View.post() 原理吗?
【4】https://blog.csdn.net/stven_king/article/details/78775166

相关内容

热门资讯

喜欢穿一身黑的男生性格(喜欢穿... 今天百科达人给各位分享喜欢穿一身黑的男生性格的知识,其中也会对喜欢穿一身黑衣服的男人人好相处吗进行解...
发春是什么意思(思春和发春是什... 本篇文章极速百科给大家谈谈发春是什么意思,以及思春和发春是什么意思对应的知识点,希望对各位有所帮助,...
网络用语zl是什么意思(zl是... 今天给各位分享网络用语zl是什么意思的知识,其中也会对zl是啥意思是什么网络用语进行解释,如果能碰巧...
为什么酷狗音乐自己唱的歌不能下... 本篇文章极速百科小编给大家谈谈为什么酷狗音乐自己唱的歌不能下载到本地?,以及为什么酷狗下载的歌曲不是...
华为下载未安装的文件去哪找(华... 今天百科达人给各位分享华为下载未安装的文件去哪找的知识,其中也会对华为下载未安装的文件去哪找到进行解...
怎么往应用助手里添加应用(应用... 今天百科达人给各位分享怎么往应用助手里添加应用的知识,其中也会对应用助手怎么添加微信进行解释,如果能...
家里可以做假山养金鱼吗(假山能... 今天百科达人给各位分享家里可以做假山养金鱼吗的知识,其中也会对假山能放鱼缸里吗进行解释,如果能碰巧解...
四分五裂是什么生肖什么动物(四... 本篇文章极速百科小编给大家谈谈四分五裂是什么生肖什么动物,以及四分五裂打一生肖是什么对应的知识点,希...
一帆风顺二龙腾飞三阳开泰祝福语... 本篇文章极速百科给大家谈谈一帆风顺二龙腾飞三阳开泰祝福语,以及一帆风顺二龙腾飞三阳开泰祝福语结婚对应...
美团联名卡审核成功待激活(美团... 今天百科达人给各位分享美团联名卡审核成功待激活的知识,其中也会对美团联名卡审核未通过进行解释,如果能...