Glide中的Fragment缓存原理

一、前言

​ 在Glide中,通过使用空白Fragment对整个加载的生命周期进行控制。为了避免创建很多个Fragment,对此进行一个巧妙的设计,本文将该流程进行记录

二、FragmentManager的创建问题

​ 我们知道关于Fragment的添加移除是通过FragmentManager来维护的,那么FragmentManager是怎么创建的。这里可以查看以下源码:

public class FragmentActivity extends ComponentActivity implements
        ActivityCompat.OnRequestPermissionsResultCallback,
        ActivityCompat.RequestPermissionsRequestCodeValidator {
          ...
        final FragmentController mFragments = FragmentController.createController(new HostCallbacks());
          ...
@NonNull
    public FragmentManager getSupportFragmentManager() {
        return mFragments.getSupportFragmentManager();
    }
          ...
}

public class FragmentController {
    private final FragmentHostCallback<?> mHost;

    /**
     * Returns a {@link FragmentController}.
     */
    @NonNull
    public static FragmentController createController(@NonNull FragmentHostCallback<?> callbacks) {
        return new FragmentController(checkNotNull(callbacks, "callbacks == null"));
    }

    private FragmentController(FragmentHostCallback<?> callbacks) {
        mHost = callbacks;
    }

    /**
     * Returns a {@link FragmentManager} for this controller.
     */
    @NonNull
    public FragmentManager getSupportFragmentManager() {
        return mHost.mFragmentManager;
    }
	...
}

​ 这里以Activity中的supportFragmentManager为例,可以看到是通过维护一个静态对象最终获取的实例。

三、宿主为Activity的问题描述

​ 我们设想这样一种场景。假设使用Glide进行加载图片,因为每一个图片都要进行开启网络请求,那么势必就有异步任务的问题,为了性能考虑需要在页面暂停的时候进行暂停下载,页面活动的时候开启下载,那么就需要和页面的生命周期绑定。在Glide中是通过每次加载图片都时候创建一个Fragment去进行生命周期控制,那么倘若有很多个请求就会出现创建很多Fragment的问题。那么该如何解决这个问题?我们先写一个简单的有问题的代码来复现下这个问题。

class BlankFragment: Fragment() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
    }
}

class FragmentCacheActivity: FragmentActivity() {
    private val handler = Handler(Looper.getMainLooper())
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_fragment_cache)
        addFragments()
        handler.postDelayed({// Fragment添加是异步的
            val fragmentLength = supportFragmentManager.fragments.size
            Log.e("YM","添加的Fragment数量:${fragmentLength}")
        },1000)
    }

    private fun addFragment(){
        val fragment = BlankFragment()
        supportFragmentManager.commit {
            add(fragment,"fragment")
        }
    }

    private fun addFragments(){
        for (index in 1..10){
            addFragment()
        }
    }
}

输出结果:

E/YM: 添加的Fragment数量:10

可以看到这个调用十次添加了十次Fragment。那么怎么优化呢?我们可以将这个Fragment保存到缓存里面然后进行判断,如果有就不添加了。因为Fragment依赖于不同的FragmentManager。所以采取Map方式进行存储

class FragmentCacheActivity: FragmentActivity() {
    private val handler = Handler(Looper.getMainLooper())
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_fragment_cache)
        addFragments(supportFragmentManager)
        handler.post{// Fragment添加是异步的
            val fragmentLength = supportFragmentManager.fragments.size
            Log.e("YM","添加的Fragment数量:${fragmentLength}")
        }
    }

    private val fragmentMap = mutableMapOf<FragmentManager,BlankFragment>()

    private fun addFragment(fragmentManager: FragmentManager): Fragment{
        var fragment = fragmentMap[fragmentManager]
        if (null == fragment){
            fragment = BlankFragment()
            fragmentMap[fragmentManager] = fragment
            fragmentManager.commit {
                add(fragment,"fragment")
            }
        }
        return fragment
    }

    private fun addFragments(fragmentManager: FragmentManager){
        for (index in 1..10){
            addFragment(fragmentManager)
        }
    }
}

执行结果:

E/YM: 添加的Fragment数量:1

可以看到数量就只剩下一个了。

上面的过程正常来说是没有问题的,但是假如页面意外销毁然后又重建了,由于Glide是静态的,所以里面的数据不会销毁,这时候再次进行加载图片,就会找不到之前的Fragment,然后创建新的Fragment。这里稍稍修改一下代码为以下方式:

class FragmentCacheManager {
    private val fragmentMap = mutableMapOf<FragmentManager,BlankFragment>()
    companion object{
        val fragmentCache = FragmentCacheManager()
    }

    fun addFragment(fragmentManager: FragmentManager): Fragment {
        var fragment = fragmentMap[fragmentManager]
        if (null == fragment){
            fragment = BlankFragment()
            fragmentMap[fragmentManager] = fragment
            fragmentManager.commit {
                add(fragment,"fragment")
            }
        }
        return fragment
    }

    fun addFragments(fragmentManager: FragmentManager){
        for (index in 1..10){
            addFragment(fragmentManager)
        }
       Log.e("YM--->","缓存的长度:${cacheSize}")
    }
}

class FragmentCacheActivity: FragmentActivity() {
    private val handler = Handler(Looper.getMainLooper())
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_fragment_cache)
        FragmentCacheManager.fragmentCache.addFragments(supportFragmentManager)
        handler.post{// Fragment添加是异步的
            val fragmentLength = supportFragmentManager.fragments.size
            Log.e("YM","添加的Fragment数量:${fragmentLength}")
        }
    }
}

然后稍微运行一下,然后旋转屏幕触发重建,执行结果如下:

E/YM--->: 缓存的长度:1
E/YM: 添加的Fragment数量:1
E/YM--->: 缓存的长度:2
E/YM: 添加的Fragment数量:2

可以看到假设这样的话那么旋转多少次,就会增加多少个Fragment。针对这样的问题该怎么处理呢?

只需要使用FragmentManager里面的Fragment进行判断即可,修改如下:

  fun addFragment(fragmentManager: FragmentManager): Fragment {
        var fragment = fragmentMap[fragmentManager]
        if (null == fragment){
            fragment = fragmentManager.findFragmentByTag(fragmentTag) as BlankFragment?
            if (null == fragment){
                fragment = BlankFragment()
                fragmentMap[fragmentManager] = fragment
                fragmentManager.commit {
                    add(fragment,fragmentTag)
                }
                handler.obtainMessage(removeFlag, fragmentManager)
                    .sendToTarget()
            }
        }
        return fragment
    }

输出日志如下:

E/YM: 添加的Fragment数量:1
E/YM--->: 缓存的长度:1
E/YM: 添加的Fragment数量:1
E/YM--->: 缓存的长度:1

因为使用的是静态对象的FragmentManager,所以即使Activity重建了,使用的依然是同一份FragmentManager。但是假设在Fragment环境中执行,传递的是childFragmentManager。那么每次离开这个页面然后再进入这个页面营造一种销毁重建的情况会出现什么情况呢。

四、宿主为Fragment的问题描述

​ 根据上文我们已经知道childFragmentManager会随着Fragment的销毁而销毁,所以这时候Map里面存储的数据就会一直添加而不会移除,最终导致内存占用过大。示例代码如下:

fragment_out.xml

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    xmlns:app="http://schemas.android.com/apk/res-auto">

    <androidx.appcompat.widget.AppCompatTextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="最外层的Fragment"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintBottom_toBottomOf="parent"/>

</androidx.constraintlayout.widget.ConstraintLayout>

activity_fragment_cache.xml

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    xmlns:app="http://schemas.android.com/apk/res-auto">

    <TextView
        android:id="@+id/title"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Fragment缓存测试"
        android:layout_marginTop="50dp"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent"/>

    <androidx.fragment.app.FragmentContainerView
        android:id="@+id/fragment_container"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:name="com.source.test.OutFragment"
        app:layout_constraintTop_toBottomOf="@+id/title"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintBottom_toBottomOf="parent"/>

</androidx.constraintlayout.widget.ConstraintLayout>
class OutFragment: Fragment() {
    private val handler = Handler(Looper.getMainLooper())
    private val outFragment: FragmentOutBinding by lazy {
        FragmentOutBinding.inflate(layoutInflater)
    }
    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View {
        return outFragment.root
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        FragmentCacheManager.fragmentCache.addFragments(childFragmentManager)
    }

    override fun onResume() {
        super.onResume()
        handler.postDelayed({// Fragment添加是异步的
            val fragmentLength = childFragmentManager.fragments.size
            Log.e("YM--OutFragment","添加的Fragment数量:${fragmentLength}")
        },1000)
    }

}

class FragmentCacheActivity: FragmentActivity() {
    private val handler = Handler(Looper.getMainLooper())
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_fragment_cache)

        handler.postDelayed({// Fragment添加是异步的
            val fragmentLength = supportFragmentManager.fragments.size
            Log.e("YM","添加的Fragment数量:${fragmentLength}")
        },1000)
    }
}

执行结果:

 E/YM--->: 缓存的长度:1
 E/YM---->: --->是否为null:false
 E/YM: 添加的Fragment数量:1
 E/YM--OutFragment: 添加的Fragment数量:1
 E/YM--->: 缓存的长度:2
 E/YM---->: --->是否为null:false
 E/YM: 添加的Fragment数量:1
 E/YM--OutFragment: 添加的Fragment数量:1
 E/YM--->: 缓存的长度:3
 E/YM---->: --->是否为null:false
 E/YM: 添加的Fragment数量:1
 E/YM--OutFragment: 添加的Fragment数量:1

可以发现缓存长度一直在增加。

修改FragmentCacheManager为以下:

class FragmentCacheManager {
    private val fragmentMap = mutableMapOf<FragmentManager,BlankFragment>()
    private val removeFlag = 1
    private val fragmentTag = "fragment"
    private val handler = object : Handler(Looper.getMainLooper()){
        override fun handleMessage(msg: Message) {
            super.handleMessage(msg)
            when(msg.what){
                removeFlag -> {
                    Log.e("YM--->开始移除","-----:")
                    val fm = msg.obj as FragmentManager
                    fragmentMap.remove(fm)
                }
            }
        }
    }
    companion object{
        val fragmentCache = FragmentCacheManager()
    }

    fun addFragment(fragmentManager: FragmentManager): Fragment {
        var fragment = fragmentMap[fragmentManager]
        if (null == fragment){
            fragment = fragmentManager.findFragmentByTag(fragmentTag) as BlankFragment?
            if (null == fragment){
                fragment = BlankFragment()
                fragmentMap[fragmentManager] = fragment
                fragmentManager.commit {
                    add(fragment,fragmentTag)
                }
                handler.obtainMessage(removeFlag, fragmentManager)
                    .sendToTarget()
            }
        }
        return fragment
    }

    fun addFragments(fragmentManager: FragmentManager){
        for (index in 1..10){
            addFragment(fragmentManager)
        }
        val cacheSize = fragmentMap.size
        Log.e("YM--->","缓存的长度:${cacheSize}")
    }
}

这里需要注意一点上文没有解释的一个问题,首先就是Fragment的添加是异步的,但是使用Handle运行的任务是运行在Fragment添加之后的,所以在里面运行时可以获取到添加的Fragment的。所以使用FragmentMap里面已经添加的Fragment移除,如果没有添加成功Fragment是不会移除的,因为那时候根本就执行不到这里面。

五、可能产生的问题

​ 上述逻辑也是旧版本所采用的逻辑,但是最新版本的逻辑进行了部分修改。因为上面存在一些问题。这种场景暂时无法想象,所以只是记录逻辑问题。假设执行到Handle的时候其实这次的Fragment还没有添加到FragmentManager,这时候第二次调用添加就会继续往FragmentManager。再次执行到FragmentManager里面时候,里面就会有两个Fragment。当然可能出现另外一个问题就是第一次的添加上了,但是第二次的还是没有添加成功。那么就要把上一次的Fragment进行移除,并对本次的Fragment调用commitNowAllowingStateLoss()进行同步方式添加保证添加成功。所以采取以下方式先判断两次是否一致,不一致的话再进行移除旧数据,添加新的数据。源码定义如下:

public class RequestManagerRetriever implements Handler.Callback {
  ...
// We care about the instance specifically.
  @SuppressWarnings({"ReferenceEquality", "PMD.CompareObjectsWithEquals"})
  private boolean verifyOurSupportFragmentWasAddedOrCantBeAdded(
      FragmentManager supportFm, boolean hasAttemptedToAddFragmentTwice) {
    SupportRequestManagerFragment newlyAddedSupportRequestManagerFragment =
        pendingSupportRequestManagerFragments.get(supportFm);

    SupportRequestManagerFragment actualFragment =
        (SupportRequestManagerFragment) supportFm.findFragmentByTag(FRAGMENT_TAG);
    if (actualFragment == newlyAddedSupportRequestManagerFragment) {
      return true;
    }

    if (actualFragment != null && actualFragment.getRequestManager() != null) {
      throw new IllegalStateException(
          "We've added two fragments with requests!"
              + " Old: "
              + actualFragment
              + " New: "
              + newlyAddedSupportRequestManagerFragment);
    }
    // If our parent was destroyed, we're never going to be able to add our fragment, so we should
    // just clean it up and abort.
    // Similarly if we've already tried to add the fragment, waited a frame, then tried to add the
    // fragment a second time and still the fragment isn't present, we're unlikely to be able to do
    // so if we retry a third time. This is easy to reproduce in Robolectric by obtaining an
    // Activity but not creating it. If we continue to loop forever, we break tests and, if it
    // happens in the real world, might leak memory and waste a bunch of CPU/battery.
    if (hasAttemptedToAddFragmentTwice || supportFm.isDestroyed()) {
      if (supportFm.isDestroyed()) {
        if (Log.isLoggable(TAG, Log.WARN)) {
          Log.w(
              TAG,
              "Parent was destroyed before our Fragment could be added, all requests for the"
                  + " destroyed parent are cancelled");
        }
      } else {
        if (Log.isLoggable(TAG, Log.ERROR)) {
          Log.e(
              TAG,
              "ERROR: Tried adding Fragment twice and failed twice, giving up and cancelling all"
                  + " associated requests! This probably means you're starting loads in a unit test"
                  + " with an Activity that you haven't created and never create. If you're using"
                  + " Robolectric, create the Activity as part of your test setup");
        }
      }
      newlyAddedSupportRequestManagerFragment.getGlideLifecycle().onDestroy();
      return true;
    }

    // Otherwise we should make another attempt to commit the fragment and loop back again in the
    // next frame to verify.
    FragmentTransaction transaction =
        supportFm.beginTransaction().add(newlyAddedSupportRequestManagerFragment, FRAGMENT_TAG);

    // If the Activity is re-created and a Glide request was started for that Activity prior to the
    // re-creation, then there will be an old RequestManagerFragment that is re-created as well.
    // Under normal circumstances we find and re-use that Fragment rather than creating a new one.
    // However, if the first Glide request for the re-created Activity occurs before the Activity is
    // created, then we will have been unable to find the old RequestManagerFragment and will have
    // created a new one instead. We don't want to keep adding new Fragments infinitely as the
    // Activity is re-created, so we need to pick one. If we pick the old Fragment, then we will
    // drop any requests that had been started after re-creation and are associated with the new
    // Fragment. So here we drop the old Fragment if it exists.
    if (actualFragment != null) {
      transaction.remove(actualFragment);
    }
    transaction.commitNowAllowingStateLoss();

    handler
        .obtainMessage(
            ID_REMOVE_SUPPORT_FRAGMENT_MANAGER,
            HAS_ATTEMPTED_TO_ADD_FRAGMENT_TWICE,
            /*arg2=*/ 0,
            supportFm)
        .sendToTarget();
    if (Log.isLoggable(TAG, Log.DEBUG)) {
      Log.d(TAG, "We failed to add our Fragment the first time around, trying again...");
    }
    return false;
  }
  ...
}

六、参考链接

  1. https://blog.csdn.net/Mr_Tony/article/details/122500873
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 5
    评论
你好,针对 Glide 在 submit 时出现的崩溃问题,可能是因为以下原因: 1. 在调用 `submit()` 方法时,`Target` 对象已经被回收。这可能是因为你在 `Activity` 或 `Fragment` 使用了匿名内部类作为 `Target` 对象,当 `Activity` 或 `Fragment` 被销毁时,`Target` 对象会被回收,而此时 Glide 还在处理图片,就会出现崩溃。 2. 在调用 `submit()` 方法时,`ImageView` 对象已经被回收。这可能是因为你在 `Activity` 或 `Fragment` 使用了 `ImageView` 对象,当 `Activity` 或 `Fragment` 被销毁时,`ImageView` 对象会被回收,而此时 Glide 还在处理图片,就会出现崩溃。 下面是可能的解决方案: 1. 你可以使用 `RequestManagerRetriever` 类的 `get()` 方法来获取 `RequestManager` 对象,然后使用 `into()` 方法将 `Target` 对象和 `ImageView` 对象分开设置。这样可以避免匿名内部类的问题。 2. 你可以在 `Activity` 或 `Fragment` 的 `onDestroy()` 方法,调用 `Glide` 的 `clear()` 方法来清除 Glide缓存和资源,避免出现内存泄漏的问题。另外,你也可以在 `ImageView` 对象被回收时,取消 Glide 的加载操作,避免出现崩溃。 例如: ``` // 获取 RequestManager 对象 RequestManager requestManager = Glide.with(context); // 设置 Target 对象 requestManager.load(imageUrl) .into(new CustomTarget<Drawable>() { @Override public void onResourceReady(@NonNull Drawable resource, @Nullable Transition<? super Drawable> transition) { // 处理图片 } @Override public void onLoadCleared(@Nullable Drawable placeholder) { // 取消 Glide 的加载操作 requestManager.clear(this); } }); // 设置 ImageView 对象 requestManager.load(imageUrl) .into(new ImageViewTarget<Drawable>(imageView) { @Override protected void setResource(@Nullable Drawable resource) { // 处理图片 } @Override public void onLoadCleared(@Nullable Drawable placeholder) { // 取消 Glide 的加载操作 requestManager.clear(this); } }); ``` 希望这些解决方案对你有所帮助!如果你有其他问题,可以继续提问。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值