这是我参与8月更文挑战的第10天,活动详情查看:8月更文挑战
从Activity的setContent方法看渲染流程(1)Activity,Window,View的关系
从Activity的setContent方法看渲染流程(2)初识Window
从Activity的setContent方法看渲染流程(3)再看Window
从Activity的setContent方法看渲染流程(番外篇)AppCompatActivity的setContent
AppCompatActivity
现在写的Ativity几乎都是继承AppCompatActivity的,作用嘛就是为了兼容。有的小伙伴可能已经发现AppCompatActivity的setContent方法点击去和Activity的完全不一样。那之前分析的那些东西难度就被废弃了吗?奥夫课斯 诺特。
不管是基于Android5.0也好,Android 11.0也好, 源码会有区别,但是核心代码,中心思想,不会有什么变化。下面我们去看看AppCompatActivity的setContent与Activity有和区别
可视化观察层级
把原来继承Activity改成继承AppCompatActivity,重新运行项目。打开LayoutInspector
果然与原来的不一样。原来的层级是下面这张图
Activity是4个层级,AppCompatActivity是6个层级很明显是有区别的,那就一层一层比对,看看区别在哪里。
- 最外层还是DecorView。看到这个我就放心了,如果第一层就不一样了,那之前分析的DecorView和Window的关系全都得再重现看一边了。
- 第二层也是一样的,因为主题是同一个所以外层是LinearLayout
- 第三层一样,但是不完全一样。一样是同样是actionBar+FrameLayout的样式,这也好理解,因为第二层使用的XML是一样的。 不一样的是原来下面的FrameLayout是有id,而且是很熟悉的content,现在他没有id了
- 第四层看完全不一样了,是一个id为action_bar_root的FitWindowsLinearLayout的东西。看名字也是LinearLayout
- 第五层不一样,但是不是完全不一样。因为之前没有第五层所以肯定不一样,但是看层级。。
-
相似点1:id为action_mode_bar_stub类型的ViewStubCompat和第三层的id为action_mode_bar_stub类型的ViewStub。这不是很相似吗。而且ViewStubCompat后面的Compat就是兼容的意思呀。
-
相似点2:下面的布局居是ContentFrameLayout,毫无疑问也是FrameLayout的子类,最重要的是他的id是content。并且我们我那个下一层看,他也是我们自己XML的父容器。这难度不值得思考一下吗?
- 第六层就是我们自己的XML了,和原来的第四层一模一样。
我对比之后,发现AppCompatActivity是在原来id为content的FrameLayout中又嵌套了两层。而且新增的4,5层。 和原来的2,3层很相似。然后新的id为content在第五层成为我们xml的父布局。
首先对于他这种奇怪的布局我很纳闷,但是我知道他这么做肯定是为了起到兼容的作用。其次我很想知道这个id为content的东西,怎么到第五层的。因为从分析来看,他没有修改原来的1-3层东西,只是在原来的层级上再添加了两层,但是为什么原来的content跑到第五层了呢?
代码论证
androidx.appcompat.app.AppCompatActivity
@Override
public void setContentView(@LayoutRes int layoutResID) {
getDelegate().setContentView(layoutResID);
}
好家伙,一进来就不一样,getDelegate()看样子就是委托了某个类实现喽,很熟悉的代理模式。我是建议小伙伴先去了解一下24种设计模式再去看各种源码,因为不管是官方的还是知名的第三方库都使用了很多的设计模式,熟悉设计模式能更好的理解源码思路,同时也能再次加深对设计模式的理解。
@NonNull
public AppCompatDelegate getDelegate() {
if (mDelegate == null) {
mDelegate = AppCompatDelegate.create(this, this);
}
return mDelegate;
}
去看看AppCompatDelegate的create返回了啥。
androidx.appcompat.app.AppCompatDelegate
public static AppCompatDelegate create(@NonNull Activity activity,
@Nullable AppCompatCallback callback) {
return new AppCompatDelegateImpl(activity, callback);
}
ok,找到了具体实现类:AppCompatDelegateImpl
androidx.appcompat.app.AppCompatDelegateImpl
@Override
public void setContentView(int resId) {
ensureSubDecor();
ViewGroup contentParent = mSubDecor.findViewById(android.R.id.content);
contentParent.removeAllViews();
LayoutInflater.from(mContext).inflate(resId, contentParent);
mAppCompatWindowCallback.getWrapped().onContentChanged();
}
AppCompatDelegateImpl内又好几个setContentView的重载,我们只看参数是XML布局就好,其她的都一样。 setContentView做了三件事。
-
创建初始化SubDecor,我们之前知道ensureDecor是对DecorView创建初始化。这个是对子DecorView进行创建初始化。(啥叫subDecorView,现在还是不知道是个啥,但是想想上面的视图分析,应该心中会有个猜测)
-
找到id为content的容器,清空原来所以子View,把我们写的XML添加进去(这个很熟悉)
-
执行Activity的onContentChanged回调。
第三步就不多说了,看代码也能猜到是干啥,自己点进去看看就可以确定,第二步也不多说了,把我们的XML放在id为content里本就是理所应当的事。所以重点都在第一步ensureSubDecor这个方法里。
ensureSubDecor
androidx.appcompat.app.AppCompatDelegateImpl
ViewGroup mSubDecor;
private void ensureSubDecor() {
if (!mSubDecorInstalled) {
mSubDecor = createSubDecor();
}
}
内部执行createSubDecor,createSubDecor这个方法比较长也比较重要。我决定补贴代码,下面的几张图都是原代码。一小块一小块进行分析。
最重要的是两个红框的代码,上面是根据设置对Window的一些处理。和之前分析处理DecorView是一样的。
第一个红框的地方,是对下面mWindow这个变量进行赋值,mWindow其实就是当前Activity的Phonew实例,具体代码在文章末尾贴上,现在只看ensureSubDecor方法内部
红框二处
mWindow.getDecorView()这个方法内部执行了一个我们很熟悉的方法installDecor
com.android.internal.policy.PhoneWindow
@Override
public final @NonNull View getDecorView() {
if (mDecor == null || mForceDecorInstall) {
installDecor();
}
return mDecor;
}
installDecor我们应该映象特别深刻在从Activity的setContent方法看渲染流程(1)我们看PhoneWindow的setContent方法就遇到了,内部的generateDecor方法和generateLayout把视图层级安排的明明白白,PhoneWindow的setContent方法也就是Activity的setContent的真正开始。这样看来AppCompatActivity的setContent方法虽然现在没有调用PhoneWindow的setContent方法,但是却把他的核心代码installDecor() 先给执行了。
接着看ensureSubDecor后面代码
也是比较熟悉,在PhoneWindow的generateLayout里也是根据主题给DecorView匹配使用的XML文件。这里是给subDecor这个匹配XML文件。照样随便点几个进去都会发现布局样式不同但是内部都会include一个叫abc_screen_content_include布局文件,点击进去
发现里面是一个id为action_bar_activity_content的ContentFrameLayout类型。这个就纳闷了,我还以为找到答案了呢,前面的分析里,我们的布局是被添加到ContentFrameLayout容器里,但是人家的id是content,这个却叫action_bar_activity_content,白高兴一场。
接着看ensureSubDecor后面代码
这里在subDecor为null的时候抛了个异常,也挺好理解的,就是主题不对,所以没有找到subDecor该用XML嘛。
接着看ensureSubDecor
上图代码比较多,别急,我们一行一行看。
在红框1处:先是在刚刚看到的subDecor中找到了id为action_bar_activity_content的ContentFrameLayout定义变量为contentView。然后在我们熟悉DecorView中找到了id为content容器,定义变量为windowContentView。
红框2处:对windowContentView进行遍历,把他的布局全部移除,然后添加到contentView中。
红框3处:把windowContentView的id置空,再将contentView设置id为content。(实不相瞒,我心里只有大写的“卧槽”)
红框4处:将*contentView设为前景
红框5处:将subDecor作为参数执行PhoneWindow的setContView方法。(卧槽。卧槽。卧槽)
- 我不知道你有没有意识到到底发生了什么,PhoneWindow的setContView方法我们在第一篇文章分析的中知道了,他是Activity执行setContent真正的执行处,现在他把我们的却把subDecor作为View执行了PhoneWindow的setContView方法。虽然现在还没有将我们写XML放到subDecor,但是在文章一开始的分析就知道,我们的XML最终是会在subDecor内的。
- 你可能会想,上面已经把DecorView下id为content的FrameLayout的id进行了擦除,但是别忘了我们一开始就调用了PhoneWinow的installDecor方法,这个时候内部的mContentParent已经拿到引用了,id的擦除不影响后续操作。
红框6处:返回subDecor。这个时候subDecor已经创建初始化完成了。
总结
AppCompatActivity为了起兼容作用,参照创建DercorView的形式,创建了一个SubDeorView的东西,然后把我们写的XML嵌套在里面。再将SubDeorView设置进DercorView下原来id为content的FrameLayout中。(因为后面将这个FrameLayout的id进行了擦除,又把SubDecorView下一个容器的id设置为了content)
你甚至可以理解为,将我们的XML包装在SubDecorView中,然后再调用Activity的setContent方法(不严谨,但是可以YY一下)
AppCompatActivity的setContent内部做了个移花接木的操作。虽然他现在方法名还是叫setContent,但是现在的content已经不是原来的content。
附加AppCompatDelegateImpl下的mWindow如何赋值
其实也就是确保变量mWindow有当前Activity持有的Phonew的引用罢了。创建AppCompatDelegateImpl的时候构造方法里有activity,内部也只是通过activity的getWindow获取PhoneWdidow对象。如下图