自定义View储备知识
针对于上一次讲述了一些自定义View比较关键的API,以及引生出来的问题(直接在Activity中绘制)。
这可能是前缀知识知识没有补充的关系,所以这篇笔记主要记录记录一些自定义View的底层知识。
引入:从Actvity介绍View的渲染
如果我们想要明白一个View是如何渲染到Activity上的,那么就不可避免的需要去解析Activity的源码,在这里主要还是需要去看看关于View部分的,其他部分可以由大家来一起分享。
首先,在当前版本中,大部分Activity还是会选择继承AppCompatActivity(以下简称ACA吧),作为向下兼容的选择。在实际中,Activity和ACA的代码还是有部分差异,这也是为了适配低版本。
不过我们可以先用Android Studio的工具开看看,我们所见的屏幕分为什么层次。
使用LayoutInspector,分析继承AppCompatActivity的视图:

使用LayoutInspector,分析继承Activity的视图:

这边对比分析看了下,都发现视图最外层还是DecorView。辨析一下ACA的不同之处:
- 第四层开始不一样,是一个id为
action_bar_root的FitWindowsLinearLayout的东西。看名字也是LinearLayout。 - 第五层有一定相似之处,id为
action_mode_bar_stub类型的ViewStubCompat和Activity第三层id为action_mode_bar_stub类型的ViewStub。下面的布局是ContentFrameLayout,毫无疑问也是FrameLayout的子类,和Activity的第三层一致,它的id是**content****。** - 第六层就是自己的XML。
现在写的Ativity几乎都是继承AppCompatActivity的,作用是为了兼容。大家可能已经发现AppCompatActivity的setContentView方法点进去和Activity的不同,但是实际上还是大相径庭的。
Activity:

AppCompatActivity:

我们首先看看,Activity的setContentView作为一个基础,再去看看ACA的setContentView。
主要先看它的第一行,第二行明显是设置一个状态栏的,暂且不管。

getWindow方法返回了一个Window对象,这个Window对象实际上是在Activity的attach中绑定的。
想要了解View是如何绘制在Activity前,我们需要知道,启动一个Activity的过程是怎么样的。
Activitythread的scheduleLaunchActivity方法负责启动Activity,经过Handle处理执行到handleLaunchActivity
Activity a = performLaunchActivity(r, customIntent);
//这里面执行了Activity.onResume()
handleResumeActivity(r.token, false, r.isForward,!r.activity.mFinished && !r.startsNotResumed);
performLaunchActivity会创建待启动Activity类的一个实例,并调用其attach方法进行初始化,然后调用Activity的onCreate,onStart等方法。
可以看到mWindow实际上是一个PhoneWindow类型,其是Window的一个实现类。mWindow创建实例后,将当前Activity当回调设置了进去,主要用于一些事件的回调处理。

随后,setWindowManager主要是获得系统的 WindowManager,然后传给 PhoneWindow ,最终,在 PhoneWindow 中持有了一个 WindowManagerImpl (WindowManager也是抽象类,其实现类是WMI)的引用。

由于Window的真正实现类型是PW,也就是它调用了setContentView方法,那么我们再去看看PW的具体代码。

在PW的setContentView方法中,看到了mContentParent这个变量,嗯?这个变量名是否有些眼熟。


好家伙,其实我们可以猜测,mContentParent这玩意实际上就是分析视图中的FrameLayout布局,同时由第二处关键代码点得知,我们传入的layoutID被解析进入mContentParent。那么我们再认真看看源码论证一下。我们实际发现mContentParent的创建是在installDecor方法里,那么我们就看看installDecor方法。

我们可以看到,mContentParent是由generateLayout方法创建,其中传入了mDecor, 而mDecor由generateDecor生成,是DecorView类型。

到这里我们总结一下,我们使用的Activity的attach方法里绑定了一个Window, 也就是PhoneWindow。同时在PW里面会创建一个DecorView,然后DecorView调用setWindow绑定Window,之后根据不同features来选择不同容器,来初始化mContentParent。

- 这里的id常量
ID_ANDROID_CONTENT点击去可以看到定义为com.android.internal.R.id.content所以mContentParent就是们的第三层id为content的帧布局。 - 这里
findViewById其实就是从DecorView中查找
现在可以总结出一个图片大概是这样的,

到这里我们大概会明悟,Activity并不是作为我们可以看到的视图存在,他负责的是某些操作和回调的控制,绑定一个PhoneWindow, 而我们能看到的最顶层视图DecorView就是依附在PhoneWindow上。
这也就是为什么直接在Activity中绘制View,如果不添加进入Window中,什么也不会显示的原因。
不过,其实这时候DecorView是并没有被添加进入Window中的,虽然说绑定了Window,但是要在Activity#onResume中才会变得可见。
为什么DecorView在onResume()之后才对用户可见呢?为什么DecorView绑定了Window却不添加进WIndow,需要后面WindowManager添加呢?
请详细思考这两个问题,在最后会进行补充。
深入:View的呈现流程

我们来看看Activity的onRemune具体执行的方法,里面最关键的是wm.addView(decor, l);
其中wm是ViewManager一个接口,由WindowManager实现,WindowManager的实现又是WindowManagerImpl,但是WMI的实际实现又是交给WindowManagerGlobal去处理。WindowMangerGlobal 是一个单例,每一个进程中只有一个对象

我们可以看到,在WMG的 addView 方法中,创建了一个最关键的ViewRootImpl对象,然后通过 ViewRootImpl 的 setView 方法将 DecorView 添加到 WMS 中。

在setView方法中
public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView,
int userId) {
...
requestLayout();
...
try {
mWindowSession res = mWindowSession.addToDisplayAsUser(mWindow, mSeq, mWindowAttributes, getHostVisibility(), mDisplay.getDisplayId(), userId, mTmpFrame, mAttachInfo.mContentInsets, mAttachInfo.mStableInsets, mAttachInfo.mDisplayCutout, inputChannel, mTempInsets, mTempControls);setFrame(mTmpFrame); }
}
@Override
public void requestLayout() {
if (!mHandlingLayoutInLayoutRequest){
checkThread();
mLayoutRequested = true;
scheduleTraversals();
}
}
先是做了个刷新布局的操作,内部执行的scheduleTraversals() 就是View绘制的入口,调用此方法后 ViewRootImpl 所关联的 View 执行 measure - layout - draw 操作,确保在 View 被添加到 Window 上显示到屏幕之前,已经完成测量和绘制操作。

至此由ViewRootImpl完成了添加View到Window的操作。 然后将调用mWindowSession的addToDisplayAsUser方法来完成Window的添加过程,内部真实执行的地方在WMS(WindowManagerService).
总结一下,我们遇到一个新的ViewRootImpl类,它将DecorView和PhoneWindow组合起来(之前的**DecorVIew#setWindow方法实际上是可以getWindow**的前提,也可以理解为绑定起来)。同时我们需要明确的是,ViewRootImpl是整个VewTree的管理者,不是ViewTree其中的根结点或者一员。ViewRootImpl 是 DecorView 的 parent,但是他并不是一个真正的 View,只是继承了 ViewParent 接口,用来掌管 View 的各种事件,包括 requestLayout、invalidate、dispatchInputEvent 等等。
ViewTree的概念
回扣:ACA下View的渲染
我们主要还是看看ACA类中的setContentView这个方法,其中getDelegate()方法是需要重视,Delegate的中文意思是代理、代表?!这是不是很熟悉,很容易让我联想到之前在Retrofit中了解到的代理模式,点进去看看。

XXX.create是不是特别熟悉,这就是之前介绍Retrofit的中代理模式啊。
再看看create返回了AppCompatDelegateImpl对象

进入AppCompatDelegateImpl里看看,和Activity内部一样,有一系列的setContentView方法重载,主要看看是加载XML文件的方法。

这个方法里实现了三件事:
ensureSubDecor可以猜测是对子DecorView进行创建初始化。- 找到id为content的容器,清空原来子View,把我们写的XML添加进去(是不是很眼熟?)
- 执行Activity的
onContentChanged回调。
我们主要还是看看ensureSubDecor方法,这个方法中比较关键的还得看看createSubDecor方法。

进入createSubDecor方法后,我们先分析前半部分,在重点标注前,都是对WIndow样式的一些处理,所以还是看蓝色标注处。

ensureWindow方法实际上是给Activity绑定PW,也是给mWindow初始化PW。
我们就知道接下来和Activity类似了,getDecorView方法,实际上调用了insatllDeocor。

AppCompatActivity的setContentView方法虽然现在没有调用PhoneWindow的setContent方法,但是却把他的核心代码installDecor() 先给执行了。
上面看完了ensureSubDecor的前半部分代码,我们再来看后半部分。

在PhoneWindow的generateLayout里也是根据主题给DecorView匹配使用的XML文件。这里是给subDecor这个匹配XML文件。
我们再继续看

- 先是在刚刚看到的subDecor中找到了id为****
action_bar_activity_content的ContentFrameLayout定义为contentView。然后通过PW在我们熟悉的DecorView中找到了id为****content容器,定义为windowContentView。 - 把windowContentView内的所有View添加到contentView中,然后清空windowContentView。
- 把windowContentView的id置空,再将contentView设置id为content。
- 删除contentView的前景
- 将subDecor作为参数执行PhoneWindow的setContentView方法。接下来就会重复和Activity类似的事。
总结一下,在Android 引入了 Material Design 的设计,为了支持 Material,Color、调色版、Toolbar等各种新特性,AppCompatActivity 就应用而生。AppCompatActivity 为了兼容以前的东西,就做了偷梁换柱,其中一个就是替换了以前 Activity 加载内容布局的父容器,即在原 DecorView 的 content 区域添加一个 SubDecor,我们通过 setContentView 设置的布局最终被添加到该 SubDecor 的 content 容器中,这样完成布局兼容操作。
参考一下下图,就能很简单理解了。

总结
二图以蔽之


问题答案
//TODO 😋
参考
https://juejin.cn/post/6999144331340677150
https://blog.csdn.net/zxm528/article/details/123134387?spm=1001.2014.3001.5501
https://www.jianshu.com/p/8b7f075be3fc
https://www.cnblogs.com/huansky/p/11911549.html
https://carsonho.blog.csdn.net/article/details/91359867
