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
Here's what the leaktrace you shared tells us:
MainActivity
that is alive (mDestroyed
= false), that's the new activity after the configuration change (dark mode)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)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.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.