一、前言
在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
的。所以使用Fragment
将Map
里面已经添加的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;
}
...
}