Android 10 及以上,(Shared Element) Return Transition在特定情况下失效的问题探究
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
动画,那么就从这里入手:
很明显在异常情况下startExitBackTransition()
返回了false
。点这个方法,进入到android.app.ActivityTransitionState
类中(注:以下所有图片中的代码若未说明都来自这个类):
通过断点调试发现正常情况和异常情况的区别在于getPendingExitNames()
是否为null
:如果它为null
,该方法就会返回false
,那么Transition
就不会执行。这个方法里的逻辑很简单,就是懒加载并返回了mPendingExitNames
这个成员变量:
还是通过断点发现,正常和异常情况的区别在于mEnterTransitionCoordinator
是否为null
,异常情况下这个变量为null
,导致mPendingExitNames
无法初始化,从而返回null
。那么我们的思路就是找到何时mEnterTransitionCoordinator
被置空。代码里有多处置空的地方,但是鉴于异常情况只有在Activity
切换出去以后才会发生。那么,最可疑的地方莫过于onStop
方法里面的置空处理:
验证之后,问题就明了了:正常情况下,getPendingExitNames
会初始化mPendingExitNames
。但是异常情况下,由于走了onStop
,mEnterTransitionCoordinator
被置空,导致mPendingExitNames
始终无法初始化最终导致了这个BUG。
安卓10之前?
但是为什么在安卓10之前没有这个问题呢?翻阅源码的修改记录,我发现了一条可疑的commit: d85bed5 。我们看一下此commit对android.app.ActivityTransitionState
这个类的修改:
这个提交对这个类的修改在逻辑上的改动不大,主要修改就是成员mEnteringNames
在语义上改为mPendingExitNames
,本质上还是储存进入退出的共享元素的Transition Names
的列表。关键在于,他将mPendingExitNames
的获取方式变成了getPendingExitNames()
方法来懒加载。而在安卓9的源码中,mEnteringNames
会在startEnter
方法中就被初始化,并且只会在clear
方法中清空,那么此变量在整个Acitivity Transition
的生命周期中始终是可用的,所以安卓10以前没有这个问题。
但是我们不禁疑问,谷歌程序员真的会犯这样的错误吗?继续看上面代码的第154行的saveState
方法,这个方法会在Acitivty.onSaveInstance()
中调用。因为onSaveInstance
会在onStop
之前调用,那么实际上onStop
之前mPendingExitNames
就会被初始化,所以…理论上…Android 10上的代码也是没有问题的,🤔怎么会是呢?
通过断点发现,实际上onSaveInstance
在onStop
之后调用了,所以mPendingExitNames
保存了个寂寞。查阅文档才知道,原来onSaveInstance
在安卓9有行为变更:
显然,那位代码贡献者并没有考虑到这一变更,所以这才是罪魁祸首~
补丁代码
反射式
既然已经知道了问题所在,那么解决方法便呼之欲出了。解决方法很简单,在onStop
前,我们调用至少一次saveState
对mPendingExitNames
进行初始化,这个方法是无副作用的,我们可以放心调用。但是onStop
之前有很多时机,我们要确保mEnterTransitionCoordinator
已经初始化。搜索发现,mEnterTransitionCoordinator
初始化的地方在enterReady(Activity)
中,而这个方法在Activity.onStart()
中调用的,那么我们在Activity
的onStart
和onStop
之间打补丁就行了:
- 注:这个
Acitivty
指的是复现方式中的Acitivty B;saveState
不在反射黑名单,我们可以反射调用。
@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()
}
}
非反射式
反射的代码总让人感觉嘎吱作响,需要很小心地维护,那么我们能否找到不需要反射的解决方法呢?我们之前分析过了,saveState
在Activity.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/