Fragment创建注意事项
Fragment创建Tips
基于开发中遇到的问题,分享几个Fragment在不同场景,创需要注意的问题。
场景1 FragmentContainerView
<androidx.fragment.app.FragmentContainerView
android:id="@+id/container"
android:name="com.mihoyo.packaging.fragmentcreatedemo.DemoFragment"
android:layout_width="match_parent"
android:layout_height="match_parent" />
添加name和id即可,无需特殊操作,FragmentContainerView本身是通过add去添加Fragment的,并且做好了恢复重建,可以浅看下源码
FragmentContainerView(
@NonNull Context context,
@NonNull AttributeSet attrs,
@NonNull FragmentManager fm) {
super(context, attrs);
String name = attrs.getClassAttribute();
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.FragmentContainerView);
if (name == null) {
name = a.getString(R.styleable.FragmentContainerView_android_name);
}
String tag = a.getString(R.styleable.FragmentContainerView_android_tag);
a.recycle();
int id = getId();
Fragment existingFragment = fm.findFragmentById(id);
// If there is a name and there is no existing fragment,
// we should add an inflated Fragment to the view.
if (name != null && existingFragment == null) {
if (id <= 0) {
final String tagMessage = tag != null
? " with tag " + tag
: "";
throw new IllegalStateException("FragmentContainerView must have an android:id to "
+ "add Fragment " + name + tagMessage);
}
Fragment containerFragment =
fm.getFragmentFactory().instantiate(context.getClassLoader(), name);
containerFragment.onInflate(context, attrs, null);
fm.beginTransaction()
.setReorderingAllowed(true)
.add(this, containerFragment, tag)
.commitNowAllowingStateLoss();
}
fm.onContainerAvailable(this);
}
不难发现是先findFragmentById如果没找到才创建新的Fragment再add,之所以这么做是因为在内存不足页面销毁的情况下,再次进入该页面系统会恢复Fragment实例,这个时候如果没find直接新建一个并且add的话就会添加两个Fragment实例,恰巧没设置背景的话就会有重影效果。
场景2 Activity or Fragment中
在Activity和Fragment中创建Fragment实例都是遵循先find,找不到在new,否则add和replace场景下会产生不同bug
add
//❎ 恢复的时候会有多个实例,导致重叠
val f = DemoFragment()
//✅
val f =
supportFragmentManager.findFragmentByTag(DemoFragment::class.java.generateDefaultTag())
?: DemoFragment()
supportFragmentManager
.beginTransaction().apply {
if (f.isAdded) {
show(f)
} else {
add(R.id.container, f, f.generateDefaultTag())
}
commitAllowingStateLoss()
}
对于add情况下,和FragmentContainerView处理方式类似,要先find,找不到在new,不然就会出现多个Fragment情况,
replace
//❎ 恢复的时候,系统帮忙恢复Fragment实例,再创建一个replace会导致整个流程重新在走一遍,导致接口、绘制等两次操作
val f = DemoFragment()
//✅
val f =
supportFragmentManager.findFragmentByTag(DemoFragment::class.java.generateDefaultTag())
?: DemoFragment()
supportFragmentManager
.beginTransaction().apply {
replace(R.id.container, f, f.generateDefaultTag())
commitAllowingStateLoss()
}
对于replace这个场景下,直接new虽然不会有问题,但是再恢复的时候,系统会帮忙恢复Fragment实例,再创建一个replace会导致整个流程重新在走一遍,导致接口、绘制等两次操作。
场景3 ViewPager2加载Fragment
ViewPager2加载Fragment分为两种情况
- 数据源Fragment数量固定且顺序不变
- 数据源Fragment可变
数据源Fragment数量固定且顺序不变
先来看一个常见的写法
class ViewPager2Activity : AppCompatActivity() {
val list by lazy {
mutableListOf(DemoFragment(), DemoFragment(), DemoFragment())
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
ActivityViewPagerBinding.inflate(layoutInflater).also { binding ->
binding.vp.adapter = MyAdapter(list, this)
}
}
class MyAdapter(
private val fragmentList: List<DemoFragment>,
fragmentActivity: FragmentActivity
) :
FragmentStateAdapter(fragmentActivity) {
override fun getItemCount() = fragmentList.size
override fun createFragment(position: Int) = fragmentList[position]
}
}
相信很多人这么写过,并且在开发阶段也没发生什么问题,但这个一旦到了线上则会出现非常难查的bug fragment no attach view一类的错误。
即使告知原因是恢复重建产生的bug,再从代码层面分析,每次重建都新建了list给到adapter,并给vp2设置了新adapter,依旧看不出问题,那只能从vp2的源码进行分析。
vp2是通过RecyclerView实现的,所以fragment实际创建和添加是在adapter中,于是跳到FragmentStateAdapter源码主要看其onBindViewHolder如何添加fragment
public final void onBindViewHolder(final @NonNull FragmentViewHolder holder, int position) {
ensureFragment(position);
/** Special case when {@link RecyclerView} decides to keep the {@link container}
* attached to the window, but not to the view hierarchy (i.e. parent is null) */
final FrameLayout container = holder.getContainer();
if (ViewCompat.isAttachedToWindow(container)) {
if (container.getParent() != null) {
throw new IllegalStateException("Design assumption violated.");
}
container.addOnLayoutChangeListener(new View.OnLayoutChangeListener() {
@Override
public void onLayoutChange(View v, int left, int top, int right, int bottom,
int oldLeft, int oldTop, int oldRight, int oldBottom) {
if (container.getParent() != null) {
container.removeOnLayoutChangeListener(this);
placeFragmentInViewHolder(holder);
}
}
});
}
}
删除多余代码,Fragment创建在ensureFragment()方法实现,添加到view在placeFragmentInViewHolder()方法实现
private void ensureFragment(int position) {
long itemId = getItemId(position);
if (!mFragments.containsKey(itemId)) {
// TODO(133419201): check if a Fragment provided here is a new Fragment
Fragment newFragment = createFragment(position);
newFragment.setInitialSavedState(mSavedStates.get(itemId));
mFragments.put(itemId, newFragment);
}
}
可以看到只有itemId不在mFragments中才会调用createFragment获取Fragment实例,也就是说如果id存在则不会调用createFragment
public long getItemId(int position) {
return position;
}
而默认情况下getItemId返回值为position,再来看添加的逻辑
void placeFragmentInViewHolder(@NonNull final FragmentViewHolder holder) {
Fragment fragment = mFragments.get(holder.getItemId());
FrameLayout container = holder.getContainer();
scheduleViewAttach(fragment, container);
mFragmentManager.beginTransaction()
.add(fragment, "f" + holder.getItemId())
.setMaxLifecycle(fragment, STARTED)
.commitNow();
}
通过add的方式添加,并且tag为f+holder.getItemId,而holder#getItemId值就是等于adater#getItemId也就是上面的position。
大体梳理下流程,onBindViewHolder的时候先判断itemId是否存在,如果不存在才会调用adapter#createFragment创建Fragment,然后通过add方式添加,tag为f+position。
既然bug是恢复情况下产生,再来看看恢复逻辑
@Override
public final @NonNull Parcelable saveState() {
/** TODO(b/122670461): use custom {@link Parcelable} instead of Bundle to save space */
Bundle savedState = new Bundle(mFragments.size() + mSavedStates.size());
/** save references to active fragments */
for (int ix = 0; ix < mFragments.size(); ix++) {
long itemId = mFragments.keyAt(ix);
Fragment fragment = mFragments.get(itemId);
if (fragment != null && fragment.isAdded()) {
String key = createKey(KEY_PREFIX_FRAGMENT, itemId);
mFragmentManager.putFragment(savedState, key, fragment);
}
}
return savedState;
}
@Override
public final void restoreState(@NonNull Parcelable savedState) {
Bundle bundle = (Bundle) savedState;
for (String key : bundle.keySet()) {
if (isValidKey(key, KEY_PREFIX_FRAGMENT)) {
long itemId = parseIdFromKey(key, KEY_PREFIX_FRAGMENT);
Fragment fragment = mFragmentManager.getFragment(bundle, key);
mFragments.put(itemId, fragment);
continue;
}
}
}
在saveState的时候存储了itemId和Fragment到FragmentManager,restoreState读取出了itemId和Fragment恢复到了mFragments中。
那再来分析下之前那个demo,假设一进来加载了第一个页面,那么在mFragments中会存在[{0,DemoFragment}]
实例,这个时候app退到后台,内存不足杀死了app,再次打开系统会帮忙恢复,给mFragments中添加[{0,DemoFragment}]
,然后ensureFragment的时候判断itemId等于0已经存在则不会调用adapter#createFragment,而是直接使用系统恢复的Fragment添加到了视图上,我们自己创建的Fragment并没有附着到视图上,此时如果在给Fragment做对应的操作,修改到了ui则会出现crash。
这里你可能会问,我新setAdapter了啊,不应该会自动恢复才对,那看到vp2#setAdapter方法
public void setAdapter(@Nullable @SuppressWarnings("rawtypes") Adapter adapter) {
mRecyclerView.setAdapter(adapter);
mCurrentItem = 0;
restorePendingState();
}
private void restorePendingState() {
Adapter<?> adapter = getAdapter();
if (adapter instanceof StatefulAdapter) {
((StatefulAdapter) adapter).restoreState(mPendingAdapterState);
}
}
会发现恢复的时机正好就是在setAdapter中,这样也就串起来了,默认情况下itemId为position,在恢复重建的时候系统会帮忙恢复Fragment实例,这个时候即使新设置一个Adapter,显示在界面上的Fragment也是系统恢复的,所以正确写法也是先find没有才new,find的tag则为f+position
class ViewPager2Activity : AppCompatActivity() {
val list by lazy {
mutableListOf(
supportFragmentManager.findFragmentByTag("f${0}") ?: DemoFragment(),
supportFragmentManager.findFragmentByTag("f${1}") ?: DemoFragment(),
supportFragmentManager.findFragmentByTag("f${2}") ?: DemoFragment()
)
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
ActivityViewPagerBinding.inflate(layoutInflater).also { binding ->
binding.vp.adapter = MyAdapter(list, this)
}
}
class MyAdapter(
private val fragmentList: List<DemoFragment>,
fragmentActivity: FragmentActivity
) :
FragmentStateAdapter(fragmentActivity) {
override fun getItemCount() = fragmentList.size
override fun createFragment(position: Int) = fragmentList[position]
}
}
数据源Fragment可变
前面说道itemId默认是position,在Fragment数据可变的情况下无法做到position和Fragment一一对应,则会出现问题,所以adapter需要重写getItemId
class MyAdapter2(
private val fragmentList: List<Pair<Long, DemoFragment>>,
fragmentActivity: FragmentActivity
) :
FragmentStateAdapter(fragmentActivity) {
override fun getItemCount() = fragmentList.size
override fun createFragment(position: Int) = fragmentList[position].second
override fun getItemId(position: Int): Long {
return fragmentList[position].first
}
}
但如果仅仅重写getItemId,在恢复重建的时候依旧会crash
java.lang.IllegalStateException: Design assumption violated.
at androidx.viewpager2.adapter.FragmentStateAdapter.placeFragmentInViewHolder(FragmentStateAdapter.java:287)
at androidx.viewpager2.adapter.FragmentStateAdapter.onViewAttachedToWindow(FragmentStateAdapter.java:276)
原因是因为在恢复重建的时候,系统会尝试移除不新鲜的Fragment。类似于我们这种会变的情况,上次显示的是a、b、c重建的时候恢复了a、b、c但这次实际要展示d、e、f那恢复的数据是没有意义的,系统会尝试移除
public final void restoreState(@NonNull Parcelable savedState) {
Bundle bundle = (Bundle) savedState;
for (String key : bundle.keySet()) {
if (isValidKey(key, KEY_PREFIX_FRAGMENT)) {
long itemId = parseIdFromKey(key, KEY_PREFIX_FRAGMENT);
Fragment fragment = mFragmentManager.getFragment(bundle, key);
mFragments.put(itemId, fragment);
continue;
}
}
if (!mFragments.isEmpty()) {
mHasStaleFragments = true;
mIsInGracePeriod = true;
gcFragments();
scheduleGracePeriodEnd();
}
}
如果有恢复的mFragments则不为空,mHasStaleFragments和mIsInGracePeriod都设置为true,然后gcFragments()尝试移除不新鲜的Fragment,scheduleGracePeriodEnd()是间隔10秒后再次尝试移除不新鲜的
void gcFragments() {
if (!mHasStaleFragments || shouldDelayFragmentTransactions()) {
return;
}
// Remove Fragments for items that are no longer part of the data-set
Set<Long> 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
}
}
for (Long itemId : toRemove) {
removeFragment(itemId);
}
}
移除这块有个比较关键的判断if (!containsItem(itemId))
不包含的item则移除,否则不会移除
public boolean containsItem(long itemId) {
return itemId >= 0 && itemId < getItemCount();
}
默认实现是判断itemId在不在条目数的范围内,前面我们只重写了getItemId(),只要返回值不在条目范围内则会被认为不包含,被移除掉。
@Override
public final void onBindViewHolder(final @NonNull FragmentViewHolder holder, int position) {
//省略创建Fragment代码
gcFragments();
}
而在onBindViewHolder创建成功后,会调用gcFragments()尝试移除多余Fragment,在只重写getItemId的情况下大概率会符合if (!containsItem(itemId))
被移除,导致刚创建就被移除
@Override
public final void onViewAttachedToWindow(@NonNull final FragmentViewHolder holder) {
placeFragmentInViewHolder(holder);
gcFragments();
}
不巧的是在onViewAttachedToWindow()回调中会再次尝试placeFragmentInViewHolder放置Fragment到ViewHodler的View上
void placeFragmentInViewHolder(@NonNull final FragmentViewHolder holder) {
Fragment fragment = mFragments.get(holder.getItemId());
if (fragment == null) {
throw new IllegalStateException("Design assumption violated.");
}
}
由于前面已经被移除了,所以通过itemId获取的Fragment为null就boom了,因此在可变数据源的情况下需要重写itemId和containsItem两个方法
class MyAdapter2(
private val fragmentList: List<Pair<Long, DemoFragment>>,
fragmentActivity: FragmentActivity
) :
FragmentStateAdapter(fragmentActivity) {
override fun getItemCount() = fragmentList.size
override fun createFragment(position: Int) = fragmentList[position].second
override fun getItemId(position: Int): Long {
return fragmentList[position].first
}
override fun containsItem(itemId: Long): Boolean {
return fragmentList.map { it.first }.contains(itemId)
}
}
并且还是遵循先find,没有才new的原则,tag为f+itemId
总结
- 无论是add还是replace的方式都遵循先find,找不到才new的原则,否则恢复重建的时候add会添加多个实例,replace会恢复的实例生命周期走一次,替换的再走一次,导致流程执行两次浪费资源的情况
- 在Fragment结合ViewPager2时候,依旧是遵循先find,找不到才new的原则,只不过tag为f+itemId,否则恢复重建的时候,系统会帮助恢复展示的Fragment,这个时候如果新的itemId和上次的itemId相同,系统会直接使用恢复的Fragment附着到View上,而自己新创建的Fragment并未真正添加到View,后续对新Fragment的操作就会产生异常
- 数据源不变情况下itemId默认为position,find的时候需根据f+position去找
- 数据源会变情况下需要重写getItemId和containsItem方法,find时候需要根据f+itemId去找
转载自:https://juejin.cn/post/7153094280427339784