天天看点

深入理解View知识系列二- View底层工作原理以及View的绘制流程

一般我们都知道一个View到展示出来会经过onMeasure、onLayout、onDraw三个方法,但是在分析完了setContentView后发现这几个方法都还没有执行,这篇将会上一篇的基础上继续分析View的工作原理

深入理解View知识系列一- setContentView和LayoutInflater源码原理分析

深入理解View知识系列二- View底层工作原理以及View的绘制流程

深入理解View知识系列三-Window机制、Canvas的由来、Android事件的由来

深入理解View知识系列四-View的测量规则以及三大方法流程

本篇你会学到什么?

  • Activitiy是在哪里开始准备显示View的
  • View的三大方法是在什么时候开始执行的,又是再哪里被调用的
  • 我们经常使用的View.post后就可以获取到View的宽高,为什么呢?
  • View的requestLayout,invalidate请求重绘的原理
  • 子线程真的不能更新ui吗?为什么

上一篇回顾

我们带着问题在上一篇的基础上继续分析,View的绘制流程及工作原理,在正式分析之前我们先回顾一下上一篇的内容.

setContentView

  • 我们知道了Activity的三个setContentView方法内部全部调用了getWindow.setContentView()
  • Activity的getWindow类型是PhoneWindow,而且也是Window的唯一实现类,PhoneWindow在Activity的attach方法中被初始化,并且设置了一些回调接口指向自己,例如Window的Callback接口,这个接口中有不少我们熟悉的方法,例如dispatchTouchEvent等。
  • 在PhoneWindow的setContentView方法中主要执行三个逻辑

1.判断装在我们设置布局的FrameLayout是否为空,如果为空调用installDecor方法,方法中首先会先执行generateDecor()创建DecorView,接着执行generateLayout()方法设置一些Window的样式,根据样式来选择需要加载的布局,然后将布局添加到DecorView中,最后找到id为content的FrameLayout,也就是来装我们设置布局的父View。

2.通过LayoutInflater将我们设置的布局添加到id为content的FrameLayout中

3.回调Callback接口的onContentChanged()方法,这个方法在Activity中是个空实现。

LayoutInflater

  • PhoneWindow中的LayoutInflater是在构造函数中被创建,创建的方式和我们平时使用一样调用的是LayoutInflater.from(content);
  • LayoutInflater.from方法中就是封装了context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
  • LayoutInflater中一共有4个inflater方法,其中分为两类,一类是传入资源id的,一类是传入XmlPullParser的,但是最终都会执行带xml解析器的三参方法中,这里注意第三个参数源码中传入的是root!=null,该参数代表了是否将加载后的View添加到父View中,如果使用了两个参数的inflater,并且传入的父view不为空,则默认会将加载后的布局添加到父view,并最后返回的是父view。
  • inflater方法中首先会获取到xml的跟节点,接着判断该节点是否为merger标签,如果是merger标签则继续判断传入的父view是否为空,或者第三个参数是否为flase,如果满足一项则直接抛出异常,因为merger标签只是为了减少布局的嵌套才存在,他并不是View的子类,所以不能单独存在,反之则会执行rInflate方法进行递归加载所有子View。
  • 如果不是merger标签则会走到else方法中,先活着节点的名称,根据名称创建View,在创建View的逻辑中最终全部会调用createView方法,在这之前首先会判断是否存在自定义的加载工厂,如果存在则调用加载工厂的创建View方法,接着通过名称中是否包含 . 来判断是否为Android自带的控件,如果是Android自带的控件第二个参数中传入的值为android.view.,反之传入null
  • 在createView方法中创建View的时候会执行如下逻辑

1.首先会判断这个名称的构造函数是否存在,如果存在则还需要验证这个函数所属的ClassLoader是否合法,如果不合法则会置空这个构造函数并清除缓存。

2.接着继续判断构造函数是否为空,如果为空则会通过一个三元运算来加载这个名称的Class对象,这个三元运算主要是用来判断是否需要拼接要加载Class的名字。

3.然后会判断是否存在Filter过滤器,这个过滤器是用来判断是否可以创建这个Class的View对象,通过查看设置过滤器的RemoteViews和AppWeightHostView都是通过clazz.isAnnotationPresent(RemoteView.class)来判断的,如果返回为flase则执行抛出异常。

4.或者该Class的构造函数并存入HashMap中

5.在else逻辑中,主要是用来优化存在过滤器的情况,为了优化性能,系统只会在第一次的时候去验证是否允许创建这个View,并将结果存入HashMap中,下次只会取map中结果进行判断。

6.通过构造函数实例化这个View并返回

  • 在创建了跟节点的View后,接着同样会递归加载所有的子view,在加载的过程中会根据不同的标签进行不同的操作,例如merger、include。如果是merger则直接抛出异常,因为merger只能作为根节点。如果是include则会拿到layout属性设置的布局文件,如果未设置这个属性执行抛出异常,接着也是根据标签进行递归加载逻辑类似。
  • inflater最后根据传入的父view和第三个参数的值进行判断是返回父view还是返回刚加载的View

好了,回顾完了,在上一次讲的setContentView过程中只是初始化了PhoneWindow、DecorView并将我们设置的View入到了id为content的FrameLayout中,这时Activity的界面还是不可见的,因为View还没有开始绘制的流程呢,那么Activity中DecorView绘制的流程到底在哪里开始的呢?其实是在ActivityThread中的handleResumeActivity()方法中,下面正式开始分析。

本篇源码基于Android 7.1.1

1.逻辑开始点:ActivityThread中的handleResumeActivity()

这里又提到了ActivityThread这个类,简单说一下这个类,要知道所有的程序都是需要一个入口的,Android也不例外,ActivityThread就是Android应用的入口类,Activity、Service、ContentProvider几乎都直接或者间接在这里调度。现在暂时知道这么多就好了,先不要纠结这个类,后续的会专门在四大组件系列去讲。

源码位置:/frameworks/base/core/java/android/app/ActivityThread.java

2. 上面的代码中主要执行如下了三个逻辑。

  1. 首先调用执行了performResumeActivity方法,在这里方法中会调用Activity的onResume方法
  2. 获取Activity的PhoneWindow,接着拿到PhoneWindow中的DecorView并设置为隐藏,获取Activity的中WindowManger并执行addView方法将DecorView添加到Window中。
  3. 执行Activity的makeVisible方法展示DecorView,方法中就是调用了View.VISIBLE

而且我们通过上面的可以总结两个问题。

  1. 其实在Activity的onResume方法执行了以后才开始将DecorView添加到Window中,换句话说,也就是在onResume方法中是不能直接获取View的宽度等参数的,因为这个时候连DecorView都没有添加到Window中呢,所以这时也还没有执行View的三大流程呢,又如何产生这是东西。
  2. DecorView最终也是通过WindowManager来添加的,或者说Activity可以显示内容其实也是通过WindowManager添加View来实现的,其实Android中所有设计View展示的最终全部都是通过WindowManager的addView来添加的,例如PopupWindow、Dialog、Toast等,下一篇会详细分析

3.在上面的代码中,使用了ViewManager执行了addView方法,在上一篇我们简单的说过了,WindowManger是继承自ViewManger的,而且WindowManger中的addView、updateViewLayout、removeView全部都是ViewManger中的方法,下面先简单看一下这两个类的声明

4.我们可以看到WindowManager也是一个接口,它的具体实现是WindowMangerImpl,那么我们继续上面的逻辑去看一下它的addView方法,顺便看一下ViewManger中的其余两个方法

源码位置:/frameworks/base/core/java/android/view/WindowManagerImpl.java

5.可以看到WindowManagerImpl中三个方法的实现全部都桥接到了WindowManagerGlobal中,并且这个类是单例的。我们只分析addView方法,其他方法原理相通

源码位置:/frameworks/base/core/java/android/view/WindowManagerGlobal.java

6. 在WindowManagerGlobal中的addView方法中主要有三个逻辑如下,并且我们通过上面的代码可以看出来WindowManager每次在添加View的时候都需要创建一个ViewRootImpl,或者说每个Window中都会对应一个ViewRootImple。

  1. 检查参数的合法性
  2. 创建ViewRootImpl,并将要添加的view、ViewRootImpl和LayoutParams缓存起来
  3. 最后调用了ViewRootImpl的setView方法后逻辑跳转到ViewRootImpl中。

源码位置:/frameworks/base/core/java/android/view/ViewRootImpl.java

7. ViewRootImpl的setView方法相等的长,最重要的是执行了上面的两个方法,一个是requestLayout方法,这个方法中最终开始了绘制的流程,但是并不是直接开始的,一会我们再分析。还有一个就是通过mWindowSession的addToDisplay最终在WindowManagerService中完成Window的添加过程,至于到首先我们先来看一下requestLayout这个方法,WindowSession的addToDisplay逻辑和原理我们会在下次讲解Window时在去讲,还有就是要记住这里调用了view.assignParent(this),下面分析View的requestLayout原理时会说

源码位置:/frameworks/base/core/java/android/view/ViewRootImpl.java

8.上面的代码中绘制的入口跳转到了scheduleTraversals中,在看这个方法前我们先来看一下checkThread这个方法

源码位置:/frameworks/base/core/java/android/view/ViewRootImpl.java

9.这里看到了在开始绘制前首先会检查线程,而且上面抛出的异常再熟悉不过了,子线程更新ui了,这里就说到了最开始说的子线程真的不能更新ui吗?答案是可以的,但是有一个前提条件就是必须是在ViewRootImpl没有创建之前,简单说可以在Activitiy的onCreate里边开一个线程去更新ui是不会报错的,但是这个线程不能太耗时,因为Activity的绘制入口是在onResume以后开始的,但是也不能太耗时,毕竟我们的Activitiy只是一个回调方法而已,当ViewRootImpl被创建后就会抛出异常了。下面我们就去看一下scheduleTraversals的方法吧

源码位置:/frameworks/base/core/java/android/view/ViewRootImpl.java

10. scheduleTraversals中开启个消息屏障,目的是为了使View更快速的布局和绘制完成。然后执行mChoreographer的postCallback,Choreographer这个类是用来异步更新ui的,里面也是使用了Handler,也就是在这里真正的开始了绘制的流程,这个类还涉及了Android中的VSYNC机制,感兴趣可以自己查阅。接着我们来看一下这个Runnable

源码位置:/frameworks/base/core/java/android/view/ViewRootImpl.java

11. 终于到了真正的绘制绘制起点了,这个方法相等的长,有800行代码,我们只截取部分重要的代码

源码位置:/frameworks/base/core/java/android/view/ViewRootImpl.java

13.好了,到了这里就开始了View真正的绘制流程了,至于具体的每个流程会在下几次分别的详情的去分析,这一次我们只是为了弄清View的工作原理和整体的流程,我们来看一下getRunQueue().executeActions这个方法有什么用?,在说这个方法之前我们要先说一下平时我们经常使用View.post来获取View的宽高信息,但是为什么这样就可以获取到呢,我们去看一下View.post中的源码。

源码位置:/frameworks/base/core/java/android/view/View.java

14.这个AttachInfo是在哪里被赋值呢?我们再回去看一下ViewRootImpl中performTraversals的一行代码host.dispatchAttachedToWindow(mAttachInfo, 0),host就是一个顶层的View,但是这个View,而这个AttachInfo则是在ViewRootImpl中传过去的,我们去View中看一下

源码位置:/frameworks/base/core/java/android/view/View.java

15.好了接着上边的View.post说,这个方法是在View被添加到窗口后,马上绘制前被回调的方法,那么也就是说我们的View在被添加到窗口之前这个mAttachInfo就是null,那么就会走到 ViewRootImpl.getRunQueue().post(action)这个代码,我们去VIewRootImpl中去看一下

源码位置:/frameworks/base/core/java/android/view/ViewRootImpl.java

16.看上面的代码可以看出最终是调用了ViewRootImpl类中的RunQueue.post方法,接着会将传过来的Runnable封装成一个HandlerAction,然后存入ArrayList中,那么我们现在再来看一下ViewRootImpl中performTraversals方法里的一句代码, getRunQueue().executeActions(mAttachInfo.mHandler),上面我们已经知道了getRunQueue就是返回了RunQueue,我们看看RunQueue的executeActions方法

源码位置:/frameworks/base/core/java/android/view/ViewRootImpl.java

17.到这里是不是豁然开朗了呢?还不明白?好吧,其实就是在View还没有View完成的时候是不会执行View.post过来的Runnable的,只是将他添加到了一个集合中,然后当绘制的时候会执行executeActions这句话遍历所有的消息然后将这些消息添加到队列里依次的执行,真正执行被post过来的Runnable时View已经执行完了绘制流程,所以我们可以通过View.post获取到View的宽高属性。

18.完了我们再想一个问题,我们经常会使用View.requestLayout来重绘界面,那么它到底是怎么回事呢,我们来看一下View中的这个方法

源码位置:/frameworks/base/core/java/android/view/View.java

18.mParent是ViewParent类型,而且我们知道了被赋值的方法是assignParent,那么这个方法是怎么调用的呢?首先我们来回顾一下之前分析的东西,在上面的ViewRootImpl中的setView方法中有这个方法的调用view.assignParent(this),那么这个时候这个View是视图的最顶层View,也就是DecorView,它的Parent是ViewRootImpl,那么DecorView是一个FrameLayout的ViewGroup,它又会在添加View的时候给所属子View赋值,接着子View如果还是ViewGroup则是一样的逻辑,直到所有的View添加完毕,那么这个时候所有的View也就存在了Parent的值,还记得我们分析的LayoutInflater的过程吗,最终也是通过root.addView来完成添加的,所以说所有的子View都会存在它的父Parent。

源码位置:/frameworks/base/core/java/android/view/ViewGroup.java

19.那么到这里我们就知道了所有的View都会存在它自己的Parent,而最顶层的View的Parent是ViewRootImpl,那么也就是说View在调用requestLayout的会层层向上调用,直到最顶层的ViewRootImpl,也就是我们之前分析完了的ViewRootImpl,在这里会开始View的绘制流程,我们上面已经分析过了,至于invalidate的原理其实和requestLayout的原理是一样的,最终都会执行到ViewRootImpl中,这里由于篇幅的缘故就不再贴代码了,还有就是这一篇我们还有一个分支没有分析就是WMS添加window的过程,会在下一篇去讲。

总结:

继续阅读