深入理解 Android 之 View 的绘制流程,androidview
概述
本篇文章会从源码(基于Android 6.0)角度分析Android中View的绘制流程,侧重于对整体流程的分析,对一些难以理解的点加以重点阐述,目的是把View绘制的整个流程把握好,而对于特定实现细节则可以日后再对相应源码进行研读。
在进行实际的分析之前,我们先来看下面这张图:

我们来对上图做出简单解释:DecorView是一个应用窗口的根容器,它本质上是一个FrameLayout。DecorView有唯一一个子View,它是一个垂直LinearLayout,包含两个子元素,一个是TitleView(ActionBar的容器),另一个是ContentView(窗口内容的容器)。关于ContentView,它是一个FrameLayout(android.R.id.content),我们平常用的setContentView就是设置它的子View。上图还表达了每个Activity都与一个Window(具体来说是PhoneWindow)相关联,用户界面则由Window所承载。
Window
Window即窗口,这个概念在Android Framework中的实现为android.view.Window这个抽象类,这个抽象类是对Android系统中的窗口的抽象。在介绍这个类之前,我们先来看看究竟什么是窗口呢?
实际上,窗口是一个宏观的思想,它是屏幕上用于绘制各种UI元素及响应用户输入事件的一个矩形区域。通常具备以下两个特点:
- 独立绘制,不与其它界面相互影响;
- 不会触发其它界面的输入事件;
在Android系统中,窗口是独占一个Surface实例的显示区域,每个窗口的Surface由WindowManagerService分配。我们可以把Surface看作一块画布,应用可以通过Canvas或OpenGL在其上面作画。画好之后,通过SurfaceFlinger将多块Surface按照特定的顺序(即Z-order)进行混合,而后输出到FrameBuffer中,这样用户界面就得以显示。
android.view.Window这个抽象类可以看做Android中对窗口这一宏观概念所做的约定,而PhoneWindow这个类是Framework为我们提供的Android窗口概念的具体实现。接下来我们先来介绍一下android.view.Window这个抽象类。
这个抽象类包含了三个核心组件:
- WindowManager.LayoutParams: 窗口的布局参数;
- Callback: 窗口的回调接口,通常由Activity实现;
- ViewTree: 窗口所承载的控件树。
下面我们来看一下Android中Window的具体实现(也是唯一实现)——PhoneWindow。
PhoneWindow
前面我们提到了,PhoneWindow这个类是Framework为我们提供的Android窗口的具体实现。我们平时调用setContentView()方法设置Activity的用户界面时,实际上就完成了对所关联的PhoneWindow的ViewTree的设置。我们还可以通过Activity类的requestWindowFeature()方法来定制Activity关联PhoneWindow的外观,这个方法实际上做的是把我们所请求的窗口外观特性存储到了PhoneWindow的mFeatures成员中,在窗口绘制阶段生成外观模板时,会根据mFeatures的值绘制特定外观。
从setContentView()说开去
在分析setContentView()方法前,我们需要明确:这个方法只是完成了Activity的ContentView的创建,而并没有执行View的绘制流程。
当我们自定义Activity继承自android.app.Activity时候,调用的setContentView()方法是Activity类的,源码如下:
public void setContentView(@LayoutRes int layoutResID) {
getWindow().setContentView(layoutResID);
. . .
}
getWindow()方法会返回Activity所关联的PhoneWindow,也就是说,实际上调用到了PhoneWindow的setContentView()方法,源码如下:
@Override
public void setContentView(int layoutResID) {
if (mContentParent == null) {
// mContentParent即为上面提到的ContentView的父容器,若为空则调用installDecor()生成
installDecor();
} else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
// 具有FEATURE_CONTENT_TRANSITIONS特性表示开启了Transition
// mContentParent不为null,则移除decorView的所有子View
mContentParent.removeAllViews();
}
if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
// 开启了Transition,做相应的处理,我们不讨论这种情况
// 感兴趣的同学可以参考源码
. . .
} else {
// 一般情况会来到这里,调用mLayoutInflater.inflate()方法来填充布局
// 填充布局也就是把我们设置的ContentView加入到mContentParent中
mLayoutInflater.inflate(layoutResID, mContentParent);
}
. . .
// cb即为该Window所关联的Activity
final Callback cb = getCallback();
if (cb != null && !isDestroyed()) {
// 调用onContentChanged()回调方法通知Activity窗口内容发生了改变
cb.onContentChanged();
}
. . .
}
LayoutInflater.inflate()
在上面我们看到了,PhoneWindow的setContentView()方法中调用了LayoutInflater的inflate()方法来填充布局,这个方法的源码如下:
public View inflate(@LayoutRes int resource, @Nullable ViewGroup root) {
return inflate(resource, root, root != null);
}
public View inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot) {
final Resources res = getContext().getResources();
. . .
final XmlResourceParser parser = res.getLayout(resource);
try {
return inflate(parser, root, attachToRoot);
} finally {
parser.close();
}
}
在PhoneWindow的setContentView()方法中传入了decorView作为LayoutInflater.inflate()的root参数,我们可以看到,通过层层调用,最终调用的是inflate(XmlPullParser, ViewGroup, boolean)方法来填充布局。这个方法的源码如下:
public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {
synchronized (mConstructorArgs) {
. . .
final Context inflaterContext = mContext;
final AttributeSet attrs = Xml.asAttributeSet(parser);
Context lastContext = (Context) mConstructorArgs[0];
mConstructorArgs[0] = inflaterContext;
View result = root;
try {
// Look for the root node.
int type;
// 一直读取xml文件,直到遇到开始标记
while ((type = parser.next()) != XmlPullParser.START_TAG &&
type != XmlPullParser.END_DOCUMENT) {
// Empty
}
// 最先遇到的不是开始标记,报错
if (type != XmlPullParser.START_TAG) {
throw new InflateException(parser.getPositionDescription()
+ ": No start tag found!");
}
final String name = parser.getName();
. . .
// 单独处理<merge>标签,不熟悉的同学请参考官方文档的说明
if (TAG_MERGE.equals(name)) {
// 若包含<merge>标签,父容器(即root参数)不可为空且attachRoot须为true,否则报错
if (root == null || !attachToRoot) {
throw new InflateException("<merge /> can be used only with a valid "
+ "ViewGroup root and attachToRoot=true");
}
// 递归地填充布局
rInflate(parser, root, inflaterContext, attrs, false);
} else {
// temp为xml布局文件的根View
final View temp = createViewFromTag(root, name, inflaterContext, attrs);
ViewGroup.LayoutParams params = null;
if (root != null) {
. . .
// 获取父容器的布局参数(LayoutParams)
params = root.generateLayoutParams(attrs);
if (!attachToRoot) {
// 若attachToRoot参数为false,则我们只会将父容器的布局参数设置给根View
temp.setLayoutParams(params);
}
}
// 递归加载根View的所有子View
rInflateChildren(parser, temp, attrs, true);
. . .
if (root != null && attachToRoot) {
// 若父容器不为空且attachToRoot为true,则将父容器作为根View的父View包裹上来
root.addView(temp, params);
}
// 若root为空或是attachToRoot为false,则以根View作为返回值
if (root == null || !attachToRoot) {
result = temp;
}
}
} catch (XmlPullParserException e) {
. . .
} catch (Exception e) {
. . .
} finally {
. . .
}
return result;

