Android 10 及以上,(Shared Element) Return Transition在特定情况下失效的问题探究

Author Avatar
xjunz 3月 28, 2022
  • 在其它设备中阅读本文章

Android 10 及以上,(Shared Element) Return Transition在特定情况下失效的问题探究

文章可能存在错误或疏漏,欢迎批评斧正…

复现

复现方式Activity A通过(Shared Element) Enter Transition 进入Activity B, 在Actitivty B 内进入另一个Activity(APP内或其他APP都行),然后回到Activity B,点击返回键

预期结果Activity B 会通过(Shared Element) Return Trasition 回到Activity A

实际结果:完全没有执行任何Transition动画,直接回到了Activity A

影响范围Android 10及以上版本,并且到目前(2022-3-26)都没有修复

切入

其实很久以前就发现了这个问题,但是一直没有实际去解决,因为这个BUG的影响并不是很大。在搜索引擎上,国内外许多人也遇到了相同的问题,可是看起来大家都很忙,并没有人提供一个特别好的解决方案。不巧我今天正好又遇到了,而且有点闲暇时间,便决定去一探究竟。

切入点很好找,当我们点击返回键时,就会走finishAfterTransition以执行Transition动画,那么就从这里入手:

image-20220327022215849

很明显在异常情况下startExitBackTransition()返回了false。点这个方法,进入到android.app.ActivityTransitionState类中(:以下所有图片中的代码若未说明都来自这个类):

image-20220327022518986通过断点调试发现正常情况和异常情况的区别在于getPendingExitNames()是否为null:如果它为null,该方法就会返回false,那么Transition就不会执行。这个方法里的逻辑很简单,就是懒加载并返回了mPendingExitNames这个成员变量:

image-20220327022714802

还是通过断点发现,正常和异常情况的区别在于mEnterTransitionCoordinator是否为null,异常情况下这个变量为null,导致mPendingExitNames无法初始化,从而返回null。那么我们的思路就是找到何时mEnterTransitionCoordinator被置空。代码里有多处置空的地方,但是鉴于异常情况只有在Activity切换出去以后才会发生。那么,最可疑的地方莫过于onStop方法里面的置空处理:

image-20220327022929123验证之后,问题就明了了:正常情况下,getPendingExitNames会初始化mPendingExitNames。但是异常情况下,由于走了onStopmEnterTransitionCoordinator被置空,导致mPendingExitNames始终无法初始化最终导致了这个BUG。

安卓10之前?

但是为什么在安卓10之前没有这个问题呢?翻阅源码的修改记录,我发现了一条可疑的commit: d85bed5 。我们看一下此commit对android.app.ActivityTransitionState这个类的修改:

image-20220327024045779

这个提交对这个类的修改在逻辑上的改动不大,主要修改就是成员mEnteringNames在语义上改为mPendingExitNames,本质上还是储存进入退出的共享元素的Transition Names的列表。关键在于,他将mPendingExitNames的获取方式变成了getPendingExitNames()方法来懒加载。而在安卓9的源码中,mEnteringNames会在startEnter方法中就被初始化,并且只会在clear方法中清空,那么此变量在整个Acitivity Transition的生命周期中始终是可用的,所以安卓10以前没有这个问题。

但是我们不禁疑问,谷歌程序员真的会犯这样的错误吗?继续看上面代码的第154行的saveState方法,这个方法会在Acitivty.onSaveInstance()中调用。因为onSaveInstance会在onStop之前调用,那么实际上onStop之前mPendingExitNames就会被初始化,所以…理论上…Android 10上的代码也是没有问题的,🤔怎么会是呢?

通过断点发现,实际上onSaveInstanceonStop之后调用了,所以mPendingExitNames保存了个寂寞。查阅文档才知道,原来onSaveInstance在安卓9有行为变更:

image-20220327160844844

显然,那位代码贡献者并没有考虑到这一变更,所以这才是罪魁祸首~

补丁代码

反射式

既然已经知道了问题所在,那么解决方法便呼之欲出了。解决方法很简单,在onStop前,我们调用至少一次saveStatemPendingExitNames进行初始化,这个方法是无副作用的,我们可以放心调用。但是onStop之前有很多时机,我们要确保mEnterTransitionCoordinator已经初始化。搜索发现,mEnterTransitionCoordinator初始化的地方在enterReady(Activity)中,而这个方法在Activity.onStart()中调用的,那么我们在ActivityonStartonStop之间打补丁就行了:

  • :这个Acitivty指的是复现方式中的Acitivty BsaveState不在反射黑名单,我们可以反射调用。
@SuppressLint("DiscouragedPrivateApi")
fun patchActivity(activity: Activity) {
    if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) return
    try {
        val field = Activity::class.java.getDeclaredField("mActivityTransitionState").run {
            isAccessible = true
            get(activity)
        }
        field.javaClass.getDeclaredMethod("saveState", Bundle::class.java).run {
            invoke(field, Bundle())
        }
    } catch (t: Throwable) {
        Log.e("ReturnTransitionPatcher", "Patch failed for [${activity.javaClass.simpleName}]")
        t.printStackTrace()
    }
}

非反射式

反射的代码总让人感觉嘎吱作响,需要很小心地维护,那么我们能否找到不需要反射的解决方法呢?我们之前分析过了,saveStateActivity.onSaveInstance中调用,那么我们手动在onStop之前onStart之后手动调用onSaveInstance也能修复这个问题,这个方法不用反射调用,稳健性较好:

fun patchActivity(activity: Activity) {
    if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) return
    Instrumentation().callActivityOnSaveInstanceState(activity, Bundle())
}

Android 12 PE上奇怪的现象

在Android 11及以前,将APP退回出到最近任务,也会走onStop触发这个BUG。但是在我的Pixel 5的Android 12上,退出到最近任务,并不会触发这个BUG。调试发现,是因为没走onStop。原来PE 的Launcher对于刚刚退回到最近任务的APP,不再是显示其静态快照,而是将APP缩放,因此在最近任务页面仍然能看到该APP的UI更新。谷歌你做得好啊,做得好啊。

使用ReturnTransitionPatcher

代码已经上传到github和Maven仓库,如果你想在项目中修复这个问题,可以在项目中引入,该库提供了完美的Patch时机和全局Patch功能:

implementation 'io.github.xjunz:return-transition-patcher:+'

知识共享许可协议
本作品采用知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议进行许可。

本文链接:http://www.xjunz.top/post/Android-Return-Transition-Patcher/