Activity 使用 AndroidX Navigation Compose 后方向改变后泄漏
Activity leaked after orientation change after using AndroidX Navigation Compose
MainActivity
在第二次方向改变后泄漏,但只有在使用 navHostController
导航到新目的地后才会泄漏。
可以复制的工作项目可用 here。
这些是复制步骤:
- 运行 应用程序(它将加载
FooScreen
,其中仅包含一个 TopAppBar
和一个 Button
)。
- 单击“打开栏屏幕”
Button
(它将加载仅包含 TopAppBar
的 BarScreen
)
- 将设备的方向从纵向更改为横向
- 将方向改回纵向
此时您应该看到 StrictMode
抱怨 logcat:
内部泄漏
2021-11-02 16:23:20.672 31230-31230/com.leinardi.template E/StrictMode: class com.leinardi.template.ui.MainActivity; instances=2; limit=1
android.os.StrictMode$InstanceCountViolation: class com.leinardi.template.ui.MainActivity; instances=2; limit=1
at android.os.StrictMode.setClassInstanceLimit(StrictMode.java:1)
LeakCanary 通知也应该可见。这是 LeakCanary 日志:
┬───
│ GC Root: System class
│
├─ leakcanary.internal.InternalLeakCanary class
│ Leaking: NO (MainActivity↓ is not leaking and a class is never leaking)
│ ↓ static InternalLeakCanary.resumedActivity
├─ com.leinardi.template.ui.MainActivity instance
│ Leaking: NO (Activity#mDestroyed is false)
│ mApplication instance of com.leinardi.template.Template
│ mBase instance of android.app.ContextImpl
│ ↓ MainActivity.navHostController
│ ~~~~~~~~~~~~~~~~~
├─ androidx.navigation.NavHostController instance
│ Leaking: UNKNOWN
│ Retaining 5.5 kB in 140 objects
│ activity instance of com.leinardi.template.ui.MainActivity with mDestroyed = false
│ context instance of com.leinardi.template.ui.MainActivity with mDestroyed = false
│ lifecycleOwner instance of com.leinardi.template.ui.MainActivity with mDestroyed = false
│ ↓ NavController.viewModel
│ ~~~~~~~~~
├─ androidx.navigation.NavControllerViewModel instance
│ Leaking: UNKNOWN
│ Retaining 128.4 kB in 2406 objects
│ ↓ NavControllerViewModel.viewModelStores
│ ~~~~~~~~~~~~~~~
├─ java.util.LinkedHashMap instance
│ Leaking: UNKNOWN
│ Retaining 128.3 kB in 2404 objects
│ ↓ LinkedHashMap.header
│ ~~~~~~
├─ java.util.LinkedHashMap$LinkedHashMapEntry instance
│ Leaking: UNKNOWN
│ Retaining 32 B in 1 objects
│ ↓ LinkedHashMap$LinkedHashMapEntry.after
│ ~~~~~
├─ java.util.LinkedHashMap$LinkedHashMapEntry instance
│ Leaking: UNKNOWN
│ Retaining 127.8 kB in 2389 objects
│ ↓ HashMap$HashMapEntry.value
│ ~~~~~
├─ androidx.lifecycle.ViewModelStore instance
│ Leaking: UNKNOWN
│ Retaining 127.7 kB in 2388 objects
│ ↓ ViewModelStore.mMap
│ ~~~~
├─ java.util.HashMap instance
│ Leaking: UNKNOWN
│ Retaining 127.7 kB in 2387 objects
│ ↓ HashMap.table
│ ~~~~~
├─ java.util.HashMap$HashMapEntry[] array
│ Leaking: UNKNOWN
│ Retaining 127.7 kB in 2385 objects
│ ↓ HashMap$HashMapEntry[].[1]
│ ~~~
├─ java.util.HashMap$HashMapEntry instance
│ Leaking: UNKNOWN
│ Retaining 126.6 kB in 2348 objects
│ ↓ HashMap$HashMapEntry.value
│ ~~~~~
├─ androidx.navigation.compose.BackStackEntryIdViewModel instance
│ Leaking: UNKNOWN
│ Retaining 126.5 kB in 2347 objects
│ ↓ BackStackEntryIdViewModel.saveableStateHolder
│ ~~~~~~~~~~~~~~~~~~~
├─ androidx.compose.runtime.saveable.SaveableStateHolderImpl instance
│ Leaking: UNKNOWN
│ Retaining 126.4 kB in 2343 objects
│ ↓ SaveableStateHolderImpl.parentSaveableStateRegistry
│ ~~~~~~~~~~~~~~~~~~~~~~~~~~~
├─ androidx.compose.ui.platform.DisposableSaveableStateRegistry instance
│ Leaking: UNKNOWN
│ Retaining 125.9 kB in 2321 objects
│ ↓ DisposableSaveableStateRegistry.onDispose
│ ~~~~~~~~~
├─ androidx.compose.ui.platform.DisposableSaveableStateRegistry_androidKt$DisposableSaveableStateRegistry instance
│ Leaking: UNKNOWN
│ Retaining 125.6 kB in 2312 objects
│ Anonymous subclass of kotlin.jvm.internal.Lambda
│ ↓ DisposableSaveableStateRegistry_androidKt$DisposableSaveableStateRegistry.$androidxRegistry
│ ~~~~~~~~~~~~~~~~~
├─ androidx.savedstate.SavedStateRegistry instance
│ Leaking: UNKNOWN
│ Retaining 125.6 kB in 2310 objects
│ ↓ SavedStateRegistry.mComponents
│ ~~~~~~~~~~~
├─ androidx.arch.core.internal.SafeIterableMap instance
│ Leaking: UNKNOWN
│ Retaining 125.5 kB in 2309 objects
│ ↓ SafeIterableMap.mEnd
│ ~~~~
├─ androidx.arch.core.internal.SafeIterableMap$Entry instance
│ Leaking: UNKNOWN
│ Retaining 125.3 kB in 2302 objects
│ ↓ SafeIterableMap$Entry.mValue
│ ~~~~~~
├─ androidx.activity.ComponentActivity$$ExternalSyntheticLambda1 instance
│ Leaking: UNKNOWN
│ Retaining 125.3 kB in 2301 objects
│ f[=12=] instance of com.leinardi.template.ui.MainActivity with mDestroyed = true
│ ↓ ComponentActivity$$ExternalSyntheticLambda1.f[=12=]
│ ~~~
╰→ com.leinardi.template.ui.MainActivity instance
Leaking: YES (ObjectWatcher was watching this because com.leinardi.template.ui.MainActivity received
Activity#onDestroy() callback and Activity#mDestroyed is true)
Retaining 125.3 kB in 2300 objects
key = 2c6ea34a-19c0-4d26-a5ec-88625d79531f
watchDurationMillis = 42581
retainedDurationMillis = 37580
mApplication instance of com.leinardi.template.Template
mBase instance of android.app.ContextImpl
METADATA
Build.VERSION.SDK_INT: 24
Build.MANUFACTURER: Google
LeakCanary version: 2.7
App process name: com.leinardi.template
Stats: LruCache[maxSize=3000,hits=1619,misses=30537,hitRate=5%]
RandomAccess[bytes=1518368,reads=30537,travel=10623307269,range=18917491,size=20769812]
Heap dump reason: user request
Analysis duration: 1249 ms
我注意到的事情:
- 泄漏仅在第二次改变方向后发生(因此,如果您从纵向 phone 开始,当您从横向返回纵向时)
- 如果我使用导航组件导航到它,调试屏幕也会发生泄漏
- 如果通过深度访问栏屏幕,则不会发生泄漏 link (
adb shell am start -d "template://bar" -a android.intent.action.VIEW
)
- 如果使用顶部应用栏的向上导航按钮从 Bar 屏幕返回到 Foo 屏幕,则不会发生泄漏(Foo 屏幕永远不会泄漏)
- 可在 API 级别 24、28 和 30 上重现(我没有在其他级别上测试)
我已经尝试过的事情:
- 从 activity (
lateinit var navHostController: NavHostController
) 中删除导航主机实例
- 使用
rememberCoroutineScope()
而不是 LaunchedEffect()
作为 templateNavigator.destinations.collect {}
(MainActivity.kt:71)
我也曾尝试使用 Android Studio Profiled 来调查泄漏,但我并没有真正成功。这里有一些屏幕截图:
我怀疑这可能是某些 AndroidX 库的错误,但也许我只是做错了什么。
repro 项目并没有完全精简:它是一个多模块单一 activity 我正在构建的仅 MVVM-MVI Compose 模板项目。尚未完成,但大部分事情应该到位。
这是模块的概述:
简而言之,该应用程序只有一个Activity,除了设置导航主机之外没有任何逻辑。导航逻辑在 core-navigation
内。每个功能模块都提供 UI 和业务逻辑。目前有 3 个功能,每个功能有 1 个屏幕:Foo、Bar 和 Debug。 Foo 屏幕只有一个按钮可通往 Bar 屏幕。如果您旋转设备两次,泄漏会发生在条形屏幕内。如果您通过 Deeplink.
访问 Bar,则不会发生这种情况
泄漏实际上是导航组合库中的一个错误,下一个版本将提供修复:https://issuetracker.google.com/issues/204905432#comment4
MainActivity
在第二次方向改变后泄漏,但只有在使用 navHostController
导航到新目的地后才会泄漏。
可以复制的工作项目可用 here。
这些是复制步骤:
- 运行 应用程序(它将加载
FooScreen
,其中仅包含一个TopAppBar
和一个Button
)。 - 单击“打开栏屏幕”
Button
(它将加载仅包含TopAppBar
的BarScreen
) - 将设备的方向从纵向更改为横向
- 将方向改回纵向
此时您应该看到 StrictMode
抱怨 logcat:
2021-11-02 16:23:20.672 31230-31230/com.leinardi.template E/StrictMode: class com.leinardi.template.ui.MainActivity; instances=2; limit=1
android.os.StrictMode$InstanceCountViolation: class com.leinardi.template.ui.MainActivity; instances=2; limit=1
at android.os.StrictMode.setClassInstanceLimit(StrictMode.java:1)
LeakCanary 通知也应该可见。这是 LeakCanary 日志:
┬───
│ GC Root: System class
│
├─ leakcanary.internal.InternalLeakCanary class
│ Leaking: NO (MainActivity↓ is not leaking and a class is never leaking)
│ ↓ static InternalLeakCanary.resumedActivity
├─ com.leinardi.template.ui.MainActivity instance
│ Leaking: NO (Activity#mDestroyed is false)
│ mApplication instance of com.leinardi.template.Template
│ mBase instance of android.app.ContextImpl
│ ↓ MainActivity.navHostController
│ ~~~~~~~~~~~~~~~~~
├─ androidx.navigation.NavHostController instance
│ Leaking: UNKNOWN
│ Retaining 5.5 kB in 140 objects
│ activity instance of com.leinardi.template.ui.MainActivity with mDestroyed = false
│ context instance of com.leinardi.template.ui.MainActivity with mDestroyed = false
│ lifecycleOwner instance of com.leinardi.template.ui.MainActivity with mDestroyed = false
│ ↓ NavController.viewModel
│ ~~~~~~~~~
├─ androidx.navigation.NavControllerViewModel instance
│ Leaking: UNKNOWN
│ Retaining 128.4 kB in 2406 objects
│ ↓ NavControllerViewModel.viewModelStores
│ ~~~~~~~~~~~~~~~
├─ java.util.LinkedHashMap instance
│ Leaking: UNKNOWN
│ Retaining 128.3 kB in 2404 objects
│ ↓ LinkedHashMap.header
│ ~~~~~~
├─ java.util.LinkedHashMap$LinkedHashMapEntry instance
│ Leaking: UNKNOWN
│ Retaining 32 B in 1 objects
│ ↓ LinkedHashMap$LinkedHashMapEntry.after
│ ~~~~~
├─ java.util.LinkedHashMap$LinkedHashMapEntry instance
│ Leaking: UNKNOWN
│ Retaining 127.8 kB in 2389 objects
│ ↓ HashMap$HashMapEntry.value
│ ~~~~~
├─ androidx.lifecycle.ViewModelStore instance
│ Leaking: UNKNOWN
│ Retaining 127.7 kB in 2388 objects
│ ↓ ViewModelStore.mMap
│ ~~~~
├─ java.util.HashMap instance
│ Leaking: UNKNOWN
│ Retaining 127.7 kB in 2387 objects
│ ↓ HashMap.table
│ ~~~~~
├─ java.util.HashMap$HashMapEntry[] array
│ Leaking: UNKNOWN
│ Retaining 127.7 kB in 2385 objects
│ ↓ HashMap$HashMapEntry[].[1]
│ ~~~
├─ java.util.HashMap$HashMapEntry instance
│ Leaking: UNKNOWN
│ Retaining 126.6 kB in 2348 objects
│ ↓ HashMap$HashMapEntry.value
│ ~~~~~
├─ androidx.navigation.compose.BackStackEntryIdViewModel instance
│ Leaking: UNKNOWN
│ Retaining 126.5 kB in 2347 objects
│ ↓ BackStackEntryIdViewModel.saveableStateHolder
│ ~~~~~~~~~~~~~~~~~~~
├─ androidx.compose.runtime.saveable.SaveableStateHolderImpl instance
│ Leaking: UNKNOWN
│ Retaining 126.4 kB in 2343 objects
│ ↓ SaveableStateHolderImpl.parentSaveableStateRegistry
│ ~~~~~~~~~~~~~~~~~~~~~~~~~~~
├─ androidx.compose.ui.platform.DisposableSaveableStateRegistry instance
│ Leaking: UNKNOWN
│ Retaining 125.9 kB in 2321 objects
│ ↓ DisposableSaveableStateRegistry.onDispose
│ ~~~~~~~~~
├─ androidx.compose.ui.platform.DisposableSaveableStateRegistry_androidKt$DisposableSaveableStateRegistry instance
│ Leaking: UNKNOWN
│ Retaining 125.6 kB in 2312 objects
│ Anonymous subclass of kotlin.jvm.internal.Lambda
│ ↓ DisposableSaveableStateRegistry_androidKt$DisposableSaveableStateRegistry.$androidxRegistry
│ ~~~~~~~~~~~~~~~~~
├─ androidx.savedstate.SavedStateRegistry instance
│ Leaking: UNKNOWN
│ Retaining 125.6 kB in 2310 objects
│ ↓ SavedStateRegistry.mComponents
│ ~~~~~~~~~~~
├─ androidx.arch.core.internal.SafeIterableMap instance
│ Leaking: UNKNOWN
│ Retaining 125.5 kB in 2309 objects
│ ↓ SafeIterableMap.mEnd
│ ~~~~
├─ androidx.arch.core.internal.SafeIterableMap$Entry instance
│ Leaking: UNKNOWN
│ Retaining 125.3 kB in 2302 objects
│ ↓ SafeIterableMap$Entry.mValue
│ ~~~~~~
├─ androidx.activity.ComponentActivity$$ExternalSyntheticLambda1 instance
│ Leaking: UNKNOWN
│ Retaining 125.3 kB in 2301 objects
│ f[=12=] instance of com.leinardi.template.ui.MainActivity with mDestroyed = true
│ ↓ ComponentActivity$$ExternalSyntheticLambda1.f[=12=]
│ ~~~
╰→ com.leinardi.template.ui.MainActivity instance
Leaking: YES (ObjectWatcher was watching this because com.leinardi.template.ui.MainActivity received
Activity#onDestroy() callback and Activity#mDestroyed is true)
Retaining 125.3 kB in 2300 objects
key = 2c6ea34a-19c0-4d26-a5ec-88625d79531f
watchDurationMillis = 42581
retainedDurationMillis = 37580
mApplication instance of com.leinardi.template.Template
mBase instance of android.app.ContextImpl
METADATA
Build.VERSION.SDK_INT: 24
Build.MANUFACTURER: Google
LeakCanary version: 2.7
App process name: com.leinardi.template
Stats: LruCache[maxSize=3000,hits=1619,misses=30537,hitRate=5%]
RandomAccess[bytes=1518368,reads=30537,travel=10623307269,range=18917491,size=20769812]
Heap dump reason: user request
Analysis duration: 1249 ms
我注意到的事情:
- 泄漏仅在第二次改变方向后发生(因此,如果您从纵向 phone 开始,当您从横向返回纵向时)
- 如果我使用导航组件导航到它,调试屏幕也会发生泄漏
- 如果通过深度访问栏屏幕,则不会发生泄漏 link (
adb shell am start -d "template://bar" -a android.intent.action.VIEW
) - 如果使用顶部应用栏的向上导航按钮从 Bar 屏幕返回到 Foo 屏幕,则不会发生泄漏(Foo 屏幕永远不会泄漏)
- 可在 API 级别 24、28 和 30 上重现(我没有在其他级别上测试)
我已经尝试过的事情:
- 从 activity (
lateinit var navHostController: NavHostController
) 中删除导航主机实例
- 使用
rememberCoroutineScope()
而不是LaunchedEffect()
作为templateNavigator.destinations.collect {}
(MainActivity.kt:71)
我也曾尝试使用 Android Studio Profiled 来调查泄漏,但我并没有真正成功。这里有一些屏幕截图:
我怀疑这可能是某些 AndroidX 库的错误,但也许我只是做错了什么。
repro 项目并没有完全精简:它是一个多模块单一 activity 我正在构建的仅 MVVM-MVI Compose 模板项目。尚未完成,但大部分事情应该到位。
这是模块的概述:
简而言之,该应用程序只有一个Activity,除了设置导航主机之外没有任何逻辑。导航逻辑在 core-navigation
内。每个功能模块都提供 UI 和业务逻辑。目前有 3 个功能,每个功能有 1 个屏幕:Foo、Bar 和 Debug。 Foo 屏幕只有一个按钮可通往 Bar 屏幕。如果您旋转设备两次,泄漏会发生在条形屏幕内。如果您通过 Deeplink.
泄漏实际上是导航组合库中的一个错误,下一个版本将提供修复:https://issuetracker.google.com/issues/204905432#comment4