背景:怎么在onCreate()中获取View的宽高?
在某些场景下,需求咱们在Activity
的onCreate()
中获取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
完成原理,别的几种办法不是本文要点,咱们可自行查找查看。经过学习本文,能够处理下面的几个问题:
- View.post() 是怎么拿到宽高的?
- 一个
Activity
对应一个Window
,那么Window
加载View
的流程又是怎样的?
View.post()原理
先把定论贴出来,后边再具体剖析:
- View.post(Runnable)履行时,会依据View当前状态履行不同的逻辑:当View还没有履行丈量、布局、制作时,View.post()会将Runnable使命放入一个使命行列中以待后续履行;反之,当View已经履行了丈量、制作后,Runnable使命会直接经过AttachInfo中的Handler履行(UI线程中的Handler)。总归View.post()能够确保提交的使命是在View丈量、制作之后履行,所以能够得到正确的宽高;
- 当前View只有在依附到View树之后,调用View.post()中的使命才有机会履行;反之只是new一个View实例,并未关联到View树的话,那么该View.post()中的Runnable使命永远都不会得到履行。
下面来剖析View.post()
的源码完成,文中的源码根据API 30
~
// View.java
public boolean post(Runnable action) {
//1
final 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、针对1处,在View.java
类中查找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()
中挂起的Runnbale
使命取出并经过AttachInfo.Handler
发送并履行,由于Android
是根据音讯模型运转的,所以能够确保这些Runnable使命
是在View丈量、制作之后
履行的,终究在View.post{}
中获取正确的宽高。
2、回到View.post()
的2处,来看getRunQueue().post(action)
里的流程:
// View.java
private HandlerActionQueue getRunQueue() {
if (mRunQueue == null) {
mRunQueue = new HandlerActionQueue();
}
return mRunQueue;
}
getRunQueue()
中返回了一个HandlerActionQueue
,假如该目标为空会对其进行初始化,持续看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
是HandlerActionQueue
的静态内部类,该类保存了要履行的Runnable使命
及其delay时间
。
getRunQueue().post(action)
终究调用了HandlerActionQueue#post()
,内部持续调用本身的postDelay()
办法,该办法将Runnable使命
保存在了HandlerAction
数组中,所以getRunQueue().post(Runnable)
只是将Runnable
使命进行保存,以待后续履行。
Window加载View流程
从setContentView()开端
业务开发中,最常使用的便是在Activity
的onCreate()
里调用setContentView()
来设置页面布局。实际上调用该办法之后是将操作托付给了PhoneWindow
,如上面UML
类图所示,咱们在setContentView()
里经过layoutId生成的View
被增加到了树的顶层 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
,其内部经过托付调用了WindowManagerGlobal
的addView()
,WindowMangerGlobal
是一个单例类,一个进程中只有一个WindowMangerGlobal
实例目标。来看WindowMangerGlobal#addView()
的完成:
//WindowMangerGlobal.java
public void addView(View view, ViewGroup.LayoutParams params,
Display display, Window parentWindow, int userId) {
ViewRootImpl root;
//1、创立ViewRootImpl
root = 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 things
try {
//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();
//初始化了AttachInfo
mAttachInfo = 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处终究会履行到View
的measure、layout、draw
流程,2处创立了InputChannel
用于接收接触事情,终究在3处经过Binder
将View
增加到了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处host
是DecorView(DecorView承继自FrameLayout)
,终究调用到了ViewGroup
的dispatchAttachedToWindow()
办法:
// ViewGroup.java
@Override
void 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()同享AttachInfo
child.dispatchAttachedToWindow(info,
combineVisibility(visibility, child.getVisibility()));
}
}
办法内部又会经过循环遍历调用了各个子View
的dispatchAttachedToWindow()
办法,然后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()
履行过后才会将使命取出来履行。
剖析到这儿,咱们再回看下上面的UML类图
联系,整个Window
加载View
及View.post()
的原理是不是比较清晰了。
总结
-
WindowManager
承继自ViewManager
接口,提供了增加、删除、更新View的API
,WindowManager
能够看作是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
了解成是Window
与View
之间的桥梁。
是否能够在子线程中更新UI
回看下ViewRootImpl
中的办法:
//ViewRootImpl.java
public ViewRootImpl(Context context, Display display, IWindowSession session,
boolean useSfChoreographer) {
...
mThread = Thread.currentThread();
}
@Override
public 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】blog.csdn.net/stven_king/…