2024年安卓最新Android Navigation 遇坑记 - 真实项目经历,大厂测试面试题

总结:

各行各样都会淘汰一些能力差的,不仅仅是IT这个行业,所以,不要被程序猿是吃青春饭等等这类话题所吓倒,也不要觉得,找到一份工作,就享受安逸的生活,你在安逸的同时,别人正在奋力的向前跑,这样与别人的差距也就会越来越遥远,加油,希望,我们每一个人,成为更好的自己。

  • BAT大厂面试题、独家面试工具包,

  • 资料包括 数据结构、Kotlin、计算机网络、Framework源码、数据结构与算法、小程序、NDK、Flutter,


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

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

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

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.

void performDestroyView() {

mChildFragmentManager.dispatchDestroyView();

if (mView != null && mViewLifecycleOwner.getLifecycle().getCurrentState()

.isAtLeast(Lifecycle.State.CREATED)) {

mViewLifecycleOwner.handleLifecycleEvent(Lifecycle.Event.ON_DESTROY);

}

}

其实 Android 的生命周期管理还是值得信任的,前提是我们得彻底搞清楚状态流转的细节。

5. 当 ViewPager2 遇到 Navigation

ViewPager 是在应用开发的过程中,高频的用到的组件。Android 的官网有对基本的使用有详细的介绍。

一直都很美好,直到遇到 Navigation。

让我们来看官方例子里面 ViewPager2 的 Adapter 的类的声明。

class DemoCollectionAdapter(fragment: Fragment) : FragmentStateAdapter(fragment) {

override fun getItemCount(): Int = 100

override fun createFragment(position: Int): Fragment {

// Return a NEW fragment instance in createFragment(int)

val fragment = DemoObjectFragment()

fragment.arguments = Bundle().apply {

// Our object is just an integer 😛

putInt(ARG_OBJECT, position + 1)

}

return fragment

}

}

不避讳的说,我们实际项目中的代码,也犯了同样的问题。不是说官网的写法有问题,而是在 Navigation 的框架下,才会导致的内存泄漏问题。这个泄漏是如何发生的呢?我们来看一下 FragmentStateAdapter 的构造函数。

/**

* @param fragment if the {@link ViewPager2} lives directly in a {@link Fragment} subclass.

* @see FragmentStateAdapter#FragmentStateAdapter(FragmentActivity)

* @see FragmentStateAdapter#FragmentStateAdapter(FragmentManager, Lifecycle)

学习宝典

对我们开发者来说,一定要打好基础,随时准备战斗。不论寒冬是否到来,都要把自己的技术做精做深。虽然目前移动端的招聘量确实变少了,但中高端的职位还是很多的,这说明行业只是变得成熟规范起来了。竞争越激烈,产品质量与留存就变得更加重要,我们进入了技术赋能业务的时代。

不论遇到什么困难,都不应该成为我们放弃的理由!

很多人在刚接触这个行业的时候或者是在遇到瓶颈期的时候,总会遇到一些问题,比如学了一段时间感觉没有方向感,不知道该从那里入手去学习,对此我针对Android程序员,我这边给大家整理了一套学习宝典!包括不限于高级UI、性能优化、移动架构师、NDK、混合式开发(ReactNative+Weex)微信小程序、Flutter等全方面的Android进阶实践技术;希望能帮助到大家,也节省大家在网上搜索资料的时间来学习,也可以分享动态给身边好友一起学习!

【Android核心高级技术PDF文档,BAT大厂面试真题解析】

【算法合集】

【延伸Android必备知识点】

【Android部分高级架构视频学习资源】

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

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

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

!包括不限于高级UI、性能优化、移动架构师、NDK、混合式开发(ReactNative+Weex)微信小程序、Flutter等全方面的Android进阶实践技术;希望能帮助到大家,也节省大家在网上搜索资料的时间来学习,也可以分享动态给身边好友一起学习!

【Android核心高级技术PDF文档,BAT大厂面试真题解析】

[外链图片转存中…(img-X7ARmzuw-1715799880671)]

【算法合集】

[外链图片转存中…(img-BRn3Qi8G-1715799880671)]

【延伸Android必备知识点】

[外链图片转存中…(img-ND4IkG78-1715799880672)]

【Android部分高级架构视频学习资源】

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

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

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

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值