前言
说到View的原理,我相信很多开发者都能脱口而出什么onMeasure、onLayout、onDraw等回调方法,但是这个View的绘制流程是哪里开始的、是谁负责绘制View的这些问题可能没有仔细思考过,在说具体的原理之前,必须要搞清楚一个基础知识,即ViewRoot和DecorView,这样我们才可以知其然知其所以然。
正文
为什么要理解除了View绘制流程之外的一些知识呢?因为这些知识可以让我们更加清晰地认识Android体系架构,让我们更容易把知识点给串联起来。
以Activity能展示出界面来看,这里面涉及了Activity的启动流程,其中的重点是Window机制。关于Window机制,在Android中非常重要,可以仔细阅读下面文章:
这里我们回顾一些文章中重要结论:
- Android中所有视图包括Activity、Dialog和Toast都是通过Window为载体来显示,而Window是一个抽象概念,这里我们可以把一个View树看成是一个Window。
- Window的真实创建、操作的类是WindowManagerService,而和WMS进行IPC的类就是ViewRootImpl类,所以ViewRootImpl是Window和View之间的桥梁。
上面结论只是很少一部分本篇文章用得到的结论,我们下面就来看看ViewRootImpl的工作流程。
整体架构
关于Activity、PhoneWindow、DecorView这3者关系如下图:
这个示意图其实有点问题,看过上面链接中的文章会知道,PhoneWindow其实是一个Window工具类,它并不是一个真正的Window。
但是这里不影响我们理解Activity的整体架构。
ViewRootImpl
下图是前面文章中的图:
通过阅读Activity的Window的创建过程,我们会知道一个View树即DecorView会对应一个Window,同时也对应一个ViewRootImpl,而ViewRootImpl一边负责绘制View,一边通过Session和WMS进行IPC通信。这里除了绘制的部分,其他内容在上面Android Window详解中都有介绍,本篇文章就来看看ViewRootImpl的绘制。
ViewRootImpl绘制过程
Activity的启动流程中,会调用ActivityThread类中的handleResumeActivity方法,即Activity会回调onResume方法,在该方法中:
public void handleResumeActivity(ActivityClientRecord r, boolean finalStateRequest,
boolean isForward, String reason) {
...
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 (a.mVisibleFromClient) {
if (!a.mWindowAdded) {
a.mWindowAdded = true;
wm.addView(decor, l);
} else {
...
}
}
...
}
...
}
我们会发现它会取出Activity的DecorView,然后通过WindowManager.addView方法把DecorView这个View树配合LayoutParams给添加到Window中,方法如下:
public void addView(View view, ViewGroup.LayoutParams params,
Display display, Window parentWindow, int userId) {
...
ViewRootImpl root;
View panelParentView = null;
...
root = new ViewRootImpl(view.getContext(), display);
...
try {
root.setView(view, wparams, panelParentView, userId);
} catch (RuntimeException e) {
...
}
}
}
这时WindowManager逻辑实现类WindowManagerGlobal中的代码,我们发现在这个时候会创建出我们期望的ViewRootImpl类,同时调用其setView方法:
public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView,
int userId) {
synchronized (this) {
...
//绘制View树
requestLayout()
...
try {
//IPC通信
res = mWindowSession.addToDisplayAsUser(mWindow, mWindowAttributes,
getHostVisibility(), mDisplay.getDisplayId(), userId,
mInsetsController.getRequestedVisibilities(), inputChannel, mTempInsets,
mTempControls);
...
}
}
上述代码比较多,但是核心就是requestLayout方法与和WMS进行IPC通信的部分,这里也更验证了我们结论:ViewRootImpl是View和Window的桥梁,一边和WMS通信,一边绘制我们的View树。
requestLayout
该方法我相信很多开发者再熟悉不过了,我们可以通过任意一个View来调用该方法,而该方法是定义在ViewParent接口中的,ViewRootImpl真是实现该接口的地方。
我们来看一下该方法定义:
/**
* Called when something has changed which has invalidated the layout of a
* child of this view parent. This will schedule a layout pass of the view
* tree.
*/
public void requestLayout();
当一个View树的子View的布局失效时调用,该方法会对整个View树的布局做重新处理。
所以该方法的调用时机我们也要把握清楚,当自定义View时,其布局改变时,我们才有必要调用该方法进行重新绘制。
话不多说,我们看看ViewRootImpl中该方法的实现:
@Override
public void requestLayout() {
if (!mHandlingLayoutInLayoutRequest) {
checkThread();
mLayoutRequested = true;
scheduleTraversals();
}
}
这里会调用scheduleTraversals()方法,在该方法中会调用performTravsals,该方法特别长,不过思路很清晰,它会调用3个方法:
如上图所示,performTravsals会依次调用performMeasure、perfromLayout和performDraw三个方法,这3个方法分别完成顶级View即DecorView,也就是一个FrameLayout的测量、布局和绘制三大流程。
- performMeasure中会调用View(DecorView)的measure方法,在measure方法中又会调用onMeasure方法,在onMeasure(这时已经是ViewGroup,即FrameLayout重写)方法中会对所有子元素进行measure过程,这时measure流程就从父容器传递到了子元素。
这里要明白ViewGroup也是View,所以必须当DecorView是一个ViewGoup时,必须要重写onMeasure,然后在onMeasure进行子View的measure,这样只有当子View全部measure完时,才能确定ViewGroup的测量宽高。
-
performLayout类似,它也是先调用DecorView的layout方法,在View的layout方法中会调用onLayout方法,这时需要DecorView(它是ViewGroup)重写onLayout方法,同样在ViewGoup的onLayout中进行子View的布局,这样只有子View全部layout完,才能确定ViewGoup的位置。
-
performDraw有一点不一样,它也是先调用View的draw方法,但是在draw方法中,会分为如下几个步骤:
- 绘制背景,调用drawBackground方法;
- 绘制View自己内容,调用onDraw方法;
- 绘制子View,调用dispatchDraw方法;
- 绘制前景图或者滚动条,调用onDrawForeground方法;
当ViewRootImpl调用DecorView的draw方法时,它会通过onDraw先绘制自己,然后DecorView因为是ViewGoup,它会重写dispatchDraw方法,在该方法中进行子View的绘制即可。
这里是否有点奇怪,为什么绘制的流程API设计和测量和布局不一样呢?
原因是因为测量和布局必须要等待子View全部测量和布局完,才能在onMeasure和onLayout中设置值;但是绘制就不一样了,在进行绘制的时候,每个View的大小和位置都确定了,我就可以按照绘制背景、绘制内容、绘制子View和绘制前景这个一般的顺序来完成,至于为什么要这么多步骤,因为绘制是有遮盖效果的,所以这样绘制让内容更丰富。
通过查看了不少ViewGoup实现类,比如LinearLayout和RecyclerView等,它的dispatchDraw都是使用ViewGoup的默认实现,这说明其实该步骤我们不必太多干涉,我们在自定义ViewGoup时其实只需要安排好子View的位置和大小即可。
总结
在正式开启View工作原理前,这篇文章还是非常重要的,可以让我们在心里有个大致的思路,至少知道了View的绘制流程从哪里开始,调用了哪些方法以及如何传递。至于每个流程的细节,我们后面文章再仔细分析。
笔者能力有限,欢迎大家评论、讨论。