Search code examples
androidkotlinmemory-leaksleakcanary

Android MainActivity Data Leak after Changing app to Darkmode


I have a basic android app for now, where there's 2 fragments they are showing text only and 1 bottom navigation bar The app checks if the default mode is Darkmode or no so i can update my design... For some reason after changing the app to dark mode or light mode, The app flashes and onDestroy is called and there's a Memory Leak

LeakCanary Log:

====================================
1 APPLICATION LEAKS

References underlined with "~~~" are likely causes.
Learn more at https://squ.re/leaks.

175008 bytes retained by leaking objects
Signature: 11f05db2bd6fd24ef9e96dc39221a0e6e79ac535
┬───
│ GC Root: System class
│
├─ android.net.ConnectivityManager class
│    Leaking: NO (MainActivity↓ is not leaking and a class is never leaking)
│    ↓ static ConnectivityManager.sInstance
├─ android.net.ConnectivityManager instance
│    Leaking: NO (MainActivity↓ is not leaking)
│    mContext instance of com.yousefelsayed.example.activity.MainActivity with mDestroyed = false
│    ↓ ConnectivityManager.mContext
├─ com.yousefelsayed.example.activity.MainActivity instance
│    Leaking: NO (DecorView↓ is not leaking and Activity#mDestroyed is false)
│    mApplication instance of android.app.Application
│    mBase instance of androidx.appcompat.view.ContextThemeWrapper
│    ↓ Activity.mDecor
├─ com.android.internal.policy.DecorView instance
│    Leaking: NO (View attached)
│    View is part of a window view hierarchy
│    View.mAttachInfo is not null (view attached)
│    View.mWindowAttachCount = 1
│    mContext instance of com.android.internal.policy.DecorContext, wrapping activity com.yousefelsayed.example.
│    activity.MainActivity with mDestroyed = false
│    ↓ DecorView.mMSActions
│                ~~~~~~~~~~
├─ com.samsung.android.multiwindow.MultiSplitActions instance
│    Leaking: UNKNOWN
│    Retaining 43 B in 1 objects
│    ↓ MultiSplitActions.mWindow
│                        ~~~~~~~
├─ com.android.internal.policy.PhoneWindow instance
│    Leaking: YES (Window#mDestroyed is true)
│    Retaining 15.0 kB in 285 objects
│    mContext instance of com.yousefelsayed.example.activity.MainActivity with mDestroyed = true
│    mOnWindowDismissedCallback instance of com.yousefelsayed.example.activity.MainActivity with mDestroyed = true
│    ↓ Window.mContext
╰→ com.yousefelsayed.example.activity.MainActivity instance
​     Leaking: YES (ObjectWatcher was watching this because com.yousefelsayed.example.activity.MainActivity received
​     Activity#onDestroy() callback and Activity#mDestroyed is true)
​     Retaining 175.0 kB in 3213 objects
​     key = b5185190-4e4a-405f-845a-c271e3a3fd46
​     watchDurationMillis = 5639
​     retainedDurationMillis = 638
​     mApplication instance of android.app.Application
​     mBase instance of androidx.appcompat.view.ContextThemeWrapper
====================================

onCreate

//Views
private lateinit var view: ActivityMainBinding
private lateinit var navController: NavController

//Backend
private lateinit var sp: SharedPreferences

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    view = DataBindingUtil.setContentView(this, R.layout.activity_main)
    init()
    setupLightMode(sp.getInt("DarkMode",0))

}

init() fun

private fun init(){
    val navHostFragment = supportFragmentManager.findFragmentById(R.id.fragmentView) as NavHostFragment
    navController = navHostFragment.findNavController()
    sp = getSharedPreferences("Example",0)
    //setup bottomNav
    view.bottomNav.setupWithNavController(navController)
    //check for darkMode to setupValues, Default value is 3 to make sure if it's app first run
    if (sp.getInt("DarkMode",3) == 3){
        Log.d("Debug","FirstAppRun")
        when (resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK) {
            Configuration.UI_MODE_NIGHT_YES -> {
                view.lightModeImageView.setImageResource(R.drawable.ic_baseline_dark_mode_24)
                sp.edit().putInt("DarkMode",1).apply()
            }
            Configuration.UI_MODE_NIGHT_NO -> {
                view.lightModeImageView.setImageResource(R.drawable.ic_baseline_wb_sunny_24)
                sp.edit().putInt("DarkMode",0).apply()
            }
        }
    }
}

setupLightMode() fun

private fun setupLightMode(darkMode: Int){
    if (darkMode == 0){
        view.lightModeImageView.setImageResource(R.drawable.ic_baseline_wb_sunny_24)
        AppCompatDelegate.setDefaultNightMode(MODE_NIGHT_NO)
    }else if(darkMode == 1) {
        view.lightModeImageView.setImageResource(R.drawable.ic_baseline_dark_mode_24)
        AppCompatDelegate.setDefaultNightMode(MODE_NIGHT_YES)
    }
}

Thanks


Solution

  • Here's what the leaktrace you shared tells us:

    • You have an instance of your MainActivity that is alive (mDestroyed = false), that's the new activity after the configuration change (dark mode)
    • That alive MainActivity has a DecorView (that's the root of the view hierarchy of the activity) which is attached (that's expected, all is fine so far)
    • The DecorView has a mMSActions field which references an instance of com.samsung.android.multiwindow.MultiSplitActions. This field does not exist in Android Open Source (e.g. see DecorView sources). As you can guess from the package name (com.samsung), that added mMSActions field is a modification of the Android framework by Samsung.
    • The MultiSplitActions instance has an mWindow field which references a PhoneWindow which is destroyed. This is the PhoneWindow from the activity that got destroyed as you changed configuration to dark mode, you can see it has a mContext field which points to the destroyed MainActivity.

    So what does this mean? Samsung phones have custom Android features, and one of those features seem to be the ability to split windows, and they've done changes to the Android Framework to support that. Unfortunately, the MultiSplitActions object that seems to help with that keeps a reference to an old window instead of updating the reference as the activity gets reconfigured, and therefore causes a leak.

    What can you do? Not much, besides reaching out to Samsung to let them know there's a leak they should fix.