HZWZ
现在的年轻人一上来就粘源码,对我这样一个小菜瓜,这样合适吗,这样不合适。
背景
故事是这样开始的
- 有一天,我发现自己写的布局没有
- 按照我的想法打印
- 带上了莫名其妙的开头
- 有一天,两个年轻人,不讲武德
- 非要告诉我这是
AppCompatActivity
的原因 - 我不信
- 他们偷袭,显然是有备而来
- 我大意了
- 我没有闪
- 今天,我要自证事实
- 混元门代码 第三代大弟子,打工牛子 参见
熟悉的味
为什么会这样,明明是一个普通的TextView,为什么变成了MaterialTextView,难不成你在逗我。
顺藤摸瓜
打工人,打工魂,我乃混元门…
呸,跑题了,我们进入正轨,今天非要扒了你的裤衩子。
先进入 AppCompatActivity
的 setContenView():
public void setContentView(@LayoutRes int layoutResID) {
getDelegate().setContentView(layoutResID);
}
索然无味,进入 AppCompatDelegateImpl
-setContentView()
@Override
public void setContentView(int resId) {
//1
ensureSubDecor();
ViewGroup contentParent = mSubDecor.findViewById(android.R.id.content);
contentParent.removeAllViews();
LayoutInflater.from(mContext).inflate(resId, contentParent);
mAppCompatWindowCallback.getWrapped().onContentChanged();
}
这里就很直接嘛,看过 Activity-setContentView
的同学都知道 contentParent
是啥,我们的根布局嘛,这里后面的 inflate 就不用说了,主要在于 ensureSubDecor(),看看它里面做了甚。
进入 ensureSubDecor()
代码语言:javascript复制private void ensureSubDecor() {
if (!mSubDecorInstalled) {
mSubDecor = createSubDecor();
...省略一大段代码
}
}
mSubDecorInstalled
见名之意,windows
是否已经安装了 DecorView
,如果已经安装,就忽略。
我为什么会知道呢?翻译啊,ohhhh。
代码语言:javascript复制// true if we have installed a window sub-decor layout.
private boolean mSubDecorInstalled;
进入 createSubDecor()
代码语言:javascript复制 private ViewGroup createSubDecor() {
//1
TypedArray a = mContext.obtainStyledAttributes(R.styleable.AppCompatTheme);
if(xxx)xxx
//2
ensureWindow();
//3
mWindow.getDecorView();
...//忽略一段代码
ViewGroup subDecor = null;
//4
subDecor = (ViewGroup) inflater.inflate(R.layout.abc_screen_simple, null);
//5
final ContentFrameLayout contentView = (ContentFrameLayout) subDecor.findViewById(
R.id.action_bar_activity_content);
final ViewGroup windowContentView = (ViewGroup) mWindow.findViewById(android.R.id.content);
//6
if (windowContentView != null) {
// There might be Views already added to the Window's content view so we need to
// migrate them to our content view
while (windowContentView.getChildCount() > 0) {
final View child = windowContentView.getChildAt(0);
windowContentView.removeViewAt(0);
contentView.addView(child);
}
windowContentView.setId(View.NO_ID);
contentView.setId(android.R.id.content);
}
//7
mWindow.setContentView(subDecor);
return subDecor;
}
这个方法相对较复杂,理解了其,这篇也就可以结束了。
阶段小思考
问题来了,为什么源码里要将 PhoneWindows-windowsContent
里的所有view复制到新的 contentView
里?
因为,Activity 默认的
DecorView
加载的是R.layout.screen_simple***,而我们的根布局是其中的一个子FrameLayout
,当使用 AppCompatActivity时,为了兼容性,其有自己相应的主题layout,所以在设置时,先将当前根容器里的所有子view放到新的这个容器里,再将这个容器的id设置为R.id.content*,即让其成为新的根容器,再将这个容器add到我们的DecorView
里的根布局(即FrameLayout)上,这样就达到了在不影响原有view显示情况下的兼容效果。
这样说你明白了吗?
还不明白?好吧,我再从 背景
说一遍
正如上面所述,AppCompatActivity 有自己特定的容器 layout,如果在设计时,让它直接替代了Activity的默认根容器,就意味着 AppCompatActivity 必须独立的去写一份,这合适吗,显然不适合。所以正因为如此,AppCompatActivity 里的 DecorView 变量名叫做
mSubDecor
,而我们基础 PhoneWindows 里的叫做mDecor
,再想想为什么AppCompatActivity 里会单独再定义一个 所谓的 DecorView,意义何在,再配合 AppCompatDelegateImpl-createSubDecor() 方法里上述的操作,现在你懂了吗?
具体如下图所示:
检查层级我们就会发现,原来AppCompatActivity 是在原 Activity 布局层级上嵌套的,正如上面所描述,是不是有种ohhhh,就这啊的感觉。
串一下思路
- 当我们在
AppCompatActivity
里调用 setContentView() 时,其内部调用的是AppCompatDelegateImpl
的 setContentView(),最终调用了ensureSubDecor(),即用来确保DecorView是否已经初始化成功。 - 在 ensureSubDecor() 方法里,先判断
子DecorView
(为什么是子
,因为它不可能直接替代根DecorView,AppCompatActivity只是做了一个兼容,即在DecorView
之上再添加一个副级,从而做到对调用者屏蔽,对于使用者而言,其实毫无察觉) 有没有安装,如果没有,则调用 createSubDecor() 去初始化它; - createSubDecor() 内部会根据当前主题进行相关配置,最终设置当前的根容器,并将当前
Windows-DecorView
根容器-FrameLayout里的所有子view全部add到新的容器里,再将新容器的id改为 R.id.content,然后windows.setContentView(),内部即***add*** 到旧容器FrameLayou
上,成为唯一子容器。而这个新的容器就是最新的根容器。 - 随后的方法很简单,我们自己的布局直接 add 到根容器
ViewGroup
上即可。
为什么打印不一致
等等,最开始的 View
打印增加前缀的原因是啥,这和 setContentView() 有何关系?
是啊,好像没什么关系,那你在这说xxx,不好意思,我们切换下一个话题。
我们回到最开始的 AppCompatActivity-Create()
代码语言:javascript复制@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
final AppCompatDelegate delegate = getDelegate();
//入口
delegate.installViewFactory();
delegate.onCreate(savedInstanceState);
super.onCreate(savedInstanceState);
}
代码语言:javascript复制@Override
public void installViewFactory() {
LayoutInflater layoutInflater = LayoutInflater.from(mContext);
//1
if (layoutInflater.getFactory() == null) {
LayoutInflaterCompat.setFactory2(layoutInflater, this);
}
...
}
如果我们没有设置 LayoutInflater
工厂,则会设置默认的工厂,然后最终创建布局时会调用 onCreateView() 方法。
我们进入相应的 onCreateView()
代码语言:javascript复制@Override
public View createView(View parent, final String name, @NonNull Context context,
@NonNull AttributeSet attrs) {
...
return mAppCompatViewInflater.createView(parent, name, context, attrs, inheritContext,IS_PRE_LOLLIPOP,true,VectorEnabledTintResources.shouldBeUsed()
);
}
没什么好说的,进入 mAppCompatViewInflater.createView()
代码语言:javascript复制final View createView(View parent, final String name, @NonNull Context context,
@NonNull AttributeSet attrs, boolean inheritContext,
boolean readAndroidTheme, boolean readAppTheme, boolean wrapContext) {
...
switch (name) {
case "TextView":
view = createTextView(context, attrs);
verifyNotNull(view, name);
break;
...
哦呵呵呵,原来这里是对我们的默认的 View
进行了替换,这也就是为什么我们使用AppCompatActivity
打印出来的子 View
自带了前缀显示。