前言
View作为整个app的颜值担当,在Android体系中占有重要的地位。深入理解Android View的绘制流程,对正确使用View来构建赏心悦目的外观,以及用自定义View来设计理想中的酷炫效果等方面,有着极其重要的帮助作用,所以将View的绘制流程作为自定义View系列文章的第一篇。当然,View的绘制流程原理,在现实的工作中是成为高级工程师路上必须克服的障碍;在面试中,也是面试高级一点的职位,面试官几乎一定会问的问题。总体来说,Android View的绘制流程原理,是一个Android程序员的基本内功之一。
本文最大的特点,就是最大限度地向源码要答案。从源码中追流程的来龙去脉,在注释中查功能的点点滴滴,所有的结论都尽量在源码和注释中找根据,关键的流程尽量说细致,非重点的地方尽量简略。所以读者会看到,本文中贴了大量的源码,且基本都是和分析绘制流程相关的代码,同时附上了大量的源码注释,以及对这些注释的翻译及整理。在每一个过程的最后,还根据源码的流程走向,绘制了一幅简易的关键流程图,以此来帮助读者理解及加深印象。事实上,本文就是记录的笔者从几乎一无所知到追踪源码来搞弄明白整个绘制流程的完整经历的,中途遇到的一些困惑及疑难点都会花不少篇幅来解释。当然仅仅学习本文还是不够的,因为这方面的内容很多,仅仅一篇文章是不可能面面俱到的。正如其他的知名博客文一样,都有自己的侧重点,有的侧重根据示例来分析流程,有的会附着demo来验证关键结论,本文中也会适当给出一些比较好的博文的链接,读者可以去这些地方弥补本文的不足。最后,再啰嗦一句,本文的侧重点是从源码中“顺藤摸瓜”,带领读者“登堂入室”,希望能给读者带来一点帮助。另外,这里说明一点,本文中的源码是基于API26的,即Android8.0系统版本。
本文的主要内容大致如下:
一、View绘制的三个流程
我们知道,在自定义View的时候一般需要重写父类的onMeasure()、onLayout()、onDraw()三个方法,来完成视图的展示过程。当然,这三个暴露给开发者重写的方法只不过是整个绘制流程的冰山一角,更多复杂的幕后工作,都让系统给代劳了。一个完整的绘制流程包括measure、layout、draw三个步骤,其中:
measure:测量。系统会先根据xml布局文件和代码中对控件属性的设置,来获取或者计算出每个View和ViewGrop的尺寸,并将这些尺寸保存下来。
layout:布局。根据测量出的结果以及对应的参数,来确定每一个控件应该显示的位置。
draw:绘制。确定好位置后,就将这些控件绘制到屏幕上。
如果你对编程感兴趣或者想往编程方向发展,可以关注微信公众号【筑梦编程】,大家一起交流讨论!小编也会每天定时更新既有趣又有用的编程知识!
二、Android视图层次结构简介
在介绍View绘制流程之前,咱们先简单介绍一下Android视图层次结构以及DecorView,因为View的绘制流程的入口和DecorView有着密切的联系。
咱们平时看到的视图,其实存在如上的嵌套关系。上图是针对比较老的Android系统版本中制作的,新的版本中会略有出入,还有一个状态栏,但整体上没变。我们平时在Activity中setContentView(...)中对应的layout内容,对应的是上图中ViewGrop的树状结构,实际上添加到系统中时,会再裹上一层FrameLayout,就是上图中最里面的浅蓝色部分了。
这里咱们再通过一个实例来继续查看。AndroidStudio工具中提供了一个布局视察器工具,通过Tools > Android > Layout Inspector可以查看具体某个Activity的布局情况。下图中,左边树状结构对应了右边的可视图,可见DecorView是整个界面的根视图,对应右边的红色框,是整个屏幕的大小。黄色边框为状态栏部分;那个绿色边框中有两个部分,一个是白框中的ActionBar,对应了上图中紫色部分的TitleActionBar部分,即标题栏,平时咱们可以在Activity中将其隐藏掉;另外一个蓝色边框部分,对应上图中最里面的蓝色部分,即ContentView部分。下图中左边有两个蓝色框,上面那个中有个“contain_layout”,这个就是Activity中setContentView中设置的layout.xml布局文件中的最外层父布局,咱们能通过layout布局文件直接完全操控的也就是这一块,当其被add到视图系统中时,会被系统裹上ContentFrameLayout(显然是FrameLayout的子类),这也就是为什么添加layout.xml视图的方法叫setContentView(...)而不叫setView(...)的原因。
三、故事开始的地方
如果对Activity的启动流程有一定了解的话,应该知道这个启动过程会在ActivityThread.java类中完成,在启动Activity的过程中,会调用到handleResumeActivity(...)方法,关于视图的绘制过程最初就是从这个方法开始的。
1、View绘制起源UML时序图
整个调用链如下图所示,直到ViewRootImpl类中的performTraversals()中,才正式开始绘制流程了,所以一般都是以该方法作为正式绘制的源头。
图3.1 View绘制起源UML时序图
2、handleResumeActivity()方法
在这咱们先大致看看ActivityThread类中的handleResumeActivity方法,咱们这里只贴出关键代码:
1 //==============ActivityThread.java================= 2 final void handleResumeActivity(...) { 3 ...... 4 //跟踪代码后发现其初始赋值为mWindow = new PhoneWindow(this, window, activityConfigCallback); 5 r.window = r.activity.getWindow(); 6 //从PhoneWindow实例中获取DecorView 7 View decor = r.window.getDecorView(); 8 ...... 9 //跟踪代码后发现,vm值为上述PhoneWindow实例中获取的WindowManager。 10 ViewManager wm = a.getWindowManager(); 11 ...... 12 //当前window的属性,从代码跟踪来看是PhoneWindow窗口的属性 13 WindowManager.LayoutParams l = r.window.getAttributes(); 14 ...... 15 wm.addView(decor, l); 16 ...... 17 }
上述代码第8行中,ViewManager是一个接口,addView是其中定义个一个空方法,WindowManager是其子类,WindowManagerImpl是WindowManager的实现类(顺便啰嗦一句,这种方式叫做面向接口编程,在父类中定义,在子类中实现,在Java中很常见)。第4行代码中的r.window的值可以根据Activity.java的如下代码得知,其值为PhoneWindow实例。
1 //===============Activity.java============= 2 private Window mWindow; 3 public Window getWindow() { 4 return mWindow; 5 } 6 7 final void attach(...){ 8 ...... 9 mWindow = new PhoneWindow(this, window, activityConfigCallback); 10 ...... 11 }
3、两个重要参数分析
之所以要在这里特意分析handleResumeActivity()方法,除了因为它是整个绘制流程的最初源头外,还有就是addView的两个参数比较重要,它们经过一层一层传递后进入到ViewRootImpl中,在后面分析绘制中要用到。这里再看看这两个参数的相关信息:
(1)参数decor
1 //==========PhoneWindow.java=========== 2 // This is the top-level view of the window, containing the window decor. 3 private DecorView mDecor; 4 ...... 5 public PhoneWindow(...){ 6 ...... 7 mDecor = (DecorView) preservedWindow.getDecorView(); 8 ...... 9 } 10 11 @Override 12 public final View getDecorView() { 13 ...... 14 return mDecor; 15 }
可见decor参数表示的是DecorView实例。注释中也有说明:这是window的顶级视图,包含了window的decor。
(2)参数l
//===================Window.java=================== //The current window attributes. private final WindowManager.LayoutParams mWindowAttributes = new WindowManager.LayoutParams(); ...... public final WindowManager.LayoutParams getAttributes() { return mWindowAttributes; } ...... //==========WindowManager.java的内部类LayoutParams extends ViewGroup.LayoutParams============= public LayoutParams() { super(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT); ...... } //==============ViewGroup.java内部类LayoutParams==================== public LayoutParams(int width, int height) { this.width = width; this.height = height; }
该参数表示l的是PhoneWindow的LayoutParams属性,其width和height值均为LayoutParams.MATCH_PARENT。
在源码中,WindowPhone和DecorView通过组合方式联系在一起的,而DecorView是整个View体系的根View。在前面handleResumeActivity(...)方法代码片段中,当Actiivity启动后,就通过第14行的addView方法,来间接调用ViewRootImpl类中的performTraversals(),从而实现视图的绘制。
四、主角登场
无疑,performTraversals()方法是整个过程的主角,它把控着整个绘制的流程。该方法的源码有大约800行,这里咱们仅贴出关键的流程代码,如下所示:
1 // =====================ViewRootImpl.java================= 2 private void performTraversals() { 3 ...... 4 int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width); 5 int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height); 6 ...... 7 // Ask host how big it wants to be 8 performMeasure(childWidthMeasureSpec, childHeightMeasureSpec); 9 ...... 10 performLayout(lp, mWidth, mHeight); 11 ...... 12 performDraw(); 13 }
上述代码中就是一个完成的绘制流程,对应上了第一节中提到的三个步骤:
1)performMeasure():从根节点向下遍历View树,完成所有ViewGroup和View的测量工作,计算出所有ViewGroup和View显示出来需要的高度和宽度;
2)performLayout():从根节点向下遍历View树,完成所有ViewGroup和View的布局计算工作,根据测量出来的宽高及自身属性,计算出所有ViewGroup和View显示在屏幕上的区域;
3)performDraw():从根节点向下遍历View树,完成所有ViewGroup和View的绘制工作,根据布局过程计算出的显示区域,将所有View的当前需显示的内容画到屏幕上。
咱们后续就是通过对这三个方法来展开研究整个绘制过程。
五、measure过程分析
这三个绘制流程中,measure是最复杂的,这里会花较长的篇幅来分析它。本节会先介绍整个流程中很重要的两个类MeasureSpec和ViewGroup.LayoutParams类,然后介绍ViewRootImpl、View及ViewGroup中测量流程涉及到的重要方法,最后简单梳理DecorView测量的整个流程并链接一个测量实例分析整个测量过程。
1、MeasureSpec简介
这里咱们直接上源码吧,先直接通过源码和注释认识一下它,如果看不懂也没关系,在后面使用的时候再回头来看看。
1 /** 2 * A MeasureSpec encapsulates the layout requirements passed from parent to child. 3 * Each MeasureSpec represents a requirement for either the width or the height. 4 * A MeasureSpec is comprised of a size and a mode. There are three possible 5 * modes: 6 * <dl> 7 * <dt>UNSPECIFIED</dt> 8 * <dd> 9 * The parent has not imposed any constraint on the child. It can be whatever size 10 * it wants. 11 * </dd> 12 * 13 * <dt>EXACTLY</dt> 14 * <dd> 15 * The parent has determined an exact size for the child. The child is going to be 16 * given those bounds regardless of how big it wants to be. 17 * </dd> 18 * 19 * <dt>AT_MOST</dt> 20 * <dd> 21 * The child can be as large as it wants up to the specified size. 22 * </dd> 23 * </dl> 24 * 25 * MeasureSpecs are implemented as ints to reduce object allocation. This class 26 * is provided to pack and unpack the <size, mode> tuple into the int. 27 */ 28 public static class MeasureSpec { 29 private static final int MODE_SHIFT = 30; 30 private static final int MODE_MASK = 0x3 << MODE_SHIFT; 31 ...... 32 /** 33 * Measure specification mode: The parent has not imposed any constraint 34 * on the child. It can be whatever size it wants. 35 */ 36 public static final int UNSPECIFIED = 0 << MODE_SHIFT; 37 38 /** 39 * Measure specification mode: The parent has determined an exact size 40 * for the child. The child is going to be given those bounds regardless 41 * of how big it wants to be. 42 */ 43 public static final int EXACTLY = 1 << MODE_SHIFT; 44 45 /** 46 * Measure specification mode: The child can be as large as it wants up 47 * to the specified size. 48 */ 49 public static final int AT_MOST = 2 << MODE_SHIFT; 50 ...... 51 /** 52 * Creates a measure specification based on the supplied size and mode. 53 *...... 54 *@return the measure specification based on size and mode 55 */ 56 public static int makeMeasureSpec(@IntRange(from = 0, to = (1 << MeasureSpec.MODE_SHIFT) - 1) int size, 57 @MeasureSpecMode int mode) { 58 if (sUseBrokenMakeMeasureSpec) { 59 return size + mode; 60 } else { 61 return (size & ~MODE_MASK) | (mode & MODE_MASK); 62 } 63 ...... 64 65 } 66 ...... 67 /** 68 * Extracts the mode from the supplied measure specification. 69 *...... 70 */ 71 @MeasureSpecMode 72 public static int getMode(int measureSpec) { 73 //noinspection ResourceType 74 return (measureSpec & MODE_MASK); 75 } 76 77 /** 78 * Extracts the size from the supplied measure specification. 79 *...... 80 * @return the size in pixels defined in the supplied measure specification 81 */ 82 public static int getSize(int measureSpec) { 83 return (measureSpec & ~MODE_MASK); 84 } 85 ...... 86 }
从这段代码中,咱们可以得到如下的信息:
1)MeasureSpec概括了从父布局传递给子view布局要求。每一个MeasureSpec代表了宽度或者高度要求,它由size(尺寸)和mode(模式)组成。
2)有三种可能的mode:UNSPECIFIED、EXACTLY、AT_MOST
3)UNSPECIFIED:未指定尺寸模式。父布局没有对子view强加任何限制。它可以是任意想要的尺寸。(笔者注:这个在工作中极少碰到,据说一般在系统中才会用到,后续会讲得很少)
4)EXACTLY:精确值模式。父布局决定了子view的准确尺寸。子view无论想设置多大的值,都将限定在那个边界内。(笔者注:也就是layout_width属性和layout_height属性为具体的数值,如50dp,或者设置为match_parent,设置为match_parent时也就明确为和父布局有同样的尺寸,所以这里不要以为笔者搞错了。当明确为精确的尺寸后,其也就被给定了一个精确的边界)
5)AT_MOST:最大值模式。子view可以一直大到指定的值。(笔者注:也就是其宽高属性设置为wrap_content,那么它的最大值也不会超过父布局给定的值,所以称为最大值模式)
6)MeasureSpec被实现为int型来减少对象分配。该类用于将size和mode元组装包和拆包到int中。(笔者注:也就是将size和mode组合或者拆分为int型数据)
7)分析代码可知,一个MeasureSpec的模式如下所示,int长度为32位置,高2位表示mode,后30位用于表示size
8)UNSPECIFIED、EXACTLY、AT_MOST这三个mode的示意图如下所示:
9)makeMeasureSpec(int mode,int size)用于将mode和size打包成一个int型的MeasureSpec。
10)getSize(int measureSpec)方法用于从指定的measureSpec值中获取其size。
11)getMode(int measureSpec)方法用户从指定的measureSpec值中获取其mode。
2、ViewGroup.LayoutParams简介
该类的源码及注释分析如下所示。
1 //============================ViewGroup.java=============================== 2 /** 3 * LayoutParams are used by views to tell their parents how they want