从 setContentView 走进 Android

<b>个人博客地址: <a href="https://kehanchenk.github.io/">https://kehanchenk.github.io/</a></b>
<h2>前言</h2>

说起 setContentView,大家肯定不陌生,从名字上理解就能知道它设置内容的视图,很多人觉得现在网上关于它的资料很多,但是为什么我还是要重复制造轮子呢?,这里我想说的是,自己花时间去整理的内容,期间你学到的内容是很多的,而且站在更高的基础上也会有更多的收获,比如 AppCompatActivity 的流程分析,现在 as 使用的都是AppCompatActivity,你不应该去了解 AppCompatActivity ? 还有关于 Google 是如何去适配不同版本的。AppCompatActivity 和 Activity 的 UI 分析流程有什么不同? 整个过程中设置的主题,布局是如何被加载上去呢?这些都需要你自己亲自去实践才能明白 Google 工程师在背后所做的努力。

而以上的疑问我都在下文能一一解答,只要你用心看肯定是能收获的,下面我从两个方面去分析,先分析 Activity,然后是 AppCompatActivity 的 UI 绘制流程,期间我也会解答很多关于平时你遇到的问题,从 setContentView 走进 Framework 。

<h4>关于如何去阅读源码</h4>
如果有经验的可以直接跳过,对于源码阅读有限的可以参考。

  • 阅读源码一定要有线索,带着问题去研究源码(比如研究 setContentView 抓住主线程,对其他的暂时忽略,等研究完主线程,再消化个别细节)
  • 源码报错?其实源码内容在,对阅读研究源码影响不大。
  • 找不到类?学会使用搜索,比如本文 Phonewindow 类可能代码跳转无法进入,试试双击 Shift 直接搜索。或者查找文件 ctrl+shift+N ,查找类中方法快捷键:Ctrl+F12 。源码太长?找不到当前方法?Ctrl+F12 搜索方法。如果没有源码直接去 SDk Manager 下载。

<h4> 一. 从setContentView开始,了解view的加载过程</h4>


setContentView 到底做了什么,为什么调用之后就能加载我们想要的布局文件?我们从Activity 的setContentView 开始, 而它通过 Window 类调取的方法,而 Window 是抽象类。最终调用的是PhoneWindow 这个实现类的 setContentView 方法。Phonewindow 又是如何来的呢?这个先剧透下,Activity 的启动过程中的 attach() 方法创建的。

其中 mContentParent 其实就是装载界面最外层的 ViewGroup ,分析:首先如果当前 Viewgroup 为空,则执行installDecor();

分析:截取方法的前一部分,mDecor 是什么呢?其实就是感觉都见过的 DecorView (Google 对它的注释是:它是 window 窗口的顶级视图,同时包括widow 的装饰。抽象不容易理解,下文就会揭开它到底是什么鬼?和我们的布局关系是什么)

DecorView 具体是作为什么存在的,且看分析:上述代码的意思是当 mDecor 为空执行generateDecor。很多博客分析都是默认为空,直接执行。缺乏说服力。那他是否为空呢?
接着看代码:

分析:其实在 PhoneWindow 的构造方法中就新建了 <a name="decorview" >DecorView</a>,通过 getDecorView() 创建当前 DecorView。我们继续看下这个方法的实现:

是否恍然大悟了?installDecor() 方法似曾相识,就是上文的 installDecor(),所以其实在 PhoneWindow 的构造方法最终执行的是 installDecor() 方法,所以不管 mDecor 是否为 null,它都是执行了该方法创建的 DecorView 的。 我们回到 installDecor() 中,如果 mDecor (DecorView) 是null,通过 generateDecor() 方法创建一个 DecorView。【方法图示代码在下文】当 DecorView 创建成功之后,接下来就是通过 generateLayout 创建 mContentParent。(mContentParent 是一个 Viewgroup 对象)

分析:generateDecor() 方法相对而已逻辑很清晰,上述代码只是判断了 context 对象,最后返回值很暴力直接 new DecorView 返回结果

分析:generateLayout() 方法很长,我截取俩部分,上半部分首先它是通过获取 windowstyle. 判断布局是否是 dialog ,接下来是解析是否为 Activity 设置主题,标题栏等,源码这段代码很长,可以通过查看源码分析,很容易入手的代码,在你们分析这段代码中,注意这段代码:

注:我们平时会遇到的问题,获取 requestFeature 必须在 setContentView 之前设置。因为在这里获取本地配置的 LocalFeatures,所以你必须在之前设置 requestFeature,也就是 setContentView

继续分析下半部分:

分析:这段代码承接之前,是通过获取本地配置的主题,或者 Activity 设置的属性,通过这些属性去针对性加载不同的 xml 资源。这就是我们 Activity 选择不同的主题,界面显示不一样的原因。因为每一个主题都会加载不同的 xml 资源布局。当通过 feature 选择不同的主题之后接下来就是如何把我们自己写的布局加载上。此处我取其中一个布局(R.layout.screen_title)作为例子分析。

分析:上图同样是 generateLayout 的后半部分,为什么需要单独提出,就是因为这里解释了 DecorView 为什么是顶级视图。
上图中通过id获取到 viewGroup 对象 id 其实就是 com.android.internal.R.id.content,就是下图xml中的 content 对象

<b>如果解释 DecorView 作为顶级View 就是接下来的关键代码:</b>

mDecor.onResourcesLoaded(mLayoutInflater, layoutResource);

layoutResource 是我们作为例子的 xml 文件, DecorView 通过以上方法解析主题文件提供最上层的 xml 资源文件,并且将解析的 view执行 addView() 方法作为最外层的View,而我们做的只是其实是将 提供的xml 文件内容部分解析加载,也就是为什么方法名字叫做 setCOntentView 了。

分析:onResourcesLoaded 将最外层的布局通过 addView() ,其实就是添加到了 DecorView 中,而 DecorView 是如何如何被加载到屏幕上显示出来。这个都是 Activity 启动的过程中的操作。接下来的文章我会一一解答

xml 是:screen_title.xml 就是上文我们作为例子讲解的 layoutResource 的代码。

分析:当通过 features 确定使用某个具体的 xml 文件后,首先是 DecorView 先解析通过主题确定的xml资源文件,然后获取xml资源文件中的 id 为 content 的FrameLayout 对象 (其实每一个不同的xml文件都会有一个相同 的id (R.id.content)的 FrameLayout )

最后其实 generateLayout() 方法就是把这个找到的 id 为 content 的对象 return 了。并且这个返回值 Viewgroup 其实就已经是 mContentParent 了。返回查看下对 generateLayout 的引用的方法 installDecor 。 它是什么???一脸懵逼?? 哈哈。
想想我们分析这段代码的最初是从哪里入手的,我们是从 setContentView 开始分析的源码。现在回到 setCOntentView 中查看下, 最初从 installDecor() 分析下去,generateLayout() 方法返回的 mContentParent 对象,它其实就是我们最初方法里的 mContentParent 对象。 然后我们接着分析 setContentView(), 它在获得 mContentParent 这个 viewgroup 对象后。接下去是判断了Activity 是否存在类似于转场动画之类的效果,如果有则先执行转场动画,没有则将我们分析的 mContentParent 对象(id 为 content 的 FrameLyout)。解析。看以下代码:

mLayoutInflater.inflate(layoutResID, mContentParent);//第一个参数就是我们提供的布局id

mLayoutInflater 布局解析器将我们通过 setContentView 传进去的 布局id 解析到 mContentParent 中。至此我们加载布局这一部分流程算正式告一段落。

<b>附:</b>

为了更好的理解这个这段代码:再提供一个结构图提供思路参考:

分析:你会发现所有的我们所有的view的绘制都是从DecorView开始的,通过xml文件加载设定好的 actionbar
、title 和为我们提供自定义内容的 Framelayout。

总结:回顾这段过程,带着问题去源码中寻找答案,针对线索去跟踪源码。我们从 Activity 的 setContentView() 开始,找到 Window 类,但是因为 window 是抽象类, 通过注释可以知道它有唯一的实现类 PhoneWindow ,我们查看实现类的方法 setContentView(),它通过 installDecor() 一步步创建DecorView,FrameLayout,到最后解析setContentView().整个过程其实并不复杂。

我们进一步分析:其实每一个 Activity 都有一个关联的 Window 对象,用来呈现,描述应用程序窗口,每一个 window 对象 又包含了一个 DecorView 对象。而 DecorView 对象就是描述窗口的视图-就是对应的 xml 资源布局。 结构关系理解:

  • Window 作为抽象类,提供通用的绘制窗口的 API。

  • Phonewindow 作为具体实现类,同时包含 DecorView 对象,它是所有窗口的根 view

<h3>二. AppCompatActivity的流程分析</h3>

终于分析完 Activity 的流程,接下来我们继续分析,Google 工程师是如何做到只是替换一个 Activity 就能做到支持 Material design,同时兼容之前所有版本呢?下面我同样以最详细的方式解释源码中的奥秘。

在我们现在使用 as开 发,现在都是默认使用 AppCompatActivity 而不是 Activity,所以这套流程对于 AppCompatActivity 有那么一点不适用,整体的过程肯定还是对的,只是 google 做了很多适配工作,AppCompatActivity 中 setContentView 方法是通过了代理

分析:在 AppCompatActivity 的 setContentView 是调用 getDelegate() 的,但是返回的 AppCompatDelegate 其实是抽象类,直接查找返回的 create() ,我们能知道它有着很多实现类。

分析: 可以看到 AppCompatDelegate 不同的 version 有着不同的版本,但是其实随着版本提高他们的实现类其实是逐级实现的。 换句话说 AppCompatDelegateImplN extends AppCompatDelegateImplV23 ,而 AppCompatDelegateImplV23 extends AppCompatDelegateImplV14 ,类推,随着版本的提高去新的实现类去拓展,同时兼容低版本,这样的设计其实就是符合 Android 向下兼容的特性的。回到代码中, getDelegate().setContentView() ,那他的实现类中去查线索,基于兼容,其实只在 AppCompatDelegateImplV9 中实现了 setContentView();

分析:AppCompatDelegateImplV9 的 setContentView 实现 ,进一步查看 ensureSubDecor() ,从下面代码可以知道我们还需要查看 createSubDecor()。

分析:在这段代码中,其实我们能发现和 Activity 分析是类似的,首先也是先获取 AppCompatTheme 主题 ,不同的是由于兼容 material design 必须要实现 AppCompatTheme 主题,代码中也很明确如果获取的属性没有 AppCompatTheme_windowActionBar 会抛出异常。这就是当平时我们使用 AppCompatActivity 的时候但是没有使用 windowNoTitle 属性会报错的原因。接下来到了关键的这段代码:
<pre >mWindow.getDecorView();</pre>

不知道是否还有印象,这个是在 Activity 的分析中出现过,<a href="#decorview">回到过去</a>,在之前的分析中我们知道它其实是调用 PhoneWindow,走的流程是Activity的流程,通过 installDecor() 方法 到 generateLayout() ,此处再强调下 generateLayout() 的返回值如上文分析,返回值是id为 content 的FrameLayout ,而现在其实还是同样的会按照之前分析,DecorView 其实和之前没有区别的。接着代码下一部分分析。

分析:这段代码是承接上图的。此处重新定义新的 viewgroup 对象 subDecor,首先做了一个判断 mWindowNoTitle,它是什么?其实对主题是否设置 Window.FEATURE_NO_TITLE,是则为 false,它的赋值是 我们上文中 createSubDecor() 这个方法的 requestWindowFeature(Window.FEATURE_NO_TITLE),这个方法判断赋值为 true , 这段代码是通过是否设置主题,是否支持 Actionbar 来判断 subDecor 来加载对应的布局,或是否设置 FEATURE_NO_TITLE 来显示隐藏 title 内容的。

接下里的分析 Google 如何做到一个巧妙的替换:

分析:上图重要代码我已经标注,首先 subDecor 我们新的 viewgroup,并且是已经赋值相应布局的。接下来的<pre >mWindow.findViewById(android.R.id.content);</pre>

他拿到的是 <b>mWindow.getDecorView();</b> 然后走的Activity分析的流程的返回的结果。接下来它判断了 content 可能已将视图添加到窗口的内容视图中,因此需要将其迁移到我们的内容视图中。然后才是重点,原来的 content 的 id抹除,而把这边 subDecor 的新主题中的内容视图的 id 改为了 content!然后把整个 subDecor 都 setContentView 给了 window。而他的实现其实都是 Phonewindow。

分析:上图的 setContentView 实现,其实是执行俩个参数的方法,前半部分因为 mContentParent 不为null,所以不会执行 installDecor(), 最后把我们新建的 subDecor 的添加到了 mContentParent上。然后我们回到最初 AppCompatDelegateImplV9 的 setContentView() 方法中。

分析: mSubDecor 这个其实就是之前 mSubDecor = createSubDecor(); 返回的 Viewgroup ,就是我们上文的 subDecor. 那就是现在获得的 content 就是变换过的,然后通过 LayoutInflater 去解析我们自己写的布局到 content上。至此整个 AppCompatActivity 的分析就结束了。也就是关系适配工作结束。剩下的就是渲染 xml 资源。

小结:Google 为了适配,在不影响之前的版本,采用的方法是在基础上重新加了一层,首先UI的分析 Activity 还是会执行,然后如果是 AppCompatActivity 才会执行获取他特有的主题,进一步获取布局文件,将布局文件放在新的 Viewgroup 上,然后把之前的 id 做修改,把原本是 content 的赋值给新的Viewgroup ,然后之前的 id 为 content 直接 addview

如果不熟悉可以再看看上文的内容,对照源码学习会有意想不到的收获。

<h3>一点点感悟</h3>

整个流程下来有没有决定 Google 工程师其实在代码的实现上也是很有想法的,或者说其实也是无奈之举,由于Android 的开源性,造成了市场上其实各个版本参差不齐,而 android 势必要发展,但是为了适配缺一直是难题,相对于ios的闭源环境,每一个 ios 都再自己的控制范围内,利于管理。因为开源成功,但是有得有失,随着Android适配工作不断进行,代码会越来越臃肿。这是不是 google 想研发新的系统的动力呢?时间给出答案。

<h3>结语</h3>

此文作为博文开篇,分享给大家自己的理解,希望能帮助能帮助的人,同时也更加希望能给我提出文章的不足或者漏洞,以此共勉!

推荐阅读更多精彩内容