深入理解 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;