Android 解决XXX Layout leaked 使用Navigation 踩坑 XML内存泄漏
Posted 余震l
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了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内存泄漏的主要内容,如果未能解决你的问题,请参考以下文章