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
}
}
轉載請註明出處,本文鏈接:https://www.uj5u.com/yidong/374817.html
標籤:其他
