Dialog里面用ComposeView竟会直接闪退?深挖Lifecycle与Compose的爱恨情仇

1,591 阅读6分钟

23年8月22日15:20勘误:文章提到View实现了Lifecycle接口是错的,正确的情况是将一个LifecycleoOwner通过View的Tag的方式绑在了它身上。从而可以通过该View的getTag()访问到LifecycleOwner。 原文已修复。

故事背景:

小明是一个25岁刚入行不久的安卓初级摸鱼工程师,他仍然记得第一次使用Compose的时候的兴奋与紧张,自从有机会在公司项目部署Compose之后,小明就开始将公司的基于xml实现的组件过渡到Compose,但是有一天在修改了某个Dialog的的内部View之后,小明惊讶的发现Dialog弹出时直接崩溃了,这让小明深入了深深的自我怀疑:Compose不是可以和安卓原生混合开发吗,为什么连Dialog这种基础场景都出问题了?

所以这一切发生了什么,是人性的扭曲还是道德的沦丧?欢迎收看今天的《走进Compose》,为你解密真相。

1.代码复盘

让我们看看小明的Dialog代码:

image-20230822095551103.png

小明选择直接继承了Dialog,然后在setContentView中加入一个ComposeView,运行的时候就直接崩溃了,让我们再看看崩溃:

image-20230822100243553.png

核心崩溃是在ComposeView找不到ViewTreeLifecycleOwner,而执行这段代码是在WindowRecomposerFactory下面的一个伴生对象LifecycleAware在执行createRecomposer()中。

由于涉及到了Compose底层源码,本文中不会细究源码而是只聚焦崩溃产生的原因。

2.核心崩溃代码定位

刚才提到了,问题代码是在WindowRecomposerFactory下面的一个伴生对象LifecycleAware在执行createRecomposer()中。

于是我们直接跳到WindowRecomposerFactory

image-20230822101608559.png

这是一个工厂类,作用是构建Recomposer,而且是Android Window ScopedRecomposer

Recomposer是用于执行重组的核心类,但是这不是这篇文章的重点,重点是Android Window Scoped,这是什么概念呢,其实指的就是这个Decomposer是会感知ComposeView所在的顶级父ViewGroup所持有的Lifecycle。

也许你看到这里会有点晕,我稍微做个总结:Recomposer是执行重组的核心类,而它会通过ComposeView来获取到LifecycleOwner来感知的生命周期。

那么问题来了:为什么要监听生命周期?能通过View获取到Lifecycleowner从而感知生命周期吗?

答:

  1. 如果当前的ComposeView处于不可见的状态,则不需要浪费手机性能去绘制帧或者绘制动画,因此需要感知生命周期。
  2. View在安卓原生代码中是没有生命周期一说的,其实安卓原生代码都没有生命周期这个概念,注意这里指的生命周期是特指Lifecycle这个库,这个库是Jetpack库额外提供的能力。

当问题分析到这里,真相已经呼之欲出了:构建Recomposer的时候,它会去通过当前ComposeView来找到LifecycleOwner,如果找不到,就报错了,于是出现了开头的一幕:

image-20230822104033308.png

看到这里你也许会更疑惑了,ViewTreeLifecycleOwner是什么东西?为什么之前使用ComposeView的时候,从来没有人为设置过,ComposeView也能找到ViewTreeLifecycleOwner呢?

3.直击ViewTreeLifecycleOwner

我们从这里进去看看是如何根据ComposeView创建一个生命周期感知的Recomposer

image-20230822104814751.png

补:WindowRecomposerFactory里面那个rootView就是ComposeView

image-20230822105056944.png

核心代码已经出现,Recomposer是如何找到ComposeView的生命周期的?答案就是findViewTreeLifecycleOwner(),这是View的一个扩展方法,它是由Jetpack里面的Lifecycle库提供的,直接跳转进去。

image-20230822105441467.png

这个方法可能不熟悉kotlin的Sequence的童鞋会看不太懂,这个就是一个递归查找父级,看一看如果父级是否带有R.id.view_tree_lifecycle_owner这个Tag,同时它是LifecycleOwner,取结果第一个。

这里画一张简易的图来表示这个代码的含义:

b4356e7fbe95095ae44c7000566c1607.png

`ComposeView`往上查找最顶级那个符合条件的`View`,然后通过`View`的Tag找出`LifecycleOwner`,以此作为`Recomposer`来识别当前窗口生命周期的依据。

于是,回归最初的问题:为什么Dialog中使用ComposeView直接崩溃了?

:因为DialogWindow中不存在一个符合带有R.id.view_tree_lifecycle_owner的Tag而且该Tag的实例为LifecycleOwner这个接口的View。

问题又又来了:为什么之前在Activity、Fragment、DialogFragment中不存在崩溃的问题?

:因为这些组件的Window中存在符合条件的View

问题又又又来了:为什么存在?似乎自己没有设置过。

:Jetpack库的Activity、Fragment等组件都默认实现了。

细心的读者已经发现,ViewTreeLifecycleOwner有一个设置的方法:

image-20230822111314270.png

通过查看调用的地方,终于破案:

image-20230822111422849.png

image-20230822111525850.png

在`ComponentActivity`的专门为Compose提供的扩展方法中已经为`DecordView`提前设置了`ViewTreeLifecycleOwner`,这是`ComposeView`能在Activity中感知生命周期的原因。

至于Fragment,设置则是这里:

image-20230822111840774.png

注意:这里设置viewTreeLifecycleOwner的对象并不是Window的DecordView,而是Fragment的onCreateView()创建的

小明这时候终于想起来了,之前用的Activity并不是按照源码的基类,而是ComponentActivityFragment也是AndroidX库里面的,这些组件都是赋值了viewTreeLifecycleOwner,因此ComposeView可以识别他们的生命周期并做一些不可见的时候的优化,并在合适的时候释放资源。

而安卓原生的Dialog是没有做以上工作的,这就是直接在Dialog中使用ComposeView会导致崩溃的原因。

重新查看setViewTreeLifecycleOwner()调用的地方,小明惊喜的发现了ComponentDialog这个类使用了这个方法,而这个类是Dialog的直接子类。

image-20230822112656752.png

于是小明兴奋的把Dialog改成ComponentDialog,重新运行代码,完美解决问题!

image-20230822112804251.png

Screen_recording_20230822_112841.gif

4.总结

本案的核心问题是ComposeView要尝试在它所在的View树中找到Lifecycle,而这个Lifecycle是Jetpack库为ActivityFragment额外提供的能力,因此我们可以在Jetpack库的AndroidFragmentDialogFragment直接使用这个能力,而安卓原生的Dialog是没有这个能力的,因此在Dialog中直接使用ComposeView会导致崩溃。

为什么谷歌要搞一个viewTreeLifecycleOwner的方式呢,其实就是早期的安卓源码设计中缺乏一种优良的生命周期监听的方式,这种能力只能靠Jetpack库来额外提供了,因此我们在现代化安卓编程中尽量不要直接只用安卓原生的组件,而是使用Jetpack库中提供的,

如果这个文章解决了你的问题,请给笔者点一个赞,谢谢你的支持