[Android] Measure,Layout,Draw 源码阅读
Android Measure,Layout,Draw 源码阅读
Android View的测量、布局、绘制过程详解(上)_>进阶的程序员>的博客-CSDN博客
Android View的测量、布局、绘制过程详解(下)_>进阶的程序员>的博客-CSDN博客
根据这两篇文章,我对 measure/layout/draw 的原理有了初步了解, 但这系列函数调用的时机还不是很清楚,以及他们与view生命周期的关系
scheduleTraversals
通过阅读源码, 选择从ViewRootImpl的scheduleTraversals函数入手
// ViewRootImpl.java
void scheduleTraversals() {
// 防止重入
if (!mTraversalScheduled) {
mTraversalScheduled = true;
mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
// 关键代码,注册 vsync mTraversalRunnable 回调
mChoreographer.postCallback(
Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
if (!mUnbufferedInputDispatch) {
scheduleConsumeBatchedInput();
}
notifyRendererOfFramePending();
pokeDrawLockIfNeeded();
}
}
// 一条初始化的调用路径
PhoneWindow::setContentView()
View::requestApplyInsets()
ViewParent::requestFitSystemWindows()
// 其他调用场景 ViewRootImpl.java
requestLayout()
invalidate()
{request,clear}ChildFocus()
handleAppVisibility()
handleGetNewSurface()
DisplayListener::onDisplayChanged()
注册完成后会在 vsync 发生后触发 doTraversal
void doTraversal() {
// 下一帧可以继续 doTraversal
if (mTraversalScheduled) {
mTraversalScheduled = false;
// 移除 sync barrier
mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);
// method tracing
if (mProfile) {
Debug.startMethodTracing("ViewAncestor");
}
performTraversals();
if (mProfile) {
Debug.stopMethodTracing();
mProfile = false;
}
}
}
performTraversals()
就是要找的函数, measure
,layout
,draw
三大函数都在这里完成调用
Measure
ViewRootImple 有一个 mFirst 标记了对 performTraversals 的第一次调用,我们以此为线索, 来分析他的内部逻辑
// ViewRootImpl.java
private void performTraversals() {
WindowManager.LayoutParams lp = mWindowAttributes;
// :1732 1.算宽高
Rect frame = mWinFrame;
if (mFirst) {
// 第一次必须要 layout + redraw
mFullRedrawNeeded = true;
mLayoutRequested = true;
// 算可用的 window size
final Configuration config = mContext.getResources().getConfiguration();
if (shouldUseDisplaySize(lp)) {
// NOTE -- system code, won't try to do compat mode.
Point size = new Point();
mDisplay.getRealSize(size);
desiredWindowWidth = size.x;
desiredWindowHeight = size.y;
} else {
desiredWindowWidth = mWinFrame.width();
desiredWindowHeight = mWinFrame.height();
}
// AttachedToWindow, 这里也会做很多事情
// ...
// Set the layout direction if it has not been set before (inherit is the default)
if (mViewLayoutDirectionInitial == View.LAYOUT_DIRECTION_INHERIT) {
host.setLayoutDirection(config.getLayoutDirection());
}
host.dispatchAttachedToWindow(mAttachInfo, 0);
mAttachInfo.mTreeObserver.dispatchOnWindowAttachedChange(true);
dispatchApplyInsets(host);
}
// :1803 mesure
boolean layoutRequested = mLayoutRequested && (!mStopped || mReportNextDraw);
if (layoutRequested) {
final Resources res = mView.getContext().getResources();
// ...
// Ask host how big it wants to be
windowSizeMayChange |= measureHierarchy(host, lp, res,
desiredWindowWidth, desiredWindowHeight);
}
// :1904 mesure 2
// 这个 flag 在 requestFitSystemWindows 中设置
// TODO 为什么要 measure 两次
if (mApplyInsetsRequested) {
mApplyInsetsRequested = false;
mLastOverscanRequested = mAttachInfo.mOverscanRequested;
dispatchApplyInsets(host);
if (mLayoutRequested) {
// Short-circuit catching a new layout request here, so
// we don't need to go through two layout passes when things
// change due to fitting system windows, which can happen a lot.
windowSizeMayChange |= measureHierarchy(host, lp,
mView.getContext().getResources(),
desiredWindowWidth, desiredWindowHeight);
}
}
if (layoutRequested) {
// Clear this now, so that if anything requests a layout in the
// rest of this function we will catch it and re-run a full
// layout pass.
mLayoutRequested = false;
}
}
先从 performTraversals 跳出,看一下 measure 的逻辑 (去掉debug-log)
private boolean measureHierarchy(final View host, final WindowManager.LayoutParams lp,
final Resources res, final int desiredWindowWidth, final int desiredWindowHeight) {
int childWidthMeasureSpec;
int childHeightMeasureSpec;
boolean windowSizeMayChange = false;
boolean goodMeasure = false;
if (lp.width == ViewGroup.LayoutParams.WRAP_CONTENT) {
// 这里的逻辑是针对 大屏中的 dialog, WRAP_CONTENT 不需要 match 整个屏幕
// 如果 measure 成功就会设置 goodMeasure 为 true
// dialog 与底层的 view 不是一个 window, 因此会有独立的 ViewRoot
// ...
}
// 我们的逻辑肯定会进到这里
// getRootMeasureSpec 就是将 layout param + size 转化拼装成 MeasureSpec
// WRAP_CONTENT => AT_MOST, other => EXACTLY
if (!goodMeasure) {
childWidthMeasureSpec = getRootMeasureSpec(desiredWindowWidth, lp.width);
childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height);
// 这个函数就是带trace的 host.measure 方法调用
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
// 判断 measure 完的 size 是否发生改变
// 发生改变后的逻辑 会推迟到 windowSizeMayChange 中
if (mWidth != host.getMeasuredWidth() || mHeight != host.getMeasuredHeight()) {
windowSizeMayChange = true;
}
}
return windowSizeMayChange;
}
接下来就是 view.measure , 这是一个 final 方法,它会帮你规避不必要的重调与缓存调用结果, 真正的 measure 逻辑是在 onMeasure 中完成的,默认的实现就是直接设置自身宽高, 自定义view需要重写该方法来更好的测量自身, 可以看一下它的注释
Measure the view and its content to determine the measured width and the measured height. This method is invoked by measure(int, int) and should be overridden by subclasses to provide accurate and efficient measurement of their contents.
CONTRACT: When overriding this method, you must call setMeasuredDimension(int, int) to store the measured width and height of this view. Failure to do so will trigger an IllegalStateException, thrown by measure(int, int). Calling the superclass' onMeasure(int, int) is a valid use.
view实现该方法的形式五花八门,有两个基本的contract:
-
调用子view的measure 而非 onMeasure,
-
测量完成后通过 setMeasuredDimension 更新 measured
Layout
现在回到 performTraversals, 中间有一段window相关逻辑,我们也不看了,直奔layout
// ViewRootImpl.java
private void performTraversals() {
// :2315
final boolean didLayout = layoutRequested && (!mStopped || mReportNextDraw);
boolean triggerGlobalLayoutListener = didLayout
|| mAttachInfo.mRecomputeGlobalAttributes;
if (didLayout) {
// mWidth, mHeight 直接是 mWinFrame 的 size
// 可见 :2195
performLayout(lp, mWidth, mHeight);
// By this point all views have been sized and positioned
// We can compute the transparent area
// ...
}
}
performLayout 同样是 host.layout 的 traced wrap caller,
但为了防止 layout 过程中子view再调用request layout,损坏内部状态, android 为我们做了处理,
逻辑就是在 layout 过程中有 requestLayout call 的 view 都加入一个列表中,layout 返回后如果子view还的flag还包含 requestLayout, 就再做一边 requestlayout/measure/layout , 如果做完这些还存在 requestLayout 的 view, 则post到 runqueue 等下一次时机再让 view 做 requestLayout. 具体可见 requestLayoutDuringLayout 的注释.
可能有点绕, 先插队看下 requestLayout 到底干了啥
// View.java
@CallSuper
public void requestLayout() {
if (mMeasureCache != null) mMeasureCache.clear();
if (mAttachInfo != null && mAttachInfo.mViewRequestingLayout == null) {
// Only trigger request-during-layout logic if this is the view requesting it,
// not the views in its parent hierarchy
ViewRootImpl viewRoot = getViewRootImpl();
if (viewRoot != null && viewRoot.isInLayout()) {
// 这里就是 ViewRootImpl layout 中就加 list
if (!viewRoot.requestLayoutDuringLayout(this)) {
return;
}
}
mAttachInfo.mViewRequestingLayout = this;
}
mPrivateFlags |= PFLAG_FORCE_LAYOUT;
mPrivateFlags |= PFLAG_INVALIDATED;
// 查看 mPrivateFlags 是否被设置了 PFLAG_FORCE_LAYOUT
if (mParent != null && !mParent.isLayoutRequested()) {
// 如果父view的标志位没有设置,就向上调用
mParent.requestLayout();
}
if (mAttachInfo != null && mAttachInfo.mViewRequestingLayout == this) {
mAttachInfo.mViewRequestingLayout = null;
}
}
// ViewRootImpl.java
@Override
public void requestLayout() {
// 最终调用到这里, 再走一遍 scheduleTraversals
if (!mHandlingLayoutInLayoutRequest) {
checkThread();
mLayoutRequested = true;
scheduleTraversals();
}
}
可见 requestLayout 就是标记自身 dirty,为layout提供信息,然后向上调用到ViewRootImpl::requestLayout 在下一帧触发view tree重新layout。
然后来看一下layout, 它会设置view 的上下左右属性,再调用onLayout, 给自定义view做自己的逻辑(通常是layout 子 view)
// View.java
public void layout(int l, int t, int r, int b) {
// PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT 判断是否要调用 onMeasure
// 记录老的 上下左右
// setFrame: 更新 上下左右
boolean changed = isLayoutModeOptical(mParent) ?
setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);
if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
onLayout(changed, l, t, r, b);
// ...
// 触发 onLayoutChange 回调
}
mPrivateFlags &= ~PFLAG_FORCE_LAYOUT;
mPrivateFlags3 |= PFLAG3_IS_LAID_OUT;
// 焦点相关
// PFLAG3_NOTIFY_AUTOFILL_ENTER_ON_LAYOUT
}
Draw
最后一步是绘制 view 调用链如下
performTraversals
||
\/
performDraw
||
\/
draw ===========================
|| \ \
\/ \\
mAttachInfo.mThreadedRenderer.draw drawSoftware
|| ||
\/ \/
ThreadedRenderer.updateRootDisplayList View.draw(canvas)
|| ||
\/ ||
updateRootDisplayList ||
|| ||
\/ ||
View.updateDisplayListIfDirty ||
|| ||
\/ \/
View.onDraw(canvas
右边的路径可能比较熟悉, 直接创建 canvas 并交给 view 去做绘制,它的名字叫软绘,
那么相应的左面的绘制就是GPU绘制了, 简单概述它的流程就是通过 view的RenderNode去创建DisplayListCanvas, 然后view生成displaylist,记录到canvas上, 最后交给native gl渲染。
由于DisplayListCanvas 继承了 Canvas, 因此可以适配软绘的逻辑.
还有一点要注意的是 ViewRootImpl.draw 只会重绘 dirty 的部分, 因此需要调用 View.invalidate 向上报告 dirty 区域, 从而在下次 performTraversals 绘制到需要重绘的部分
总结
ViewRootImpl(VRI) 处在整个 viewtree的根部,
-
performTraversals 负责触发 measure, layout, draw
-
measure : 在给定大小中测量 view 应占宽高 width, height
-
layout : 设置 view 相对于 parent 的位置与空间 Left,right,top,bottom
-
draw : 给 view 一个 canvas, 让他绘制自己
-
scheduleTraversals 向 Choreographer 注册 vsync 信号回调,在信号发生后调用 performTraversals ,保证一帧只会触发一次。 (第一次触发时机,activity调用: setContentView)
view 通过
-
requestLayout(触发scheduleTraversals, 类似于reflow)
-
invalidate(更新dirty rect, 类似于repaint)
向上报告,直到VRI
常见问题: addView 或修改view属性后拿不到更新后的宽高等信息
由于 performTraversals 需要 vsync 触发, 在requestLayout之后还要等一个异步时机,因此可以在它之后 post 一个回调, 消息队列保证了前后关系, 因此在回调中可以正确拿到 view measure/layout 后的信息