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', auiMode
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 ViewModel
s 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 aDecorView
(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 ofcom.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 addedmMSActions
field is a modification of the Android framework by Samsung. - The
MultiSplitActions
instance has anmWindow
field which references aPhoneWindow
which is destroyed. This is thePhoneWindow
from the activity that got destroyed as you changed configuration to dark mode, you can see it has amContext
field which points to the destroyedMainActivity
.
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.