欢迎光临散文网 会员登陆 & 注册

DEVLOG 10.21 Android UI体系知识&面试题

2021-10-21 15:46 作者:房顶上的铝皮水塔  | 我要投稿

参考内容:AndroidUI体系

问题:

  1. 在Activity中Window和View如何分工协作?

  2. 何时在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实现,之所以使用这么复杂的【套娃】逻辑,好处有两点:

  1. 这是一种外观模式,我们通过WindowManager就可以操作WindowManagerGlobal和framework层通信。

  2. 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中。

解决问题的思路:

  1. handler.post( Runnable {}, delay):这里不可以不加delay。如果不加delay,这个消息在消息队列中还是会先执行,而我们的目的是想等到onResume执行完成,WindowManager#addView之后再拿到View宽高,这个大小设置为100ms。

  2. 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线程,所以没有问题。


DEVLOG 10.21 Android UI体系知识&面试题的评论 (共 条)

分享到微博请遵守国家法律