SharedTransitionObserver 内存泄漏问题 问题背景:
项目中使用 SharedTransitionLayout 来完成 两个元素之间的共享元素动画,但是,在持有该共享元素组件的页面退出之后,发现有内存泄漏,GCROOT 指向的是 SharedTransitionObserver 单例对象 中的一个列表中的元素。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 java.lang.Object[] array │ Leaking: NO (SharedTransitionScopeKt↓ is not leaking) │ ↓ Object[1722 ] ├─ androidx.compose.animation.SharedTransitionScopeKt class │ Leaking: NO (a class is never leaking ) │ ↓ static SharedTransitionScopeKt.SharedTransitionObserver$delegate │ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ├─ kotlin.UnsafeLazyImpl instance │ Leaking: UNKNOWN │ Retaining 16 B in 1 objects │ ↓ UnsafeLazyImpl._value │ ~~~~~~ ├─ androidx.compose.runtime.snapshots.SnapshotStateObserver instance │ Leaking: UNKNOWN │ Retaining 401.3 kB in 9944 objects │ ↓ SnapshotStateObserver.observedScopeMaps │ ~~~~~~~~~~~~~~~~~ ├─ androidx.compose.runtime.collection.MutableVector instance │ Leaking: UNKNOWN │ Retaining 401.2 kB in 9940 objects │ ↓ MutableVector.content │ ~~~~~~~ ├─ androidx.compose.runtime.snapshots.SnapshotStateObserver$ObservedScopeMap[] │ array │ Leaking: UNKNOWN │ Retaining 401.2 kB in 9939 objects │ ↓ SnapshotStateObserver$ObservedScopeMap[0 ]
经过分析,在页面销毁之后,会从 单例对象 SharedTransitionObserver 中清除所有持有的观察者。
1 2 internal valSharedTransitionObserverbylazy(LazyThreadSafetyMode.NONE){SnapshotStateObserver{ it()}.also{ it.start()}}
在 clear 方法中调用 remove 方法 清空 observedScopeMaps。
但是,实际过程中发现,当页面销毁完之后,成功调用了 SharedTransitionObserver 的clear 方法,但是,在clear之后,还有 保存在 observedScopeMaps 列表里面的数据。推断是这里产生了泄漏。
怀疑 是 compose sdk 的 缺陷,所以 到 issuetracker 上搜索:
果然发现有类似的反馈,并且和我们的相同。
最终找到了原因:https://issuetracker.google.com/issues/347035242 ,
最终的 修复版本为 1.8.0-alpha ,但是 我们的版本无法升级到 1.8.0-alpha。所以尝试通过反射,在页面销毁时,手动清理 observedScopeMaps。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 private fun fixMemoryLeak () { kotlin.runCatching { val myModuleClass = Class.forName("androidx.compose.animation.SharedTransitionScopeKt" ) val fieldName = "SharedTransitionObserver\$delegate" val field: Field = myModuleClass.getDeclaredField(fieldName) field.isAccessible = true val lazyInstance = field.get (null ) val sharedTransitionObserverInstance = (lazyInstance as ? Lazy<*>)?.value BLog.d(TAG, "SharedTransitionObserver 实例: $sharedTransitionObserverInstance " ) if (sharedTransitionObserverInstance != null ) { val observedScopeMapsField = sharedTransitionObserverInstance::class .java.getDeclaredField("observedScopeMaps" ) observedScopeMapsField.isAccessible = true val observedScopeMapsValue = observedScopeMapsField.get (sharedTransitionObserverInstance) (observedScopeMapsValue as ? MutableVector<*>)?.clear() BLog.d(TAG, "fixMemoryLeak: $observedScopeMapsValue cleared" ) } else { BLog.e(TAG, "fixMemoryLeak: can't find SnapshotStateObserver object" ) } }.onFailure { BLog.e(TAG, "fixMemoryLeak: exception: ${it.getReportLog()} " ) } }
通过测试,发现没有内存泄漏了。
问题产生原因:
DisposableEffect 确实按预期在 SharedTransitionScope 中被调用,随后从 SnapshotObserver 中移除了scope。但是当稍后移除共享元素状态时(这是每次转换结束时的常见用例),会调用一个代码路径,让scope再次开始观察。
官方修复方式:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 Fix SharedTransitionObserver leaking scopes This change adds a diposed flag to the SharedTransitionScope to ensure after the scope is diposed, no observations can be made on the shared transition scope or on shared elements created in that scope. This prevents scopes from being added to the observer after the they are disposed, therefore preventing leaks. 修复 SharedTransitionObserver 泄漏作用域 此更改为 SharedTransitionScope 添加了一个已处置标志,以确保在作用域被处置后, 无法对共享过渡作用域或在该作用域内创建的共享元素进行观察。 这防止了在作用域处置后将其添加到观察者中,从而防止了内存泄漏。
https://android-review.googlesource.com/c/platform/frameworks/support/+/3132164/1/compose/animation/animation/src/commonMain/kotlin/androidx/compose/animation/SharedTransitionScope.kt#b671