Android 内存管理之Fragment回退栈管理

开篇

  又是周末了,有一段时间没有给童鞋们分享点什么东西了。今天熬夜给童鞋们分享一个Fragment回退栈管理。

意欲何为

  Fragment是3.0API加入的组件,它已被广泛用于应用开发中。support-v4包迭代到当前版本,已经是非常成熟非常好用的一个组件了。但是,API里提供的往往不能满足现实开发中。今天就说说Fragment回退栈的管理。
先看看以下需求:

  • 1、像Activity一样可以正常回退
  • 2、部分Fragment缓存,部分Fragment不缓存
  • 3、部分Fragment不加入回退栈

BackStackRecord源码分析

且看support-v4(27.1.1)源码:
一、FragmentManangerbeginTransaction():

    @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));OpBackStackRecord中的一个内部静态类。

    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。看看BackStackRecordgenerateOps(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;
    }

四、回退
AppCompatActivityvoid 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

简析源码

    @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.javacreateInstanceIfNecessary():

    @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

没有加倍的勤奋,就既没有才能,也没有天才。 —— 门捷列夫

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 220,192评论 6 511
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,858评论 3 396
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 166,517评论 0 357
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 59,148评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 68,162评论 6 397
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,905评论 1 308
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,537评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,439评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,956评论 1 319
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,083评论 3 340
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,218评论 1 352
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,899评论 5 347
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,565评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,093评论 0 23
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,201评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,539评论 3 375
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,215评论 2 358

推荐阅读更多精彩内容