DEVLOG 10.21 Android UI体系知识&面试题
参考内容:AndroidUI体系

问题:
在Activity中Window和View如何分工协作?
何时在Activity中获取View宽高?
这篇文章主要是回答以上的问题,但是在回答以上的问题中也会出现一些子问题。弄清楚这些问题,有利于我们了解Window WindowManager Activity View的关系。

在Activity中Window和View如何分工协作?
我们知道ActivityThread相当于是App的主函数类,Activity#onCreate的开始可以追溯到ActivityThread#handleLaunchActivity中:
handleLaunchActivity中首先初始化了WindowManagerGlobal,从名字可以看出,这个类一定和Window以及WindowManager是有关的,至于这个类如何和Window和WM产生关联,我们待会会总结。然后在handleLaunchActivity中,又调用了performLaunchActivity,在这个方法中会调用Activity#attach。
ActivityThread#performLaunchActivity
performActivity通过反射机制创建了Activity的实例,并且构建了Application的实例,当attach方法执行完成之后,使用记录当前的Activity状态是ON_CREATE。那么,不言而喻,attach方法中应该会调用我们实现的Activity#onCreate回调。接着我们来看看Activity#attach方法:
Activity#attach
因为Activity的回调方法中通常我们使用setContentView加载当前resId指定的布局,但是这个布局是在DecorView的ContentView中,而DecorView又在PhoneWindow中。所以当前Activity#attach时需要创建PhoneWindow的实例,并且绑定PhoneWindow到WindowManager中。

继续看ActivityThread#performLaunchActivity
当Activity#attach执行完成之后,performLaunchActivity继续执行,会执行到这一行代码:
在Instrumentation#callActivityOnCreate中会调用Activity#performCreate,这里面就回调用我们写的onCreate回调。因此,一个不太标准的从ActivityThread#handlePerformLaunchActivity到Activity#onCreate的时序图,如下图所示:

虽然我们知道在Activity的生命周期方法调度完成时我们可以看到我们写的布局文件,但是我们目前并不能看到Window和View的关系,于是我们可以猜想,既然onCreate方法中没有,那么,Window加载View的代码可能会在onResume上。这是因为在官方文档中也说明,onResume是程序到前台的标志。
ActivityThread#handleResumeActivity
ActivityThread#handleResumeActivity方法首先调用performResumeActivity,在performResumeActivity完成之后在会调用WindowManager#addView向Window中添加布局。
所以,实际上布局是在onResume完成之后才被加载在PhoneWindow中的,不过具体的内容我们还是需要看看performResumeActivity:
ActivityThread#performResumeActivity
套路和前面的都差不多,在ActivityThread中执行Activity#performResume然后转到启动相关类Instrumentation#callActivityOnResume,在执行onResume回调。

因此我们可以做一个小小的总结,关于Activity中Window和View,他们之间的合作关系和Activity的生命周期是密切相关的:
onCreate阶段:

但是在onCreate中,并不会把View加载到PhoneWindow中,这个说来也非常好理解,毕竟我们在回调中才解析布局文件xml,怎么会在PhoneWindow创建之前addView呢?
onResume阶段:

ViewRootImpl如何成为Window和View的桥梁?
刚才我们看到WindowManager可以将DecorView加载到PhoneWindow中,这个过程还可以仔细地分析一番。在分析之前我们先总结一下Window相关的类之间的关系:

ViewManager只是一个接口,定义了基本的对于View的添加和删除工作
WindowManager也是一个接口,但是我们通常操作的都是这个类,他的实现类是WindowManageImpl。WindowManagerImpl又通过将职责委托给WindowManagerGlobal实现,之所以使用这么复杂的【套娃】逻辑,好处有两点:
这是一种外观模式,我们通过WindowManager就可以操作WindowManagerGlobal和framework层通信。
WindowManagerGlobal也是一个全局单例。根据上面的代码分析,创建Activity就回创建对应的WindowManager和PhoneWindow,但是这些所有的WindowManager都基于WindowManagerGlobal,节省内存。
回到问题【ViewRootImpl如何成为Window和View的桥梁?】本身,我们需要查看一下WindowManager#addView的代码:
在WindowManagerGlobal中初始化了ViewRootImpl,然后调用了setView。跟踪ViewRootImpl#setView可以发现这个方法最后会调用WindowSession#addToDispaly,再调用WindowManagerService#addWindow,整体的调用链如图:

所以可以看到ViewRootImpl确实充当了一个桥梁,上面抓住了WindowManager,下面连接的是WindowSession(是IWindowSession.Stub的实现类,是Binder机制的一部分)。
如何onResume中获取View的宽高?
这个问题可以转换成另外的一个子问题,View#measure是在什么时候执行的?
View#measure的执行时机
上面已经说过个, onResume会先于WindowManager#addView,所以回调函数本身会在测量之前执行,这样在onResume中尝试获取宽高一定会返回0,更不用说在onCreate中。
解决问题的思路:
handler.post( Runnable {}, delay):这里不可以不加delay。如果不加delay,这个消息在消息队列中还是会先执行,而我们的目的是想等到onResume执行完成,WindowManager#addView之后再拿到View宽高,这个大小设置为100ms。
View.post(Runnable {}): View.post的原理就是将当前的Runnable放入了HandlerActionQueue的数组中,然后在ViewRootImpl#performTraversals中执行。ViewRootImpl#performTraversals是执行测量 布局 绘制的开始,肯定也会在WindowManager#addView之后
3. onWindowFocusChanged回调:当失去焦点时会被调用
4. addOnGlobalLayoutListener:当ViewTree变化的时候会被调用。
在子线程中能不能更新UI?
这个例子中代码会报错吗?
其实并不会,因为在这里,textView被setContentView加载之后设置setText时只是将String变量存储到TextView中,此时TextView并没有开始performTraversals,不会检查UI线程,所以没有问题。