Android自定义View的基石——View工作原理总结
前言
View可以说是我们在Android开发中接触得最多的一个类了,虽然不属于四大组件,但是发挥的作用却一点都不亚于四大组件,页面中的各种控件、布局都直接或间接地继承自View,可以说View无处不在。因而了解View的工作原理能让我们更好地处理开发中的诸多问题,尤其是对于老生常谈的自定义View来说,View的工作原理更是必须要掌握的。
在进入正文之前还是要强调一下,本文的分析基于Android 9.0(API Level 28)的源码,不同版本的源码可能会有不同,但是基本思路不会变化太多,可以进行参考。此外,本文的篇幅较长,可以根据目录选择章节来查看。
几个相关类
在介绍View的工作原理之前首先要介绍几个相关的类,它们在View的工作流程中扮演了重要的角色,了解它们能让我们对于View的工作原理有一个更全面的认识。
Window和WindowManager
Window顾名思义是表示一个窗口,虽然我们在开发中可能很少直接去操作Window,我目前接触过的也仅仅是给Window添加一些Flag的操作,但是Window在Android的视图体系中其实是很重要的一环,它可以说是所有视图的承载器,我们最熟悉的Activity的视图实际上也是附加在Window上,通过Window来管理的。Window是一个抽象类,它只有一个实现类PhoneWindow,因此我们在分析源码时直接看PhoneWindow就可以了。
WindowManager可以译为窗口管理者,是外界访问Window的入口,我们可以通过WindowManager来操作Window。WindowManager是一个接口,它的实现类是WindowManagerImpl。
DecorView
DecorView是最顶层的View,是整个视图的根节点,继承自FrameLayout,因此它也是一个ViewGroup。下面以一张图来展示可能更直观一些。
DecorView下包含一个竖直方向的LinearLayout,它的内部根据页面主题的不同可能会有所不同,但是一定会包含一个子View,它的id为android.R.id.content,是一个FrameLayout,我们调用setContentView()
设置的布局就是添加到了这个contentView中。
ViewRoot
ViewRoot对应于ViewRootImpl,是连接WindowManager和DecorView的纽带,View的measure、layout和draw流程都是通过ViewRootImpl完成的。
准备阶段
这里将以下几个流程称作View工作流程的准备阶段,可能不是很确切,主要还是为了和我们熟知的measure、layout、draw三大流程区分开,这一阶段完成的工作是Window和DecorView的创建以及对三大流程的调用。
Window的创建
Window的创建时机是在ActivityThread的performLaunchActivity()
方法中,在之前Activity的启动流程中也分析过该方法,我们再来简单回顾一下:
ActivityThread的performLaunchActivity方法
1 | private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) { |
该方法内部会依次创建Activity对象和Application对象,最后通过Instrumentation对象调用Activity的onCreate()
方法,这些都不是我们这里要关注的,我们只需要分析Activity的attach()
方法。
Activity的attach方法
1 | final void attach(Context context, ActivityThread aThread, |
可以发现PhoneWindow对象就是在attach()
方法中创建的,之后会为PhoneWindow设置相关回调并创建WindowManager对象(实际上是WindowManagerImpl对象)。
DecorView的创建
在Activity的onCreate()
方法中我们会调用setContentView()
来设置页面的布局,DecorView的创建就要从该方法来分析。
Activity的setContentView方法
1 | public void setContentView(@LayoutRes int layoutResID) { |
getWindow()
方法获取到的就是上面创建好的PhoneWindow对象,我们接着来看PhoneWindow的setContentView()
方法:
PhoneWindow的setContentView方法
1 |
|
这里首先会判断mContentParent是否为空,如果为空就调用installDecor()
方法,否则调用removeAllViews()
方法移除mContentParent的所有子View。那么这个mContentParent是什么呢,它是一个ViewGroup,我们通过名称可能能够猜到它就是我们页面所展示的内容。全局搜索了一下mContentParent的赋值时机,发现只有在installDecor()
方法中才会对其赋值,因此这里mContentParent为空,我们来看看installDecor()
方法:
1 | private void installDecor() { |
可以看到installDecor()
方法主要做的事有两件:调用generateDecor()
方法创建DecorView,将返回值赋给mDecor;调用generateLayout()
方法创建一个ViewGroup,赋值给mContentParent。这里的generateDecor()
和generateLayout()
方法都省略了大量代码,只保留了最核心的部分。
创建好了DecorView和mContentParent之后,我们回到PhoneWindow的setContentView()
方法,可以发现这之后调用了mLayoutInflater.inflate(layoutResID, mContentParent)
,inflate()
方法的作用我们都很熟悉了,就是根据我们传入的布局文件构建出View树,这里调用的是两个参数的方法,因此会将创建好的View树添加到mContentParent中。如果不是很清楚inflate()
方法几个参数的意义可以查阅网上的相关文章,或者参考我之前写过的一篇文章LayoutInflate的使用,这里我就不具体讲了。现在我们就清楚了mContentParent是什么了吧,它是我们setContentView()
方法中指定布局的父View,指定的布局会作为一个子View添加到mContentParent中。
我一开始分析时有一个疑问,mContentParent是何时与DecorView产生联系的呢,分析到这里好像并没有看到诸如mDecor.addView()
之类的代码,我们回到generateLayout()
方法中,方法内部调用了findViewById()
方法获取到contentParent,并将其赋给mContentParent,而这个id为content,是DecorView中的一个子View,是一个Framelayout,因此contentParent就是DecorView中的这个子View,mContentParent自然也表示这个子View。generateLayout()
方法中会根据Window的主题样式为DecorView加载相应的布局,我们就以其中一个R.layout.screen_simple为例,看看它的布局层级关系:
screen_simple.xml
1 |
|
可以看到最外层是一个LinearLayout,内部有两个子View,一个是ViewStub,它的id为action_mode_bar_stub,从名称上大概可以猜出它是页面的标题栏,会根据主题的不同来选择是否加载;另一个子View是id为content的FrameLayout,也就是上面分析中findViewById()
方法获取到的contentParent,因此mContentParent就是这个FrameLayout。
看到这里,我们对于整个页面的层级关系就很清楚了,最外层是一个DecorView,它的内部有一个LinearLayout,LinearLayout中有一个FrameLayout,我们在setContentView()
中指定的布局文件会被添加到这个FrameLayout中,当然实际上根据主题样式的不同可能会更复杂一些,这里只是说明最简单的一种情况。
值得一提的是,AppCompatActivity中的setContentView()
方法和Activity的有所不同,简单分析一下。
AppCompatActivity的setContentView方法
1 |
|
getDelegate()
获取到的是一个代理对象,类型为AppCompatDelegateImpl(前几个版本的源码中这里会根据SDK版本不同返回不同的对象如AppCompatDelegateImplN、AppCompatDelegateImplV23等,后面的分析是差不多的),之后调用了AppCompatDelegateImpl的setContentView()
方法。
AppCompatDelegateImpl的setContentView方法
1 |
|
方法内部首先调用了ensureSubDecor()
方法,将返回值赋值给mSubDecor:
1 | private void ensureSubDecor() { |
ensureSubDecor()
方法内部调用了createSubDecor()
方法,我们具体分析一下该方法。
分析1、mWindow.getDecorView()
mWindow的类型为PhoneWindow,通过查看PhoneWindow的getDecorView()
方法我们可以发现,由于此时mDecor并未被赋值过,因此会调用此前分析过的installDecor()
方法,创建DecorView和mContentParent。
1 |
|
之后和generateLayout()
方法很类似,通过判断主题样式创建出subDecor,加载不同的布局文件。
分析2、“偷梁换柱”
这里的逻辑还是很巧妙的,涉及到了两个View,contentView是subDecor中id为action_bar_activity_content的子View,类型为ContentFrameLayout;另一个windowContentView的id为android.R.id.content,没错,就是此前分析过的那个FrameLayout。之后的操作是将windowContentView的子View移除,添加到contentView中,并将windowContentView的id设置为View.NO_ID,将contentView的id设置为android.R.id.content,看上去很像是两个View之间的互换,因此我把这个操作称为“偷梁换柱”。
分析3、mWindow.setContentView(subDecor)
这里调用了PhoneWindow的setContentView()
方法,将subDecor添加到了mContentParent中,这里的mContentParent其实就是上面的windowContentView,此时它的id已经变成了View.NO_ID。
这时我们再回到AppCompatDelegateImpl的setContentView()
方法,之后就是根据我们指定的布局文件构建出View树并添加到id为android.R.id.content的ViewGroup中,即上面的ContentFrameLayout。
我的表述不是很清楚,大家可能有些懵了,这都是什么乱七八糟的,我最后总结一下,其实大体上的流程和Activity的setContentView()
方法还是很类似的,不同之处就是在Activity中,我们指定布局文件对应的View树会被添加到FrameLayout中,而对于AppCompatActivity,View树会被添加到一个ContentFrameLayout中,它们的id均为android.R.id.content,层级关系如下,ContentFrameLayout的直接父View是mSubDecor所加载布局的根布局,对应不同的主题样式可能不同,这个根布局的父View就是FrameLayout,因此可以看做就是多嵌套了几层View。
其实这里我就不明白了,我们的Activity都是继承自AppCompatActivity,那么相比于继承自Activity,我们的布局层级是要复杂一些的,大家都知道Android开发中是要避免过多的布局层级嵌套的,那么AppCompatActivity这样做的目的是什么呢?鉴于个人能力和认识都还很浅显,想不明白为什么要这样设计,欢迎大佬们提出自己的见解。
三大流程的调用
上面的两个流程中已经完成了PhoneWindow和DecorView的创建,那么大名鼎鼎的View绘制三大流程又是从何时开始的呢,就是ActivityThread的handleResumeActivity()
方法,该方法在分析Activity的启动流程时也分析过,onResume()
回调方法就是经由该方法调用的。
ActivityThread的handleResumeActivity方法
1 |
|
该方法主要做了两件事:调用performResumeActivity()
方法,进而调用Activity的生命周期回调onResume()
;获取此前创建好的PhoneWindow、DecorView以及WindowManager对象,调用WindowManager的addView()
方法将DecorView添加到Window中。前面也说过,WindowManager的实现类是WindowManagerImpl,因此我们来具体看一下WindowManagerImpl的addView()
方法都做了些什么。
WindowManagerImpl的addView方法
1 |
|
方法内部调用了mGlobal的addView()
方法,mGlobal的类型为WindowManagerGlobal,我们接着看:
WindowManagerGlobal的addView方法
1 | public void addView(View view, ViewGroup.LayoutParams params, |
addView()
方法内部首先会创建出ViewRootImpl对象,将要添加的View(即DecorView)、ViewRootImpl和布局参数添加到列表中,最后调用ViewRootImpl的setView()
方法,我们来看一下这个方法。
ViewRootImpl的setView方法
1 | public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) { |
setView()
方法内部调用了requestLayout()
方法,这个方法可能大家在自定义View时用到过,用于刷新视图,不过需要注意的是,我们在自定义View中调用的requestLayout()
方法是在View中定义的,和ViewRootImpl中的逻辑是不一样的。接下来我们就来看看ViewRootImpl的requestLayout()
方法:
1 |
|
方法内部又调用了scheduleTraversals()
方法:
1 | void scheduleTraversals() { |
scheduleTraversals()
方法内部首先执行了mHandler.getLooper().getQueue().postSyncBarrier()
,这行代码的作用是开启Handler的同步屏障机制,关于Handler的同步屏障机制,我这里简单解释一下(因为我了解得也不是很透彻o(╥﹏╥)o),我们都知道Handler处理的消息是放到一个消息队列中的,消息默认情况下都是同步的,如果需要发送异步消息需要使用代码来声明,同步屏障机制就使得Looper在从消息队列中获取消息时,只获取异步消息并进行处理。我可能解释得不太好,如果想深入了解一下Handler的同步屏障机制可以自行查找资料,这里推荐一下鸿洋大神WanAndroid上的每日一问Handler应该是大家再熟悉不过的类了,那么其中有个同步屏障机制,你了解多少呢?。开启了同步屏障后,调用了mChoreographer的postCallback()
方法,该方法内部就是利用了Handler,发送了一个Runable对象,如果跟踪源码的话可以发现最后会把Runable封装为一个Message,并将Message设置为异步消息,我就不展示了。清楚了postCallback()
方法的原理后,我们就知道了需要分析mTraversalRunnable对象,它是一个Runable对象,类型为TraversalRunnable ,在run()
方法中调用了doTraversal()
方法。
1 | final class TraversalRunnable implements Runnable { |
由于开启了同步屏障,因此当前线程(主线程)的Looper会优先获取异步消息,即直接执行doTraversal()
方法。
1 | void doTraversal() { |
doTraversal()
方法内部首先会关闭同步屏障机制,否则主线程的同步消息就永远无法被处理了,然后调用了performTraversals()
方法,我们来看一下:
1 | private void performTraversals() { |
这里省略了大量代码,可以看出,在performTraversals()
方法内会依次调用measureHierarchy()
、performLayout()
、performDraw()
,进而开始View的三大流程。
分析到这里,View的绘制准备阶段就算完成了,最后再回顾一下,主要分为三个阶段:
- Activity的
onCreate()
方法调用之前,创建Window(PhoneWindow) - Activity的
onCreate()
方法中调用setContentView()
方法,创建DecorView和contentView(页面内容根布局),将指定的布局文件加载到contentView中 - Activity的
onResume()
方法调用之后,将DecorView添加到Window中,之后依次开始View的measure、layout和draw流程
从上面几个流程的先后顺序我们就能清楚为什么在onResume()
方法中或者onResume()
方法之前获取不到View的宽高,就是因为此时View还未执行measure和layout流程。
measure阶段
到这里算是正式进入到了View的三大流程,首先要分析的是measure流程。在分析View的measure流程之前,我们首先要介绍两个相关的类:MeasureSpec和LayoutParams。
MeasureSpec
MeasureSpec简介
关于MeasureSpec大家可能都很熟悉了,它是由一个32位int值表示的,高2位表示SpecMode,即测量模式,低30位表示SpecSize,即测量尺寸大小。
1 | public static class MeasureSpec { |
可以通过调用getMode()
和getSize()
方法获取到测量模式和测量尺寸,方法内部就是通过简单的位运算保留指定位数上的数值。不由得称赞Android系统开发人员的设计巧妙,将两个值封装成了一个变量,可以通过位运算获取相应数值,减少了多余对象的内存分配,其实Android源码中很多地方都有类似设计(比如MotionEvent),这里就不多提啦。
MeasureSpec内部定义了三种测量模式:
UNSPECIFIED:父View不会限制子View的大小,一般用于系统内部,开发中使用很少
EXACTLY:父View能够确定子View的大小,对应两种情况:精确尺寸(dp或px)和match_parent
AT_MOST:子View的大小不能超过父View尺寸,具体尺寸需要由子View自身来确定,对应wrap_content
虽然我们在开发中用到UNSPECIFIED模式的情况不多,但是了解一下还是有必要的,我在后面会单独介绍一下这个模式的应用。
如何确定MeasureSpec的值
MeasureSpec的值是由View自身的LayoutParams和父View的MeasureSpec共同确定的。对于特定的View来说,它的MeasureSpec是通过父View(即ViewGroup)的getChildMeasureSpec()
方法得到的。
ViewGroup的getChildMeasureSpec方法
1 | /** |
getChildMeasureSpec()
方法也验证了子View的MeasureSpec是由父View的MeasureSpec和子View的LayoutParams共同确定的。上面的判断可能有些复杂,不过别担心。已经有很多大佬总结出了表格,看起来更加直观一些,下图摘自Carson_Ho大佬的博客,表中的childSize表示子View的LayoutParams指定的大小,parentSize表示父View可用空间的大小。
我们可以先不去看最后一列UNSPECIFIED的情况,单看前两列可以找出一定的规律:
- 当子View的LayoutParams指定为精确数值时,不管父View的测量模式是什么,子View的测量模式均为EXACTLY,测量尺寸为LayoutParams指定的值
- 当子View的LayoutParams指定为match_parent时,子View的测量模式取决于父View,即如果父View的测量模式为EXACTLY,那么子View的测量模式为EXACTLY;如果父View的测量模式为AT_MOST,那么子View的测量模式为AT_MOST,子View的测量尺寸均为父View可用空间大小
- 当子View的LayoutParams指定为wrap_content时,不管父View的测量模式是什么,子View的测量模式均为AT_MOST,测量尺寸为父View可用空间大小
普通View的MeasureSpec是如何获取的我们已经清楚了,那么对于DecorView来说,它是没有父View的,它的MeasureSpec是如何得到的呢?我们在上一节分析到ViewRootImpl的performTraversals()
方法时,介绍到方法内部调用了measureHierarchy()
方法,进而调用performMeasure()
方法开始View的measure流程,现在我们就来具体看一下measureHierarchy()
方法。
ViewRootImpl的measureHierarchy方法
1 | private boolean measureHierarchy(final View host, final WindowManager.LayoutParams lp, |
可以看出这里调用getRootMeasureSpec()
方法获取到childWidthMeasureSpec和childHeightMeasureSpec,之后调用performMeasure()
方法,方法内部又调用了mView的measure()
方法,这个mView是什么呢,我全文检索了一下mView的赋值时机,发现它是在setView()
方法中被赋值的,还记得setView()
方法是什么时候调用的吗,就是在handleResumeActivity()
方法中调用wm.addView(decor,l)
这行代码之后被调用的,因此这里的mView就是传过来的DecorView,调用measure()
方法就开始了对DecorView的测量流程。现在就要关注childWidthMeasureSpec和childHeightMeasureSpec了,这两个值就是DecorView的MeasureSpec,我们来看一下获取到这两个值的getRootMeasureSpec()
方法:
1 | private static int getRootMeasureSpec(int windowSize, int rootDimension) { |
逻辑还是比较简单的,参数windowSize传递过来的值是desiredWindowWidth和desiredWindowHeight,通过查看源码可以发现这两个值表示屏幕的宽高尺寸,因此我们可以得出以下结论:
- DecorView的LayoutParams指定为MATCH_PARENT时,它的测量模式为EXACTLY,测量尺寸为屏幕尺寸
- DecorView的LayoutParams指定为WRAP_CONTENT时,它的测量模式为WRAP_CONTENT,测量尺寸为屏幕尺寸
可以看出,DecorView作为最顶层的View,它的MeasureSpec只取决于自己的LayoutParams参数。
LayoutParams
LayoutParams简介
LayoutParams这个类在开发中还是很常见的,顾名思义就是布局参数,View中定义了一个LayoutParams类型的成员变量,它的作用就是确定View的宽高,我们平时在xml布局文件中指定的layout_width和layout_height属性就是用于生成LayoutParams。需要注意的是,这两个属性的前面都带上layout前缀,而不是直接使用width和height来命名,因此我们要清楚它们的值并不是View的宽高,也可以说它们并不属于View自身的属性。
LayoutParams是ViewGroup中的一个内部类,我们看一下它的定义:
1 | public static class LayoutParams { |
LayoutParams中定义了几个重载构造函数,分别用于xml文件中指定宽高、手动指定宽高等场景。每个ViewGroup的子类(直接或间接继承)都有对应的LayoutParams类,比如LinearLayout.LayoutParams,在各自的LayoutParams中可以定义相应的布局参数属性。因此不止layout_width和layout_height这两个属性,其他以layout开头的属性(比如layout_weight、layout_margin等等)也都和LayoutParams相关。
View的LayoutParams属性是何时设置的
了解了LayoutParams的定义后,接下来需要弄清楚View的LayoutParams属性是何时设置的,我们知道在ViewGroup中添加子View的方式有两种:xml文件中添加和代码中添加,我们分别来看一下这两种情况。
- xml文件中添加View
在setContentView()
方法的分析中我们知道xml文件中添加的View最终是通过LayoutInflater的inflate()
方法来解析的。
1 | public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) { |
在inflate()
方法中有一行代码是params = root.generateLayoutParams(attrs);
,generateLayoutParams()
方法的作用就是就是根据xml指定的属性构造出LayoutParams对象。
1 | public LayoutParams generateLayoutParams(AttributeSet attrs) { |
构造出LayoutParams对象后根据参数attachToRoot的值有两种处理逻辑:
如果attachToRoot为true,则会调用
addView()
方法并传入构造好的LayoutParams对象,addView()
方法内部会将LayoutParams对象设置给View,详细代码后面会展示,这里先这样记住就好如果attachToRoot为false,则会调用View的
setLayoutParams()
方法直接将构造好的LayoutParams对象设置给View1
2
3
4
5
6
7
8
9
10
11
12
13public void setLayoutParams(ViewGroup.LayoutParams params) {
if (params == null) {
throw new NullPointerException("Layout parameters cannot be null");
}
// 给mLayoutParams变量赋值
mLayoutParams = params;
resolveLayoutParams();
if (mParent instanceof ViewGroup) {
((ViewGroup) mParent).onSetLayoutParams(this, params);
}
// 重新执行View的绘制流程,measure->layout->draw
requestLayout();
}代码中添加View
我们一般会使用ViewGroup的addView()
方法来添加子View,就像下面这样:
1 | LinearLayout llContainer = findViewById(R.id.ll_container); |
addView()
方法有很多个重载方法,上面的代码我只展示了最简单的一个参数的情况,下面我们就来具体看一下所有的重载方法。
1 | /** |
不难看出,方法1内部调用了方法2,而在方法2内部会先判断子View是否设置了LayoutParams属性,如果没有设置,就调用generateDefaultLayoutParams()
方法创建出一个默认的LayoutParams对象,最后调用方法5。再看方法3和方法4,这两个方法最终同样会调用方法5,因此我们只需要看方法5就好。
在此之前,我们先看一下generateDefaultLayoutParams()
方法是如何创建出默认的LayoutParams对象的:
1 | protected LayoutParams generateDefaultLayoutParams() { |
可以看出,ViewGroup默认创建出LayoutParams对象的宽高属性均为WRAP_CONTENT。
回到方法5,可以看到最后调用了addViewInner()
方法:
1 | private void addViewInner(View child, int index, LayoutParams params, |
方法内部首先会调用checkLayoutParams()
方法检查LayoutParams参数是否合法,如果不合法就调用generateLayoutParams()
方法构造一个新的LayoutParams对象,generateLayoutParams()
方法我们其实在上面xml文件中添加View的分析中刚见过,不过这里调用的是另一个重载方法,参数为LayoutParams对象。
1 | protected boolean checkLayoutParams(ViewGroup.LayoutParams p) { |
之后就是为View设置LayoutParams属性了,这里会判断传入的preventRequestLayout
参数值,如果为true就直接对View的mLayoutParams变量赋值;如果为false则调用setLayoutParams()
方法来给View设置LayoutParams,这两种情况的区别就是setLayoutParams()
方法内部会调用requestLayout()
方法来重新进行View的measure、layout和draw流程。可以看到由于addView()
方法调用addViewInner()
时传入的参数为false,因此这里会执行setLayoutParams()
方法。额外提一下,ViewGroup中有一个addViewInLayout()
方法,和addView()
方法类似,内部也调用了addViewInner()
方法,不过该方法可以显式地指定preventRequestLayout
参数的值。
自定义LayoutParams须知
看到这里关于LayoutParams的作用和使用原理基本上就介绍得差不多了,最后再简单介绍自定义LayoutParams。我们在自定义ViewGroup的同时,根据需求可能需要自定义LayoutParams,这里就以我们最熟悉的LinearLayout来看一下有哪些需要注意的地方吧。
- 重写generateDefaultLayoutParams()方法
1
2
3
4
5
6
7
8
9
protected LayoutParams generateDefaultLayoutParams() {
if (mOrientation == HORIZONTAL) {
return new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
} else if (mOrientation == VERTICAL) {
return new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT);
}
return null;
}
通过前面的分析我们知道generateDefaultLayoutParams()
方法的作用是在调用addView()
方法时为没有指定LayoutParams的View创建一个默认的LayoutParams对象。对于LinearLayout来说,如果布局方向为水平则子View的宽和高均为WRAP_CONTENT;如果布局方向为竖直则子View的宽为MATCH_PARENT,高为WRAP_CONTENT。
- 重写checkLayoutParams(ViewGroup.LayoutParams p)方法
checkLayoutParams()
方法的作用是检查LayoutParams参数是否合法,我们已经看到了ViewGroup的判断标准为LayoutParams是否非空,那么LinearLayout是如何判断的呢。
1 | protected boolean checkLayoutParams(ViewGroup.LayoutParams p) { |
可以看到LinearLayout的判断标准为LayoutParams的类型是否为LinearLayout.LayoutParams,在自定义LayoutParams时我们可以根据需要来选择重写该方法。
- 重写generateLayoutParams(ViewGroup.LayoutParams p)方法
在checkLayoutParams()
方法返回false即LayoutParams不合法的情况下会调用generateLayoutParams()
方法重新创建一个合法的LayoutParams,LinearLayout中的定义如下:
1 |
|
generateLayoutParams()
方法内部会对传入的LayoutParams进行类型转换,转换为LinearLayout.LayoutParams或ViewGroup.MarginLayoutParams,这个MarginLayoutParams是什么呢,从名称上也能看出MarginLayoutParams和外边距有关,相比于ViewGroup.LayoutParams,它添加了对外边距的支持,我们平时在xml文件中使用的layout_margin和layout_marginLeft等属性都是MarginLayoutParams中定义的,而LinearLayout.LayoutParams就是继承自MarginLayoutParams,因此具有设置外边距的几个属性,我们自定义的LayoutParams也可以直接继承自MarginLayoutParams。
需要注意的是,虽然这里的generateLayoutParams()
方法可以保证LayoutParams类型的正确,但是如果在调用addView()
方法后再次设置了LayoutParams,就有可能会报错,就像下面这样:
1 | LinearLayout llContainer = findViewById(R.id.ll_container); |
上述代码运行后会抛出异常java.lang.ClassCastException: android.view.ViewGroup.LayoutParams cannot be cast to android.widget.LinearLayout.LayoutParams,原因是在LinearLayout的onMeasure()
方法内部会进行强制类型转换操作:
1 | final LayoutParams lp = (LayoutParams) child.getLayoutParams(); |
因此我们在调用setLayoutParams()
还是应该保证传入的LayoutParams类型正确。
measure流程
前面关于两个类的介绍还是比较详细的,现在终于进入到了measure流程的分析,这里会分为两种情况:单一View的measure和ViewGroup的measure,ViewGroup的measure要复杂一些,因为它不仅需要完成对自身的measure,还要完成对所有子View的measure,我们先分析简单的情况——单一View的measure流程。
单一View的measure流程
View的measure流程从measure()
方法开始:
1 | public final void measure(int widthMeasureSpec, int heightMeasureSpec) { |
可以看到它是一个final声明的方法,因此子类无法重写该方法。在方法内部又调用了我们熟悉的onMeasure
方法,我们自定义View时重写的都是该方法。
1 | protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { |
方法内部调用了setMeasuredDimension()
方法:
1 | protected final void setMeasuredDimension(int measuredWidth, int measuredHeight) { |
可以看到setMeasuredDimension()
方法完成的工作就是为mMeasuredWidth和mMeasuredHeight这两个变量赋值,这两个变量表示View的测量宽高(与实际宽高有区别,View的实际宽高还取决于layout过程),我们可以通过getMeasuredWidth()
和getMeasuredHeight()
方法获取到View测量后的宽高尺寸,即这两个变量的低30位。
我们接着来看View的测量宽高是如何得到的,即getDefaultSize()
方法:
1 | public static int getDefaultSize(int size, int measureSpec) { |
可以看出当View的测量模式为AT_MOST或EXACTLY时,View的测量宽/高等于specSize,即MeasureSpec中的测量尺寸;当View的测量模式为UNSPECIFIED时,View的测量宽/高等于该方法的第一个参数的值,即getSuggestedMinimumWidth()
/getSuggestedMinimumHeight()
方法的返回值,这里就以getSuggestedMinimumWidth()
方法为例,getSuggestedMinimumHeight()
同理。
1 | protected int getSuggestedMinimumWidth() { |
这里首先判断了View是否设置了背景,如果没有设置背景,返回值为mMinWidth,它对应于android:minWidth属性所指定的值,如果没有指定则为0;如果View设置了背景,返回值为mMinWidth和mBackground.getMinimumWidth()
两者的最大值,getMinimumWidth()
方法可以获取到Drawable的原始宽度,但不是所有的Drawable都有原始宽度,如果没有原始宽度,获取到的值就为0(上面这段解释基本上来自《Android开发艺术探索》,目前我对于Drawable的认识还不够,想了解更多的话自行查阅资料吧)。
这里也引出了一个问题,当View的测量模式为AT_MOST,即LayoutParams指定为wrap_content时,View的测量宽/高等于specSize,而从getChildMeasureSpec()
方法的分析中我们也得出此时specSize的值为parentSize,即父View的可用空间大小,这会导致wrap_content产生和match_parent一样的效果,因此我们在自定义View时需要重写onMeasure()
方法,解决wrap_content的失效问题,具体做法也很简单,就是为wrap_content情况指定一个默认的宽高尺寸,默认尺寸可以根据需要灵活指定,示例代码如下:
1 |
|
用一张图总结一下单一View的measure流程:
ViewGroup的measure流程
ViewGroup的measure流程同样从measure()
方法开始,和View是一样,这里就不展示了,之后会调用onMeasure()
方法,但是我们会发现ViewGroup中并没有重写onMeasure()
方法,原因其实也不难理解,就是因为每个ViewGroup的布局方式都不一样,无法得出一个统一的实现方式,在自定义ViewGroup时需要根据想要得到的布局效果来重写onMeasure()
方法。虽然ViewGroup没有提供onMeasure()
方法的实现方式,但是提供了一个measureChildren()
方法,从方法名也能猜到是用来测量ViewGroup的所有子View的,我们来看一下这个方法。
1 | protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) { |
方法内部遍历所有的子View,依次调用measureChild()
方法:
1 | protected void measureChild(View child, int parentWidthMeasureSpec, |
measureChild()
方法首先调用此前分析过的getChildMeasureSpec()
方法,根据ViewGroup的MeasureSpec和子View自身的LayoutParams确定出子View的MeasureSpec,然后调用子View的measure()
方法,对子View进行测量,后面的流程就和单一View的measure流程一样了。我们在自定义ViewGroup时可以在onMeasure()
方法调用measureChildren()
方法完成对子View的测量。
下面以ViewGroup的子类LinearLayout为例,分析一下它的measure流程,加深一下对ViewGroup的measure流程的理解。首先来看onMeasure()
方法:
1 |
|
onMeasure()
方法中会判断LinearLayout的布局方向执行相应的方法,这里就以竖直方向的measureVertical()
为例进行分析,水平方向是类似的。
1 | void measureVertical(int widthMeasureSpec, int heightMeasureSpec) { |
方法很长,省略了大量代码,说一下简单的流程吧,高度上,LinearLayout首先会遍历所有子View,调用measureChildBeforeLayout()
方法对子View进行测量,每测量一个子View,就增加mTotalLength的值,它表示LinearLayout在竖直方向上的总高度,增加的值包括子View的测量高度和子VIew竖直方向上的margin,当所有子View测量完成后,会计算LinearLayout自身的padding值,最后调用resolveSizeAndState()
方法完成对自身高度的测量。宽度上和单一View的测量类似,不需要考虑子View,调用resolveSizeAndState()
完成对自身宽度的测量。方法最后依然是调用setMeasuredDimension()
设置LinearLayout的测量宽高。接下来我们来看一下LinearLayout测量自身的方法resolveSizeAndState()
,这个方法是在View中定义的。
1 | public static int resolveSizeAndState(int size, int measureSpec, int childMeasuredState) { |
可以看出,如果LinearLayout的测量模式为EXACTLY,那么最终的测量高度为specSize,与子View无关;如果LinearLayout的测量模式为AT_MOST,会判断子View的总高度(包括margin、paddding)是否超过了LinearLayout竖直方向上的可用空间,如果没超过则最终测量高度为子View的总高度,如果超过了则最终测量高度为specSize,并设置一个MEASURED_STATE_TOO_SMALL标志。
值得一提的是,LinearLayout的测量有一种特殊情况,就是对于自身的测量模式为EXACTLY并且子View设置了layout_weight的情况,这种情况会在后面重新进行一次子View的遍历和测量,由于这不是ViewGroup测量的通用流程,这里就不细说了,感兴趣的话可以查看一下这块的源码。
最后用一张图总结一下ViewGroup的measure流程,虽然具体到每个ViewGroup的measure流程可能会有所不同,但是这几个步骤是通用的。
既然ViewGroup和View的measure流程都已经分析完了,我们可以梳理一下一个页面的完整measure流程,首先从ViewRootImpl的performMeasure()
方法开始对顶层View——DecorView进行测量,调用measure()
方法,由于DecorView继承自FrameLayout,可以看做一个ViewGroup,因此接着会遍历DecorVIew的所有子View进行测量,如果子View是一个单一View,只需要完成自身的测量,如果子View是一个ViewGroup,就又会重复上面的步骤,遍历该子View下的所有子View进行测量,之后便是一个递归的过程,最后当所有子View的测量都完成后,再进行DecorVIew自身的测量。
补充:MeasureSpec.UNSPECIFIED的应用
我们此前介绍UNSPECIFIED模式的时候基本上是一笔带过的,只介绍了该模式下是父View不限制子View大小,用于系统内部,开发中一般很少会用到,虽然是这样,我们还是有必要了解一下该模式的常见应用场景,可能我们平时在开发中已经接触过了,只是没有发现而已。
ScrollView相信大家都很熟悉了,在使用时有一个需要注意的地方,就是当ScrollView的子布局没有占满屏幕高度时,它的子View是无法占满全屏的,即使设置了layout_height为match_parent也不管用,可能大家都已经知道了这个问题,我这里就简单展示一下。布局文件很简单,ScrollView嵌套一个LinearLayout,LinearLayout中有一个高度为100dp的TextView。
1 |
|
运行效果如下:
可以看出LinearLayout的高度为100dp,并没有占满屏幕,但是我们明明设置了layout_height为match_parent,其实不止这样,即便是layout_height指定了精确数值(如200dp)也不会生效。解决方案就是为ScrollView添加android:fillViewport=”true”属性,运行之后发现LinearLayout可以占满全屏了。
现在我们从源码角度分析一下产生这个问题的原因,看一下ScrollView的onMeasure()
方法:
ScrollView的onMeasure方法
1 |
|
onMeasure()
方法中首先会判断mFillViewport的值,如果为false则直接return,不执行后面的逻辑。从变量名不难猜到这个mFillViewport就是对应于android:fillViewport属性,默认值为false,因此当我们没有设置android:fillViewport=”true”时,onMeasure()
方法只会执行父类的onMeasure()
方法。我们先简单看一下后面的代码,首先计算出ScrollView的可用高度desiredHeight,当child.getMeasuredHeight() < desiredHeight
,即子View的测量高度小于ScrollView的可用高度时,会将子View高度的测量模式指定为EXACTLY,测量尺寸指定为ScrollView的可用高度并进行重新测量,因此子View的最终测量高度就是ScrollView的可用高度,对于上面的例子来说Linearlayout自然就占满了全屏。
清楚了android:fillViewport=”true”属性为什么可以让子View占满全屏后,我们再来分析一下为什么默认情况下子View不会占满全屏,由于默认情况只会执行父类的onMeasure()
方法,我们来看一下ScrollView的父类FrameLayout的onMeasure()
方法。
FrameLayout的onMeasure方法
1 |
|
FrameLayout的onMeasure()
方法内部调用了measureChildWithMargins()
方法来对子View进行测量,ScrollView重写了该方法,我们来看一下:
ScrollView的measureChildWithMargins方法
1 |
|
可以看出ScrollView在测量子View时,将子VIew高度的测量模式直接指定为了UNSPECIFIED,还记得我们上面分析过的LinearLayout的measure过程吗,在子View测量完成后,会调用resolveSizeAndState()
方法完成自身的测量,这里再贴一遍代码。
1 | public static int resolveSizeAndState(int size, int measureSpec, int childMeasuredState) { |
可以看出,当LinearLayout的测量模式为UNSPECIFIED时,LinearLayout的测量高度为子View的总高度size,因此当LinearLayout子View的总高度小于LinearLayout指定的高度时,LinearLayout的高度不会生效。
看到这里我们清楚了ScrollView子View无法占满全屏的原因,也见到了UNSPECIFIED的应用场景,其实不止ScrollView,UNSPECIFIED模式在其他的一些可滚动的ViewGroup中也有应用,比如RecyclerView。和WRAP_CONTENT相比,UNSPECIFIED模式不会限制View的大小,正是如此,UNSPECIFIED模式非常适合应用到可滚动的ViewGroup中,此时ViewGroup不必关心子View的大小是否超出了自身范围,即时超出了也可以通过滚动来查看。
我们在自定义View时该如何处理UNSPECIFIED的情况呢,这里引用一下每日一问 详细的描述下自定义 View 测量时 MesureSpec.UNSPECIFIED中陈小缘大佬的回答,解释得很好。当遇到UNSPECIFIED时就比较自由了,既然尺寸由自己决定,那么可以写死为50,也可以固定为200,但还是建议结合实际需求来定义,比如ImageView,它的做法就是:有设置图片内容(drawable)的话,会直接使用这个drawable的尺寸,但不会超过指定的MaxWidth或MaxHeight, 没有内容的话就是0;而TextView处理UNSPECIFIED的方式,和AT_MOST是一样的。
layout阶段
measure流程的作用是对View的大小进行测量,而layout的作用就是根据测量大小确定View的最终位置,简单地说就是把View放在哪。和measure流程类似,layout流程同样分为两种情况:单一View的layout和ViewGroup的layout,ViewGroup的layout流程要复杂一些,因为它不仅要进行自身的layout,还要对所有子View进行layout,我们先来看看单一View的layout流程。
单一View的layout流程
View的layout流程从layout()
方法开始,我们来看一下这个方法:
1 | public void layout(int l, int t, int r, int b) { |
layout()
方法内部会根据isLayoutModeOptical()
的返回值调用setOpticalFrame()
方法或setFrame()
方法,isLayoutModeOptical()
方法会判断LAYOUT_MODE_OPTICAL_BOUNDS标志位,它表示一个布局模式,从名称上看应该是和布局边界有关,具体作用我也不是很了解,不过默认情况下都是没有设置该标志位的。这里可以暂且先不去管它的作用,我们会发现setOpticalFrame()
方法内部最终还是会调用setFrame()
方法,因此直接来看setFrame()
方法就可以了。
1 | private boolean setOpticalFrame(int left, int top, int right, int bottom) { |
setFrame()
方法首先会判断根据mLeft != left || mRight != right || mTop != top || mBottom != bottom
,即View的位置是否发生了改变,如果发生了改变,则返回值为true,反之返回值为false。如果View的位置发生了改变,会重新为View的四个顶点位置赋值,对应四个成员变量mLeft、mTop、mRight和mBottom,关于这四个值我们通过一个示意图就可以很清楚了,图片摘自GcsSloop大佬的博客。
首次layout 前这四个变量都没有赋过值,因此这里setFrame()
方法会返回true,我们回到layout()
方法,changed的值就为true,接下来会执行onLayout()
方法,我们接着来看onLayout()
方法。
1 | protected void onLayout(boolean changed, int left, int top, int right, int bottom) { |
View中的onLayout()
是一个空方法,没有声明任何逻辑,这是因为layout()
方法已经确定了View的四个顶点的位置,而onLayout()
方法是用于ViewGroup确定子View的位置,我们会再来分析。
单一View的layout流程就分析完了,是不是很简单,用一张流程图总结一下:
ViewGroup的layout流程
ViewGroup的layout流程同样从layout()
方法开始,我们来看一下:
1 |
|
ViewGroup的layout()
方法使用final声明,因此子类无法重写该方法。layout()
方法中调用了父类即View的layout()
方法,确定了ViewGroup自身的四个顶点位置,并调用onLayout()
方法,我们来看一下ViewGroup的onLayout()
方法:
1 |
|
可以发现ViewGroup的onLayout()
方法是一个抽象方法,因此当我们自定义ViewGroup时需要重写该方法。ViewGroup没有实现onLayout()
方法的原因同样是因为不同的ViewGroup具有不同的布局方式,无法得出一个统一实现。在自定义ViewGroup中的onLayout()
方法中我们需要遍历所有的子View,根据需要的布局方式调用子View的layout()
方法确定子View的位置。
这样说可能不是很清楚,接下来我们同样以LinearLayout为例,看一下它的layout流程,加深我们对ViewGroup的 layout流程的理解。由于ViewGroup的layout()
方法无法被子类重写,因此我们直接来看LinearLayout的onLayout()
方法:
1 |
|
和LinearLayout的onMeasure()
方法类似,onlayout()
方法中同样会根据LinearLayout的布局方向执行相应的布局方法,我们以竖直方向布局为例,分析一下layoutVertical()
方法,水平方向同理。
1 | void layoutVertical(int left, int top, int right, int bottom) { |
layoutVertical()
方法内部遍历了LinearLayout的所有子View,每次遍历都调用setChildFrame()
方法,我们来看一下这个方法:
1 | private void setChildFrame(View child, int left, int top, int width, int height) { |
可以看到setChildFrame()
方法其实就是调用了子View的layout()
方法,完成子View的布局。setChildFrame()
方法调用完成后,会增加childTop的值,它对应子View的mTop,继续下一个子View的layout,还是比较好理解的,竖直方向的LinearLayout的子View是一个接一个往下放置的。
总结一下ViewGroup的layout流程,首先会调用layout()
方法确定自身的位置,之后调用onLayout()
方法,遍历所有的子View,根据ViewGroup的布局特性依次确定出每个子View的位置。流程图如下所示:
ViewGroup的layout流程和measure流程还是很相似的,不过在顺序上有一些区别,measure是先遍历子View对子View进行测量,最后根据子View的测量结果对ViewGroup自身进行测量;而layout是先确定ViewGroup的位置,再遍历子View确定子View的位置。
最后我们来梳理一下整个页面的layout过程,前面也提到过,页面的layout流程从ViewRootImpl的performLayout()
方法开始。
ViewRootImpl的performLayout方法
1 | private void performLayout(WindowManager.LayoutParams lp, int desiredWindowWidth, |
方法内部首先将mView赋值给host,这里的mView我此前也提到过,就是在handleResumeActivity()
方法中调用wm.addView(decor,l)
时传过来的DecorView,因此后面调用host的layout()
方法实际上就是调用DecorView的layout()
方法,从这里就开始最顶层VIew的layout,而我们知道DecorView继承自FrameLayout,因此这里就是执行ViewGroup的layout()
方法,之后的步骤我们就清楚了,首先确定出DecorView的位置,然后调用onlayout()
方法遍历DecorView的子View,依次调用子View的layout()
方法来确定子View的位置,如果子View是一个ViewGroup,还需要接着遍历子View的所有子View进行layout。
getMeasureWidth/getMeasureHeight和getWidth/getHeight的区别
我在View的measure流程中提到过measure完成后可以通过getMeasuredWidth()
和getMeasuredHeight()
方法获取View的测量宽高,但是这个测量宽高并不等于View的最终实际宽高,现在就来解释一下这个问题。
我们知道View的宽高可以通过getWidth()
和getHeight()
方法来获得,首先来看一下这几个方法的定义,这里就只对比getMeasureWidth()
和getWidth()
方法,getMeasuredHeight()
方法和getHeight()
方法的区别同理。
1 | public final int getMeasuredWidth() { |
getMeasuredWidth()
获取到是mMeasuredWidth的低30位,而mMeasuredWidth是在onMeasure()
方法中通过setMeasuredDimension()
赋值的。getWidth()
获取到的是mRight - mLeft
,这两个都是在layout()
方法中通过setFrame()
赋值的。这样看上去两者获取到的值好像没有什么联系,我们可以再回头看一下layout()
方法传入的left和right参数的值,就以刚介绍过的DecorView为例吧,left参数传入了0,right参数传入了host.getMeasuredWidth()
,因此最后计算出的mRight - mLeft
就是getMeasureWidth()
方法的返回值。其实不止DecorView,所有的View在默认情况下getWidth()
的值和getMeasureWidth()
的值都是一样的,需要注意这里强调的是默认情况下,那么什么情况下这两个方法的返回值不一样呢?
我们可以重写View的layout()
方法,就像下面这样:
1 |
|
这样就会导致getWidth()
/getHeight()
获取到的值比getMeasureWidth()
/getMeasureHeight()
获取到的值大100px,虽然一般情况下不会这样做,只是为了让我们更加清楚getWidth()
/getHeight()
和getMeasureWidth()
/getMeasureHeight()
的区别。
draw阶段
通过前面的measure和layout两个流程,已经确定出了View的大小和位置,接下里就要把View显示出来了,draw的作用是就将View绘制到屏幕上。相比于前两个流程,View的绘制流程是最简单的,因为源码的逻辑很少,基本上都要靠我们自己去定义如何绘制。同样地,我们分两种情况进行分析,包括单一View的绘制和ViewGroup的绘制。
单一View的draw流程
View的绘制流程从draw()
方法开始,我们来看一下这个方法:
1 | public void draw(Canvas canvas) { |
这里精简了一下源码,可以直观地看出View的draw()
方法分为四个步骤(源码中提到了6个步骤,另外两个可以跳过的,这里就不列入了):
- 调用
drawBackground()
方法绘制背景 - 调用
onDraw()
方法绘制自身内容 - 调用
dispatchDraw()
方法绘制子View - 调用
onDrawForeground()
方法绘制装饰,包括滚动条和前景
下面我们就来分别看一下这四个方法。
View的drawBackground方法
1 | private void drawBackground(Canvas canvas) { |
drawBackground()
方法首先会获取背景Drawable,如果没有设置背景则直接返回;如果设置了背景就调用Drawable的draw()
方法完成背景的绘制,代码的逻辑还是比较简单的,我就不详细说了。
View的onDraw方法
1 | protected void onDraw(Canvas canvas) { |
onDraw()
方法可以说是我们在自定义View中最熟悉的,View的onDraw()
是一个空方法,需要子类自己决定如何进行绘制。
View的dispatchDraw方法
1 | protected void dispatchDraw(Canvas canvas) { |
View的dispatchDraw()
方法同样是一个空方法,它的作用是对子View进行绘制,因此单一View自然无需实现该方法,我们稍后会看一下ViewGroup中是如何实现该方法的。
View的onDrawForeground方法
1 | public void onDrawForeground(Canvas canvas) { |
onDrawForeground()
方法用于绘制View的一些装饰,包括滚动条和前景,我们一般很少接触到该方法,就不具体分析了。
用一张流程图总结一下单一View的draw流程:
虽然View的绘制流程可以分为以上四步,但是我们在自定义View中只需要重写onDraw()
方法,按需要进行绘制就可以了。
ViewGroup的draw流程
ViewGroup的绘制同样从draw()
方法开始,也可分为和View相同的四个步骤,这里要重点分析一下第三步调用的dispatchDraw()
方法,ViewGroup重写了该方法。其他三个步骤和View是一样的,这里就不再分析了。
1 |
|
dispatchDraw()
方法内部主要做的就是遍历所有的子View,依次调用drawChild
方法,drawChild
方法内部又会调用子View的draw()
方法,注意,这里调用的draw()
方法并不是此前分析过的那个,它有三个参数。
1 | boolean draw(Canvas canvas, ViewGroup parent, long drawingTime) { |
这里省略了大量代码,可以看出该方法内部会根据条件执行一个参数的draw()
方法(执行的条件我后面会分析),后面的流程就和单一View的绘制流程相同了。
总结一下ViewGroup的draw流程,整体步骤和单一View的draw流程是一样的,不同的是ViewGroup重写了dispatchDraw()
方法,在内部遍历子View并完成子View的绘制。
最后还是来梳理一下整个页面的draw流程,从ViewRootImpl的performDraw()
方法开始:
1 | private void performDraw() { |
performDraw()
方法内部又会调用draw()
方法,在draw()
方法中会根据是否开启了硬件加速执行相应的逻辑,硬件加速就是通过引入GPU来提高绘制和界面刷新的效率,不过也有可能导致自定义View出现问题,在API 13(Android 4.0)及以上版本中,硬件加速是默认开启的,我们可以手动关闭硬件加速。关于硬件加速我也了解得不多,这里就不多提了,感兴趣的话可以查阅一下相关资料。下面我们就分别看一下开启和关闭硬件加速的情况下都是如何完成页面绘制的吧。
- 关闭硬件加速
关闭硬件加速的情况下会执行drawSoftware()
方法,我们来看一下这个方法:
1 | private boolean drawSoftware(Surface surface, AttachInfo attachInfo, int xoff, int yoff, |
方法内部会调用mView的draw()
方法,这里的mView是DecorView,前面已经分析过了,因此现在进入了DecorView的绘制流程,接下来就和ViewGroup的绘制流程一样了,即遍历DecorView的所有子View,完成子View的绘制,如果子View是一个ViewGroup则重复该过程,直到所有的子View都绘制完成。
- 开启硬件加速
开启硬件加速的情况下会执行mAttachInfo.mThreadedRenderer.draw(mView, mAttachInfo, this, callback)
,mThreadedRenderer的类型为ThreadedRenderer,我们来看一下ThreadedRenderer的draw()
方法:
1 | void draw(View view, AttachInfo attachInfo, DrawCallbacks callbacks, |
经过一系列调用最终会调用updateViewTreeDisplayList()
方法,这里传入的view为DecorView,方法内部会根据view是否设置了PFLAG_INVALIDATED标志位来给成员变量mRecreateDisplayList赋值,由于DecorView没有设置该标志位,因此mRecreateDisplayList的值为false。接下来会调用updateDisplayListIfDirty()
方法,它定义在View中,我们来看一下:
1 | public RenderNode updateDisplayListIfDirty() { |
我们先来看一下第一个if判断的几个条件:
- mPrivateFlags & PFLAG_DRAWING_CACHE_VALID == 0
由于DecorView没有设置PFLAG_DRAWING_CACHE_VALID标志位,因此该条件满足。
- !renderNode.isValid()
isValid()
方法字面意思上是判断renderNode是否有效,那么什么时候是有效的呢?我们会发现updateDisplayListIfDirty()
方法最后调用了renderNode.end(canvas)
,点进这个end()
方法看一下:
1 | /** |
从注释Calling this method marks the display list valid and isValid() will return true中可以看出当调用了end()
方法后,isValid()
会返回true,由于此时是页面首次绘制,还没有调用过end()
方法,因此isValid()
返回false,!renderNode.isValid()
为true,该条件满足。
- mRecreateDisplayList
上面也说过了,由于设置PFLAG_INVALIDATED标志位,此时DecorView的mRecreateDisplayList值为false,该条件不满足。
由于满足了两个条件,因此会进入到第一个if判断中,接下来又是一个if判断,判断条件是renderNode.isValid() && !mRecreateDisplayList
,根据上面的分析,该条件不满足,因此不会执行if中的逻辑。接下来会判断是否设置了PFLAG_SKIP_DRAW标志位,关于这个标志位的作用我后面会分析,这里先记住ViewGroup默认情况下都会设置这个标志位,由于DecorView就是一个ViewGroup,会设置该标志位,因此会执行dispatchDraw()
方法,遍历所有子View,完成对子View的绘制,如果子View是一个ViewGroup则接着遍历下面的子View,直到所有子View都完成绘制。
ViewGroup的draw()方法调用问题
首先介绍几个Android中常见的位运算,有助于我们更好地理解源码:
a | b:为a添加标志位b
(a & b) != 0:判断a是否有标志位b
a & ~b:为a清除标志位b
a^b:取出a与b的不同部分
感叹一下,位运算在Android中还是很常见的,尤其是在View的源码中,熟悉上面这个几个位运算操作对我们阅读源码还是有很大帮助的。
下面进入正题,当我们的自定义View继承自ViewGroup时会遇到一个问题,默认情况下draw()
方法和onDraw()
方法都不会被调用,只会调用了dispatchDraw()
方法,可以自己尝试一下,我这里就不展示了。我们下面就来分析一下原因,首先来看上面分析过的三个参数的draw()
方法:
1 | boolean draw(Canvas canvas, ViewGroup parent, long drawingTime) { |
这个方法是在父View遍历子VIew依次调用drawChild()
方法后被调用的,可以很明显地看出当满足(mPrivateFlags & PFLAG_SKIP_DRAW) == PFLAG_SKIP_DRAW
条件时,执行dispatchDraw(canvas)
方法,不满足条件就执行一个参数的draw()
方法,进而执行onDraw()
方法。mPrivateFlags是View中定义的一个全局变量,用于存储各种标志位,上面的条件就是判断mPrivateFlags是否设置了PFLAG_SKIP_DRAW标志位。既然ViewGroup默认情况下不会执行draw()
方法,那么肯定是设置了PFLAG_SKIP_DRAW标志位,是在什么时候设置的呢?我们发现在ViewGroup的构造方法中调用了initViewGroup()
方法:
1 | public ViewGroup(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { |
接着来看initViewGroup()
方法:
1 | private void initViewGroup() { |
从注释ViewGroup doesn’t draw by default中也能看出ViewGroup默认情况下的确不会调用draw()
方法,在initViewGroup()
方法内部执行了setFlags(WILL_NOT_DRAW, DRAW_MASK)
,从方法名可以看出是设置了一个标志位,我们接下来看一下setFlags()
方法:
1 | void setFlags(int flags, int mask) { |
setFlags()
方法有两个参数:flags和mask,flags就是要设置的标志位,mask表示标志位的位置,mPrivateFlags和mask进行按位与运算可以得到该mask对应的标志位,举个例子,执行了setFlags(WILL_NOT_DRAW, DRAW_MASK)
后,通过mPrivateFlags & DRAW_MASK
就可以得到WILL_NOT_DRAW这个标志位。这里省略了大量代码,只保留了和DRAW_MASK相关的部分,其实View的可见状态VISIBLE、INVISIBLE和GONE也是通过标志位来实现的,感兴趣的话可以看一看。可以看出,当View设置了WILL_NOT_DRAW标志位,并且没有设置背景、焦点高亮背景或者前景(后面统称为背景)的情况下,会设置PFLAG_SKIP_DRAW标志位,由于ViewGroup默认情况下是没有设置背景的,因此会设置PFLAG_SKIP_DRAW标志位,不会执行draw()
方法,当然也不会执行onDraw()
方法。
如果想让ViewGroup的draw()
方法被执行要怎么做呢?从上面的分析中也能看出,只要ViewGroup移除了WILL_NOT_DRAW标志位或者设置了背景,就会移除PFLAG_SKIP_DRAW标志位,使得draw()
方法被调用,下面我们就看一下具体该怎么做。
- 移除WILL_NOT_DRAW标志位
View中有一个setWillNotDraw()
方法,我们来看一下:
1 | public void setWillNotDraw(boolean willNotDraw) { |
setWillNotDraw()
方法内部会根据传入的willNotDraw参数调用setFlags()
方法来设置或移除WILL_NOT_DRAW标志位,通过调用setWillNotDraw(false)
就可以移除WILL_NOT_DRAW标志位,使得ViewGroup的draw()
方法得到调用。
- 为ViewGroup设置背景(包括背景、焦点高亮背景和前景)
这里就以设置背景的setBackgroundDrawable()
方法为例分析,设置焦点高亮背景(对应setDefaultFocusHighlight()
方法)和设置前景(对应setForeground()
方法)类似。
1 | public void setBackgroundDrawable(Drawable background) { |
当设置了背景后,mPrivateFlags会移除PFLAG_SKIP_DRAW标志位,因此可以通过设置背景的方式来使得ViewGroup的draw()
方法得到执行。
通过以上两种方式就可以调用的ViewGroup的draw()
方法了,从而使得onDraw()
方法也会被调用。在开发中我们还是要考虑实际需求,因为ViewGroup本身只是一个容器,一般情况下是不需要绘制自身内容的,默认情况设置了PFLAG_SKIP_DRAW标志位也是出于系统优化的考虑,如果需要在onDraw()
中绘制内容时再通过以上两种方式移除PFLAG_SKIP_DRAW标志位,或是直接在dispatchDraw()
方法中进行绘制都可以。
开发中的常见问题
View获取宽高
根据此前的分析,View的三大流程都是在onResume()
方法调用之后才开始的,因此在onCreate()
、onStart()
和onResume()
方法中是无法通过getWidth()
/getHeight()
获取到View的宽高的。如果在开发中有这样的需求应该怎么办呢,当然还是有办法的,下面就介绍几种通过代码获取View宽高的方法。
- 在
onWindowFocusChanged()
方法中获取
重写Activity的onWindowFocusChanged()
方法,在方法内部可以获取到View的宽高。
1 |
|
需要注意,该方法在Activity获得和失去焦点时都会被调用,因此会被调用多次,不推荐采用这种方法获取View的宽高。
- 使用ViewTreeObserver监听事件
ViewTreeObserver中定义了多种监听事件,可以通过设置OnGlobalLayoutListener(当View树的状态发生改变或者View树内部的View的可见状态发生改变时会回调onGlobalLayout()
方法)和OnPreDrawListener(当View树被绘制之前会回调onPreDraw()
方法)监听,在回调方法中获取View的宽高。需要注意,回调方法可能会被执行多次,因此在获取到View的宽高后需要移除监听器。
1 | ViewTreeObserver observer = view.getViewTreeObserver(); |
- 使用View的
post()
方法
这个方法可以说是我们最熟悉的了,调用View的post()
方法,传入一个Runnable对象,在run()
方法中获取View的宽高。
1 | view.post(new Runnable() { |
简单解释一下原理,其实post()
方法内部是通过Handler来实现的,调用post()
方法后会将Runnable封装成一个同步消息添加到主线程的消息队列中,由于ViewRootImpl的scheduleTraversals()
方法内部通过开启同步屏障机制发送了一条异步消息进行View树的measure、layout和draw,因此保证了View树的三大流程执行完成后再执行消息队列中的同步消息,此时当然就可以获取到View的宽高了。
invalidate()和requestLayout()的区别
我们在自定义View时可能需要更新View的显示,比如为View添加动画等等,有两个方法是我们经常会用到的invalidate()
和requestLayout()
,下面就来具体分析一下这两个方法的区别和使用场景。
invalidate()
View的invalidate方法
1 | public void invalidate() { |
可以看出View的invalidate()
方法最终会调用invalidateInternal()
方法:
1 | void invalidateInternal(int l, int t, int r, int b, boolean invalidateCache, |
invalidateInternal()
方法中首先会根据skipInvalidate()
方法判断是否跳过绘制,如果同时满足以下三个条件就直接return,跳过重绘。
- View是不可见的
- 当前没有设置动画
- 父View的类型不是ViewGroup或者父ViewGoup不处于过渡态
接下来根据一系列条件判断是否需要重绘,如果满足以下任意一条就进行重绘。
- View已经被绘制完成并且具有边界
- invalidateCache为true并且设置了PFLAG_DRAWING_CACHE_VALID标志位,即绘制缓存可用
- 没有设置PFLAG_INVALIDATED标志位,即没有被重绘过
- fullInvalidate为true并且透明度发生了变化
接下来判断如果invalidateCache为true,就给View设置PFLAG_INVALIDATED标志位,这一步很重要,后面还会提到,通过上面的调用也能看出这里的invalidateCache传入的值为true,因此会设置这个标志位。方法的最后会调用mParent即父View的invalidateChild()
方法,将要重绘的区域damage传递给父View。下面我们来看ViewGroup的invalidateChild()
方法:
ViewGroup的invalidateChild方法
1 |
|
方法内部首先会判断是否开启了硬件加速,接下来我们分别看一下关闭和开启硬件加速情况下的重绘流程。
- 关闭硬件加速
关闭硬件加速的情况下会循环调用invalidateChildInParent()
方法,将返回值赋给parent,当parent为null时退出循环,我们来看ViewGroup的invalidateChildInParent()
方法。
1 |
|
这里省略了大量代码,主要是对子View传递过来的重绘区域进行运算处理,方法最后会返回mParent。因此在invalidateChild()
方法中会通过循环逐层调用父View的invalidateChildInParent()
方法,那么当调用到最顶层ViewGroup——DecorView的invalidateChild()
方法时,它的mParent是谁呢?我们可以回顾一下ViewRootImpl的setView()
方法:
ViewRootImpl的setView方法
1 | public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) { |
可以发现方法执行了view.assignParent(this)
,这里的view其实就是DecorView(ActivityThread的handleResumeActivity()
方法中调用wm.addView()
传过来的),我们来看一下assignParent()
方法,它定义在View中:
View的assignParent方法
1 | void assignParent(ViewParent parent) { |
很明显assignParent()
方法完成了View的成员变量mParent的赋值,因此DecorView的mParent就是上面传入的this,也就是ViewRootImpl。既然清楚了DecorView的mParent,接下来我们就来看一下ViewRootImpl的invalidateChildInParent()
方法:
ViewRootImpl的invalidateChildInParent方法
1 |
|
方法内部首先会调用checkThread()
方法:
1 | void checkThread() { |
checkThread()
方法会判断当前线程是否为主线程,如果不是主线程就直接抛出异常,因此我们需要特别注意,invalidate()
方法必须在主线程中调用。回到ViewRootImpl的invalidateChildInParent()
方法,最后调用了invalidateRectOnScreen()
方法,同时由于返回值为null,因此执行完invalidateChildInParent()
方法后parent被赋值为null,退出do-while循环。接下来我们就来看一下invalidateRectOnScreen()
方法:
1 | private void invalidateRectOnScreen(Rect dirty) { |
invalidateRectOnScreen()
方法内部会调用scheduleTraversals()
方法,这个方法我们很熟悉了,接下来会调用performTraversals()
方法,开始View的三大流程,这里再来回顾一下:
1 | private void performTraversals() { |
由于此时没有给mLayoutRequested赋值,它的默认值为false,因此不会调用measureHierarchy()
和performLayout()
方法,只调用performDraw()
方法,换句话说就是不会执行measure和layout流程,只执行draw流程,接下来就是调用DecorView的draw()
方法,遍历DecorView的子View,逐层完成子View的绘制。
- 开启硬件加速
开启硬件加速时在ViewGroup的invalidateChild()
方法中会调用onDescendantInvalidated()
方法并直接返回,不会执行后面的invalidateChildInParent()
方法,我们来看一下onDescendantInvalidated()
方法:
1 |
|
方法内部会调用mParent的onDescendantInvalidated()
方法,和invalidateChildInParent()
类似,接下来会逐级调用父View的onDescendantInvalidated()
方法,最后来到ViewRootImpl的onDescendantInvalidated()
方法。
ViewRootImpl的onDescendantInvalidated方法
1 |
|
接下来会调用ViewRootImpl的invalidate
方法:
1 | void invalidate() { |
可以看到这里同样调用了scheduleTraversals()
方法,之后的流程和关闭硬件加速的情况类似,同样是调用performDraw()
方法,不同的是开启硬件加速的情况下会执行mAttachInfo.mThreadedRenderer.draw(mView, mAttachInfo, this, callback)
,前面也分析过了,之后会依次调用ThreadedRenderer的updateRootDisplayList()
、updateViewTreeDisplayList()
方法。
1 | private void updateViewTreeDisplayList(View view) { |
这里需要注意,根据此前的分析,在调用View的invalidate()
方法后,会给当前View设置PFLAG_INVALIDATED标志位,因此它的mRecreateDisplayList变量值为true,而其他的父级View由于没有设置PFLAG_INVALIDATED标志位,mRecreateDisplayList值为false。接下来会调用view的updateDisplayListIfDirty()
方法,这里的view是DecorView。
1 | public RenderNode updateDisplayListIfDirty() { |
这里就和此前的分析不同了,由于页面首次绘制完成后执行了renderNode.end(canvas)
,因此这里renderNode.isValid()
返回值为true,而DecorView的mRecreateDisplayList值为false,因不会执行后面的重新绘制逻辑,取而代之的是调用dispatchGetDisplayList()
方法,我们来看一下这个方法:
1 |
|
dispatchGetDisplayList()
方法内部会遍历子View,依次调用recreateChildDisplayList()
方法,不难看出recreateChildDisplayList()
方法和updateViewTreeDisplayList()
方法很像,接下来同样会调用updateDisplayListIfDirty()
方法,对于没有设置PFLAG_INVALIDATED标志位的View,它的mRecreateDisplayList值为false,会重复上面的过程,即调用dispatchGetDisplayList()
方法;而对于调用了invalidate()
方法的View,由于设置了PFLAG_INVALIDATED标志位,它的mRecreateDisplayList值为true,会执行updateDisplayListIfDirty()
方法最后的重绘逻辑,即调用dispatchDraw()
方法或者draw()
方法完成自身及子View的绘制。
最后总结一下invalidate()
方法,调用View的invalidate()
方法后会逐级调用父View的方法,最终导致ViewRootImpl的scheduleTraversals()
方法被调用,进而调用performTraversals()
方法。由于mLayoutRequested的值为false,因此不会执行measure和layout流程,只执行draw流程。draw流程的执行过程和是否开启硬件加速有关:
- 如果关闭了硬件加速,从DecorView开始的所有View都会重新完成绘制
- 如果开启了硬件加速,只有调用
invalidate()
方法的View(包括它的子View)会完成重新绘制
由此也可以看出,开启硬件加速确实可以提高重绘的效率。
此外,由于invalidate()
方法必须在主线程中调用,那么如果我们想要在子线程中刷新视图要怎么做呢?不用担心,官方还为我们提供了一个postInvalidate()
方法,其实从名称上我们也能猜到它的作用了,就是用于在子线程中刷新视图,简单看一下它的定义:
1 | public void postInvalidate() { |
哈哈,果然还是用到了Handler,mHandler是ViewRootImpl中的一个成员变量,类型为ViewRootHandler,我们来看一下ViewRootHandler对MSG_INVALIDATE消息的处理:
1 | final class ViewRootHandler extends Handler { |
可以看出最后还是调用了invalidate()
方法,因此postInvalidate()
方法其实就是通过Handler完成了线程的切换,使得invalidate()
方法在主线程中被调用。
requestLayout()
View的requestLayout方法
1 | public void requestLayout() { |
requestLayout()
方法内部会调用mParent即父View的requestLayout()
方法,最终会来到ViewRootImpl的requestLayout()
方法:
ViewRootImpl的requestLayout方法
1 |
|
首先还是会进行线程的检查,因此requestLayout()
方法同样只能在主线程中调用。接着会把mLayoutRequested赋值为true并调用scheduleTraversals()
方法。后面的流程相信也不用我多说了,调用performTraversals()
方法,由于将mLayoutRequested赋值为true,因此会依次执行measureHierarchy()
、performLayout()
和performDraw()
方法,开始View的三大流程。
到这里还没完,我们需要探究一下View调用requrestLayout()
是否会导致View树中的所有View都进行重新测量、布局和绘制。我们注意到调用requestLayout()
方法后,会为当前View及所有父级View添加PFLAG_FORCE_LAYOUT和PFLAG_INVALIDATED标志位。首先来回顾一下View的measure()
方法:
1 | public final void measure(int widthMeasureSpec, int heightMeasureSpec) { |
可以看出,当View设置了PFLAG_FORCE_LAYOUT标志位后,forceLayout的值为true,因此会执行onMeasure()
方法,而对于没有设置PFLAG_FORCE_LAYOUT标志位的View,需要判断测量尺寸是否发生了改变,如果改变了才会调用onMeasure()
方法。在调用onMeasure()
方法后会给View设置PFLAG_LAYOUT_REQUIRED标志位,我们再来看View的layout()
方法:
1 | public void layout(int l, int t, int r, int b) { |
对于设置了PFLAG_LAYOUT_REQUIRED标志位的View,onLayout()
方法肯定会执行,另一种情况就是View的四个顶点坐标发生改变,也会执行onLayout()
方法。
结合上面的分析可以得出结论,当View调用了requestLayout()
方法后,自身及父级View的onMeasure()
和onLayout()
方法会被调用,对于它的子View,onMeasure()
和onLayout()
方法不一定被调用。
对于draw流程,前面分析过performDraw()
方法会调用ViewRootImpl中的draw()
方法:
ViewRootImpl的draw方法
1 | private boolean draw(boolean fullRedrawNeeded) { |
dirty指向ViewRootImpl中的一个成员变量mDirty,类型为Rect,在ViewRootImpl的invalidate()
方法中会调用set()
方法为其设置四个边界值,由于此时没有调用invalidate()
方法,因此mDirty.isEmpty()
返回true,不会执行后面的绘制方法,因此整个View树不会进行重新绘制。不过也有这样一种情况,我们知道在执行VIew的layout流程时会调用setFrame()
方法,在setFrame()
方法中有这样的逻辑:
1 | int oldWidth = mRight - mLeft; |
可以看出当View的宽或高发生改变时会调用invalidate()
方法,导致View的重新绘制。
最后总结一下invalidate()
和requestLayout()
的异同:
相同点
1.invalidate()
和requestLayout()
方法最终都会调用ViewRootImpl的performTraversals()
方法。
不同点
1.invalidate()
方法不会执行measureHierarchy()
和performLayout()
方法,也就不会执行measure和layout流程,只执行draw流程,如果开启了硬件加速则只进行调用者View的重绘。
2.requestLayout()
方法会依次measureHierarchy()
、performLayout()
和performDraw()
方法,调用者View和它的父级View会重新进行measure、layout,一般情况下不会执行draw流程,子View不一定会重新measure和layout。
综上,当只需要进行重新绘制时就调用invalidate()
,如果需要重新测量和布局就调用requestLayout()
,但是requestLayout()
不保证进行重新绘制,如果要进行重新绘制可以再手动调用invalidate()
。
下面就以一个简单的例子验证一下上面的几个结论,我定义了两个ViewGroup和一个View,代码很简单,如下所示:
MyViewGroup1.java
1 | public class MyViewGroup1 extends ViewGroup { |
MyViewGroup2.java
1 | public class MyViewGroup2 extends ViewGroup { |
MyView.java
1 | public class MyView extends View { |
为了保证ViewGroup的onDraw()
方法执行,我在构造方法中调用了setWillNotDraw(false)
。布局文件也很简单,一个三级的嵌套:
activity_test.xml
1 |
|
运行后的效果如下:
我给三个View添加了点击事件,点击后分别调用自身的invalidate()
或requestLayout()
方法,下面分情况看一下。
- 调用
invalidate()
方法
开启硬件加速
调用MyView的invalidate()
方法:
调用MyViewGroup2的invalidate()
方法:
调用MyViewGroup1的invalidate()
方法:
关闭硬件加速
调用MyView的invalidate()
方法:
调用MyViewGroup2的invalidate()
方法:
调用MyViewGroup1的invalidate()
方法:
可以看出,关闭硬件加速时,调用任何一个View的invalidate()
方法都会导致整个View树的重新绘制;开启硬件加速时,调用哪一个View的invalidate()
方法就会重绘哪一个View。invalidate()
方法不会导致onMeasure()
和onLayout()
被调用。
- 调用
requestLayout()
方法
开启硬件加速
调用MyView的requestLayout()
方法:
调用MyViewGroup2的requestLayout()
方法:
调用MyViewGroup1的requestLayout()
方法:
关闭硬件加速
调用MyView的requestLayout()
方法:
调用MyViewGroup2的requestLayout()
方法:
调用MyViewGroup1的requestLayout()
方法:
可以看出此时是否开启硬件加速对于requestLayout()
方法的调用流程没有影响,调用View的requestLayout()
方法会导致自身及其父View的onMeasure()
和onLayout()
方法被调用,并不会调用onDraw()
方法进行重绘,当然前面也分析过了,onDraw()
方法不是一定不会被调用,当View重新绘制时硬件加速的作用就会有所体现了。
总结
这篇文章其实算是知识点总结,篇幅很长,很多地方我都想尽可能涵盖多一些知识点而不是直接一笔带过,前前后后整理了差不多一个月时间吧,期间阅读了很多优秀的文章,对于我自己来说收获还是很大的,让我更加系统地认识了View的工作原理,源码的阅读能力也有了一定的提升。自定义View这块对于我来说一直是一块难啃的骨头,了解View的工作原理算是打好了坚实的基础,纸上得来终觉浅,绝知此事要躬行,想要真正要提高自己的自定义View水平还是要亲自写几个实例来练习。
由于自身水平的原因,对于文章中分析得不正确的地方,欢迎大家交流指出。
参考文章
《Android开发艺术探索》
Android:一篇文章带你完全梳理自定义View工作流程!
死磕Android_View工作原理你需要知道的一切
invalidate、postInvalidate与requestLayout浅析