Android 解决XXX Layout leaked 使用Navigation 踩坑 XML内存泄漏

Posted 余震l

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Android 解决XXX Layout leaked 使用Navigation 踩坑 XML内存泄漏相关的知识,希望对你有一定的参考价值。

android 解决XXX Layout leaked 使用Navigation 踩坑 XML内存泄漏


最近维护一个项目,一个内存泄漏的的原因查了很久,这里记录一下。
文章开始建议简单看一下排查过程和错误原因,再去看解决结果,避免浪费大家时间

报错日志

打开项目后LeakCanary检测出一个内存泄漏,地址指向的也和之前的不太一样,指向的是一个layout,具体信息如下

排查过程

场景是这样的 ,项目只有一个Activity,里面使用 Navigation,其中包含两个fragment,一个MainFragment(Default Destination),一个SettingDragment
项目第一次打开 会因为没有设置过服务器地址而跳转到SettingFragment 也就是这一步的时候报错的。
分析一下,我们知道Navigation源码里默认每次navigate一个新界面走的是replace,从而销毁了一个fragment的 所以LeakCanary第一个路线指向的是MainFragment。
继续向下,指向的是MainFragment.bind变量

 // 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
    

也没问题啊,之前代码都是这么写的,安卓开发者官网也是这么写的没有错啊,应该不是这里的问题吧(就是这的问题!!如果你和我写的一样就要留意了!!!这里有大坑!等下说)
那继续向下看leakCancary的日志吧 下一条指向的是DataBinding生成的MainFragmentBindingImpl类中一个叫做 mboundView0的变量,贴一下相关代码

    @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();
    

里面不过是把XML中的View添加进去而已,再正常不过了。其中这个0位置就是根布局的ConstraintLayout

<?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?!!
这个我代码根本就没用到啊,他甚至连个id都没有!其中的mContext更是雨我无瓜啊
源码里都是隐藏这个变量的
那是不是ConstraintLayout的子View有使用到MainFragment的引用呢?那就一个个全删了,只留一个ConstraintLayout 。。无果,还是一样泄漏日志
那就换一种Layout呗。。失败告终,一模一样的错误!
会不会是ViewModel的里有占用,虽然和这日志看起来没关系,也删了排除一下。。。无果
各种百度,谷歌一顿无果,最后猜测是不是Navtigation的原因,虽然看起来好像没给他传过Context,但是只有他可以被怀疑了
我的代码

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

    

泄漏原因

推荐一下这个好文,这方面说的很详细,有兴趣的可以点进去详细看看,这里引用一下,简单地说就是两个
1 一般情况下,就是在使用Navigation 之前,Fragment的生命周期和View的是同步的,Fragment replace后被OnDesroy后 View也OnDestroy了,所以我们之前那么些不会有事,
使用了Navigation后,View (就是MainFragmentBinding)被销毁,但是fragment 不会被销毁,从而导致内存泄漏。
2. 虽然Android官方文档里介绍LiveData 不会造成内存泄漏,但是如果用了Navgation的话。LiveData 未必会在 lifecycleOwner 销毁的时候进行反注册,内存泄漏还是会发生。
如果这个页面马上跳到下一个的页面,之前订阅的 LiveData 就不会进行反注册。原因出在当跳出这个页面的时候,页面还处于生命周期的状态 INITIALIZED,但是反注册的条件是这个页面的生命周期状态至少是 CREATED。就像我之前在MainFragment的onViewCreated中的操作就会造成。看下面fragment中的源码

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

可能是因为使用jetPack这些库的原因,leakCanary定位表达的并不直观,Navigation还不够完善吧

Navigation 相关的坑,都有个中心。一般情况下,Fragment 就是一个 View,View 的生命周期就是 Fragment 的生命周期,但是在 Navigation 的架构下,Fragment 的生命周期和 View 的生命周期是不一样的。当 navigate 到新的 UI,被覆盖的 UI,View 被销毁,但是保留了 fragment 实例(未被 destroy),当这个 fragment 被 resume 的时候,View 会被重新创建。这是“罪恶”之源。
————————————————
版权声明:本文为CSDN博主「 字节跳动技术团队」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/ByteDanceTech/article/details/120052166

解决方案

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
    

  
 

以上是关于Android 解决XXX Layout leaked 使用Navigation 踩坑 XML内存泄漏的主要内容,如果未能解决你的问题,请参考以下文章

Android中RelativeLayout各个属性的含义

android布局

Android中RelativeLayout各个属性的含义

Android中RelativeLayout各个属性

错误日志:Resource leak: 'xxx' is never closed(Scanner类的注意事项)

Android VideoView中SubtitleController Leak(泄漏)