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

多说一句啊,这个问题是如何被发现的呢?我们有一套检查框架逻辑的代码,对于这个问题,我们会在 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)

*/

public FragmentStateAdapter(@NonNull Fragment fragment) {

this(fragment.getChildFragmentManager(), fragment.getLifecycle());

}

/**

* @param fragmentManager of {@link ViewPager2}'s host

* @param lifecycle of {@link ViewPager2}'s host

* @see FragmentStateAdapter#FragmentStateAdapter(FragmentActivity)

* @see FragmentStateAdapter#FragmentStateAdapter(Fragment)

*/

public FragmentStateAdapter(@NonNull FragmentManager fragmentManager,

@NonNull Lifecycle lifecycle) {

mFragmentManager = fragmentManager;

mLifecycle = lifecycle;

super.setHasStableIds(true);

}

可以看到 FragmentStateAdapter 最终会走两个参数的构造函数。第一个是 fragmentManager of ViewPager2’s Host,第二个参数是 lifecycle of ViewPager2’s host。如果你看懂了之前的问题,你就会知道这个问题出在哪里了。在 Navigation 下面,fragment 和 view 的生命周期是不一致的,如果我们在 FragmentStateAdapter 的构造函数中,只传入 fragment 的实例的话,第二个参数 lifecycle 用的是第一个参数 fragment 的 lifecycle。但是很显然,viewpager2’s host 的 lifecycleOwner 是 fragment 的 viewlifecycleOwner,而不是其本身。

具体导致的问题是,在 ViewPager2 实例被销毁的时候,对应的 FragmentStateAdapter 并不会被销毁,因为如果只传一个参数的话,使用的是 Fragment 的生命周期,只有在 fragment 退出的时候,才会被销毁。

这里多说一句啊,FragmentStateAdapter 实例不能被设置到多个 ViewPager2 的对象,所以当 ViewPager2 被重建的时候,这个 Adapter 不能被重用。

这些问题其实很难被发现,LeakCanary 也不能发现。幸好我们有个工具,在每个需要被检查的类的构造函数里面进行记录,然后在类的 finalize 方法对这个记录进行处理,如果发现某个类一直被构造,但是不执行 finalize 方法,这个类就需要被好好关照了。

6. ViewPager2 设置 Adapter 导致的 Fragment 重建问题

先来看以下的代码片段:

Line1:val viewPager2: ViewPager2 = …

Line2:val adapter: FragmentStateAdapter = …

Line3:viewPager2.adapter = adapter

Line4:model.getContentList.observe(viewLifecycleOwner) {

Line5:    adapter.data = it

Line6:    adapter.notifyDataSetChanged()

Line7:}

大家应该看不出来这段代码的问题所在的吧,这个是非常常规的写法。当然这段代码在非 Navigation 的架构下面是没有问题的。但是如果在 Navigation 的架构下,就会有比较严重的问题了。

说明一下问题出现的场景,如果用户先进入这个页面,执行上面代码,viewpager 正常显示。然后注意,重要的步骤来了,在这个页面上,导航到另外一个页面。那当前的这个页面会执行 fragment 的 onStop,注意并不会执行 onDestory。但是会执行 onDestoryView,也就是说 viewPager 将会被销毁,但是 fragment 被保留了。

那如果重新回到这个页面会发生什么事情呢,之前 onStop 的 fragment 会执行 onStart,包括 Adatper 里面生成的 fragment 也会进行重建,并创建 View。

出人意料的事情发生了,Adatper 里面的 fragment 在重建完成之后,立刻又被销毁掉了,这里的销毁是真正的销毁,执行了 onDestory 方法。然一个新的 fragment 被重新创建出来,这就是 fragment 重建问题。是什么导致了这个问题呢?

具体执行销毁 Fragment 的代码如下,在 FragmentStateAdapter 的 gcFragments 的方法。

void gcFragments() {

if (!mHasStaleFragments || shouldDelayFragmentTransactions()) {

return;

}

// Remove Fragments for items that are no longer part of the data-set

Set toRemove = new ArraySet<>();

for (int ix = 0; ix < mFragments.size(); ix++) {

long itemId = mFragments.keyAt(ix);

if (!containsItem(itemId)) {

toRemove.add(itemId);

mItemIdToViewHolder.remove(itemId); // in case they’re still bound

}

}

// Remove Fragments that are not bound anywhere – pending a grace period

if (!mIsInGracePeriod) {

mHasStaleFragments = false; // we’ve executed all GC checks

for (int ix = 0; ix < mFragments.size(); ix++) {

long itemId = mFragments.keyAt(ix);

if (!isFragmentViewBound(itemId)) {

toRemove.add(itemId);

}

}

}

for (Long itemId : toRemove) {

removeFragment(itemId);

}

}

因为这个函数判断,之前 adapter 里面产生的 fragment 需要被回收,依据就是当前的 adatper.containsItem(id)的方法返回 false 了。再提供一个信息,这个函数会在 viewpager2 设置 adatper 的时候被调用。到现在为止,答案已经出来了。因为在 viewpager2 设置 adatper 的时候,adatper 里面什么数据也没有的啊,containsItem 函数必然返回为空的啊,真相大白了。

所以逻辑正确的代码应该如下:

val viewPager2: ViewPager2 = …

model.getContentList.observe(viewLifecycleOwner) {

if(viewPager2.adapter == null){

val adapter: FragmentStateAdapter = …

adapter.data = it

viewPager2.adapter = adapter

} else {

viewPager2.adapter.data = it

}

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数初中级Android工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则近万的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Android移动开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。

img

img

img

img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注:Android)

最后送福利了,现在关注我可以获取包含源码解析,自定义View,动画实现,架构分享等。
内容难度适中,篇幅精炼,每天只需花上十几分钟阅读即可。
大家可以跟我一起探讨,有flutter—底层开发—性能优化—移动架构—资深UI工程师 —NDK相关专业人员和视频教学资料,还有更多面试题等你来拿

录播视频图.png

《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!

-1713721022520)]

[外链图片转存中…(img-Py7ITeZg-1713721022521)]

[外链图片转存中…(img-GRoSMVYK-1713721022522)]

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注:Android)

[外链图片转存中…(img-azlxo1BI-1713721022523)]

最后送福利了,现在关注我可以获取包含源码解析,自定义View,动画实现,架构分享等。
内容难度适中,篇幅精炼,每天只需花上十几分钟阅读即可。
大家可以跟我一起探讨,有flutter—底层开发—性能优化—移动架构—资深UI工程师 —NDK相关专业人员和视频教学资料,还有更多面试题等你来拿

[外链图片转存中…(img-heZbjyWk-1713721022524)]

《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!

  • 16
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值