前言
在 Android 开发中,View 的绘制是一个至关重要的环节,无论是简单的 TextView、Button 等,还是复杂的自定义控件,都需要经历一系列的绘制过程才能最终呈现在屏幕上。本文将从 View 的绘制整体流程到具体的绘制细节,由浅入深理解 View 的绘制流程。
1、View 显示到 Window 上的整体流程
View 添加到 Window 的起点是 ActivityThread 的 handleResumeActivity()
方法:
Java
public void handleResumeActivity(ActivityClientRecord r, boolean finalStateRequest,
boolean isForward, boolean shouldSendCompatFakeFocus, String reason) {
......
......
if (!performResumeActivity(r, finalStateRequest, reason)) {
return;
}
......
......
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;
l.softInputMode |= forwardBit;
if (r.mPreserveWindow) {
a.mWindowAdded = true;
r.mPreserveWindow = false;
ViewRootImpl impl = decor.getViewRootImpl();
if (impl != null) {
impl.notifyChildRebuilt();
}
}
if (a.mVisibleFromClient) {
if (!a.mWindowAdded) {
a.mWindowAdded = true;
wm.addView(decor, l);
} else {
a.onWindowAttributesChanged(l);
}
}
} else if (!willBeVisible) {
if (localLOGV) Slog.v(TAG, "Launch " + r + " mStartedActivity set");
r.hideForNow = true;
}
......
......
}
这里先调用了 performResumeActivity()
方法,获取记录了 activity
信息的对象并保存到传入的引用变量 ActivityClientRecord
中,也就是下面代码用到的 r
变量,如果获取失败就直接返回;随后调用 wm.addView(decor, l)
方法添加 decorView
和 WindowManager.LayoutParams
,这里的 wm
就是根据 r
中记录的 activity
拿到对应的 WindowManagerImpl
对象,让我们看看 WindowManagerImpl
中的 addView()
方法:
Java
public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
applyTokens(params);
mGlobal.addView(view, params, mContext.getDisplayNoVerify(), mParentWindow,
mContext.getUserId());
}
继续看 WindowManagerGlobal
中的 addView()
方法:
Java
public void addView(View view, ViewGroup.LayoutParams params,
Display display, Window parentWindow, int userId) {
......
......
mViews.add(view);
mRoots.add(root);
mParams.add(wparams);
try {
root.setView(view, wparams, panelParentView, userId);
} catch (RuntimeException e) {
final int viewIndex = (index >= 0) ? index : (mViews.size() - 1);
if (viewIndex >= 0) {
removeViewLocked(viewIndex, true);
}
throw e;
}
......
......
}
这里先把 view
,root
,wparams
保存到 WindowManagerGlobal
内部的数组中,也就是:
Java
private final ArrayList<View> mViews = new ArrayList<View>();
private final ArrayList<ViewRootImpl> mRoots = new ArrayList<ViewRootImpl>();
private final ArrayList<WindowManager.LayoutParams> mParams = new ArrayList<WindowManager.LayoutParams>();
WindowManagerGlobal
是用来全局管理 View 的类,因此这里要保存起来,随后调用 root.setView(view, wparams, panelParentView, userId)
方法将 View 添加到 root
中,root
即 ViewRootImpl
,继续点进 setView()
中,其中调用了 request()
方法来请求布局:
Java
public void requestLayout() {
if (!mHandlingLayoutInLayoutRequest) {
checkThread();
mLayoutRequested = true;
scheduleTraversals();
}
}
然后是 scheduleTraversals()
方法:
Java
void scheduleTraversals() {
if (!mTraversalScheduled) {
mTraversalScheduled = true;
mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
mChoreographer.postCallback(
Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
notifyRendererOfFramePending();
pokeDrawLockIfNeeded();
}
}
其中的 mTraversalRunnable
是:
Java
final TraversalRunnable mTraversalRunnable = new TraversalRunnable();
final class TraversalRunnable implements Runnable {
@Override
public void run() {
doTraversal();
}
}
也就是这里又调用到了 doTraversal()
方法:
Java
void doTraversal() {
if (mTraversalScheduled) {
mTraversalScheduled = false;
mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);
if (mProfile) {
Debug.startMethodTracing("ViewAncestor");
}
performTraversals();
if (mProfile) {
Debug.stopMethodTracing();
mProfile = false;
}
}
}
最后通过 performTraversals()
方法执行 View 的具体绘制方法 。
流程总结: ActivityThread.handleResumeActivity()
-> WindowManagerImpl.addView()
-> WindowManagerGlobal.addView()
-> ViewRootImpl.setView()
-> ViewRootImpl.requestLayout()
-> ViewRootImpl.scheduleTraversals()
-> ViewRootImpl.doTraversal()
-> ViewRootImpl.performTraversals()
2、ViewRootImpl 的 performTraversals()
方法中做了什么
在上面的流程介绍中,我们知道,最终调用 View 的方法进行真正的绘制是在 ViewRootImpl.performTraversals()
方法中,这个方法有多重要,从它的代码行数就能看出来,在 Android 34 的源码中,这个方法在 ViewRootImpl.java 的 2907 - 3845 行,整整 939 行。
由于代码量太大,这里就不把源码粘贴过来,有需要或者感兴趣的可以自己去看,接下来,我们将介绍 performTraversals()
方法里 5 个重要的步骤:
- 预测量:
windowSizeMayChange |= measureHierarchy(host, lp, mView.getContext().getResources(), desiredWindowWidth, desiredWindowHeight, shouldOptimizeMeasure);
- 布局窗口:
relayoutResult = relayoutWindow(params, viewVisibility, insetsPending);
- 控件树测量:
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
- 布局:
performLayout(lp, mWidth, mHeight);
- 绘制:
if (!performDraw() && mActiveSurfaceSyncGroup != null) { mActiveSurfaceSyncGroup.markSyncReady(); }
下面,让我们看看这些方法里面做了些什么。
1) 预测量 measureHierarchy()
Java
private boolean measureHierarchy(final View host, final WindowManager.LayoutParams lp,
final Resources res, final int desiredWindowWidth, final int desiredWindowHeight,
boolean forRootSizeOnly) {
int childWidthMeasureSpec;
int childHeightMeasureSpec;
boolean windowSizeMayChange = false;
if (DEBUG_ORIENTATION || DEBUG_LAYOUT) Log.v(mTag,
"Measuring " + host + " in display " + desiredWindowWidth
+ "x" + desiredWindowHeight + "...");
boolean goodMeasure = false;
if (lp.width == ViewGroup.LayoutParams.WRAP_CONTENT) { // 1.
final DisplayMetrics packageMetrics = res.getDisplayMetrics();
res.getValue(com.android.internal.R.dimen.config_prefDialogWidth, mTmpValue, true); // 2.
int baseSize = 0;
if (mTmpValue.type == TypedValue.TYPE_DIMENSION) {
baseSize = (int)mTmpValue.getDimension(packageMetrics); // 3.
}
if (DEBUG_DIALOG) Log.v(mTag, "Window " + mView + ": baseSize=" + baseSize
+ ", desiredWindowWidth=" + desiredWindowWidth);
if (baseSize != 0 && desiredWindowWidth > baseSize) {
childWidthMeasureSpec = getRootMeasureSpec(baseSize, lp.width, lp.privateFlags);
childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height,
lp.privateFlags);
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
if (DEBUG_DIALOG) Log.v(mTag, "Window " + mView + ": measured ("
+ host.getMeasuredWidth() + "," + host.getMeasuredHeight()
+ ") from width spec: " + MeasureSpec.toString(childWidthMeasureSpec)
+ " and height spec: " + MeasureSpec.toString(childHeightMeasureSpec));
if ((host.getMeasuredWidthAndState()&View.MEASURED_STATE_TOO_SMALL) == 0) { // 4.
goodMeasure = true;
} else {
baseSize = (baseSize+desiredWindowWidth)/2; // 5.
if (DEBUG_DIALOG) Log.v(mTag, "Window " + mView + ": next baseSize="
+ baseSize);
childWidthMeasureSpec = getRootMeasureSpec(baseSize, lp.width, lp.privateFlags);
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
if (DEBUG_DIALOG) Log.v(mTag, "Window " + mView + ": measured ("
+ host.getMeasuredWidth() + "," + host.getMeasuredHeight() + ")");
if ((host.getMeasuredWidthAndState()&View.MEASURED_STATE_TOO_SMALL) == 0) { // 6.
if (DEBUG_DIALOG) Log.v(mTag, "Good!");
goodMeasure = true;
}
}
}
}
if (!goodMeasure) { // 7.
childWidthMeasureSpec = getRootMeasureSpec(desiredWindowWidth, lp.width,
lp.privateFlags);
childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height,
lp.privateFlags);
if (!forRootSizeOnly || !setMeasuredRootSizeFromSpec(
childWidthMeasureSpec, childHeightMeasureSpec)) {
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
} else {
mViewMeasureDeferred = true;
}
if (mWidth != host.getMeasuredWidth() || mHeight != host.getMeasuredHeight()) {
windowSizeMayChange = true;
}
}
if (DBG) {
System.out.println("======================================");
System.out.println("performTraversals -- after measure");
host.debug();
}
return windowSizeMayChange;
}
上述代码按序号标记了主要的执行步骤,下面我们来一一分析:
- 判断父容器宽度是否设置为 WRAP_CONTENT ,如果是,则进入
if
继续运行,否则直接跳到第 7 步; - 获取一个系统默认的最小宽度,并保存到
mTmpValue
中,这个默认值为 320 dp ,根据变量名R.dimen.config_prefDialogWidth
就知道这是系统给 dialog 设置的默认最优宽度; - 将第 2 步获取到的值赋给
baseSize
,下面调用performMeasure()
方法测量子 View 是否适合这个宽度,如果当前子 View 不适合 320 dp,则运行到第 4 步; - 如果上面赋予的 320 dp 不合适,则进入到
else
执行第 5 步; - 将
baseSize
根据当前屏幕宽度增大一定值,下面再次调用performMeasure()
测量子 View,如果这步合适,则进行第 6 步,否则运行到第 7 步; - 设置
goodMeasure
标志位为true
,则不会运行第 7 步; - 运行到这里就直接将父容器能给的最大值赋给子 View,并判断父容器给子 View 的大小是否合适,不合适则
windowSizeMayChange = true
,之后还将测量,若合适,则之后不再测量。
综上所述:如果如容器设置的宽度是 WRAP_CONTENT ,则会至多进行三次 performMeasure()
,如果其中有满足的则不会进入后面的 if
判断;设置的宽度不是 WRAP_CONTENT,则直接进入第 7 步,赋予父容器能给的最大值。
2) 布局窗口 relayoutWindow()
根据预测阶段量第 7 步中 windowSizeMayChange
的值,如果满足条件则会运行到 relayoutWindow()
方法,这个方法的主要工作包括:
- 计算窗口的尺寸和位置:根据窗口的布局参数和屏幕的尺寸,计算出窗口应该显示的位置和大小。
- 更新视图的布局参数:遍历窗口中的所有视图,根据它们的布局参数,更新它们的位置和大小。
- 执行视图的绘制:根据更新后的布局参数,重新绘制窗口中的所有视图。
- 更新窗口的显示状态:根据需要,更新窗口的显示状态,比如显示、隐藏或者改变透明度等。
因此我们知道:relayoutWindow()
方法是重新布局窗口的重要方法,它确保窗口中的所有视图都能按照最新的布局规则正确显示。
3) 控件树测量 performMeasure()
同样也是根据上文 windowSizeMayChange
的值,为 true
则会运行到 performMeasure()
方法再次进行测量。
这个方法就是进行了 View 的测量,调用链:ViewRootImpl.performMeasure()
-> View.measure()
-> View.onMeasure()
其中需要注意的点是,如果 View 重写了 onMeasure()
方法,则必须调用 setMeasureDimension()
方法( super.onMeasure 也包括 ),因为在 View 的 measure()
方法的末尾有判断语句,若没调用 setMeasureDimension()
方法,则会报错,如下:
Java
if ((mPrivateFlags & PFLAG_MEASURED_DIMENSION_SET) != PFLAG_MEASURED_DIMENSION_SET) {
throw new IllegalStateException("View with id " + getId() + ": "
+ getClass().getName() + "#onMeasure() did not set the"
+ " measured dimension by calling"
+ " setMeasuredDimension()");
}
4) 布局控件树 performLayout()
这个方法比较简单,调用链:ViewRootImpl.performLayout()
-> View.layout()
-> View.onLayout()
而源码中的 View.onLayout()
方法为空,因为只有容器才需要布局子 View,我们知道 ViewGroup 是继承 View 的,而 ViewGroup.onLayout()
是抽象方法,因此继承 ViewGroup 自定义容器时,要重写 onLayout()
方法。
5) 绘制 performDraw()
这个方法也没什么好说的,调用链:ViewRootImpl.performDraw()
-> ViewRootImpl.draw()
-> 硬件加速 或 软件绘制
其中在 ViewRootImpl.draw()
中会判断是 硬件加速 还是 软件绘制,源码如下:
Java
if (isHardwareEnabled()) {
......
......
mAttachInfo.mThreadedRenderer.draw(mView, mAttachInfo, this);
} else {
......
......
if (!drawSoftware(surface, mAttachInfo, xOffset, yOffset, scalingRequired, dirty, surfaceInsets)) {
return false;
}
}
这里的 硬件加速 和 软件绘制,感兴趣的读者可以自行搜索了解。
3、requestLayout()
和 invalidate()
的区别
我们知道调用 requestLayout()
或 invalidate()
方法都会重新绘制 View,那么这两者有什么区别呢?答案是 requestLayout()
方法会重新测量、布局并绘制,而 invalidate()
方法则是直接到绘制阶段。具体流程如下图所示:
👉 以上就是对 View 绘制流程的简单梳理。