Activity Fragment 状态保存与恢复

参考资料:

  1. http://www.cnblogs.com/mengdd/p/5582244.html
  2. https://inthecheesefactory.com/blog/fragment-state-saving-best-practices/en
    大部分内存摘自上述博客,感谢原作者的分享;

开发中,由于状态保存这种场景需要模拟,会造成了一定的开发成本,如:内存不够时,app被回收,唤醒时,可能出现错误情况;

Activity的销毁与重建

  1. 正常情况:back键,与调用finish方法;
  2. 特殊情况:当Activity处于onStop状态时,如:退到后台,并且长时间不用时,极有可能会被系统回收,用来释放一些内存;
  3. 旋屏情况:如果Activity支持旋屏,每次旋屏都会导致activity的销毁与重建;

特殊情况下
当activity回到前台时,如果被回收了,此时,系统会重新创建新的Activity实例,并利用旧实例存下来的数据来恢复界面;这些数据称为:instance state,存在Bundle对象中;

缺省状态下,系统会把每一个View对象保存起来(比如EditText对象中的文本,ListView中的滚动条位置等(注意:需要提供android:id)),即如果activity实例被销毁和重建,那么不需要你编码,layout状态会恢复到前次状态。但是如果你的activity需要恢复更多的信息,比如成员变量信息,则需要自己动手写了。

在这里就涉及到回调函数onSaveInstanceState(),注意 Activity onSaveInstanceState() 有2个重载方法,一般我们使用下面的:

 @Override
    protected void onSaveInstanceState(Bundle outState) {
        super.onSaveInstanceState(outState);
    }

系统会在用户离开activity的时候调用这个函数,并且传递给它一个Bundle object,如果系统稍后需要重建这个activity实例,它会传递同一个Bundle object到onRestoreInstanceState() 和 onCreate() 方法中去。

举个例子:当ActivityA在前台时,如果用户按下home键,或者 打开一个新的ActivityB,或来电等情形下,系统会自动调用 onSaveInstanceState()方法;

Activity - onSaveInstanceState()触发的2个情况

  1. 系统回收时,调用;唤醒时,执行回调onRestoreXXX;
  2. 用户离开Activity时,调用;唤醒时,如果系统未回收,不执行onRestoreXXX;

** 存储Activity状态**
我们就在 onSaveInstanceState() 方法中来存储状态,一定要调用super;

 @Override
    protected void onSaveInstanceState(Bundle outState) {
      outState.putString(KEY_FRAGMENT_TAG, mFragmentCurrentTag);
        super.onSaveInstanceState(outState);
    }

恢复Activity状态
当被回收唤醒时,会执行 onCreate() 和onRestoreInstanceState()回调函数;

 @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        .....
        .....

        // 如果程序唤醒了
        if (savedInstanceState != null) {
            restoreFragments();
            mFragmentCurrentTag = savedInstanceState.getString(KEY_FRAGMENT_TAG);
            mIsSaveInstanceCalled = true;
        }

Activity的数据加载

一般在onCreate中,加载Activity的数据,其他回调方法很可能被调用,比如:如在onStart中加载了数据,按home,马上又回到页面时,onStart会执行;
示例代码:

/**
     * 自动记录 滚动文字
     */
    ListView listView;
    /**
     * 记录内容
     */
    EditText et;

    // 在这里初始化数据
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_state1);

        listView = (ListView) findViewById(R.id.list);
        et = (EditText) findViewById(R.id.et_test);

        Log.e(TAG, "onCreate: " + savedInstanceState);

        String[] a = new String[255 - 64 + 1];
        for (int i = 64; i < 255; i++) {
            a[i-64] = ">>>>>" + ((char) i);
        }
        listView.setAdapter(new ArrayAdapter<>(getApplicationContext(), android.R.layout.simple_list_item_1, a));
    }

    @Override
    protected void onStart() {
        super.onStart();
    }

Fragment的状态保存和恢复

相对于Activity,Fragment的情况,就显得特别复杂,如果有嵌套Fragment,则更复杂了。如果页面不是特别复杂,能不用嵌套fragment,则不用;
** Fragment启动时的生命周期回调:**

fragment启动时

** 按home键时:**

按home或退到后台

之所以执行 onSaveInstanceXXX是因为Activity执行了这个方法;

旋转屏幕时(view的状态自己维护了):

旋转时

Fragment add 与 remove

remove()是移除fragment, 如果fragment不加入到back stack, remove()的时候, fragment的生命周期会一直走到onDetach().类似于 按 back,activity 正常结束一样;

添加到 backStack就不一样了;remove(), fragment 的生命会走到 onDestroyView(),不会执行onDetach(),此时 fragment本身的实例是存在的,成员变量也存在,但是view销毁了;不要把Fragment的实例状态和View状态混在一起处理,这点非常重要

当Fragment从back stack中返回, 实际上是经历了一次View的销毁和重建, 但是它本身并没有被重建.
即View状态需要重建, 实例状态不需要重建.

当Fragment被另一个Fragment replace(), 并且压入back stack中, 此时它的View是被销毁的, 但是它本身并没有被销毁.
也即, 它走到了onDestroyView(), 却没有走onDestroy()和onDetact().
等back回来的时候, 它的view会被重建, 重新从onCreateView()开始走生命周期.
在这整个过程中, 该Fragment中的成员变量是保持不变的, 只有View会被重新创建.
在这个过程中, instance state的saving并没有发生.

我们来看看:

// 添加FragmentB       
findViewById(R.id.addFragmentB).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                FragmentManager fragmentManager = getSupportFragmentManager();
                Fragment f = fragmentManager.findFragmentByTag(FragmentB.class.getName());
                if (f == null) {
                    f = Fragment.instantiate(getApplicationContext(), FragmentB.class.getName());
                }
                // 如果不添加返回栈,remove() 该fragment实例会销毁的
                fragmentManager.beginTransaction().add(R.id.container, f, FragmentB.class.getName())
                        .addToBackStack(null).commit();
            }
        });

// 移除FragmentB
        findViewById(R.id.removeFragmentB).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                FragmentManager fragmentManager = getSupportFragmentManager();
                Fragment f = fragmentManager.findFragmentByTag(FragmentB.class.getName());
                if (f != null) {
                    fragmentManager.beginTransaction().remove(f).commit();
                }
            }
        });

上面的代码,我们第一段添加FragmentB,第二段,移除FragmentB,我们打印一下生命周期方法:

add, remove

可以看到Fragment并没有回到onDetach,onDestroy,也即:fragment 是其对应的View消耗了。但是Fragment的示例还是存在的;
下面显示再次add FragmentB,打印如下:

remove后,再Add

这个时候我们看看成员变量吧:

成员变量存在

如果FragmentB不添加返回键,调用remove(),就类似按返回键一样了,会直接消耗FragmentB,这个机制跟Activity是一致的;

Fragment onCreateView多次执行

了解了上面之后,也就明白为什么 onCreateView会多次执行了吧。
常见的做法,是记录 一个 rootView来记录一下 onCreateView中返回的view,下一次Fragment onCreateView回调时,判断rootView是否为null,来进行是否加载数据,等其他操作;

如下代码:

    private View rootView;//缓存Fragment view
    
    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        if(rootView==null){
            rootView=inflater.inflate(R.layout.tab_fragment, null);
        }
 //缓存的rootView需要判断是否已经被加过parent,
// 如果有parent需要从parent删除,要不然会发生这个rootview已经有parent的错误。
        ViewGroup parent = (ViewGroup) rootView.getParent();
        if (parent != null) {
            parent.removeView(rootView);
        } 
        return rootView;
    }
      

这样可以解决问题,特别是界面layout元素特别多的时候,这样可以看上去可以避免 inflate多次执行一样,的确可以这样,但就是看着特别别扭
但google这样设计,或许是有其他考虑的吧,这里不是很明白;
;如果界面真的切换频率过高,可以考虑使用 hide与show来操作了,来避免上面的代码;

如果不考虑上面的实现方式,我们完全可以不加判断来做,让其直接 inflate吧,但是,这里 界面上view元素的 状态是如何恢复的呢?也没看到,调用onSaveInstacneXX等之类的方法,还是从大神博客中,找到了;

Fragment状态保存入口:####

摘自:http://www.cnblogs.com/mengdd/p/5582244.html

Fragment状态保存入口

3个入口:

  1. Activity的状态保存, 在Activity的onSaveInstanceState()里, 调用了FragmentManger的saveAllState()方法, 其中会对mActive中各个Fragment的实例状态和View状态分别进行保存.
  1. FragmentManager还提供了public方法: saveFragmentInstanceState(), 可以对单个Fragment进行状态保存, 这是提供给我们用的, 其中调用的saveFragmentBasicState()方法即为情况一中所用, 图中已画出标记.
  2. FragmentManager的moveToState()方法中, 当状态回退到ACTIVITY_CREATED, 会调用saveFragmentViewState()方法, 保存View的状态.

Fragment状态恢复入口:####

状态恢复

三个恢复的入口和三个保存的入口刚好对应.

  1. 在Activity重新创建的时候, 恢复所有的Fragment状态.
  1. 如果调用了FragmentManager的方法: saveFragmentInstanceState(), 返回值得到的状态可以用Fragment的setInitialSavedState()方法设置给新的Fragment实例, 作为初始状态.
  2. FragmentManager的moveToState()方法中, 当状态正向创建到CREATED时, Fragment自己会恢复View的状态.

这三个入口分别对应的情况是:

  1. 入口1对应系统销毁和重建新实例.
  1. 入口2对应用户自定义销毁和创建新Fragment实例的状态传递.
  2. 入口3对应同一Fragment实例自身的View状态重建.

Fragment状态保存恢复和Activity关联:

对应入口1的情况,类似于Activity状态保存于恢复处理;比较好理解,不进行分析了;

Fragment同一实例的View状态恢复

对应入口3的情况,也即:activity是resume状态下,切换fragment,是如何保存自己的状态的?
Fragment被add过,当remove()此fragment时,发现 view 的 onSaveInstanceState会被调用,调用栈如下:

remove时,view状态保存调用栈

因为 commit不是立刻执行,所以跟踪的堆栈,commit那部分调用丢失了,在这里,可以看到 moveToState, saveFragmentViewState调用了,这也就说明了,remove时,fragment其内部的view会保存状态;

来看看立即执行 commit的调用栈:

立刻执行commit,remove时,view状态保存调用栈

不同Fragment实例间的状态保存和恢复

入口1与入口3都是自动处理,入口2需要用户手动来处理;
如果需要在不同fragment实例间传递状态,就需要用到入口2了,手动调用
FragmentManager 的 saveFragmentInstanceState 方法:

public abstract Fragment.SavedState saveFragmentInstanceState(Fragment f);

// 具体实现为:
@Override
    public Fragment.SavedState saveFragmentInstanceState(Fragment fragment) {
        if (fragment.mIndex < 0) {
            throwException( new IllegalStateException("Fragment " + fragment
                    + " is not currently in the FragmentManager"));
        }
        if (fragment.mState > Fragment.INITIALIZING) {
            Bundle result = saveFragmentBasicState(fragment);
            return result != null ? new Fragment.SavedState(result) : null;
        }
        return null;
    }

恢复时,调用Fragment setInitialSavedState 来实现;

 /**
     * Set the initial saved state that this Fragment should restore itself
     * from when first being constructed, as returned by
     * {@link FragmentManager#saveFragmentInstanceState(Fragment)
     * FragmentManager.saveFragmentInstanceState}.
     *
     * @param state The state the fragment should be restored from.
     */
    public void setInitialSavedState(SavedState state) {
        if (mIndex >= 0) {
            throw new IllegalStateException("Fragment already active");
        }
        mSavedFragmentState = state != null && state.mState != null
                ? state.mState : null;
    }

注意: 只能在Fragment被加入之前设置(add, replace).
利用这两个方法可以更加自由地保存和恢复状态, 而不依赖于Activity.
这样处理以后, 不必保存Fragment的引用, 每次切换的时候虽然都new了新的实例, 但是旧的实例的状态可以设置给新实例.

详细例子请参考:
http://www.cnblogs.com/mengdd/p/5582244.html

 final String STATE_ = "state_";
 // 存储fragment状态
 SparseArray<Fragment.SavedState> savedStateSparseArray = new SparseArray<>();

   @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_state_restore_demo);

        // 恢复
        if (savedInstanceState != null) {
            savedStateSparseArray = savedInstanceState.getSparseParcelableArray(STATE_);
        }
          
        // 切换
        findViewById(R.id.tab1).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                // save current tab
                Fragment tab2Fragment = getSupportFragmentManager().findFragmentByTag(FragmentF.class.getName());
                if (tab2Fragment != null) {
                    // 保存 tab2Fragment的状态
                    saveFragmentState(1, tab2Fragment);
                }

                // restore last state, 每次都new
                FragmentE tab1Fragment = new FragmentE();
                restoreFragmentState(0, tab1Fragment);

                // show new tab
                getSupportFragmentManager().beginTransaction()
                        .replace(R.id.content_container, tab1Fragment, FragmentE.class.getName())
                        .commit();
            }
        });

        findViewById(R.id.tab2).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Fragment tab1Fragment = getSupportFragmentManager().findFragmentByTag(FragmentE.class.getName());
                if (tab1Fragment != null) {
                    saveFragmentState(0, tab1Fragment);
                }

                // 每次都new
                FragmentF tab2Fragment = new FragmentF();
                restoreFragmentState(1, tab2Fragment);

                getSupportFragmentManager().beginTransaction()
                        .replace(R.id.content_container, tab2Fragment, FragmentF.class.getName())
                        .commit();
            }
        });

    }

    @Override
    protected void onSaveInstanceState(Bundle outState) {
        super.onSaveInstanceState(outState);
        outState.putSparseParcelableArray(STATE_, savedStateSparseArray);
    }

/**
     * 手动存状态
     *
     * @param index
     * @param fragment
     */
    private void saveFragmentState(int index, Fragment fragment) {
        Fragment.SavedState savedState = getSupportFragmentManager().saveFragmentInstanceState(fragment);
        savedStateSparseArray.put(index, savedState);
    }

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

推荐阅读更多精彩内容