Android开发者都知道要想建立一个页面,最普遍常见的做法就是新建一个Activity,并且在res/layout中新建一个Layout布局,然后Activity继承自Activity或者AppCompatActivity之后重写onCreate方法,最后使用setContentView(resId)让这个Activity和布局id为resId的布局产生联系,这样跳转到这个Activity的时候就可以显示resId的布局了。
那么大家是否想过了,为什么setContentView()这个方法就可以将一个xml格式的布局文件显示在手机屏幕上?在它的后面系统究竟做了什么操作?
可以看看setContentView()的源码是这样的(注意:这个是基于Android api26的情况下)
AppCompatActivity的setContentView实现由于该activity是继承AppCompatActivity,所以在AppCompatActivity下面的setContentView实现有三个。相信大家也能看到,AppCompatActivity不直接操作view,而是通过一个叫AppCompatDelegate的类进行view的操作。其中getDelegate()方法是这样的
AppCompatActivity里面的getDelegate方法那么,问题就来了,这个mDelegate这个AppCompatDelegate类是干什么的呢?有什么作用?好,继续跟踪下去,探索AppCompatDelegate.create方法
AppCompatDelegate.create方法注意,create方法是定义在AppCompatDelegate这个类下面的静态方法,一般普通的activity都会调用最上面的那个create(activity, activity.getWindow(), callback)方法,本质来说最终都会调用最下面的需要判断Build.VERSION.SDK_INT版本的create方法。那么其中的这些个AppCompatDelegateImplXX的关系是这样的
AppCompatDelegateImplN extends AppCompatDelegateImplV23
AppCompatDelegateImplV23 extends AppCompatDelegateImplV14
AppCompatDelegateImplV14 extends AppCompatDelegateImplV11
AppCompatDelegateImplV11 extends AppCompatDelegateImplV9
AppCompatDelegateImplV9 extends AppCompatDelegateImplBase
那么这个一步步继承过来到最后的AppCompatDelegateImplBase又是什么呢?会发现,这个AppCompatDelegateImplBase是继承AppCompateDelegate的。那么回过头来看看AppCompatDelegate这个类,首先它是个抽象类,其次这个类里面定义了很多activity生命周期的抽象方法,如下:
AppCompatDelegate里面定义的部分抽象方法看到这里,可能大家心里差不多明白了,这个AppCompatDelegate更像是个代理的作用,所有不同版本的activity的生命周期方法也好、setContentView等方法也好,都可以通过继承AppCompatDelegate这个类产生不同的操作。比如,在api是26的手机上,进行setContentView实际上是AppCompatDelegate的子类AppCompatImplN的setContentView方法;那么同理在api是10的手机上,进行setContentView实际上是AppCompatDelegate的子类AppCompatDelegateImplV9的setContentView方法。从Java语言角度来看,这是多态和继承的典型表现。AppCompatDelegate也是官方实现夜间模式最好的工具。
继续往下走,在众多的AppCompatDelegateImplBase的实现类中,除了AppCompatDelegateImplV9这个实现类以外,发现均没有重写setContentView这个方法,那么最终activity中的setContentView经过一系列的辗转,最终是在这里面实现的。
AppCompatDelegateImplV9中的setContentView方法可以小结一下,在继承自AppCompatActivity的activity中,setContentView方法在系统根据不同的api版本找到AppCompatDelegate的对应版本的实现子类,经过一系列的继承,最终会在AppCompatDelegateImplV9中进行实现。
那么开始分析setContentView方法中仅仅只有五行的代码:
第一个是ensureSubDecor这个方法。这个方法在AppCompatDelegateImplV9中可以找到
AppCompatDelegateImplV9中的ensureSubDecor方法mSubDecorInstalled是个boolean类型的变量,这个变量是用来标识window sub-decor layout布局是否初始化的,在mSubDecor初始化后会发现mSubDecorInstalled会被赋值为true。那么变量mSubDecor是什么东西呢?往前找到定义变量的地方,会发现
private ViewGroup mSubDecor;
那么,这个mSubDecor是个ViewGroup,好,接下来来看createSubDecor方法,看看这个ViewGroup类型的mSubDecor是如何创建出来的。
createSubDecor方法比较长,进行分段分析:
createSubDecor方法的上部分可以看到,在最开始,是先获取了AppCompatTheme属性的TypedArray,然后我们会找到一个经常出现的一个异常
"You need to use a Theme.AppCompat theme (or descendant) with this activity."
也就是经常说的使用了AppCompatActivity却没有指定Theme.AppCompat主题。
其中,最关键的一句话:mWindow.getDecorView()。这句话放这里是什么意思呢?通过注释大概了解到是要确保Window已经初始化了该Window的decor。
那么先来研究一下mWindow.getDecorView这个方法。首先,这个mWindow是个全局变量,那么它在哪里初始化赋值的呢?我们通过跟踪,会发现mWindow这个对象是父类AppCompatDelegateImplBase中的一个Window类型变量,赋值实在父类AppCompatDelegateImplBase的构造方法中赋值的。
mWindow对象的定义和赋值那么继续找下去,会在AppCompatDelegate中的create方法中找到,如下图:
在AppCompatDelegate中传进来的activity.getWindow方法那么继续跟踪,activity.getWindow又是什么东西呢?接下去会发现mWindow对象是定义在Activity里面的一个全局变量,mWindow赋值是在Activity的attach方法中赋值的。
在Activity中的定义mWindow对象 在Activity中赋值mWindow对象Activity方法attach这个是涉及到了Activity的启动流程,它是在启动一个Activity过程中由android.app.ActivityThread.performLaunchActivity()这个方法调用的,暂时不去深究。
Window是Android里面的一个抽象类,而PhoneWindow是Window的唯一的实现类。去继续研究PhoneWindow类,如果有无法打开PhoneWindow这个源码的情况,可以找到本地文件下的android.jar包,复制到Android studio里面的libs目录下,添加为依赖包,就可以打开PhoneWindow源码了。
PhoneWindow的构造方法
PhoneWindow的构造方法构造方法中很重要的一个全局变量,mDecor,这个是DecorView类的实例。那么mDecor = (Decor) preservedWindow.getDecorView()这个方法是给DecorView类型进行赋值的方法。我们经常说的Android最底层的布局是DecorView,那么实际上DecorView是一个继承自FrameLayout的自定义布局。那么如何mDecor是如何初始化的呢?
继续看getDecorView这个方法,这个方法很简单,就是判断mDecor为null的话就执行installDecor方法
PhoneWindow中的installDecor方法那么类型为DecorView的实例mDecor是通过generateDecor方法去初始化的
PhoneWindow下面初始化mDecor简而言之,就是在这个方法里面new了一个DecorView对象,赋值给mDector。
还有个重要的变量,mContentParent这个,是ViewGroup的实例,是通过generateLayout方法进行初始化的,注意,generateLayout是需要传入刚刚初始化好的mDecor对象的。
着重看下contentParent的初始化
在PhoneWindow类下的generateLayout方法里面的对contentParent的初始化有没有发现很熟悉,findViewById方法,里面的ID_ANDROID_CONTENT实际上就是com.android.internal.R.id.content。所以PhoneWindow里面的mContentParent实际上是通过findViewById找到控件id为content而来的。
再回过头来看,之前所说的在AppCompatDelegateImplV9里面的createSubDecor方法里面的ViewGroup类型mSubDecor是如何初始化的呢?
继续来看createSubDecor方法下半部分
createSubDecor中的下班部分(省略部分赋值)由于根据主题的设定不一样,这里面的subDecor有不同的赋值,不仅仅只是包括上图几项。
继续看下去,最关键是是mWindow.setContentView(subDecor);
createSubDecor方法里面的mWindow.setContentView方法那么在之前大段篇幅讲的是mWindow对象是什么,是从哪里来的,在哪里定义,在哪里初始化,现在,在这个地方,如果已经明白了mWindow对象的来龙去脉,那么这里就不难理解,我们看下mWindow.setContentView方法。由于Window唯一抽象类是PhoneWindow,那么需要去PhoneWindow里面去找setContentView方法
PhoneWindow里面的setContentView方法不知道大家还记得,mContentParent是什么?它是一个ViewGroup对象,并且是通过findViewById找到id为content的控件来的。那么我们在AppCompatDelegateImplV9里面的setContentView里面的ensureSubDecor里面的createSubDector方法里面,上半部分已经通过mWindow.getDector方法来进行generateDector和generateLayout的初始化,即mDector和mContentParent已经准备好了,那么在AppCompatDelegateImplV9里面的setContentView里面的ensureSubDecor里面的createSubDector方法里面下半部分的mWindow.setContentView,最后直接进行mContentParent.addView方法,将AppCompatDelegateImplV9里面辛苦创建出来的ViewGroup类型subDecor添加到PhoneWindow对象里面的父容器mContentParent里面去了。
那么自此,最主要最复杂的ensureSubDecor方法已经完成了。
AppCompatDelegateImplV9中的setContentView方法接下去的就好理解了,同样也是从android.R.id.content这个控件id找到父容器contentParent,按照PhoneWindow中的generateLayoutf方法里面来分析,这里的contentParent和PhoneWindow里面的mContentParent指向的是同一个控件。
LayoutInflate.from(mContext).inflate(resId, contentParent);
在类LayoutInflate中找到
在LayoutInflate中的inflate方法当然,如果我们最开始的Activity不是继承自AppCompatActivity的话,而且继承Activity,那么上述的分析流程是否还是成立的?
答案是肯定的,可以看到
Activity下面的setContentViewgetWindow()方法是返回当前的Window对象,即mWindow。那么作为Window唯一的实现类PhoneWindow,getWindow().setContentView在PhoneWindow中的方法又回到了setContentView方法里面,所以实际上是和AppCompatDelegateImplV9里面的createSubDecor里面的mWindow.setContentView一样的。Google在推出AppcompatActivity肯定是考虑过与以前版本的Activity兼容的,本质上是相通的。
总结一下,在AppCompatActivity中,系统会创建一个类型为ViewGroup的mSubDecor对象,该对象是需要根据主题属性inflate成一个ViewGroup对象(包含是否是悬浮的、是否有ActionBar、是不是OverlayActionMode等等),最终是需要用PhoneWindow对象mWindow调用setContentView方法,将该具有AppCompat主题属性的mSubDecor当作参数传过去,添加到由PhoneWindow通过findViewById方法找到id为content的控件mContentParent调用addView方法,将mSubDecor添加到根ViewGroup即mContentParent中去。
那么相对的,如果是在Activity中,则会简单很多,不会有AppCompatDelegate对象,直接会调用mWindow的setContentView方法,殊途同归。不过需要注意的一点是,如果直接调用PhoneWindow里面的setContentView(int resId),那么布局文件的解析工作是需要在这里进行的;如果是在AppCompatDelegate的createSubDecor方法调用mWindow.setContentView(View view),那么在PhoneWindow里面仅仅只是将mSubDecor添加到mContentParent里面而已,布局的解析还是需要在AppCompatDelegateImplV9里面的setContentView里面完成的,可以对比一下
思考:之前在PhoneWindow类里面大量出现的DecorView实例mDecorView和PhoneWindow里面的ViewGroup类型实例mContentParent的关系是什么呢?这个可以通过PhoneWindow中的方法generateLayout找到答案。
PhoneWindow类的generateLayout方法节选其中有个int类型的
layoutResource对象,发现在各个判断中都有赋值,那么我们随便选个layout去看看
系统提供的R.layout.screen_simple不管是哪个布局,其中肯定会定义一个FrameLayout,并且id固定为"content",那么解析去的mDecor.startChanging和mDecor.onResourceLoaded(mLayoutInflater, layoutResource)大家肯定也才猜想得出来,作用就是去解析layoutResource的布局,并且添加到mDecor中。
DecorView类里面的onResourcesLoaded方法因为DecorView本身就是个FrameLayout,所以自然而然的可以使用addView方法
到这一步,我们心中大概很清楚,mDecor是Activity的底层View,其中有个固定id为content的控件(实际上都是FrameLayout),PhoneWindow可以通过findViewById直接获取到该FrameLayout,例如PhoneWindow里面的mContentParent就是这么来的,然后我们所有的在AppCompatActivity里面也好,还是本身就在Activity里面也好,所有控件的添加、移除等都是通过mContentParent来进行控制操作的。
那么自此,setContentView方法已经差不多研究完了。其实还遗留下一个问题,那么就是mDecorView这个代表是一个FrameLayout的对象,它在Activity被加载进来了,那么它是如何显示在屏幕上的呢?需要借助于ActivityThread的performResumeActivity方法。需要使用WindowManager对象调用addView方法。
网友评论