一、测试获取view尺寸
class MainActivity : BaseLifecycleActivity() {
private val binding by lazy { ActivityMainBinding.inflate(layoutInflater) }
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(binding.root)
// 在 onCreate() 中获取宽高
Log.e("measure","measure in onCreate: width=${window.decorView.width}, height=${window.decorView.height}")
// 在 View.post() 回调中获取宽高
binding.activity.post {
Log.e("measure","measure in View.post: width=${window.decorView.width}, height=${window.decorView.height}")
}
}
override fun onResume() {
super.onResume()
// 在 onResume() 回调中获取宽高
Log.e("measure","measure in onResume: width=${window.decorView.width}, height=${window.decorView.height}")
}
}
上面的示例,运行结果是
- E/measure: measure in onCreate: width=0, height=0
- E/measure: measure in onResume: width=0, height=0
- E/measure: measure in View.post: width=1080, height=2340
结论是:在onCreate和onResume中是无法获取到屏幕尺寸的
二、分析View绘制的时间节点
2.1、从ActivityThread#performLaunchActivity开始
*在handleLaunchActivity()会调用到performLaunchActivity*
private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) {
...
// 获取 ComponentName
ComponentName component = r.intent.getComponent();
...
// 创建 ContextImpl 对象
ContextImpl appContext = createBaseContextForActivity(r);
...
// 反射创建 Activity 对象
activity = mInstrumentation.newActivity(cl, component.getClassName(), r.intent);
......
// 创建 Application 对象
Application app = r.packageInfo.makeApplication(false, mInstrumentation);
......
// attach 方法中会创建 PhoneWindow 对象
activity.attach(appContext, this, getInstrumentation(), r.token,
r.ident, app, r.intent, r.activityInfo, title, r.parent,
r.embeddedID, r.lastNonConfigurationInstances, config,
r.referrer, r.voiceInteractor, window, r.configCallback);
// 执行 onCreate()
if (r.isPersistable()) {
mInstrumentation.callActivityOnCreate(activity, r.state, r.persistentState);
} else {
mInstrumentation.callActivityOnCreate(activity, r.state);
}
...
return activity;
}
mInstrumentation.callActivityOnCreate()
方法最终会回调Activity.onCreate()
方法。- 到这里,
setContentView()
方法就被执行了。setContentView()
中主要执行两件事情:- 创建
DecorView
,然后根据我们传入的布局文件 id 解析 xml, - 将得到的 view 塞进 DecorView 中。
- 创建
- 注意,到现在,我们得到的只是一个 空壳子 View 树,它并没有被添加到屏幕上,其实也不能添加到屏幕上。
- 所以,在
onCreate()
回调中获取视图宽高显然是不可取的。
2.2、从ActivityThread#handleResumeActivity
(1)第一步
public void handleResumeActivity(IBinder token, boolean finalStateRequest, boolean isForward,
String reason) {
...
// 1. 回调 onResume
final ActivityClientRecord r = performResumeActivity(token, finalStateRequest, reason);
···
// 2. 获取在onCreate中创建好的decorView
View decor = r.window.getDecorView();
decor.setVisibility(View.INVISIBLE);
ViewManager wm = a.getWindowManager();
WindowManager.LayoutParams lp = r.window.getAttributes();
// 3. 添加 decorView 到 WindowManager
wm.addView(decor, lp);
...
}
onResume()
回调中获取 view 的宽高其实和onCreate()
中没啥区别,都获取不到
(2)第二步
-
wm.addView(decor, lp),最终会调到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(); }
- 初始化mWindowSession,与WMS 进行 Binder 通信
- 这里能看到宽高还未赋值
- 初始化 AttachInfo
- 初始化 Choreographer
-
2>ViewRootImpl#setView()方法
// 参数 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); } } }
(3)第三步 ViewRootImpl#requestLayout()方法
public void requestLayout() {
if (!mHandlingLayoutInLayoutRequest) {
// 检查线程
checkThread();
mLayoutRequested = true;
// 重点
scheduleTraversals();
}
}
(4)第四步 -ViewRootImpl.scheduleTraversals()
void scheduleTraversals() {
//防止重复调用,一次Vsync信号期间调用多次没什么用
if (!this.mTraversalScheduled) {
this.mTraversalScheduled = true;
// 利用了Handler的同步屏障机制,发送一个同步屏障,保证优先处理异步消息
this.mTraversalBarrier = this.mHandler.getLooper().getQueue().postSyncBarrier();
//提交一个异步任务mTraversalRunnable
this.mChoreographer.postCallback(3, this.mTraversalRunnable, (Object)null);
if (!this.mUnbufferedInputDispatch) {
this.scheduleConsumeBatchedInput();
}
this.notifyRendererOfFramePending();
this.pokeDrawLockIfNeeded();
}
}
-
(1)
Choreographer.postCallback()
方法通过DisplayEventReceiver.nativeScheduleVsync()
方法向系统底层注册了下一次vsync
信号的监听。 -
(2)当下一次
vsync
来临时,系统会回调其dispatchVsync()
方法,最终回调FrameDisplayEventReceiver.onVsync()
方法。 -
(3)
FrameDisplayEventReceiver.onVsync()
方法中取出之前提交的mTraversalRunnable
并执行。这样就完成了一次绘制流程。 (5)第五步 -
mTraversalRunnable
中执行的是doTraversal()
方法。final class TraversalRunnable implements Runnable { TraversalRunnable() { } public void run() { //最终还是回调到ViewRootImpl ViewRootImpl.this.doTraversal(); } }
(6)第六步
-
ViewRootImpl#doTraversal
void doTraversal() { if (this.mTraversalScheduled) { //把这个变量重置一下 this.mTraversalScheduled = false; // 移除同步屏障 this.mHandler.getLooper().getQueue().removeSyncBarrier(this.mTraversalBarrier); if (this.mProfile) { Debug.startMethodTracing("ViewAncestor"); } //调用自己的方法处理绘制流程 this.performTraversals(); if (this.mProfile) { Debug.stopMethodTracing(); this.mProfile = false; } } }
(7)第七步
-
ViewRootImpl#performTraversals
private void performTraversals() { ... // 1. 绑定 Window ,这个地方在下面的3.3分析会用到 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(); }
三、View.post()做了什么
3.1、View#post()方法
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
是在ViewRootImpl
的构造函数中初始化的,ViewRootImpl
是在WindowManagerGlobal.addView()
创建的WindowManagerGlobal.addView()
是在 ActivityThread 的handleResumeActivity()
中调用的,但是是在Activity.onResume()
回调之后。- 针对“2”处,把
post()
方法要执行的 Runnable 存储在一个队列中,在合适的时机(View 已被测量)拿出来执行
3.2、HandlerActionQueue
-
getRunQueue()获取的Queue就是HandlerActionQueue
-
官方解释就是:
- Class used to enqueue pending work from Views when no Handler is attached.
-
是一个初始容量是 4 的
HandlerAction
数组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; } }
-
executeActions方法就是在View的dispatchAttachedToWindow中调用
-
通过传入的 handler 进行任务分发
3.3、View#dispatchAttachedToWindow
- ViewRootImpl#performTraversals->View#dispatchAttachedToWindow()->HandlerActionQueue#executeActions(info.mHandler)
- 先调用的
dispatchAttachedToWindow()
,再进行的测量流程 performTraversals()
是在主线程消息队列的一次消息处理过程中执行的,而dispatchAttachedToWindow()
间接调用的mRunQueue.executeActions()
发送的任务也是通过 Handler发送到主线程消息队列的,那么它的执行就一定在这次的performTraversals()
方法执行完之后
四、总结
- 根据ViewRootImpl是否已经创建,View.post()会执行不同的逻辑。
- 如果ViewRootImpl已创建
- 即mAttachInfo已经初始化,直接通过Handler发送消息来执行任务。
- 如果ViewRootImpl未创建
- 即View尚未开始绘制,会将任务保存为HandlerAction,暂存在队列HandlerActionQueue中,
- 等到View开始绘制,执行performTraversal()方法时,在dispatchAttachedToWindow()方法中
- 如果ViewRootImpl已创建
- 通过Handler分发HandlerActionQueue中暂存的任务。
- 简单的时序图总结一下: