Android 内存管理之Fragment回退栈管理
开篇
又是周末了,有一段时间没有给童鞋们分享点什么东西了。今天熬夜给童鞋们分享一个Fragment回退栈管理。
意欲何为
Fragment是3.0API加入的组件,它已被广泛用于应用开发中。support-v4包迭代到当前版本,已经是非常成熟非常好用的一个组件了。但是,API里提供的往往不能满足现实开发中。今天就说说Fragment回退栈的管理。
先看看以下需求:
- 1、像Activity一样可以正常回退
- 2、部分Fragment缓存,部分Fragment不缓存
- 3、部分Fragment不加入回退栈
BackStackRecord源码分析
且看support-v4(27.1.1)源码:
一、FragmentMananger
之beginTransaction()
:
@Override
public FragmentTransaction beginTransaction() {
return new BackStackRecord(this);
}
二、BackStackRecord
继承FragmentTransaction
:
final class BackStackRecord extends FragmentTransaction implements
FragmentManager.BackStackEntry, FragmentManagerImpl.OpGenerator {
//省略部分代码
@Override
public FragmentTransaction addToBackStack(String name) {
if (!mAllowAddToBackStack) {
throw new IllegalStateException(
"This FragmentTransaction is not allowed to be added to the back stack.");
}
mAddToBackStack = true;
mName = name;
return this;
}
//省略部分代码
}
1、实现了
addToBackStack(@Nullable String name)
方法。标识本次transaction是被加入回退栈中的。当然,前提是本次是允许被加入回退栈的。
@Override
public FragmentTransaction replace(int containerViewId, Fragment fragment) {
return replace(containerViewId, fragment, null);
}
@Override
public FragmentTransaction replace(int containerViewId, Fragment fragment, String tag) {
if (containerViewId == 0) {
throw new IllegalArgumentException("Must use non-zero containerViewId");
}
doAddOp(containerViewId, fragment, tag, OP_REPLACE);
return this;
}
2、实现
replace(int containerViewId, Fragment fragment, String tag)
方法。其中调用了doAddOp(containerViewId, fragment, tag, OP_REPLACE);
。opcmd是OP_REPLACE
。且往下看在doAddOp(int containerViewId, Fragment fragment, String tag, int opcmd)
中做了什么样的逻辑操作:
private void doAddOp(int containerViewId, Fragment fragment, String tag, int opcmd) {
final Class fragmentClass = fragment.getClass();
final int modifiers = fragmentClass.getModifiers();
//匿名类、非public类、成员类且非static类,
//如果是这三种类其中之一都会抛出异常
//当且仅当基础类才有可能是匿名类、成员类
if (fragmentClass.isAnonymousClass() || !Modifier.isPublic(modifiers)
|| (fragmentClass.isMemberClass() && !Modifier.isStatic(modifiers))) {
throw new IllegalStateException("Fragment " + fragmentClass.getCanonicalName()
+ " must be a public static class to be properly recreated from"
+ " instance state.");
}
fragment.mFragmentManager = mManager;
if (tag != null) {
if (fragment.mTag != null && !tag.equals(fragment.mTag)) {
throw new IllegalStateException("Can't change tag of fragment "
+ fragment + ": was " + fragment.mTag
+ " now " + tag);
}
fragment.mTag = tag;
}
if (containerViewId != 0) {
if (containerViewId == View.NO_ID) {
throw new IllegalArgumentException("Can't add fragment "
+ fragment + " with tag " + tag + " to container view with no id");
}
if (fragment.mFragmentId != 0 && fragment.mFragmentId != containerViewId) {
throw new IllegalStateException("Can't change container ID of fragment "
+ fragment + ": was " + fragment.mFragmentId
+ " now " + containerViewId);
}
fragment.mContainerId = fragment.mFragmentId = containerViewId;
}
//新增一条操作记录
addOp(new Op(opcmd, fragment));
}
3、我们很清楚地看到每一次replace都会新增一条操作记录:
addOp(new Op(opcmd, fragment));
。Op
是BackStackRecord
中的一个内部静态类。
static final class Op {
int cmd;
Fragment fragment;
int enterAnim;
int exitAnim;
int popEnterAnim;
int popExitAnim;
Op() {
}
Op(int cmd, Fragment fragment) {
this.cmd = cmd;
this.fragment = fragment;
}
}
4、每一条操作记录都有
fragment
实例,这是强引用。这里的cmd
就是前面传递进来的OP_REPLACE
。
ArrayList<Op> mOps = new ArrayList<>();
void addOp(Op op) {
mOps.add(op);
op.enterAnim = mEnterAnim;
op.exitAnim = mExitAnim;
op.popEnterAnim = mPopEnterAnim;
op.popExitAnim = mPopExitAnim;
}
5、本次
op
被存储到操作列表中。
三、提交本次transaction:
@Override
public int commit() {
return commitInternal(false);
}
int commitInternal(boolean allowStateLoss) {
if (mCommitted) throw new IllegalStateException("commit already called");
if (FragmentManagerImpl.DEBUG) {
Log.v(TAG, "Commit: " + this);
LogWriter logw = new LogWriter(TAG);
PrintWriter pw = new PrintWriter(logw);
dump(" ", null, pw, null);
pw.close();
}
mCommitted = true;
if (mAddToBackStack) {
mIndex = mManager.allocBackStackIndex(this);
} else {
mIndex = -1;
}
mManager.enqueueAction(this, allowStateLoss);
return mIndex;
}
6、如果允许本次
op
加入回退栈,则分配本次op
的在回退栈中的序号。交给FragmentManager执行本次事务:mManager.enqueueAction(this, allowStateLoss);
。this
指向OpGenerator实例。
/**
* An add or pop transaction to be scheduled for the UI thread.
*/
interface OpGenerator {
/**
* Generate transactions to add to {@code records} and whether or not the transaction is
* an add or pop to {@code isRecordPop}.
*
* records and isRecordPop must be added equally so that each transaction in records
* matches the boolean for whether or not it is a pop in isRecordPop.
*
* @param records A list to add transactions to.
* @param isRecordPop A list to add whether or not the transactions added to records is
* a pop transaction.
* @return true if something was added or false otherwise.
*/
boolean generateOps(ArrayList<BackStackRecord> records, ArrayList<Boolean> isRecordPop);
}
7、Api注释很明白。开头就已经告诉我们:
BackStackRecord implements FragmentManagerImpl.OpGenerator
。看看BackStackRecord
中generateOps(ArrayList<BackStackRecord> records, ArrayList<Boolean> isRecordPop)
方法的实现:
/**
* Implementation of {@link FragmentManagerImpl.OpGenerator}.
* This operation is added to the list of pending actions during {@link #commit()}, and
* will be executed on the UI thread to run this FragmentTransaction.
*
* @param records Modified to add this BackStackRecord
* @param isRecordPop Modified to add a false (this isn't a pop)
* @return true always because the records and isRecordPop will always be changed
*/
@Override
public boolean generateOps(ArrayList<BackStackRecord> records, ArrayList<Boolean> isRecordPop) {
if (FragmentManagerImpl.DEBUG) {
Log.v(TAG, "Run: " + this);
}
records.add(this);
isRecordPop.add(false);
if (mAddToBackStack) {
//添加到FragmentManager回退栈管理中
mManager.addBackStackState(this);
}
return true;
}
四、回退
AppCompatActivity
的void onBackPressed()
:
/**
* Take care of popping the fragment back stack or finishing the activity
* as appropriate.
*/
@Override
public void onBackPressed() {
FragmentManager fragmentManager = mFragments.getSupportFragmentManager();
final boolean isStateSaved = fragmentManager.isStateSaved();
if (isStateSaved && Build.VERSION.SDK_INT <= Build.VERSION_CODES.N_MR1) {
// Older versions will throw an exception from the framework
// FragmentManager.popBackStackImmediate(), so we'll just
// return here. The Activity is likely already on its way out
// since the fragmentManager has already been saved.
return;
}
if (isStateSaved || !fragmentManager.popBackStackImmediate()) {
super.onBackPressed();
}
}
fragmentManager.popBackStackImmediate()
回退到回退栈里的上一次操作。
BackStackRecord
就分析到这里了。FragmentManager
就是管理和执行BackStackRecord
并在UI线程中显示Fragment
界面。总的来说,每一次transaction
可能有多个op
,而FragmentManager
可能会有N条BackStackRecord
。
基于Api的Fragment回退栈效果
- 实现方式一以及效果图
int index = -1;
ArrayMap<Integer, String> contentCache = new ArrayMap<>();
private void showNext1(){
Fragment fragment = new DefaultFragment();
//绑定data
String content = "fragment" + index;
Bundle bundle = new Bundle();
bundle.putString(DefaultFragment.EXTRA_CONTENT, content);
fragment.setArguments(bundle);
contentCache.put(index, content);
FragmentTransaction transaction = getSupportFragmentManager().beginTransaction();
transaction.addToBackStack(null);
transaction.replace(R.id.fragment_container, fragment);
transaction.commit();
}
👌,在所有Fragment都加入回退栈后,方式一能正常回退。
- 实现方式二以及效果图
int index = -1;
ArrayMap<Integer, String> contentCache = new ArrayMap<>();
private void showNext2(){
boolean returnable = new Random().nextBoolean();
Fragment fragment = new DefaultFragment();
//绑定data
String content = "fragment" + index + (returnable ? "" : "\u2000X");
Bundle bundle = new Bundle();
bundle.putString(DefaultFragment.EXTRA_CONTENT, content);
fragment.setArguments(bundle);
contentCache.put(index, content);
FragmentTransaction transaction = getSupportFragmentManager().beginTransaction();
if (returnable)
transaction.addToBackStack(null);
else
transaction.disallowAddToBackStack();
transaction.replace(R.id.fragment_container, fragment);
transaction.commit();
}
😳,请告诉我这是什么鬼👻?很显然,当部分Fragment不加入回退栈时回退是有问题的。还是我哪里漏写code了?(如果这是这样,大神可以在评论中给我回复,非常感谢!)
在回头看看需求,我就想说一句:MMP。
很显然,API这时候就不能满足我们的日常开发了。不过别慌,接下来福利来了。
😊😊😊😊😊😊...
新福利:效果截屏
说明:后面带有X的说明这个Fragment实例不加入到Fragment回退栈中。GO创建新的Fragment实例并显示。BACK返回上一个回退栈里的Fragment。CLEAR清空Fragment回退栈除了当前显示的Fragment。
立即体验
扫描以下二维码下载体验App(从0.2.3
版本开始,体验App内嵌版本更新检测功能):
JSCKit库传送门:https://github.com/JustinRoom/JSCKit
简析源码
- 先看Fragment的回退记录存储BackRecord.java:
@IdRes
private int containerViewId;// container view id for add fragment
private WeakReference<Fragment> fragmentWeakReference;//fragment cache, it will be recycled by system if necessary
private String clzName;//class name for creating a instance when fragment's cache is recycled by system.
private boolean returnable;//true, add to back stack
private Bundle bundle;//a copy of original data
public BackRecord(@IdRes int containerViewId, @NonNull Fragment fragment, Bundle bundle, boolean returnable) {
this.containerViewId = containerViewId;
if (returnable)
this.fragmentWeakReference = new WeakReference<>(fragment);
this.returnable = returnable;
this.clzName = fragment.getClass().getName();
if (bundle == null)
return;
//copy data. The original data will be recycled if fragment was recycled.
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O){
this.bundle = bundle.deepCopy();
} else {
this.bundle = new Bundle(bundle);
}
}
containerViewId——Fragment容器Id
fragmentWeakReference——Fragment缓存管理。这里创建WeakReference实例,当触发系统内存回收时(内存不足或者System.gc()主动通知系统可清理内存),可以回收掉这些弱引用的Fragment实例。
clzName——Fragment的class name。用于创建被系统回收掉的Fragment实例。
returnable——true,Fragment实例被加入Fragment回退栈里。
bundle——创建Fragment实例时传入data的备份。用于创建被系统回收掉的Fragment实例。this.bundle = bundle.deepCopy();
或者this.bundle = new Bundle(bundle);
,v26Api以及之上支持深拷贝,低于v26Api用浅拷贝。如果不用copy的方式,bundle一直放在回退栈里,以至于bundle关联的但不显示的Fragment实例内存不能被回收掉,一直占用着内存。
public void show(@IdRes int containerViewId, @NonNull Fragment fragment, Bundle bundle, boolean returnable) {
//remember the current showing fragment.
currentShowFragment = fragment;
//bundle data
if (bundle != null)
fragment.setArguments(bundle);
else
bundle = fragment.getArguments();
fragmentManager.beginTransaction().replace(containerViewId, fragment).commit();
backRecordStack.push(new BackRecord(containerViewId, fragment, bundle, returnable));
}
1、显示并存储当前显示的Fragment实例。这里我们可以看到用的是
replace(int, Fragment)}
方法。每显示Fragment都会创建一个回退记录,returnable
标识这个回退记录是否是有效回退记录。
@Nullable
private BackRecord getLastReturnableStepRecord() {
if (backRecordStack.isEmpty())
return null;
//get the top of stack
BackRecord record = backRecordStack.pop();
if (!record.isReturnable()) {
return getLastReturnableStepRecord();
}
return backRecordStack.push(record);
}
2、获取上一个可回退的Fragment。这里递归
pop()
,直到获取到可回退的Fragment。
public boolean back() {
if (backRecordStack.empty())
return false;
//back current step record
backRecordStack.pop();
if (currentShowFragment != null) {
fragmentManager.beginTransaction().remove(currentShowFragment).commit();
currentShowFragment = null;
}
//get the last returnable step record
BackRecord lastReturnableBackRecord = getLastReturnableStepRecord();
if (lastReturnableBackRecord != null) {
Fragment tempFragment = lastReturnableBackRecord.createInstanceIfNecessary();
if (tempFragment != null) {
currentShowFragment = tempFragment;
fragmentManager.beginTransaction()
.replace(lastReturnableBackRecord.getContainerViewId(), currentShowFragment)
.commit();
}
}
return true;
}
3、回退并显示上一个Fragment。A、移除掉当前显示的Fragment。B、查找上一个可回退的Fragment(如果有缓存则取缓存,如果缓存已经被系统回收,则利用java反射机制创建一个Fragment实例并绑定bundle数据)。
BackRecord.java中createInstanceIfNecessary()
:
@Nullable
public Fragment createInstanceIfNecessary() {
Fragment fragment = fragmentWeakReference.get();
if (fragment == null) {
try {
Log.i(TAG, "createInstanceIfNecessary: newInstance " + (bundle == null ? "" : bundle.getString("extra_content")));
Object object = Class.forName(clzName).newInstance();
if (object instanceof Fragment)
fragment = (Fragment) object;
if (fragment != null)
fragment.setArguments(bundle);
} catch (Exception e) {
e.printStackTrace();
}
}
return fragment;
}
4、获取Fragment缓存实例,如果已经被系统回收,则通过Java反射创建一个新的实例并传入bundle数据。
如何使用?
FragmentBackHelper fragmentBackHelper = new FragmentBackHelper(getSupportFragmentManager());
public void widgetClick(View v){
switch (v.getId()){
case R.id.btn_back:
if (!fragmentBackHelper.back())
Toast.makeText(this, "can't back", Toast.LENGTH_SHORT).show();
break;
case R.id.btn_show_next:
boolean returnable = new Random().nextBoolean();
int index = new Random().nextInt(100);
fragmentBackHelper.show(R.id.fragment_container, new DefaultFragment(), newBundle(index, returnable), returnable);
break;
}
}
点击查看简单使用示例
如此,很轻松能管理Fragment回退栈了,也不用担心会OutOfMemoryError了。童鞋们,给我点个💗吧!!!
篇尾
Wechat:eoy9527
。
没有加倍的勤奋,就既没有才能,也没有天才。 —— 门捷列夫