Android Navigation 遇坑记 - 真实项目经历

先声明一下,这篇文章不是 Navigation 的入门文章和使用说明,也不是原理介绍,可以作为我们团队对 Navigation 使用的一些心得体会的说明,比较适合对 Navigation 有项目实践的同学。

这里也欢迎有兴趣的同学,如果有这方面想交流的话,欢迎联系,一起探讨。以下都是一些从项目实践中的个人见解,免不了出纰漏,不吝斧正。

Navigation 优点体会


在使用过程中,我们感受到如下的优点。

  1. 页面跳转性能更好,在单 Activity 的架构下,都是 fragment 的切换,每次 fragment 被压栈之后,View 被销毁,相比之前 Activity 跳转,更加轻量,需要的内存更少。

  2. 通过 Viewmodel 进行数据共享更便捷,不需要页面之间来回传数据。

  3. 统一的 Navigation API 来更精细的控制跳转逻辑。

所有坑的中心


Navigation 相关的坑,都有个中心。一般情况下,Fragment 就是一个 View,View 的生命周期就是 Fragment 的生命周期,但是在 Navigation 的架构下,Fragment 的生命周期和 View 的生命周期是不一样的。当 navigate 到新的 UI,被覆盖的 UI,View 被销毁,但是保留了 fragment 实例(未被 destroy),当这个 fragment 被 resume 的时候,View 会被重新创建。这是“罪恶”之源。

整理了 8 个坑,我们一个一个趟


先来个小坑感受一些 Navigation。

1. Databinding 需要 onDestroyView 设置为 Null。

现在大家都会使用 Jetpack 里面的 databinding 技术,这个确实可以帮助我们简化很多代码,其中的自感知的生命周期,可以帮我们只有在必要的时候来更新 UI。

一般会在 Fragment 的 onCreateView 模板函数中初始化 ViewDataBing,这样就会有 Fragment 持有对 View 的引用。但是 fragment 和 view 的生命周期是不一样的,当 view 被销毁的时候,fragment 并不一定被销毁,所以一定要在 fragment.onDestroyView 函数中把对 view 的引用变量设置为 null,不然会导致 view 回收不掉。上一段官方的代码来说明一下。

private var _binding: ResultProfileBinding? = null

// This property is only valid between onCreateView and

// onDestroyView.

private val binding get() = _binding!!

override fun onCreateView(

inflater: LayoutInflater,

container: ViewGroup?,

savedInstanceState: Bundle?

): View? {

_binding = ResultProfileBinding.inflate(inflater, container, false)

val view = binding.root

return view

}

override fun onDestroyView() {

super.onDestroyView()

_binding = null

}

2. 当 Databinding 遇到错的 lifecycle.

Databinding 确实很强大,能把数据和 UI 进行绑定,这里对 UI 就有个要求,UI 一定要知道自己的生命周期的,知道自己什么时候处于 Active 和 InActive 的状态。所以我们必须要给 databinding 设置一个正确的生命周期.

下面来看一段有问题的代码:

override fun onCreateView(

inflater: LayoutInflater,

container: ViewGroup?,

savedInstanceState: Bundle?

): View {

_binding = HomeFragmentBinding.inflate(inflater, container, false)

binding.lifecycleOwner = this // 问题代码在这里!!!

return binding.root

}

这段代码运行起来没有问题,看起来都是按照预期的在执行。甚至官方代码也是这么写的。连 LeakCanary 也检测不出来内存泄漏的问题,LeakCanary 只能检测出来一些 Activity,Fragment 和 View 等实例的内存泄漏,对于普通的类的实例是没有办法分析的。

问题就出现在 databinding 遇到了一个错的 lifecycle,在没有用 Navigation 框架的时候,View 的生命周期和 Fragment 的生命周期一致的,但是在 Navigation 框架下,两者的生命周期是不一致的。我们来看下 ViewDataBinding 设置 lifecycleOwner 的具体代码。

下面的代码中,往这个 lifecycleOwner 里面加入了一个 OnStartListener 实例,因为这个 lifecycleOwner 是 fragment 的,会在 fragment 销毁的时候反注册,但是并不会在 View 被销毁的时候被反注册。而 OnStartListener 有对这个 ViewDataBinding 有引用,会导致 View 被销毁的时候(跳到另外一个页面),这个引用会阻止系统回收这个 View。

这个分析逻辑是对的,但是结果是不对的,系统还是会对这个 View 进行回收,因为 OnStartListener 的实例持有的是对这个 View 的弱引用,这个 View 还是会被回收。这就是 LeakCanary 没有报错的原因。但是这个 OnStartListener 的实例,就没这么幸运了,正是这个实例无法回收导致了内存泄漏。

@MainThread

public void setLifecycleOwner(@Nullable LifecycleOwner lifecycleOwner) {

if (mLifecycleOwner == lifecycleOwner) {

return;

}

if (mLifecycleOwner != null) {

mLifecycleOwner.getLifecycle().removeObserver(mOnStartListener);

}

mLifecycleOwner = lifecycleOwner;

if (lifecycleOwner != null) {

if (mOnStartListener == null) {

mOnStartListener = new OnStartListener(this);

// 这个实例持有了ViewDataBinging的实例,虽然是弱引用。

}

lifecycleOwner.getLifecycle().addObserver(mOnStartListener);

// 问题出现在这里,如果这个lifecycle是fragment的,View被销毁了,里面不会进行反注册。

}

for (WeakListener<?> weakListener : mLocalFieldObservers) {

if (weakListener != null) {

weakListener.setLifecycleOwner(lifecycleOwner);

}

}

}

正确的做法是需要给这个 ViewDataBinding 设置 viewLifecycleOwner.

binding.lifecycleOwner = viewLifecycleOwner

多说一句啊,这个问题是如何被发现的呢?我们有一套检查框架逻辑的代码,对于这个问题,我们会在 fragment 的 onStop 函数里面检查有多少实例在监听 fragment 的生命周期,我们发现,这个数字会一直涨,这个问题就暴露了,适当的时候,和大家分享一下这套框架。

3. Glide 自我管理的生命周期值得信赖吗?

不值得信赖了

Glide 是一个非常流行的图片加载框架,不得不说,Glide 的缓存这一块的设计非常的优秀,功能强大,可扩展强。还有它的生命周期的自我管理,通过创建一个 fragment 在当前的页面,通过这个 fragment 的生命周期,实现在 onStart 的时候进行图片加载,在 onStop 的时候,把还没有执行或者没有执行完的任务缓存下来,以便在 onStart 的再执行,当然是在没有 onDestory 的情况下。

一切都很完美,直到遇到了 Navigation。

glide.with(fragment).load(url).into(imageview).

呵呵,上面的这段,在 Navigation 的架构下,如果 Fragment 还在,但是执行了 onDestroyView,imageview 需要被销毁。这个情况下,如果图片加载任务没执行完,任务就会被缓存下来了。这个任务还有对需要被销毁的 imageview 有强引用,导致这个 imageview 销毁不了,从而内存泄漏。

如何 100%的重现这个问题呢,有个简单的方法,让大家可以验证一下这个问题。给这个任务,加一个图片的 transformation,这个 transformation 什么也不干,就是 sleep 3 秒钟,在这个 3 秒中之内,跳转到另一个页面。这会导致当前页面进行 View 的 destory,但是 fragment 并不会 destory,因为这个任务还没执行完,这个任务就会被 Glide 缓存,具体会被缓存位置为 RequestManager->RequestTracker->pendingRequests。

如何来解决这个问题呢?这个没有现成的解决方法,在 Glide 的官网有提类似的问题,但是 Glide 维护者听起来还没有意识到这个问题,没有后续的计划。当然,我们需要来解决这个问题,不然我们的代码就会存在这一点瑕疵了。

解决的方法:自己来管理 Glide 的生命周期,不要通过那个看不见的 fragment 的生命周期,因为那是靠不住的。我们自己写了一个 RequestManager,通过传入的 fragment 的 viewLifecycleOwner 来进行管理。使用也很方便,在调用的时候如下即可。

KGlide.with(fragment).load(url).into(imageview).

源码精简了一下,贴在这里,请指正。

import com.bumptech.glide.manager.Lifecycle as GlideLifecycle

class KGlide {

companion object {

private val lifecycleMap = ArrayMap<LifecycleOwner, RequestManager>()

@MainThread

fun with(fragment: Fragment): RequestManager {

Util.assertMainThread()

val lifecycleOwner = fragment.viewLifecycleOwner

if (lifecycleOwner.lifecycle.currentState == Lifecycle.State.DESTROYED) {

throw IllegalStateException(“View is already destroyed.”)

}

if (lifecycleMap[lifecycleOwner] == null) {

val appContext = fragment.requireContext().applicationContext

lifecycleMap[lifecycleOwner] = RequestManager(

Glide.get(appContext),

KLifecycle(lifecycleOwner.lifecycle),

KEmptyRequestManagerTreeNode(), appContext

)

}

return lifecycleMap[lifecycleOwner]!!

}

}

class KEmptyRequestManagerTreeNode : RequestManagerTreeNode {

override fun getDescendants(): Set {

return emptySet()

}

}

class KLifecycle(private val lifecycle: Lifecycle) : GlideLifecycle {

private val lifecycleListeners =

Collections.newSetFromMap(WeakHashMap<LifecycleListener, Boolean>())

private val lifecycleObserver = object : DefaultLifecycleObserver {

override fun onStart(owner: LifecycleOwner) {

val listeners = Util.getSnapshot(lifecycleListeners)

for (listener in listeners) {

listener.onStart()

}

}

override fun onStop(owner: LifecycleOwner) {

val listeners = Util.getSnapshot(lifecycleListeners)

for (listener in listeners) {

listener.onStop()

}

}

override fun onDestroy(owner: LifecycleOwner) {

val listeners = Util.getSnapshot(lifecycleListeners)

for (listener in listeners) {

listener.onDestroy()

}

lifecycleMap.remove(owner)

lifecycleListeners.clear()

lifecycle.removeObserver(this)

}

}

init {

lifecycle.addObserver(lifecycleObserver)

}

override fun addListener(listener: LifecycleListener) {

lifecycleListeners.add(listener)

when (lifecycle.currentState) {

Lifecycle.State.STARTED, Lifecycle.State.RESUMED -> listener.onStart()

Lifecycle.State.DESTROYED -> listener.onDestroy()

else -> listener.onStop()

}

}

override fun removeListener(listener: LifecycleListener) {

lifecycleListeners.remove(listener)

}

}

}

4. Android 组件的生命周期自我管理值得信任吗?

不值得,信任需要我们对 Android 生命周期的管理细节足够的了解。没有足够的了解,哪里来的信任,也就是盲目的信任。

我们在 Android 官方文档里面应该看到过 LiveData 的介绍,下面摘录一段。

Livedata is an observable data holder class. Unlike a regular observable, LiveData is lifecycle-aware, meaning it respects the lifecycle of other app components, such as activities, fragments, or services. This awareness ensures LiveData only updates app component observers that are in an active lifecycle state.

然后还向我们说明 Livedata 的不会导致内存泄漏。

This is especially useful for activities and fragments because they can safely observe LiveData objects and not worry about leaks—activities and fragments are instantly unsubscribed when their lifecycles are destroyed.

写的很清楚,言之昭昭啊。如果你相信了官方文档的介绍,就 too young,too simple 了。LiveData 未必会在 lifecycleOwner 销毁的时候进行反注册,内存泄漏还是会发生。我们看一段 LiveData 会产生内存泄漏的代码。

class HomeFragment : Fragment() {

private val model: NavigationViewModel by viewModels()

override fun onCreateView(

inflater: LayoutInflater,

container: ViewGroup?,

savedInstanceState: Bundle?

): View? {

return inflater.inflate(R.layout.home_fragment, container, false)

}

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {

super.onViewCreated(view, savedInstanceState)

model.getTextValue().observe(viewLifecycleOwner){

view.findViewById(R.id.text).text = it

}

if (isXXX()) {

findNavController().navigate(R.id.next_action)

}

}

}

当你进入某个页面,发现需要导航到另一个页面,这个时候就需要很小心。如果像上面这样的写法,就会导致内存泄漏。

这个 Case 里,在 Fragment.onViewCreated()的模板方法,监听了一个 LiveData,这会导致这个 LiveData 持有外面对象的引用。理想情况下,这个 LivaData 会在 LifecycleOwner 在 onDestory 的时候进行反注册,但是在一些情况下,这个反注册就不会进行。

如上代码的情况下,如果这个页面马上跳到 next_action 的页面,之前订阅的 LiveData 就不会进行反注册。原因出在当跳出这个页面的时候,页面还处于生命周期的状态 INITIALIZED,但是反注册的条件是这个页面的生命周期状态至少是 CREATED.

最后

跳槽季整理面试题已经成了我多年的习惯!在这里我和身边一些朋友特意整理了一份快速进阶为Android高级工程师的系统且全面的学习资料。涵盖了Android初级——Android高级架构师进阶必备的一些学习技能。

附上:我们之前因为秋招收集的二十套一二线互联网公司Android面试真题(含BAT、小米、华为、美团、滴滴)和我自己整理Android复习笔记(包含Android基础知识点、Android扩展知识点、Android源码解析、设计模式汇总、Gradle知识点、常见算法题汇总。)

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化学习资料的朋友,可以戳这里获取

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

少是 CREATED.

最后

跳槽季整理面试题已经成了我多年的习惯!在这里我和身边一些朋友特意整理了一份快速进阶为Android高级工程师的系统且全面的学习资料。涵盖了Android初级——Android高级架构师进阶必备的一些学习技能。

附上:我们之前因为秋招收集的二十套一二线互联网公司Android面试真题(含BAT、小米、华为、美团、滴滴)和我自己整理Android复习笔记(包含Android基础知识点、Android扩展知识点、Android源码解析、设计模式汇总、Gradle知识点、常见算法题汇总。)

[外链图片转存中…(img-DYUPQQBZ-1714285925287)]

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化学习资料的朋友,可以戳这里获取

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

  • 16
    点赞
  • 28
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值