备忘录模式在Android的应用和模拟实验

介绍

在上一篇设计模式-备忘录模式(Memento)的详解中,使用文字和示例代码解释了备忘录模式。对于备忘录模式的理解是本文基础。如果还不了解备忘录模式还请参考上一篇
首先我们都知道Android移动设备因为内存大小问题,会经常发生GC内存回收操作。关于GC内存回收的发生时机有多种,以后再详解。这里就举例一种常见情况,用户在某个Activity中按下Home键返回桌面,很长时间后通过“近期任务”列表返回App。这期间如果离开的时间够长或者期间内存不足就会发生GC操作。当我们再次返回App会发生什么?

长时间后返回App

上文的“长时间后返回App”是开发中常见的问题,如果处理不好就很有可能发生Crash崩溃。带着问题去Google,最后基本会指向onSaveInstanceState(Bundle outState)以及onRestoreInstanceState(Bundle savedInstanceState)还有最常见的onCreate( Bundle savedInstanceState)这三个Activityd的生命周期回调正是解题关键。
因为很多Activity更多的时候是作为容器,它作为父容器内部包含View或Fragment,而View或Frament作为UI组件负责绘制和处理显示GUI用户界面。我们先缩小讨论范围看看View对备忘录的应用。

View中备忘录模式的应用

我们探索源码来到ActivityonSaveInstanceState方法(源码SDK=android-25)

  protected void onSaveInstanceState(Bundle outState) {
        outState.putBundle(WINDOW_HIERARCHY_TAG, mWindow.saveHierarchyState());//View视图树的状态保存 就一行
        Parcelable p = mFragments.saveAllState();
        if (p != null) {
            outState.putParcelable(FRAGMENTS_TAG, p);
        }
        getApplication().dispatchActivitySaveInstanceState(this, outState);
    }

因为Android经过多年的版本更迭,Activity有非常多的层级父类,这些都不是我们需要关心的,进到Activity源码找关于View的状态保存操作,其实就一行代码。
这是Android架构设计中对View视图树即Window的高度抽象概括,也是备忘录模式的封装性设计所提供的。
本质上:
outState.putBundle(WINDOW_HIERARCHY_TAG, mWindow.saveHierarchyState())==caretaker.saveMemento(originator.createMemento());
都是备忘录管理者去保存了目标对象返回的封装好的备忘录对象。
对应的是:

  • Activity:作为备忘录管理者,一个单纯的管理者,控制原发器的的保存和恢复,关于数据的保存使用其他组件Bundle类的机制实现。
  • Bundle:逻辑上作为标识类(窄接口),实际上使用具有序列化保存数据能力的Bundle实现。
  • View:作为原发器,它负责向管理者提供封装好的内部数据封装类。

从一行代码就能够推断出他们的关系和在备忘录模式中各自对应的功能,理解了源码为什么这么写,这就是我们从设计模式高度看源码的优势。

View的数据保存操作和分发

理解了备忘录中的各自角色,我们就来看看View是怎么样调用保存操作的。

抽象类Window-实现类PhoneWindow

PhoneWindow:

  @Override
    public Bundle saveHierarchyState() {
        //省略其他无关代码
        SparseArray<Parcelable> states = new SparseArray<Parcelable>();//创建数据对象
        mContentParent.saveHierarchyState(states);//给数据对象赋值
        outState.putSparseParcelableArray(VIEWS_TAG, states);//存入保存结构中

//省略其他无关代码
   }

View:没有子View直接调用保存操作方法

public void saveHierarchyState(SparseArray<Parcelable> container) {
        dispatchSaveInstanceState(container);//调用内部的保存操作方法
    }

ViewGroup:因为存在子View,需要遍历分发保存操作

@Override
    protected void dispatchSaveInstanceState(SparseArray<Parcelable> container) {
        super.dispatchSaveInstanceState(container);
        final int count = mChildrenCount;
        final View[] children = mChildren;
        for (int i = 0; i < count; i++) {
            View c = children[i];
            if ((c.mViewFlags & PARENT_SAVE_DISABLED_MASK) != PARENT_SAVE_DISABLED) {
                c.dispatchSaveInstanceState(container);
            }
        }
    }

内部的保存操作方法

  protected void dispatchSaveInstanceState(SparseArray<Parcelable> container) {
        if (mID != NO_ID && (mViewFlags & SAVE_DISABLED_MASK) == 0) {
        //只有设置了View的Id 和设置保存标志位 才会继续执行
            mPrivateFlags &= ~PFLAG_SAVE_STATE_CALLED;
            Parcelable state = onSaveInstanceState();//获取备忘录封装数据实例
            if ((mPrivateFlags & PFLAG_SAVE_STATE_CALLED) == 0) {
                throw new IllegalStateException(
                        "Derived class did not call super.onSaveInstanceState()");
            }
            if (state != null) {
                // Log.i("View", "Freezing #" + Integer.toHexString(mID)
                // + ": " + state);
                container.put(mID, state);
            }
        }
    }

关于保存操作的执行条件,View有id和没有设置id是代码执行的关键,这是我们看到的一个小关键点。

View内部数据封装

因为VIew和ViewGroup都是高度抽象的父类,并没有实际的数据封装。我以TextView举例看看它是怎么样保存用户输入的。

TextView源码中有如下操作;

@Override
    public Parcelable onSaveInstanceState() {
        Parcelable superState = super.onSaveInstanceState();

        // Save state if we are forced to
        final boolean freezesText = getFreezesText();
        boolean hasSelection = false;
        int start = -1;
        int end = -1;

        if (mText != null) {
            start = getSelectionStart();
            end = getSelectionEnd();
            if (start >= 0 || end >= 0) {
                // Or save state if there is a selection
                hasSelection = true;
            }
        }

        if (freezesText || hasSelection) {
        //根据是否有输入即是否有数据值得保存 执行代码
            SavedState ss = new SavedState(superState);//标识类的内部类实现类

            if (freezesText) {
                if (mText instanceof Spanned) {
                    final Spannable sp = new SpannableStringBuilder(mText);

                    if (mEditor != null) {
                        removeMisspelledSpans(sp);
                        sp.removeSpan(mEditor.mSuggestionRangeSpan);
                    }

                    ss.text = sp;//赋值给封装对象
                } else {
                    ss.text = mText.toString();
                }
            }

            if (hasSelection) {
                // XXX Should also save the current scroll position!
                ss.selStart = start;
                ss.selEnd = end;
            }

            if (isFocused() && start >= 0 && end >= 0) {
                ss.frozenWithFocus = true;
            }

            ss.error = getError();

            if (mEditor != null) {
                ss.editorState = mEditor.saveInstanceState();
            }
            return ss;//返回封装对象
        }

        return superState;
    }

代码虽多但是思路很清晰。

  1. 是否有值得保存的内部数据,TextView即文字输入
  2. 有内部数据,使用内部类形式的封装对象,赋值内部数据给封装对象。
  3. 返回带有内部数据的封装对象。

TextView的内部类:

 public static class SavedState extends BaseSavedState {
        int selStart = -1;
        int selEnd = -1;
        CharSequence text;
        boolean frozenWithFocus;
        CharSequence error;
        ParcelableParcel editorState;  // Optional state from Editor.

这里只贴内部类的结构。和TextView需要保存的内部数据一样,一一对应。

最后我们看看恢复操作:

 @Override
    public void onRestoreInstanceState(Parcelable state) {
        if (!(state instanceof SavedState)) {
            //检查外部对象是否 是对应的内部类对象
            super.onRestoreInstanceState(state);
            return;
        }

        SavedState ss = (SavedState)state;//类型转换
        super.onRestoreInstanceState(ss.getSuperState());

        // XXX restore buffer type too, as well as lots of other stuff
        if (ss.text != null) {
            setText(ss.text);//恢复文字输入
        }
        //省略其他恢复操作 
      }

模拟长时间后返回App

因为写代码是很重实践的,关于View的保存和恢复和模拟长时间后返回App的操作,可以通过开发者选项->开启不保留活动进行模拟。打开之后,每次Activity的切换(包括Home回到桌面)都销毁Activity。这样就可以看到Android的系统自动保存和恢复操作。同时也可以看到我们App的很多问题。如果没有问题那说明你的App在保存和重建方面写得很好。
《备忘录模式在Android的应用和模拟实验》

总结

本文从备忘录设计模式的高度看源码,找到备忘录在Android在源码中的应用。还提供了一种快捷的实验方法。 接下来还会写一篇关于Fragment的重建的问题,和一些问题的解决方案。

参考

  1. 研磨设计模式
  2. Android源码设计模式解析与实战
    原文作者:算法小白
    原文地址: https://juejin.im/entry/595d954af265da6c2442f136
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞