Android AlertDialog DecorView Memory Leak - android

When initiating network communication through the Executor class, I use the showProgress function. When the result is received after communication ends, hideProgess function is used. The code is as follows and I am using it in BaseActivity.
private val networkIO = Executors.newFixedThreadPool(3)
private var progressBar: AlertDialog? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(binding.root)
progressBar = AlertDialog.Builder(this)
.setView(R.layout.dialog_loading)
.setCancelable(false)
.create()
.apply { window?.setBackgroundDrawableResource(android.R.color.transparent) }
doOnCreate()
}
override fun onDestroy() {
progressBar = null
super.onDestroy()
}
fun showProgress() {
runOnUiThread {
try {
if (progressBar?.isShowing == false) {
progressBar?.show()
}
} catch (e: Exception) {
e.printStackTrace()
}
}
}
fun hideProgress() {
runOnUiThread { progressBar?.dismiss() }
}
A memory leak occurs when moving to another activity after performing the next network communication in MainActivity.
private fun fetchCard() {
networkIO.execute {
showProgress()
networkManager.getCardInfo { _, code, message ->
Timber.d("## $code")
Timber.d("## $message")
hideProgress()
}
}
}
Leak causes provided by LeakCanary
2021-03-08 05:11:42.552 8678-8678/com.tagless D/LeakCanary: ​
┬───
│ GC Root: System class
│
├─ android.app.ActivityThread class
│ Leaking: NO (MainActivity↓ is not leaking and a class is never leaking)
│ ↓ static ActivityThread.sCurrentActivityThread
├─ android.app.ActivityThread instance
│ Leaking: NO (MainActivity↓ is not leaking)
│ mInitialApplication instance of com.tagless.TaglessApp
│ mSystemContext instance of android.app.ContextImpl
│ mSystemUiContext instance of android.app.ContextImpl
│ ↓ ActivityThread.mActivities
├─ android.util.ArrayMap instance
│ Leaking: NO (MainActivity↓ is not leaking)
│ ↓ ArrayMap.mArray
├─ java.lang.Object[] array
│ Leaking: NO (MainActivity↓ is not leaking)
│ ↓ Object[].[3]
├─ android.app.ActivityThread$ActivityClientRecord instance
│ Leaking: NO (MainActivity↓ is not leaking)
│ activity instance of com.tagless.ui.main.MainActivity with mDestroyed = false
│ ↓ ActivityThread$ActivityClientRecord.activity
├─ com.tagless.ui.main.MainActivity instance
│ Leaking: NO (Activity#mDestroyed is false)
│ mApplication instance of com.tagless.TaglessApp
│ mBase instance of androidx.appcompat.view.ContextThemeWrapper
│ ↓ BaseActivity.progressBar
│ ~~~~~~~~~~~
├─ androidx.appcompat.app.AlertDialog instance
│ Leaking: UNKNOWN
│ Retaining 106.7 kB in 1647 objects
│ mContext instance of android.view.ContextThemeWrapper, wrapping activity com.tagless.ui.main.MainActivity with
│ mDestroyed = false
│ Dialog#mDecor is null
│ ↓ Dialog.mWindow
│ ~~~~~~~
├─ com.android.internal.policy.PhoneWindow instance
│ Leaking: UNKNOWN
│ Retaining 23.3 kB in 289 objects
│ mContext instance of android.view.ContextThemeWrapper, wrapping activity com.tagless.ui.main.MainActivity with
│ mDestroyed = false
│ Window#mDestroyed is false
│ ↓ PhoneWindow.mDecor
│ ~~~~~~
╰→ com.android.internal.policy.DecorView instance
​ Leaking: YES (ObjectWatcher was watching this because com.android.internal.policy.DecorView received
​ View#onDetachedFromWindow() callback)
​ Retaining 3.4 kB in 59 objects
​ key = 5f2f819a-9640-429f-afff-4489f7b039d2
​ watchDurationMillis = 5703
​ retainedDurationMillis = 702
​ View not part of a window view hierarchy
​ View.mAttachInfo is null (view detached)
​ View.mWindowAttachCount = 1
​ mContext instance of android.view.ContextThemeWrapper, wrapping activity com.tagless.ui.main.MainActivity with
​ mDestroyed = false
METADATA
Build.VERSION.SDK_INT: 29
Build.MANUFACTURER: samsung
LeakCanary version: 2.6
App process name: com.tagless
Stats: LruCache[maxSize=3000,hits=5668,misses=70891,hitRate=7%]
RandomAccess[bytes=3887192,reads=70891,travel=27244024610,range=20775300,size=25990100]
Heap dump reason: user request
Analysis duration: 3864 ms
Thanks in advance Anyone please help!!

Add progressBar = null should solve your issue. The progressBar is referenced by the activity and it can not be recycled by GC.
fun showProgress() {
runOnUiThread {
try {
if (progressBar == null) {
progressBar = AlertDialog.Builder(this)
.setView(R.layout.dialog_loading)
.setCancelable(false)
.create()
.apply { window?.setBackgroundDrawableResource(android.R.color.transparent) }
}
if (progressBar?.isShowing == false) {
progressBar?.show()
}
} catch (e: Exception) {
e.printStackTrace()
}
}
}
fun hideProgress() {
runOnUiThread {
progressBar?.dismiss()
progressBar = null
}
}

Related

Fragment binding is leaking even after setting null

As per android docs I have implemented simple fragment but when I move to next fragment it started leaking in _binding even I have set binding null.
Here is my leak Stack trace
37740 bytes retained by leaking objects
Signature: 7162f0f053a181182714235f17d7e8c36154eb81
┬───
│ GC Root: Global variable in native code
│
├─ android.database.ContentObserver$Transport instance
│ Leaking: NO (LoginFragment↓ is not leaking)
│ ↓ ContentObserver$Transport.mContentObserver
├─ android.widget.Editor$HandleViewShowObserver instance
│ Leaking: NO (LoginFragment↓ is not leaking)
│ ↓ Editor$HandleViewShowObserver.mContext
├─ dagger.hilt.android.internal.managers.ViewComponentManager$FragmentContextWrapper instance
│ Leaking: NO (LoginFragment↓ is not leaking)
│ ViewComponentManager$FragmentContextWrapper wraps an Activity with Activity.mDestroyed false
│ ↓ ViewComponentManager$FragmentContextWrapper.fragment
├─ com.ics.homework.ui.auth.LoginFragment instance
│ Leaking: NO (WelcomeFragment↓ is not leaking and Fragment#mFragmentManager is not null)
│ ↓ LoginFragment.mFragmentManager
├─ androidx.fragment.app.FragmentManagerImpl instance
│ Leaking: NO (WelcomeFragment↓ is not leaking)
│ ↓ FragmentManagerImpl.mFragmentStore
├─ androidx.fragment.app.FragmentStore instance
│ Leaking: NO (WelcomeFragment↓ is not leaking)
│ ↓ FragmentStore.mActive
├─ java.util.HashMap instance
│ Leaking: NO (WelcomeFragment↓ is not leaking)
│ ↓ HashMap.table
├─ java.util.HashMap$Node[] array
│ Leaking: NO (WelcomeFragment↓ is not leaking)
│ ↓ HashMap$Node[].[1]
├─ java.util.HashMap$Node instance
│ Leaking: NO (WelcomeFragment↓ is not leaking)
│ ↓ HashMap$Node.value
├─ androidx.fragment.app.FragmentStateManager instance
│ Leaking: NO (WelcomeFragment↓ is not leaking)
│ ↓ FragmentStateManager.mFragment
├─ com.ics.homework.ui.auth.WelcomeFragment instance
│ Leaking: NO (Fragment#mFragmentManager is not null)
│ ↓ WelcomeFragment._binding
│ ~~~~~~~~
├─ com.ics.homework.databinding.FragmentWelcomeBindingImpl instance
│ Leaking: UNKNOWN
│ ↓ FragmentWelcomeBindingImpl.mRoot
│ ~~~~~
╰→ androidx.constraintlayout.widget.ConstraintLayout instance
​ Leaking: YES (ObjectWatcher was watching this because com.ics.homework.ui.auth.WelcomeFragment received Fragment#onDestroyView() callback (references to its views should be cleared to prevent leaks))
​ key = a1bab73c-de04-4e36-bda1-b29a54da8348
​ watchDurationMillis = 22798
​ retainedDurationMillis = 17792
​ mContext instance of com.ics.homework.ui.auth.AuthActivity with mDestroyed = false
​ View#mParent is null
​ View#mAttachInfo is null (view detached)
​ View.mWindowAttachCount = 1
====================================
0 LIBRARY LEAKS
Here is my implementation
class WelcomeFragment : Fragment() {
private var _binding: FragmentWelcomeBinding? = null
private val binding get() = _binding!!
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
_binding = FragmentWelcomeBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.handler = onClickListener
}
private val onClickListener = View.OnClickListener { view ->
with(binding) {
when (view.id) {
btnLoginSignup.id -> {
val action = WelcomeFragmentDirections.actionWelcomeFragmentToLoginFragment()
findNavController().navigate(action)
}
tvHelpline.id,
tvHelplineNumber.id -> {
val intent = Intent(Intent.ACTION_DIAL)
intent.data = Uri.parse("tel:0123456789")
startActivity(intent)
}
}
}
}
override fun onDestroy() {
super.onDestroy()
binding.handler = null
_binding = null
}
}
In the example from documentation binding is set to null in OnDestroyView() method, not in OnDestroy(). As far as I know, OnDestroy() method of Fragment A is not called when you replace it with Fragment B and add Fragment A to backstack. That's why your binding is not null.

What is causing my AlertDialog to leak in Firebase callback method?

I have an AlertDialog created using the AlertDialog Builder:
private void setProgressDialog() {
AlertDialog.Builder builder = new AlertDialog.Builder(this, R.style.alertDialogTheme);
builder.setView(R.layout.alert_dialog_login_progress);
mAlertDialog = builder.create();
mAlertDialog.setCancelable(false);
mAlertDialog.setCanceledOnTouchOutside(false);
mAlertDialog.show();
}
I'm using FirebaseAuth's sign in method:
private void signIn() {
mAuth.signInWithEmailAndPassword(mEmail, mPassword)
.addOnCompleteListener(this, new OnCompleteListener<AuthResult>() {
#Override
public void onComplete(#NonNull Task<AuthResult> task) {
if (!task.isSuccessful()) {
mAlertDialog.dismiss();
Toast.makeText(LoginActivity.this, "Authentication failed.",
Toast.LENGTH_SHORT).show();
}
}
});
}
I have reduced the code all the way down to this and it still leaks. I am NOT changing activities.
The leak is caused when I type an incorrect password, and the .isSuccessful() method is called.
I have tried running it on the UI thread too, but it still leaks:
runOnUiThread(new Runnable() {
public void run() {
mAlertDialog.dismiss();
Toast.makeText(LoginActivity.this, "Authentication failed.",
Toast.LENGTH_SHORT).show();
}
});
Here is the full code:
https://pastebin.com/bp4Xa6jx
Here is the leak:
┬───
│ GC Root: Local variable in native code
│
├─ android.os.HandlerThread instance
│ Leaking: NO (PathClassLoader↓ is not leaking)
│ Thread name: 'GoogleApiHandler'
│ ↓ Thread.contextClassLoader
├─ dalvik.system.PathClassLoader instance
│ Leaking: NO (InternalLeakCanary↓ is not leaking and A ClassLoader is never
│ leaking)
│ ↓ ClassLoader.runtimeInternalObjects
├─ java.lang.Object[] array
│ Leaking: NO (InternalLeakCanary↓ is not leaking)
│ ↓ Object[].[218]
├─ leakcanary.internal.InternalLeakCanary class
│ Leaking: NO (LoginActivity↓ is not leaking and a class is never leaking)
│ ↓ static InternalLeakCanary.resumedActivity
├─ instance
│ Leaking: NO (Activity#mDestroyed is false)
│ mApplication instance of android.app.Application
│ mBase instance of android.app.ContextImpl
│ ↓ LoginActivity.mAlertDialog
│ ~~~~~~~~~~~~
├─ androidx.appcompat.app.AlertDialog instance
│ Leaking: UNKNOWN
│ Retaining 157.5 kB in 2114 objects
│ mContext instance of android.view.ContextThemeWrapper, wrapping activity
│ with mDestroyed = false
│ Dialog#mDecor is null
│ ↓ Dialog.mWindow
│ ~~~~~~~
├─ com.android.internal.policy.PhoneWindow instance
│ Leaking: UNKNOWN
│ Retaining 15.3 kB in 300 objects
│ mContext instance of android.view.ContextThemeWrapper, wrapping activity
│ with mDestroyed = false
│ Window#mDestroyed is false
│ ↓ PhoneWindow.mDecor
│ ~~~~~~
╰→ com.android.internal.policy.DecorView instance
​ Leaking: YES (ObjectWatcher was watching this because com.android.
​ internal.policy.DecorView received View#onDetachedFromWindow() callback)
​ Retaining 4.7 kB in 42 objects
​ key = f381c0b5-587b-4d68-b453-be1c851ce257
​ watchDurationMillis = 31367
​ retainedDurationMillis = 26366
​ View not part of a window view hierarchy
​ View.mAttachInfo is null (view detached)
​ View.mWindowAttachCount = 1
​ mContext instance of android.view.ContextThemeWrapper, wrapping activity
​ with mDestroyed = false
Why is it leaking and what can I do to fix it?
After a few hours of trying to figure out the issue, the memory leak no longer happens when I set my mAlertDialog to null after calling mAlertDialog.dismiss();
mAlertDialog.dismiss();
mAlertDialog = null;
Toast.makeText(LoginActivity.this, "Authentication failed.",Toast.LENGTH_SHORT).show();
I am don't know why this works...but it stops the leak from happening.

Constraint Layout Leaking in Fragment - Leak Canary - Leak in TextInputLayout

I got a memory leak in my login page and I can`t figure why. I am using Leak Canary to identify the leak and this was the leak trace I got.
D/LeakCanary: ┬───
│ GC Root: Input or output parameters in native code
│
├─ okio.AsyncTimeout class
│ Leaking: NO (PathClassLoader↓ is not leaking and a class is never leaking)
│ ↓ static AsyncTimeout.$class$classLoader
├─ dalvik.system.PathClassLoader instance
│ Leaking: NO (InternalLeakCanary↓ is not leaking and A ClassLoader is never leaking)
│ ↓ PathClassLoader.runtimeInternalObjects
├─ java.lang.Object[] array
│ Leaking: NO (InternalLeakCanary↓ is not leaking)
│ ↓ Object[].[957]
├─ leakcanary.internal.InternalLeakCanary class
│ Leaking: NO (MainActivity↓ is not leaking and a class is never leaking)
│ ↓ static InternalLeakCanary.resumedActivity
├─ com.eim.rdoApplication.ui.activity.MainActivity instance
│ Leaking: NO (LoginFragment↓ is not leaking and Activity#mDestroyed is false)
│ mApplication instance of com.eim.rdoApplication.AppApplication
│ mBase instance of androidx.appcompat.view.ContextThemeWrapper, not wrapping known Android context
│ ↓ MainActivity.mActivityResultRegistry
├─ androidx.activity.ComponentActivity$2 instance
│ Leaking: NO (LoginFragment↓ is not leaking)
│ Anonymous subclass of androidx.activity.result.ActivityResultRegistry
D/LeakCanary: │ this$0 instance of com.eim.rdoApplication.ui.activity.MainActivity with mDestroyed = false
│ ↓ ComponentActivity$2.mKeyToCallback
├─ java.util.HashMap instance
│ Leaking: NO (LoginFragment↓ is not leaking)
│ ↓ HashMap.table
├─ java.util.HashMap$Node[] array
│ Leaking: NO (LoginFragment↓ is not leaking)
│ ↓ HashMap$Node[].[2]
├─ java.util.HashMap$Node instance
│ Leaking: NO (LoginFragment↓ is not leaking)
│ ↓ HashMap$Node.value
├─ androidx.activity.result.ActivityResultRegistry$CallbackAndContract instance
│ Leaking: NO (LoginFragment↓ is not leaking)
│ ↓ ActivityResultRegistry$CallbackAndContract.mCallback
├─ androidx.fragment.app.FragmentManager$11 instance
│ Leaking: NO (LoginFragment↓ is not leaking)
│ Anonymous class implementing androidx.activity.result.ActivityResultCallback
│ ↓ FragmentManager$11.this$0
├─ androidx.fragment.app.FragmentManagerImpl instance
│ Leaking: NO (LoginFragment↓ is not leaking)
│ ↓ FragmentManagerImpl.mParent
├─ com.eim.rdoApplication.ui.fragment.login.LoginFragment instance
│ Leaking: NO (Fragment#mFragmentManager is not null)
D/LeakCanary: │ componentContext instance of dagger.hilt.android.internal.managers.ViewComponentManager$FragmentContextWrapper,
│ wrapping activity com.eim.rdoApplication.ui.activity.MainActivity with mDestroyed = false
│ ↓ LoginFragment.mAnimationInfo
│ ~~~~~~~~~~~~~~
├─ androidx.fragment.app.Fragment$AnimationInfo instance
│ Leaking: UNKNOWN
│ Retaining 319257 bytes in 3340 objects
│ ↓ Fragment$AnimationInfo.mFocusedView
│ ~~~~~~~~~~~~
├─ com.google.android.material.textfield.TextInputEditText instance
│ Leaking: UNKNOWN
│ Retaining 319171 bytes in 3339 objects
│ View not part of a window view hierarchy
│ View.mAttachInfo is null (view detached)
│ View.mID = R.id.inputEditTextEmailField
│ View.mWindowAttachCount = 1
│ mContext instance of androidx.appcompat.view.ContextThemeWrapper, wrapping activity com.eim.rdoApplication.ui.
│ activity.MainActivity with mDestroyed = false
│ ↓ TextInputEditText.mParent
D/LeakCanary: │ ~~~~~~~
├─ android.widget.FrameLayout instance
│ Leaking: UNKNOWN
│ Retaining 2231 bytes in 14 objects
│ View not part of a window view hierarchy
│ View.mAttachInfo is null (view detached)
│ View.mWindowAttachCount = 1
│ mContext instance of androidx.appcompat.view.ContextThemeWrapper, wrapping activity com.eim.rdoApplication.ui.
│ activity.MainActivity with mDestroyed = false
│ ↓ FrameLayout.mParent
│ ~~~~~~~
├─ com.google.android.material.textfield.TextInputLayout instance
│ Leaking: UNKNOWN
│ Retaining 204872 bytes in 2728 objects
│ View not part of a window view hierarchy
│ View.mAttachInfo is null (view detached)
│ View.mID = R.id.textInputLayoutEmail
│ View.mWindowAttachCount = 1
│ mContext instance of androidx.appcompat.view.ContextThemeWrapper, wrapping activity com.eim.rdoApplication.ui.
│ activity.MainActivity with mDestroyed = false
D/LeakCanary: │ ↓ TextInputLayout.mParent
│ ~~~~~~~
╰→ androidx.constraintlayout.widget.ConstraintLayout instance
​ Leaking: YES (ObjectWatcher was watching this because com.eim.rdoApplication.ui.fragment.login.LoginFragment
D/LeakCanary: ​ received Fragment#onDestroyView() callback (references to its views should be cleared to prevent leaks))
​ Retaining 4897 bytes in 71 objects
​ key = 0d74b072-904d-4aba-9a0e-4e17260ffc96
​ watchDurationMillis = 9630
​ retainedDurationMillis = 4627
​ View not part of a window view hierarchy
​ View.mAttachInfo is null (view detached)
​ View.mWindowAttachCount = 1
​ mContext instance of dagger.hilt.android.internal.managers.ViewComponentManager$FragmentContextWrapper, wrapping
​ activity com.eim.rdoApplication.ui.activity.MainActivity with mDestroyed = false
METADATA
Build.VERSION.SDK_INT: 27
Build.MANUFACTURER: motorola
LeakCanary version: 2.5
App process name: com.eim.aplicativo_rdo
What could be causing this leak? Any help ?
I am working with one activity and multiple fragments being handled by navigation component.
From what I could understand, the main problem is that my textInputlayoutEmail somehow is not part of a view hierarchy at some point of the lifecycle of my fragment. This is my Fragment Code :
private const val ERROR_LOGIN = "ERROR_LOGIN"
#AndroidEntryPoint
class LoginFragment : Fragment() {
private val navController by lazy {findNavController()}
private val loginViewModel: LoginViewModel by navGraphViewModels(R.id.navigation_graph){
defaultViewModelProviderFactory
}
private var _binding: FragmentLoginBinding? = null
private val binding get() = _binding!!
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setHasOptionsMenu(true)
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
_binding = FragmentLoginBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
setLoadingView()
setGoToForgotPasswordFragmentButton()
setLoginButton()
super.onViewCreated(view, savedInstanceState)
}
private fun setLoginButton() {
binding.buttonSignIn.setOnClickListener {
if(areFieldsValid()){
loginViewModel.emailField = binding.inputEditTextEmailField.text.toString()
loginViewModel.passwordField = binding.editTextPasswordField.text.toString()
loginUser(loginViewModel.let { UserDataRequest(it.emailField, it.passwordField) })
}
}
}
private fun areFieldsValid(): Boolean {
return !(binding.editTextPasswordField.text.isNullOrBlank()||binding.inputEditTextEmailField.text.isNullOrBlank())
}
private fun setGoToForgotPasswordFragmentButton() {
binding.forgotPasswordEditText.setOnClickListener{
val action = LoginFragmentDirections.actionLoginFragmentToForgotPasswordFragment()
navController.navigate(action)
}
}
private fun setLoadingView() {
binding.apply {
loadingIconLoginFragment.visibility = View.INVISIBLE
loadingBgLoginFragment.visibility = View.INVISIBLE
}
}
private fun loginUser(loginUserRequest: UserDataRequest){
viewLifecycleOwner.lifecycleScope.launch {
loginViewModel.loginUser(loginUserRequest).collect {
when(it.status) {
Resource.Status.LOADING -> {
withContext(Dispatchers.Main) {
setLoadingInterface()
}
}
Resource.Status.ERROR -> {
withContext(Dispatchers.Main){
val errorMessage = "Não foi possível realizar seu Login"
clearLoadingInterface()
showErrorInterface(errorMessage, ERROR_LOGIN)
}
}
Resource.Status.SUCCESS -> {
withContext(Dispatchers.Main) {
clearLoadingInterface()
val action = LoginFragmentDirections.actionLoginFragmentToGeneralHomeFragment(0, "", "")
navController.navigate(action)
}
}
}
}
}
}
private fun setLoadingInterface() {
binding.apply {
loadingBgLoginFragment.visibility = View.VISIBLE
loadingIconLoginFragment.visibility = View.VISIBLE
}
rotateAnimation()
}
private fun showErrorInterface(errorMessage: String, errorType: String) {
clearLoadingInterface()
val dialog = NetworkErrorDialog.newInstance(errorMessage, errorType)
dialog.show(childFragmentManager, dialog.tag)
}
private fun clearLoadingInterface() {
binding.apply {
loadingBgLoginFragment.visibility = View.GONE
loadingIconLoginFragment.apply {
visibility = View.GONE
clearAnimation()
}
}
}
private fun rotateAnimation() {
val animation = AnimationUtils.loadAnimation(activity?.applicationContext, R.anim.rotate)
binding.loadingIconLoginFragment.startAnimation(animation)
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
}
In my views so I don`t know what else can I do to clear this reference.
If anyone could show me in which part of the code I am holding a reference to this view, would help me a lot. Thanks

RecyclerView↑ is leaking and View detached and has parent

I have tried to set adapter null in onDestroyView also tried with addOnAttachStateChangeListener but still there is memory leak.
Here is my Stack Trace
┬───
│ GC Root: System class
│
├─ android.app.ActivityThread class
│ Leaking: NO (MainActivity↓ is not leaking and a class is never leaking)
│ ↓ static ActivityThread.sCurrentActivityThread
├─ android.app.ActivityThread instance
│ Leaking: NO (MainActivity↓ is not leaking)
│ ↓ ActivityThread.mActivities
├─ android.util.ArrayMap instance
│ Leaking: NO (MainActivity↓ is not leaking)
│ ↓ ArrayMap.mArray
├─ java.lang.Object[] array
│ Leaking: NO (MainActivity↓ is not leaking)
│ ↓ Object[].[1]
├─ android.app.ActivityThread$ActivityClientRecord instance
│ Leaking: NO (MainActivity↓ is not leaking)
│ ↓ ActivityThread$ActivityClientRecord.activity
D/LeakCanary: ├─ com.ics.homework.ui.MainActivity instance
│ Leaking: NO (TopicFragment↓ is not leaking and Activity#mDestroyed is false)
│ ↓ MainActivity.mActivityResultRegistry
├─ androidx.activity.ComponentActivity$2 instance
│ Leaking: NO (TopicFragment↓ is not leaking)
│ Anonymous subclass of androidx.activity.result.ActivityResultRegistry
│ ↓ ComponentActivity$2.mKeyToCallback
├─ java.util.HashMap instance
│ Leaking: NO (TopicFragment↓ is not leaking)
│ ↓ HashMap.table
├─ java.util.HashMap$HashMapEntry[] array
│ Leaking: NO (TopicFragment↓ is not leaking)
│ ↓ HashMap$HashMapEntry[].[1]
├─ java.util.HashMap$HashMapEntry instance
│ Leaking: NO (TopicFragment↓ is not leaking)
│ ↓ HashMap$HashMapEntry.value
├─ androidx.activity.result.ActivityResultRegistry$CallbackAndContract instance
│ Leaking: NO (TopicFragment↓ is not leaking)
│ ↓ ActivityResultRegistry$CallbackAndContract.mCallback
├─ androidx.fragment.app.FragmentManager$10 instance
│ Leaking: NO (TopicFragment↓ is not leaking)
│ Anonymous class implementing androidx.activity.result.ActivityResultCallback
│ ↓ FragmentManager$10.this$0
├─ androidx.fragment.app.FragmentManagerImpl instance
│ Leaking: NO (TopicFragment↓ is not leaking)
│ ↓ FragmentManagerImpl.mParent
├─ com.ics.homework.ui.course.topics.TopicFragment instance
│ Leaking: NO (Fragment#mFragmentManager is not null)
│ ↓ TopicFragment.mAnimationInfo
│ ~~~~~~~~~~~~~~
├─ androidx.fragment.app.Fragment$AnimationInfo instance
│ Leaking: UNKNOWN
│ ↓ Fragment$AnimationInfo.mFocusedView
│ ~~~~~~~~~~~~
├─ androidx.recyclerview.widget.RecyclerView instance
│ Leaking: YES (View detached and has parent)
│ mContext instance of dagger.hilt.android.internal.managers.ViewComponentManager$FragmentContextWrapper, wrapping activity com.ics.homework.ui.MainActivity with mDestroyed = false
│ View#mParent is set
│ View#mAttachInfo is null (view detached)
│ View.mID = R.id.recyclerView
│ View.mWindowAttachCount = 1
│ ↓ RecyclerView.mParent
├─ androidx.swiperefreshlayout.widget.SwipeRefreshLayout instance
│ Leaking: YES (RecyclerView↑ is leaking and View detached and has parent)
│ mContext instance of dagger.hilt.android.internal.managers.ViewComponentManager$FragmentContextWrapper, wrapping activity com.ics.homework.ui.MainActivity with mDestroyed = false
│ View#mParent is set
│ View#mAttachInfo is null (view detached)
│ View.mID = R.id.swipeRefreshLayout
│ View.mWindowAttachCount = 1
│ ↓ SwipeRefreshLayout.mParent
D/LeakCanary: ╰→ androidx.constraintlayout.widget.ConstraintLayout instance
​ Leaking: YES (ObjectWatcher was watching this because com.ics.homework.ui.course.topics.TopicFragment received Fragment#onDestroyView() callback (references to its views should be cleared to prevent leaks))
​ key = b5fcd20b-86ca-432c-ab69-0e4a90881651
​ watchDurationMillis = 26087
​ retainedDurationMillis = 21084
​ mContext instance of dagger.hilt.android.internal.managers.ViewComponentManager$FragmentContextWrapper, wrapping activity com.ics.homework.ui.MainActivity with mDestroyed = false
​ View#mParent is null
​ View#mAttachInfo is null (view detached)
​ View.mWindowAttachCount = 1
====================================
0 LIBRARY LEAKS
My Implementaion is
#AndroidEntryPoint
class TopicFragment : Fragment() {
private var courseId: String? = null
private var title: String? = null
private var _binding: FragmentTopicBinding? = null
private val binding get() = _binding!!
private val topicViewModel by viewModels<TopicViewModel>()
private lateinit var topicAdapter: TopicAdapter
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
arguments?.let {
courseId = it.getString(ARG_COURSE_ID)
title = it.getString(ARG_TITLE)
}
}
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View {
_binding = FragmentTopicBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.lifecycleOwner = viewLifecycleOwner
binding.viewModel = topicViewModel
topicAdapter = TopicAdapter(topicItemClick)
binding.apply {
stateErrorView.apply {
handler = retryCallback
}
swipeRefreshLayout.setOnRefreshListener {
topicViewModel.retry()
}
recyclerView.apply {
layoutManager = LinearLayoutManager(requireContext(), LinearLayoutManager.VERTICAL, false)
addItemDecoration(DividerItemDecoration(requireContext(), DividerItemDecoration.VERTICAL))
adapter = topicAdapter
setHasFixedSize(true)
}
}
observeUI()
}
private fun observeUI() {
topicViewModel.topics.observe(viewLifecycleOwner, {
Timber.e(it.status.toString())
it.data?.let(topicAdapter::submitList)
if (it.status == Status.ERROR) topicAdapter.submitList(listOf())
if (it.status != Status.LOADING) binding.swipeRefreshLayout.isRefreshing = false
})
}
private val topicItemClick = object : TopicItemClick {
override fun onClick(topic: Topic) {
val action = TopicFragmentDirections.actionTopicFragmentToChapterFragment(
topic.postId, title!!, false, topic.id, topic.topic
)
findNavController().navigate(action)
}
}
private val retryCallback = object : RetryCallback {
override fun retry() {
topicViewModel.retry()
}
}
override fun onDestroyView() {
binding.recyclerView.addOnAttachStateChangeListener(object : View.OnAttachStateChangeListener {
override fun onViewAttachedToWindow(v: View) {}
override fun onViewDetachedFromWindow(v: View) {
binding.recyclerView.adapter=null
}
})
super.onDestroyView()
_binding = null
}
companion object {
private const val ARG_COURSE_ID = "courseId"
private const val ARG_TITLE = "title"
}
}
TopicFragment is in a created state, Fragment.mAnimationInfo retains a Fragment$AnimationInfo and Fragment$AnimationInfo.mFocusedView retains the fragments detached view which should have been GCed.
This value is set by calls to Fragment.setFocusView(), and a quick search shows that's never ever cleared: https://cs.android.com/search?q=setFocusedView&sq=&ss=androidx%2Fplatform%2Fframeworks%2Fsupport
Looks like this change was introduced in 2020: https://cs.android.com/androidx/platform/frameworks/support/+/1052c3662c40176f7f02da9e06b989dcab21d500
The best would be to file an issue against the androidx fragments library.
Actually this was already filed, fixed in the next release: https://issuetracker.google.com/issues/179925887

Android: ViewModel with paging 3 flow is leaking

My problem is, that my shopViewModel which holds an instance of a paging-flow is somehow leaking. I've tried to solve this problem by converting the flow into a livedata, but that changed nothing.
ViewModel
class ShopViewModel #ViewModelInject constructor(
private val shopPagingSource: ShopPagingSource,
) : ViewModel() {
val SHOP_PAGE_CONFIG: PagingConfig = PagingConfig(pageSize = 20, enablePlaceholders = false)
// As LiveData
val shopFlow = Pager(SHOP_PAGE_CONFIG) { shopPagingSource }.flow.cachedIn(viewModelScope).asLiveData()
// Before
val shopFlow = Pager(SHOP_PAGE_CONFIG) { shopPagingSource }.flow.cachedIn(viewModelScope)
}
Fragment
#AndroidEntryPoint
class ShopFragment(private val shopListAdapter: ShopAdapter) : Fragment(R.layout.fragment_shop), ShopAdapter.OnItemClickListener {
private val shopViewModel: ShopViewModel by viewModels()
private val shopBinding: FragmentShopBinding by viewBinding()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
shopBinding.adapter = shopListAdapter.withLoadStateFooter(ShopLoadAdapter(shopListAdapter::retry))
shopListAdapter.clickHandler(this)
collectShopList()
}
override fun forwardClick(product: #NotNull Product) {
val action = ShopFragmentDirections.actionShopFragmentToShopItemFragment(product)
findNavController().navigate(action)
}
private fun collectShopListWithLiveData() = lifecycleScope.launch {
shopViewModel.shopFlow.observe(viewLifecycleOwner) {
lifecycleScope.launch {
shopListAdapter.submitData(it)
}
}
}
// Before converting to livedata
private fun collectShopListWithFlow() = lifecycleScope.launch {
shopViewModel.shopFlow.collectLatest {
shopListAdapter.submitData(it)
}
}
// To avoid memory leak from injected adapter
override fun onDestroyView() {
requireView().findViewById<RecyclerView>(R.id.rv_shop).adapter = null
super.onDestroyView()
}
}
Adapter
class ShopAdapter #Inject constructor() : PagingDataAdapter<Product, ShopAdapter.ShopViewHolder>(Companion) {
private lateinit var clickListener: OnItemClickListener
companion object: DiffUtil.ItemCallback<Product>() {
override fun areItemsTheSame(oldItem: Product, newItem: Product): Boolean = oldItem.articelNumber == newItem.articelNumber
override fun areContentsTheSame(oldItem: Product, newItem: Product): Boolean = oldItem == newItem
}
inner class ShopViewHolder(val binding: ShopListItemBinding) : RecyclerView.ViewHolder(binding.root)
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ShopAdapter.ShopViewHolder {
val layoutInflater = LayoutInflater.from(parent.context)
val binding = ShopListItemBinding.inflate(layoutInflater, parent, false)
return ShopViewHolder(binding).also {
binding.mcvProductItem.setOnClickListener { clickListener.forwardClick(binding.product!!) }
}
}
override fun onBindViewHolder(holder: ShopAdapter.ShopViewHolder, position: Int) {
holder.binding.product = getItem(position) ?: return
holder.binding.executePendingBindings()
}
fun clickHandler(clickEventHandler: OnItemClickListener) {
clickListener = clickEventHandler
}
interface OnItemClickListener {
fun forwardClick(product: #NotNull Product)
}
}
MainFragmentFactory
class MainFragmentFactory #Inject constructor(
// .. other dependencies
private val shopAdapter: ShopAdapter,
) : FragmentFactory() {
override fun instantiate(classLoader: ClassLoader, className: String): Fragment = when(className) {
// ... other fragments
ShopFragment::class.java.name -> ShopFragment(shopAdapter)
else -> super.instantiate(classLoader, className)
}
PagingSource
class ShopPagingSource #Inject constructor(
private val shopRepository: ShopFirebaseRepository,
) : PagingSource<QuerySnapshot, Product>() {
override suspend fun load(params: LoadParams<QuerySnapshot>): LoadResult<QuerySnapshot, Product> = try {
withTimeout(SHOP_MAX_LOADING_TIME) {
val currentPage = params.key ?: shopRepository.getCurrentPage()
val lastDocumentSnapShot = currentPage.documents[currentPage.size() - 1]
val nextPage = shopRepository.getNextPage(lastDocumentSnapShot)
LoadResult.Page(
data = currentPage.toObjects(),
prevKey = null,
nextKey = nextPage
)
}
} catch (e: TimeoutCancellationException) {
Timber.d("Mediator failed, No Internet Connection")
LoadResult.Error(e)
} catch (e: ArrayIndexOutOfBoundsException) {
Timber.d("Mediator failed, ArrayIndexOutOfBounds")
LoadResult.Error(e)
} catch (e: Exception) {
Timber.d("Mediator failed, Unknown Error: ${e.message.toString()}")
LoadResult.Error(e)
}
}
LeakCanary
D/LeakCanary: ====================================
HEAP ANALYSIS RESULT
====================================
1 APPLICATION LEAKS
References underlined with "~~~" are likely causes.
Learn more at https://squ.re/leaks.
2618 bytes retained by leaking objects
Signature: 944313b4ecbdb77c99682dc8c1646e12e4f37d8
┬───
│ GC Root: Local variable in native code
│
├─ dalvik.system.PathClassLoader instance
│ Leaking: NO (InternalLeakCanary↓ is not leaking and A ClassLoader is never leaking)
│ ↓ PathClassLoader.runtimeInternalObjects
├─ java.lang.Object[] array
│ Leaking: NO (InternalLeakCanary↓ is not leaking)
│ ↓ Object[].[2142]
├─ leakcanary.internal.InternalLeakCanary class
│ Leaking: NO (MainActivity↓ is not leaking and a class is never leaking)
│ ↓ static InternalLeakCanary.resumedActivity
├─ com.example.app.framework.ui.view.MainActivity instance
│ Leaking: NO (MainNavHostFragment↓ is not leaking and Activity#mDestroyed is false)
│ ↓ MainActivity.navController$delegate
├─ kotlin.SynchronizedLazyImpl instance
│ Leaking: NO (MainNavHostFragment↓ is not leaking)
│ ↓ SynchronizedLazyImpl._value
├─ androidx.navigation.NavHostController instance
│ Leaking: NO (MainNavHostFragment↓ is not leaking)
│ ↓ NavHostController.mLifecycleOwner
├─ com.example.app.framework.ui.view.utils.MainNavHostFragment instance
│ Leaking: NO (Fragment#mFragmentManager is not null)
│ ↓ MainNavHostFragment.mainFragmentFactory
│ ~~~~~~~~~~~~~~~~~~~
├─ com.example.app.framework.ui.view.utils.MainFragmentFactory instance
│ Leaking: UNKNOWN
│ ↓ MainFragmentFactory.shopAdapter
│ ~~~~~~~~~~~
├─ com.example.app.framework.ui.adapter.recyclerview.ShopAdapter instance
│ Leaking: UNKNOWN
│ ↓ ShopAdapter.differ
│ ~~~~~~
├─ androidx.paging.AsyncPagingDataDiffer instance
│ Leaking: UNKNOWN
│ ↓ AsyncPagingDataDiffer.differBase
│ ~~~~~~~~~~
├─ androidx.paging.AsyncPagingDataDiffer$differBase$1 instance
│ Leaking: UNKNOWN
│ Anonymous subclass of androidx.paging.PagingDataDiffer
│ ↓ AsyncPagingDataDiffer$differBase$1.receiver
│ ~~~~~~~~
├─ androidx.paging.PageFetcher$PagerUiReceiver instance
│ Leaking: UNKNOWN
│ ↓ PageFetcher$PagerUiReceiver.this$0
│ ~~~~~~
├─ androidx.paging.PageFetcher instance
│ Leaking: UNKNOWN
│ ↓ PageFetcher.pagingSourceFactory
│ ~~~~~~~~~~~~~~~~~~~
├─ com.example.app.framework.ui.viewmodel.ShopViewModel$shopFlow$1 instance
│ Leaking: UNKNOWN
│ Anonymous subclass of kotlin.jvm.internal.Lambda
│ ↓ ShopViewModel$shopFlow$1.this$0
│ ~~~~~~
╰→ com.example.app.framework.ui.viewmodel.ShopViewModel instance
​ Leaking: YES (ObjectWatcher was watching this because com.example.app.framework.ui.viewmodel.ShopViewModel received ViewModel#onCleared() callback)
​ key = 0e65fcab-e6dd-475a-83d4-87b2050d797b
​ watchDurationMillis = 7771
​ retainedDurationMillis = 2769
====================================
EDIT
When scoping the ShopAdapter with #FragmentScoped, I get the following leak:
┬───
│ GC Root: Local variable in native code
│
├─ dalvik.system.PathClassLoader instance
│ Leaking: NO (InternalLeakCanary↓ is not leaking and A ClassLoader is never leaking)
│ ↓ PathClassLoader.runtimeInternalObjects
├─ java.lang.Object[] array
│ Leaking: NO (InternalLeakCanary↓ is not leaking)
│ ↓ Object[].[409]
├─ leakcanary.internal.InternalLeakCanary class
│ Leaking: NO (MainActivity↓ is not leaking and a class is never leaking)
│ ↓ static InternalLeakCanary.resumedActivity
├─ com.example.app.framework.ui.view.MainActivity instance
│ Leaking: NO (MainNavHostFragment↓ is not leaking and Activity#mDestroyed is false)
│ mApplication instance of com.example.app.App
│ mBase instance of androidx.appcompat.view.ContextThemeWrapper, not wrapping known Android context
│ ↓ MainActivity.navController$delegate
├─ kotlin.SynchronizedLazyImpl instance
│ Leaking: NO (MainNavHostFragment↓ is not leaking)
│ ↓ SynchronizedLazyImpl._value
├─ androidx.navigation.NavHostController instance
│ Leaking: NO (MainNavHostFragment↓ is not leaking)
│ mActivity instance of com.example.app.framework.ui.view.MainActivity with mDestroyed = false
│ mContext instance of dagger.hilt.android.internal.managers.ViewComponentManager$FragmentContextWrapper, wrapping
│ activity com.example.app.framework.ui.view.MainActivity with mDestroyed = false
│ ↓ NavHostController.mLifecycleOwner
├─ com.example.app.framework.ui.view.utils.MainNavHostFragment instance
│ Leaking: NO (Fragment#mFragmentManager is not null)
│ componentContext instance of dagger.hilt.android.internal.managers.ViewComponentManager$FragmentContextWrapper,
│ wrapping activity com.example.app.framework.ui.view.MainActivity with mDestroyed = false
│ ↓ MainNavHostFragment.mainFragmentFactory
│ ~~~~~~~~~~~~~~~~~~~
D/LeakCanary: ├─ com.example.app.framework.ui.view.utils.MainFragmentFactory instance
│ Leaking: UNKNOWN
│ Retaining 212 bytes in 7 objects
│ ↓ MainFragmentFactory.shopAdapter
│ ~~~~~~~~~~~
├─ com.example.app.framework.ui.adapter.recyclerview.ShopAdapter instance
│ Leaking: UNKNOWN
│ Retaining 14461 bytes in 546 objects
│ ↓ ShopAdapter.clickListener
│ ~~~~~~~~~~~~~
╰→ com.example.app.framework.ui.view.fragments.shop.ShopFragment instance
​ Leaking: YES (ObjectWatcher was watching this because com.example.app.framework.ui.view.fragments.shop.
​ ShopFragment received Fragment#onDestroy() callback and Fragment#mFragmentManager is null)
​ Retaining 2121 bytes in 79 objects
​ key = 71ec5094-8509-47a5-9e0a-070fe642ca8a
​ watchDurationMillis = 18366
​ retainedDurationMillis = 13365
​ componentContext instance of dagger.hilt.android.internal.managers.ViewComponentManager$FragmentContextWrapper,
​ wrapping activity com.example.app.framework.ui.view.MainActivity with mDestroyed = false
I know that I'm late for the party but I can see that you are using lifecycleScope.launch a lot and inside you are calling the adapter to submitData. This means this adapter will not be able to be properly garbage collected. This is probably the root of memory leak.
Try to use viewLifecycleOwner.lifecycleScope.launch instead.
This is a known mistake:
https://proandroiddev.com/5-common-mistakes-when-using-architecture-components-403e9899f4cb
Okay I've managed to solve this leak. The leak was caused, because I've injected my ShopAdapter via Constructor Injection into my Fragment. When injecting something into the fragment via constructor injection, you have to pass the dependency to the MainFragmentFactory. But because of this, the MainFragmentFactory will always hold a reference to the adapter, even when the fragment is destroyed and the fragment is not needed any more (therefore, requireView().findViewById<RecyclerView>(R.id.rv_shop).adapter = null wont't even make a change here).
To solve this problem, DON'T inject the Adapter via constructor injection and rather inject it via field injection.

Categories

Resources