网站开发公司+重庆,湖南手机版建站系统哪家好,网站建设管理制度九不准,做弹幕网站有哪些绘制原理 Android 程序员都知道 Android 的绘制流程分为 Measure、Layout、Draw 三步骤#xff0c;其中
Measure 负责测量 View 的大小Layout 负责确定 View 的位置Draw 负责将 View 画在屏幕上 由 ViewRootImpl 实现的 performTraversal 方法是 Measure、layout、draw 的真正…绘制原理 Android 程序员都知道 Android 的绘制流程分为 Measure、Layout、Draw 三步骤其中
Measure 负责测量 View 的大小Layout 负责确定 View 的位置Draw 负责将 View 画在屏幕上 由 ViewRootImpl 实现的 performTraversal 方法是 Measure、layout、draw 的真正执行者本文将不会关注 measure、layout、draw 的执行细节。而是展示 measure、layout、draw 和 Traversal 之间的关系沉浸在细节里面反而对全貌会难以知晓这一系列将会循循渐进先介绍全貌再由具体问题分析具体的细节。从 View Root 视角展示 Traversal 流程是如何统筹、控制和执行三大流程的。 本文将会带着两个问题去学习
刷新率为 60fps 的手机每秒钟会执行 60 次 traversal 吗ViewRootImpl 在一段时间内执行 60 次 TraversalMeasure / Layout / Draw 都会执行60次吗
背景知识
Measure 与 onMeasure
public class View {/*** 此方法用于确定 view 的大小由父 view 提供宽度、高度的限制条件* 真正的测量工作是由此方法调用的 onMeasure(int, int) 执行的* 因此子类只可以覆盖 onMeasure(int, int) 方法*/public final void measure(int widthMeasureSpec, int heightMeasureSpec) {onMeasure(widthMeasureSpec, heightMeasureSpec);}/*** 覆盖此方法以实现对自身内容的提供准确、高效地测量*/protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)
}public class CustomViewGroup extends ViewGroup {protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {measureSelf();children.forEach(child - {child.measure(childWidthMeasureSpec, childHeightMeasureSpec);});}
}layout 和 onLayout 关系和 measure 和 onMeasure 一致draw 和 onDraw 的关系也大体上可以认为是一致的。
ViewRootImpl Android中 view 是按照树形结构组织的根节点一般是 DecorView树中的每一个非叶子节点都是一个 ViewGroupViewGroup 负责组织、管理子 view而子 View 持有的 parent 引用却是被声明为 ViewParent 类型这暗示了 View 的父容器并不一定是 View/ViewGroup更准确的说树中的每一个非叶子节点都是一个 ViewParent。
public class View {/*** Gets the parent of this view. Note that the parent is a* ViewParent and not necessarily a View.*/public final ViewParent getParent() {return mParent;}// Finds the topmost view in the current view hierarchy.public View getRootView() {View parent this;while (parent.mParent ! null parent.mParent instanceof View) {parent (View) parent.mParent;}return parent;}// Gets the view root associated with the View.public ViewRootImpl getViewRootImpl() {if (mAttachInfo ! null) {return mAttachInfo.mViewRootImpl;}return null;}
}ViewParent 定义 View 容器的职责其中定义了子 view 想要与父 view 交互的时候所能看到的 API。ViewRootImpl 是其存在的重要理由。ViewRootImp 并没有继承 View、ViewGroup但是实现了 ViewParent 接口是 View Tree 根节点的 parent 节点因此可以认为 View Tree的物理根节点是 DecorView逻辑根节点是 ViewRootImpl 究其原因DecorView 作为物理上的 View Root但其本质上仍然只是一个 View并没有管理整颗 View 树的能力。查看其代码也可以发现其能力只有针对自己这个 view 的绘制、事件分发等等因此才会有 ViewRootImpl作为 DecorView 逻辑上、抽象 View Parent链接 View 和 Window负责事件分发、视图更新等流程。本文的主题就是 ViewRootImpl 负责实现的。
Choreographer 协调动画、输入和绘图的时间 Chereographer 是 Android APP 对于 V_SYNC信号可以简单认为 V_SYNC信号是在开启 V_SYNC的显示设备的缓冲区就绪的信号的接受者当收到 V_SYNC信号以后意味着缓冲区已经就绪可以触发和下一帧画面相关的任务包括输入响应、动画、traversal、自定义任务。V_SYNC一般默认和显示设备的刷新率同步对于主流的 Android 手机fps 通常是 60、90或者 120.
Traversal
概览 Traversal 在 Android32 中是一个超过 800行的复杂函数这里只展示一下精简版先从宏观视角对其内容有一个粗略的了解。
public final class ViewRootImpl implements ViewParent {private void performTraversals() {boolean layoutRequested mLayoutRequested !mStopped;if (layoutRequested) {performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);}if (layoutRequested) {performLayout(lp, mWidth, mHeight);} performDraw();}
}排除掉 window、surface、focus 等非本文关注内容Traversal 就是按顺序决定是否需要执行 measure、layout、draw 方法。
起源 绘制流程是 Traversal 执行的那 Traversal 的又是谁执行的呢 最普遍的情况下View 的 onMeasure 堆栈结构如下图所谓 如上文提到的编舞者在收到 V_SYNC信号以后开始通过 handler 通知 ViewRootImpl 执行绘制流程以准备下一帧的任务从 DecorView 出发最终调用自定义 View 的 onMeasure 方法。从堆栈来看V_SYNC似乎是 Traversal 的起点但是展开Choreographer的doFrame的函数发现doFrame只是执行Choreographer.CALLBACK_TRAVERSAL类型的回调任务而Traversal的callback正是ViewRootImpl自己注册的。
public final class Choreographer {private final CallbackQueue[] mCallbackQueues;void doFrame(long frameTimeNanos, int frame) {...doCallbacks(Choreographer.CALLBACK_TRAVERSAL, frameTimeNanos);...}void doCallbacks(int callbackType, long frameTimeNanos) {callbacks mCallbackQueues[callbackType]callbacks.foreach { it.run() }}
}通过进一步追溯Choreographer.CALLBACK_TRAVERSAL类型的callback注册只有ViewRootImpl的某些方法才会注册Choreographer.CALLBACK_TREAVERSAL类型的callback用于在下一次V_SYNC信号到来的时候执行Traversal流程。所以Traversal的源头并不是V_SYNC而是ViewRootImpl的某些操作V_SYNC本身只是Traversal异步任务的一个触发器。
public final class ViewRootImpl implements ViewParent {void scheduleTraversals() {if (!mTraversalScheduled) {mTraversalScheduled true;mChoreographer.postCallback(Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);}}
}ViewRootImpl 中会触发 scheduleTraversals 的方法一共有 18 个sdk 30
requestLayoutinvalidaterequestFitSystemWindowsrequestChildFocusnotifyInsetsChangedinvalidateRectOnScreenhandleDispatchSystemUiVisibilityChanged...... 从会触发的 scheduleTraversals 方法名可以看出只有当 UI 界面发生变化时才会在下一帧安排 traversal。所以Traversal 相对于 V_SYNC 信号而言是按需执行的即执行 traversal 的次数 屏幕刷新的次数。至此我们回答了第一个问题刷新率60fps的手机每秒并不会执行60次traversal。
requestLayout 以最常用的requestLayout方法为例子追溯一下Traversal的真正起源
public final class ViewRootImpl implements ViewParent {Overridepublic void requestLayout() {checkThread();mLayoutRequested true;scheduleTraversals();}
}public class View {public void requestLayout() {mPrivateFlags | PFLAG_FORCE_LAYOUT;// 避免重复调用if (mParent ! null (mparent.mPrivateFlags PFLAG_FORCE_LAYOUT) ! PFLAG_FORCE_LAYOUT) {mParent.requestLayout();}}public boolean isLayoutRequested() {return (mPrivateFlags PFLAG_FORCE_LAYOUT) PFLAG_FORCE_LAYOUT;}
}requestLayout是ViewParent接口中定义的方法同时View中也定义了同名方法当View Tree中的任何一个View需要请求重新布局的时候调用自身requestLayout该方法向上回溯执行知道最顶层的ViewParent。这会导致
对于链路上所有的View会被打上PFLAG_FORCE_LAYOUT(我们可以将该标记理解为我需要measure)标记对于最终的ViewParent节点既最顶层View的父结点→ViewRootImpl则会设置自身的mLayoutRequested标志同时开始注册下一帧的Traversal callback
Measure 许多App的UI都是长时间不变化比如阅读、工具类APP如此设计肯定可以节省大量的资源。而且UI发生变化时往往都是局部变化比如一个按钮的点赞动画、播放器画面的更新既然Traversal已经实现了按需执行measure、layout、draw也应该需要按需执行。
执行条件 Measure是Traversal的第一个环节
public final class ViewRootImpl implements ViewParent {// DeocrViewView mView;private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) {mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);}private void performTraversals() {boolean layoutRequested mLayoutRequested (!mStopped || mReportNextDraw);if (layoutRequested || windowInsetsChanged) {performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);}...}
}对于ViewRootImpl其可以通过requestLayout中设置的mLayoutRequested标志位决定是否调用performMeasure进而控制Root View是否执行measure方法。注意上述代码执行performMeasure的条件是
mLayoutRequested true应用不处在stop状态或者mReportNextDraw true 那么类似的View同样可以借助requestLayout中设置的PFLAG_FORCE_LAYOUT标志位决定自身是否执行onMeasure流程那来过滤没必要的测量操作。View的measure方法如下
public class View {public final void measure(int widthMeasureSpec, int heightMeasureSpec) {// 是否执行了 requestLayoutfinal boolean forceLayout (mPrivateFlags PFLAG_FORCE_LAYOUT) PFLAG_FORCE_LAYOUT;// measureSpec 是否变化final boolean specChange widthMeasureSpec ! mOldWidthMeasureSpec|| heightMeasureSpec ! mOldHeightMeasureSpec;final boolean isSpecExactly specMode EXACTLY;final boolean matchesSpecSize specSize currentSize;final boolean needsLayout specChanged (!isSpecExactly || !matchesSpecSize);if (forceLayout || needsLayout) {onMeasure(widthMeasureSpec, heightMeasureSpec);mPrivateFlags | PFLAG_LAYOUT_REQUIRED;} }
}如下面的代码和注释展示的。仅当满足下面条件之一时才会执行onMeasure函数进行真正的测量
measureSpec变化且layoutWidth、layoutHeight不是准确数值或者准确数值与自身大小不一致PFLAG_FORCE_LAYOUT标记存在时候。 因此Traversal流程中许多不满足以上条件的View是可以跳过自身的onMeasure以及后代的measure函数这对于复杂的视图结构是很有意义的。 当onMeasure执行完成以后并没有清空PFLAG_FORCE_LAYOUT标志而且追加设置了一个新的标志→ PFLAG_LAYOUT_REQUIRED我们可以将该标志位理解为我需要layout
从PFLAG_LAYOUT_REQUIRED名字可以看出这意味着执行过onMeasure的函数理论上一定需要执行layout流程requestLayout设置的PFLAG_FORCE_LAYOUT是强制layout的含义但是onMeasure的执行流程也依赖该标志位ViewRootImpl的Traversal流程中关于是否执行performMeasure和performLayout的判断条件也都是mLayoutRequested标志位 从以上三点不难看出layout流程是绑定measure的重新测量以后一定需要重新布局。
MeasureCache 此外View引入了MeasureCache用于保存历史widthMeasureSpec/heightMeasureSpce对应的测量结果如果命中了缓存就可以执行设置结果跳过onMeasure的测量工作(但没有完全的跳过)不过又设置了一个新的标志位PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT
public class View {private LongSparseLongArray mMeasureCache;public final void measure(int widthMeasureSpec, int heightMeasureSpec) {long key (long) widthMeasureSpec 32 | (long) heightMeasureSpec 0xffffffffL;if (mMeasureCache null) mMeasureCache new LongSparseLongArray(2);if (forceLayout || needsLayout) {int cacheIndex forceLayout ? -1 : mMeasureCache.indexOfKey(key);if (cacheIndex 0) {onMeasure(widthMeasureSpec, heightMeasureSpec);mPrivateFlags3 ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;} else {long value mMeasureCache.valueAt(cacheIndex);setMeasuredDimensionRaw((int) (value 32), (int) value);mPrivateFlags3 | PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;}mPrivateFlags | PFLAG_LAYOUT_REQUIRED;}mMeasureCache.put(key, ((long) mMeasuredWidth) 32 | (long) mMeasuredHeight 0xffffffffL); // suppress sign extension}
}Layout
执行条件 和performMeasure相同在ViewRootImpl的performTraversal中mLayoutRequested标志也是layout流程执行的必要条件
public final class ViewRootImpl implements ViewParent {// DeocrViewView mView;private void performTraversals() {...if (mLayoutRequested) {performLayout(lp, mWidth, mHeight);}...}private void performLayout(WindowManager.LayoutParams lp, int desiredWindowWidth,int desiredWindowHeight) {...mView.layout(0, 0, mView.getMeasuredWidth(), mView.getMeasuredHeight());...}
} 和 measure 类似onLayout 也只会满足一定条件时才会执行用于实现按需执行
View 的坐标发生变化类比 onMeasure 的 needsLayoutPFLAG_LAYOUT_REQUIRED ****标记存在时类比 onMeasure 的 forceLayout 以上两个条件满足任一即可。
public class View {public void layout(int l, int t, int r, int b) {int oldL mLeft;int oldT mTop;int oldB mBottom;int oldR mRight;boolean changed setFrame(l, t, r, b);if (changed || (mPrivateFlags PFLAG_LAYOUT_REQUIRED) PFLAG_LAYOUT_REQUIRED) {onLayout(changed, l, t, r, b);mPrivateFlags ~PFLAG_LAYOUT_REQUIRED;}mPrivateFlags ~PFLAG_FORCE_LAYOUT;}protected boolean setFrame(int left, int top, int right, int bottom) {...return mLeft ! left || mRight ! right || mTop ! top || mBottom ! bottom;}
} 在 onMeasure 中设置的标志位 PFLAG_LAYOUT_REQUIRED 在 onLayout 执行后被清除同时注意先前通过 requestLayout 设置的标志PFLAG_FORCE_LAYOUT 也是在此处layout清除的。
Draw draw相对于measure、layout无论是从流程控制上还是具体实现上都要复杂很多这里只是简单介绍一下执行流程。因为现在硬件加速渲染已经成为绝对主流接下来的内容以硬件渲染为例子 简单介绍一下软件绘制和硬件绘制的区别
软件绘制View通过操作Canvas调用CPU在Buffer上绘制自身内容硬件绘制View通过操作RecordingsCanvas将绘制自身内容指令DrawXXX缓存起来由GPU负责渲染 通过控制调用 View.draw(Canvas) 传入的 Canvas 类型实现了对开发者透明的软件绘制和硬件绘制的切换
class RecordingCanvas final : public SkCanvasVirtualEnforcerSkNoDrawCanvas {void onDrawRRect(const SkRRect, const SkPaint) override;DisplayListData* fDL;
}void RecordingCanvas::onDrawRRect(const SkRRect rrect, const SkPaint paint) {fDL-drawRRect(rrect, paint);
}void DisplayListData::drawRRect(const SkRRect rrect, const SkPaint paint) {this-pushDrawRRect(0, rrect, paint);
}struct DrawRRect final : Op {static const auto kType Type::DrawRRect;DrawRRect(const SkRRect rrect, const SkPaint paint) : rrect(rrect), paint(paint) {}SkRRect rrect;SkPaint paint;void draw(SkCanvas* c, const SkMatrix) const { c-drawRRect(rrect, paint); }
}; 接下来看一下 ViewRootImpl 是如何发起 Draw 流程的。
public class ViewRootImpl {private void performTraversals() {boolean cancelDraw mAttachInfo.mTreeObserver.dispatchOnPreDraw();if (!cancelDraw) {performDraw();}}private void performDraw() {mAttachInfo.mThreadedRenderer.draw(mView, mAttachInfo, this);}
}public final class ThreadedRenderer {void draw(View view, AttachInfo attachInfo, DrawCallbacks callbacks) {updateRootDisplayList(view, callbacks);}private void updateRootDisplayList(View view, DrawCallbacks callbacks) {// RecordingCanvas 是实现 硬件渲染 的关键RecordingCanvas canvas mRootNode.beginRecording(mSurfaceWidth, mSurfaceHeight);try {final int saveCount canvas.save();canvas.drawRenderNode(view.updateDisplayListIfDirty());canvas.restoreToCount(saveCount);} finally {mRootNode.endRecording();}}
} Draw 和 measure、layout 不同
ViewRootImpl Traversal 一定执行会 performDraw并没有使用 mLayoutRequested 标志。dispatchOnPreDraw 是 ViewRootImpl 暴露的一个节点可以用于业务方跳过绘制Android 的 Transform 动画就依赖此接口实现。没有直接调用 DecorView 的 View.draw 方法开始而是通过 ThreadedRenderer 开始对 View Tree 上所有 View 执行 updateDisplayListIfDirtyDisplayList 的遍历方式比 xxx 和 onXxx 的结构要复杂一些不过原理是相同的利用标记实现 DisplayList 的按需更新。 阅读 View 关于 draw 的实现在 updateDisplayListIfDirty 函数中PFLAG_DRAWING_CACHE_VALID 用于判断 View 是否需要参与本次 draw 流程而 View 内部的 mRecreateDisplayList 则决定了 View 自己的内容是否需要更新mRecreateDisplayList 标志位是 draw(canvas, this, drawingTime) 方法依据 PFLAG_INVALIDATED 确定的。
public class View {boolean mRecreateDisplayList false;public RenderNode updateDisplayListIfDirty() {final RenderNode renderNode mRenderNode;if ((mPrivateFlags PFLAG_DRAWING_CACHE_VALID) 0 !mRecreateDisplayList) {mPrivateFlags | PFLAG_DRAWING_CACHE_VALID;if (mRecreateDisplayList) {draw(canvas);} else {dispatchGetDisplayList();}}return renderNode;}public void draw(Canvas canvas) {...onDraw(canvas);dispatchDraw(canvas);...}boolean draw(Canvas canvas, ViewGroup parent, long drawingTime) {mRecreateDisplayList (mPrivateFlags PFLAG_INVALIDATED) ! 0;mPrivateFlags ~PFLAG_INVALIDATED;renderNode updateDisplayListIfDirty();((RecordingCanvas) canvas).drawRenderNode(renderNode); mRecreateDisplayList false;}
}public class ViewGroup {protected void dispatchDraw(Canvas canvas) {children.forEach(child - {child.draw(canvas, this, drawingTime);});}protected void dispatchGetDisplayList() {children.forEach(child - {child.mRecreateDisplayList (child.mPrivateFlags PFLAG_INVALIDATED) ! 0;child.mPrivateFlags ~PFLAG_INVALIDATED;child.updateDisplayListIfDirty();child.mRecreateDisplayList false;});}
} PFLAG_DRAWING_CACHE_VALID 和 PFLAG_INVALIDATED 这两个标志的生命周期和我们熟悉的 View.invalidate 相关和 requestLayout 类似invalidate 也借助于 ViewParent 接口层层向上通知直到 ViewRootImplViewRootImpl 会在收到 invalidate 请求后开始安排下一次 Traversal。
public class View {void invalidateInternal(int l, int t, int r, int b, boolean invalidateCache, boolean fullInvalidate) {mPrivateFlags | PFLAG_INVALIDATED;mPrivateFlags ~PFLAG_DRAWING_CACHE_VALID;mParent.invalidateChild(this);}
}public class ViewGroup {public final void invalidateChild(View child, final Rect dirty) {onDescendantInvalidated(child, child);}public void onDescendantInvalidated(NonNull View child, NonNull View target) {mPrivateFlags ~PFLAG_DRAWING_CACHE_VALID;if (mParent ! null) {mParent.onDescendantInvalidated(this, target);}}
}public class ViewRootImpl {public void onDescendantInvalidated(NonNull View child, NonNull View descendant) {// TODO: Re-enable after camera is fixed or consider targetSdk checking this// checkThread();invalidate();}void invalidate() {// mWillDrawSoon 在 ViewRootImpl 的 measure / layout 阶段会设置为 true// 此时 draw 流程还没开始invalidate 请求可以在即将到来的 draw 流程中被消费if (!mWillDrawSoon) {scheduleTraversals();}}
} View 和 ViewRootImpl 的 invalidate 并没有关联 requestLayout即Traversal 中的 draw 不需要以 measure / layout 作为其预备条件可以单独执行不像 measure 和 layout 总是彼此相互绑定所以 invalidate 可以满足诉求时不要使用 reqeustLayout 虽然 Draw 不必需 measure、layout但是 measure、layout 后总是需要执行 draw这是因为完整的 requestLayout 代码如下
public class View implements Drawable.Callback, KeyEvent.Callback, AccessibilityEventSource {CallSuperpublic 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 hierarchyViewRootImpl viewRoot getViewRootImpl();if (viewRoot ! null viewRoot.isInLayout()) {if (!viewRoot.requestLayoutDuringLayout(this)) {return;}}mAttachInfo.mViewRequestingLayout this;}mPrivateFlags | PFLAG_FORCE_LAYOUT;mPrivateFlags | PFLAG_INVALIDATED; // 设置了该标志位意味着一定需要drawif (mParent ! null !mParent.isLayoutRequested()) {mParent.requestLayout();}if (mAttachInfo ! null mAttachInfo.mViewRequestingLayout this) {mAttachInfo.mViewRequestingLayout null;}}
}回答最一开始的两个问题
刷新率为 60fps 的手机每秒钟会执行 60 次 traversal 吗 不会只有注册了callback并且V_SYNC执行到了才会调用traversalViewRootImpl 在一段时间内执行 60 次 TraversalMeasure / Layout / Draw 都会执行60次吗 并不会注册回调以后只能触发traversal但响应的Measure、layout、draw还有对应的条件
后续
1. 后续将进一步讨论使用requestLayout可能会遇到的问题。
2. MeasureCache
3. Layout during Layout