Home > Mobile >  Android MainActivity Data Leak after Changing app to Darkmode
Android MainActivity Data Leak after Changing app to Darkmode

Time:09-18

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

CodePudding user response:

setDefaultNightMode will destroy the current Activity by default:

This is the primary method to control the DayNight functionality, since it allows the delegates to avoid unnecessary recreations when possible.

If this method is called after any host components with attached AppCompatDelegates have been 'created', a uiMode configuration change will occur in each. This may result in those components being recreated, depending on their manifest configuration.

And that configuration change destroys and recreates Activities (unless you're specifically handling those yourself - that's not a solution here, I'm just being clear!). Basically the Activity needs to restart so it can be re-themed, and propagate that theme to all its hosted components and layouts. It's just how Android is designed.

So yeah, your Activity will be destroyed - that shouldn't be a problem if you're handling that event correctly, which you should be anyway. Lots of things can destroy Activities, including rotating the screen or the app being in the background, so you should be storing data and state in a way that lets you recreate it so the user doesn't see a difference. That last link explains it, including links to using ViewModels and their SavedState module.


I don't know about your memory leak specifically - I haven't used LeakCanary but it looks like that Samsung MultiSplitActions thing is holding onto it via its window? If that's true that's out of your control I think. Activity destruction is completely normal, so if that app isn't handling that situation correctly then it's a problem with their software.

It's also possible it gets released soon after anyway, maybe LeakCanary is running too early to see it. If you want to check, let your Activity get destroyed by switching modes, then use the Memory profiler to force a Garbage Collection and then do a heap dump. You can see how many copies of your MainActivity are in memory (should be one usually) and what's holding onto them. It'll even warn you about some potential leaks by default (usually things like multiple copies of an Activity)

CodePudding user response:

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.

  • Related