Skip to content

Memory Leak: FileKitDialog._registry Retains Activity After Destruction #487

@DanielBulavinov

Description

@DanielBulavinov

Summary

FileKit library retains a reference to ActivityResultRegistry through a static field in FileKitDialog, preventing Android Activities from being garbage collected after they are destroyed.

Environment

  • Library Version: filekit-core 0.12.0
  • Platform: Android (iOS not tested)

Steps to Reproduce

  1. Call FileKit.init(activity) on a screen
  2. Navigate away from the screen or close the app
  3. Activity remains in memory and is not garbage collected

Leak Trace

LeakCanary reports the following retention chain:

GC Root: Thread object
├─ android.net.ConnectivityThread instance
│    ↓ Thread.contextClassLoader
├─ dalvik.system.PathClassLoader instance
│    ↓ ClassLoader.runtimeInternalObjects
├─ java.lang.Object[] array
│    ↓ Object[3968]
├─ io.github.vinceglb.filekit.dialogs.FileKitDialog class
│    ↓ static FileKitDialog._registry
│                           ~~~~~~~~~
├─ androidx.activity.ComponentActivity$activityResultRegistry$1 instance
│    Retaining 615.5 kB in 12731 objects
│    Anonymous subclass of androidx.activity.result.ActivityResultRegistry
│    this$0 instance of com.vinted.carrier.MainActivity with mDestroyed = true
│    ↓ ComponentActivity$activityResultRegistry$1.this$0
│                                                 ~~~~~~
╰→ com.vinted.carrier.MainActivity instance
     Leaking: YES (ObjectWatcher was watching this because com.vinted.carrier.MainActivity received
     Activity#onDestroy() callback and Activity#mDestroyed is true)
     Retaining 608.6 kB in 12497 objects
     key = 9de4ea76-92f4-4034-9094-ec25f65f0546
     watchDurationMillis = 5546
     retainedDurationMillis = 530

Assumed Root Cause

The FileKitDialog object maintains a static reference to ActivityResultRegistry:

internal object FileKitDialog {
    private var _registry: ActivityResultRegistry? = null
    val registry: ActivityResultRegistry
        get() = _registry
            ?: throw FileKitNotInitializedException()

    fun init(registry: ActivityResultRegistry) {
        _registry = registry
    }
}

Assumed problem:

  • FileKit.init(activity) sets FileKitDialog._registry = activity.activityResultRegistry
  • The ActivityResultRegistry holds a reference to the Activity (this$0)
  • The static _registry field persists for the lifetime of the class
  • When the Activity is destroyed, the static reference prevents the ActivityResultRegistry from being garbage collected
  • The ActivityResultRegistry retains the Activity, preventing it from being garbage collected
  • There appears to be no cleanup mechanism to clear the _registry reference when the Activity is destroyed

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions