Android solves XXX Layout leaked and uses Navigation to step on pit XML memory leak

Android solves XXX Layout leaked and uses Navigation to step on pit XML memory leak


I recently maintained a project, and the cause of a memory leak has been investigated for a long time, so I will record it here.
At the beginning of the article, it is recommended to simply look at the troubleshooting process and the cause of the error, and then look at the solution result to avoid wasting everyone's time

error log

After opening the project, LeakCanary detected a memory leak, and the address pointed to was not the same as before. It pointed to a layout. The specific information is as follows
insert image description here

Troubleshooting process

The scene is like this. The project has only one Activity, which uses Navigation, which contains two fragments, one MainFragment (Default Destination), and one SettingDragment. When the project is opened for the first time, it will
jump to SettingFragment because the server address has not been set. That is this step. When it is wrong.
Analysis, we know that in the Navigation source code, every time a new interface is navigated, replace is used by default, thus destroying a fragment, so the first route of LeakCanary points to MainFragment.
Continue down, pointing to the MainFragment.bind variable

 // ViewModel & DataBinding
    private val viewModel: MainViewModel by viewModels()
    private lateinit var binding: MainFragmentBinding
      override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?,
    ): View {
    
    
        binding = MainFragmentBinding.inflate(inflater, container, false).also {
    
    
            it.lifecycleOwner = this
            it.viewModel = viewModel
        }
        return binding.root
    }

It’s okay, the code was written like this before, and the Android developer’s official website is also written like this. There is nothing wrong with it. It shouldn’t be the problem here (it’s the problem!! If you write the same as me, you should pay attention! !! There is a big hole here! Wait a minute)
Then continue to look down at the log of leakCancary. The next one points to a variable called mboundView0 in the MainFragmentBindingImpl class generated by DataBinding. Paste the relevant code

    @NonNull
    private final androidx.constraintlayout.widget.ConstraintLayout mboundView0;
    static {
    
    
        sIncludes = null;
        sViewsWithIds = new android.util.SparseIntArray();
        sViewsWithIds.put(R.id.background, 2);
        sViewsWithIds.put(R.id.vessel, 3);
        sViewsWithIds.put(R.id.state, 4);
        sViewsWithIds.put(R.id.et_healthCode, 5);
    }
    //这是mboundView具体的赋值
    private DasBindingImpl(androidx.databinding.DataBindingComponent bindingComponent, View root, Object[] bindings) {
    
    
        super(bindingComponent, root, 0
            );
        this.mboundView0 = (androidx.constraintlayout.widget.ConstraintLayout) bindings[0];
        this.mboundView0.setTag(null);
        setRootTag(root);
        // listeners
        invalidateAll();
    }

It's just adding the View in XML, which is normal. The 0 position is the ConstraintLayout of the root layout

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools">
    <data>
        <import type="com.chinz.mms.client.ui.main.MainViewModel" />
        <variable
            name="viewModel"
            type="MainViewModel" />
    </data>
    <androidx.constraintlayout.widget.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent">
        <ImageView  android:id="@+id/background"/>

        <FrameLayout   android:id="@+id/vessel"/>

        <com.***.MarqueeView android:id="@+id/marquee_tv"/>

        <TextView  android:id="@+id/state"/>

        <EditText android:id="@+id/et_healthCode"/>
    </androidx.constraintlayout.widget.ConstraintLayout>
</layout>

ConstraintLayout? WTF?! !
My code is useless at all, he doesn't even have an id! Among them, mContext is raining and I have nothing to do.
This variable is hidden in the source code.
Does the sub-View of ConstraintLayout use the reference of MainFragment? Then delete them all one by one, leaving only one ConstraintLayout. . If it doesn’t work, but still leaks the log,
then change to another layout. . Failed, exactly the same error!
Could it be that the ViewModel is occupied? Although it doesn’t seem to have anything to do with this log, I also deleted it to rule it out. . . No results
Various Baidu, Google failed, and finally guessed whether it is the reason for Navtigation, although it seems that he has not passed the Context to him, but he is the only one who can be suspected. My
code

   override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
    
    
        super.onViewCreated(view, savedInstanceState)
        listenModels()
		//initialized是VM中的一个变量,检查有没有设置过服务器地址,第一次运行肯定是false
        //重点怀疑(这里是错误的)
        if (viewModel.initialized.not()) {
    
    
            //findNavController().navigate(MainFragmentDirections.actionMainToSetting())
        }

    }

Cause of leakage

Recommend this good article. It is very detailed. If you are interested, you can click in to see it in detail. Here is a quote. Simply put, it is two 1. In general, before using Navigation, the life cycle of Fragment
and the life cycle of View It is synchronous. After the Fragment is replaced, the View is also OnDestroy after being OnDesroy, so we will be fine before.
After using Navigation, the View (that is, MainFragmentBinding) is destroyed, but the fragment will not be destroyed, resulting in a memory leak.
2. Although the official Android document describes that LiveData will not cause memory leaks, but if Navgation is used. LiveData may not be unregistered when the lifecycleOwner is destroyed, and memory leaks will still occur.
If this page immediately jumps to the next page, the previously subscribed LiveData will not be unregistered. The reason is that when you jump out of this page, the page is still in the life cycle state INITIALIZED, but the anti-registration condition is that the life cycle state of this page is at least CREATED. Just like my previous operation in MainFragment's onViewCreated will cause it. Look at the source code in the fragment below

void performDestroyView() {
    
    
    mChildFragmentManager.dispatchDestroyView();
    if (mView != null && mViewLifecycleOwner.getLifecycle().getCurrentState()
                    .isAtLeast(Lifecycle.State.CREATED)) {
    
    
        mViewLifecycleOwner.handleLifecycleEvent(Lifecycle.Event.ON_DESTROY);
    }
    ......
}

It may be because of the use of libraries such as jetPack, the leakCanary positioning expression is not intuitive, and the Navigation is not perfect enough.

Navigation-related pits have a center. In general, Fragment is a View, and the life cycle of View is the life cycle of Fragment, but under the framework of Navigation, the life cycle of Fragment is different from that of View. When navigating to the new UI, the covered UI, the View is destroyed, but the fragment instance (not destroyed) is retained. When the fragment is resumed, the View will be recreated. This is the source of "sin".
————————————————
Copyright statement: This article is the original article of the CSDN blogger "Byte Beat Technology Team", following the CC 4.0 BY-SA copyright agreement, please attach the original source for reprinting link and this statement.
Original link: https://blog.csdn.net/ByteDanceTech/article/details/120052166

solution

class MainFragment : BaseFragment() {
    
    

    // ViewModel & DataBinding
    private val viewModel: MainViewModel by viewModels()

    private var _binding: MainFragmentBinding? = null

    private val binding get() = _binding!!
        override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?,
    ): View {
    
    
        _binding = MainFragmentBinding.inflate(inflater, container, false).also {
    
    
            // 重点  解决问题1  不是this  是viewLifecycleOwner
            it.lifecycleOwner = viewLifecycleOwner
            it.viewModel = viewModel
        }
        // 返回binding对象的root 
         return binding.root
    }
        override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
    
    
        super.onViewCreated(view, savedInstanceState)
         Handler().postDelayed({
    
    
            //解决问题2 liveData 在直接跳转界面会造成内存泄漏,1S不被感知能等到创建完后状态变成CREATED 有更好的方案欢迎补充
            if (viewModel.initialized.not()) {
    
    
                findNavController().navigate(R.id.action_main_to_setting)
            }
        }, 1000)

    }
    //不要在onDestory写
    override fun onDestroyView() {
    
    
        super.onDestroyView()
         //重点 解决问题1
        _binding = null
    }
}
  
 

Guess you like

Origin blog.csdn.net/qq_39178733/article/details/121742157