Android页面销毁、重建与数据恢复

      • 一、页面销毁和重建
        • 1.页面销毁
        • 2.页面重建和数据恢复
        • 3.模拟页面销毁和重建
      • 二、一些拓展的问题
        • 1.View的数据恢复
        • 2.Fragment的数据恢复
        • 3.状态信息的存储和恢复原理
        • 4.TransactionTooLargeException
      • 三、参考文章

一、页面销毁和重建

1.页面销毁

Android的页面销毁可以分两种,正常的销毁和非正常的销毁。在正常的销毁情况下,页面的状态信息被丢弃,不会被重建,比如调用了activity 的finish()方法、杀死了进程、用户通过点击返回键退出了activity等。非正常的销毁是由于activity处于stopped状态,并且它长期未被使用,或者前台的activity需要更多的资源,这些情况下系统就会关闭后台的进程,以恢复一些内存。当activity被重新展现时会被自动重建。当手机屏幕旋转时,activity(如果没有锁定方向的话)也会被销毁并自动重建。
两种销毁方式都会伴随着Activity onDestroy()的调用和Activity对象的内存回收(如果Activity未被不恰当引用)。但正常销毁情况下,onDestroy()回调中isFinishing()为true,非正常销毁情况下,isFinishing()为false。

2.页面重建和数据恢复

非正常销毁的activity被重新展示时,会重新创建Activity对象,onCreate()等回调都会走一遍,但此时onCreate(Bundle savedInstanceState)的savedInstanceState参数不为空。如果想展示回被销毁前状态,就需要利用这个变量。
举个例子,设置用户信息页面里有更改用户头像的功能,选择图片返回到该页面时会把选择后的图片路径存储在mImagePath变量中,并显示更新后的图像;此时按下home键,等该页面会处于stopped状态,如果页面被销毁,等重新打开app时,由于页面重新创建,Activity对象重新创建,走onCreate()等流程,所以mImagePath还是初始的值,与用户未选择图片的效果一致,用户就有可能有疑惑:我选择了图片,为什么还显成原来的图像?

可以看到,选择图片前头像是一张美女的照片,选择图片后头像变成一张室内的照片,页面销毁和重建后又变成了未选择照片前美女的照片。
这种时候,重写Activity的onSaveInstanceState(Bundle outState)方法,将mImagePath变量保存到outState变量中,然后在onCreate(Bundle savedInstanceState)或onRestoreInstanceState(Bundle savedInstanceState)中将该变量读取出来,赋值给mImagePath,并让ImageView加载该路径,即可在页面重建后展示回页面销毁前的状态。实现如下:

    private String mImagePath;    @Override    public void onSaveInstanceState(Bundle outState) {        super.onSaveInstanceState(outState);        outState.putString("imagePath", mImagePath);    }    @Override    protected void onRestoreInstanceState(Bundle savedInstanceState) {        super.onRestoreInstanceState(savedInstanceState);        String imagePath = savedInstanceState.getString("imagePath");        if (!TextUtils.isEmpty(imagePath)) {            mImagePath = imagePath;            ImageLoaderUtils.displayImage(UserInfoActivity.this, "file://" + imagePath, mAvatarImg, R.drawable.all_head64);        }    }

更改后的效果如下:

可以看到页面销毁前头像显示的是一张室内的照片,等重建后仍然显示为室内的照片。

3.模拟页面销毁和重建

模拟页面销毁和重建的方法有几种。一种是在开发者模式中打开不保留活动开关,这样每打开一个背景不透明的ActivityA,底部的ActivityB都会被销毁,等从ActivityA按返回键,被销毁的ActivityB就会被重建,重新显示。这种方法,只会销毁页面,不会杀掉进程。另一种需要在targetApi为22以上使用,在应用的权限管理页面把已经打开的权限关闭,该应用的进程会被杀掉(部分小米手机不会,华为等手机会)。点击应用图标,之前打开的页面也会依次重建出来,大致重建顺序为从栈顶到栈底,但并不是一定有序。还有一种方法就是不锁定Activity方向,进行横竖屏旋转。

二、一些拓展的问题

上部分简单介绍了页面销毁、重建和数据恢复,这部分相对深入点进行一些探讨。

1.View的数据恢复

先看一个未做任何数据恢复的页面销毁和重建的效果,

在这个例子中并没有保存和恢复EditText中内容,但经过页面销毁和重建后,EditText中的内容仍然得到了保持!Android是在哪里帮我们做得呢?看下Activity的onSaveInstanceState()方法。

    protected void onSaveInstanceState(Bundle outState) {        outState.putBundle(WINDOW_HIERARCHY_TAG, mWindow.saveHierarchyState());        ... // 省略无关代码    }

可以看到,调用了mWindow的saveHierarchyState()方法,将返回值保存在了outState中,其中mWindow是PhoneWindow类型,其saveHierarchyState()方法如下。

    @Override    public Bundle saveHierarchyState() {        Bundle outState = new Bundle();        if (mContentParent == null) {            return outState;        }        SparseArray<Parcelable> states = new SparseArray<Parcelable>();        mContentParent.saveHierarchyState(states);        outState.putSparseParcelableArray(VIEWS_TAG, states);        ... // 省略无关代码        return outState;    }

可以看到,主要是调用了mContentParent的saveHierarchyState()方法,mContentParent是ViewGroup类型的变量,它的唯一子View是我们通过setContentView()添加进去的View。ViewGroup并没有声明saveHierarchyState()方法,saveHierarchyState()方法是在View中声明的。

    public void saveHierarchyState(SparseArray<Parcelable> container) {        dispatchSaveInstanceState(container);    }

直接调用了View的dispatchSaveInstanceState()方法

    protected void dispatchSaveInstanceState(SparseArray<Parcelable> container) {        if (mID != NO_ID && (mViewFlags & SAVE_DISABLED_MASK) == 0) {            mPrivateFlags &= ~PFLAG_SAVE_STATE_CALLED;            Parcelable state = onSaveInstanceState();                        if (state != null) {                container.put(mID, state);            }        }    }

主要调用了自己的onSaveInstanceState()方法获取状态信息,并将其和id值以键值对的形式存储到SparseArray中。而ViewGroup中重写了该方法,递归调用了ViewGroup作为View及其子View的dispatchSaveInstanceState()方法。

    @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);            }        }    }

从上面的代码可以看到,mWindow.saveHierarchyState()方法,以深度优先的方法遍历了整个View树,将View的状态信息以键值对的形式存储到SparseArray中,然后包装存储到Bundle中。
有保存就有恢复,View的状态恢复在Activity的onRestoreInstanceState()方法中,非常对称得调用了PhoneWindow的restoreHierarchyState()方法,进而调用了mContentParent的restoreHierarchyState()方法,按照id从SparseArray中取出保存的状态信息,并通过View的onRestoreInstanceState()方法进行恢复。
再细看下TextView的onSaveInstanceState()和onRestoreInstanceState()方法。

 @Override    public Parcelable onSaveInstanceState() {        Parcelable superState = super.onSaveInstanceState();        if (freezesText || hasSelection) {            SavedState ss = new SavedState(superState);            if (freezesText) {                if (mText instanceof Spanned) {                    final Spannable sp = new SpannableStringBuilder(mText);                    ss.text = sp;  // ①                 } else {                    ss.text = mText.toString(); // ①                }            }            return ss;        }        return superState;    }

onSaveInstanceState()方法会在状态信息中保存文字、选中状态、获取焦点状态等。

    @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); // ①        }    }

onRestoreInstanceState()方法对这些信息进行了相应的恢复处理。
我们自己定义的View如果需要在页面销毁和重建中保持状态信息,也应该通过这种方式进行处理。需要注意的是页面数据恢复的时机有两个,一个是在onCreate(Bundle savedInstanceState)中,一个在onRestoreInstanceState(Bundle savedInstanceState)中,onCreate的回调早于onRestoreInstanceState,而View的数据恢复是在onRestoreInstanceState中。这个比较好理解,因为我们通常在onCreate()中调用setContentView()方法,等onRestoreInstanceState()时恢复可以确保mContentParent及其子View都已经存在,可以放心进行恢复。这样,在页面恢复的过程中,无论我们在onCreate()中对TextView设置了什么文字,等到onRestoreInstanceState()时,都会被设置回之前保存的文字,这个还挺神奇的。

2.Fragment的数据恢复

上部分说了View的数据恢复在onRestoreInstanceState()中,那Android系统中有没有在onCreate()中进行数据恢复的组件呢?这个还真有,看下FragmentActivity的onCreate()代码:

    protected void onCreate(@Nullable Bundle savedInstanceState) {        mFragments.attachHost(null /*parent*/);        super.onCreate(savedInstanceState);        if (savedInstanceState != null) {            Parcelable p = savedInstanceState.getParcelable(FRAGMENTS_TAG);            mFragments.restoreAllState(p, nc != null ? nc.fragments : null); //①            ...        }        mFragments.dispatchCreate(); // ②    }    final FragmentController mFragments = FragmentController.createController(new HostCallbacks());

可以看到,当savedInstanceState不为空时,也就是当页面是销毁后重建时,会调用mFragments当restoreAllState()方法,其中mFragments是FragmentController类型的对象,它的restoreAllState()方法只是直接调用了FragmentManager的restoreAllState()方法。

    void restoreAllState(Parcelable state, FragmentManagerNonConfig nonConfig) {        if (state == null) return;        FragmentManagerState fms = (FragmentManagerState)state;        if (fms.mActive == null) return;        ... // 省略旋转屏幕页面重建的相关处理逻辑        // Build the full list of active fragments, instantiating them from        // their saved state.        mActive = new SparseArray<>(fms.mActive.length);        for (int i=0; i<fms.mActive.length; i++) {            FragmentState fs = fms.mActive[i];            if (fs != null) {                ... // 省略部分参数处理逻辑                Fragment f = fs.instantiate(mHost, mContainer, mParent, childNonConfig, viewModelStore); // ①                mActive.put(f.mIndex, f);            }        }        // Build the back stack.        ...// 省略    }

这个方法从FragmentManagerState中取出Fragment的状态信息(FragmentState),并以此重建了Fragment。创建方法为FragmentState的instantiate()方法,直接调用了Fragment的instantiate方法。

    public static Fragment instantiate(Context context, String fname, @Nullable Bundle args) {    ... // 省略异常处理代码            Class<?> clazz = sClassMap.get(fname);            if (clazz == null) {                clazz = context.getClassLoader().loadClass(fname);                sClassMap.put(fname, clazz);            }            Fragment f = (Fragment) clazz.getConstructor().newInstance();            if (args != null) {                args.setClassLoader(f.getClass().getClassLoader());                f.setArguments(args);            }            return f;    ... // 省略异常处理代码    }

这里通过反射创建了Fragment,并通过setArguments方法设置保存的数据。我们再回到该部分开头的onCreate()方法,除了恢复创建Fragment外,还通过mFragments.dispatchCreate()方法将标明为added的Fragment,设置为Created,触发Fragment的onCreate()回调。
总结下此部分内容,在FragmentActivity页面恢复导致的onCreate()中,FragmentActivity会把页面销毁时处于Active状态的Fragment给全部恢复出来。作为开发者,我们不需要再去创建新的Fragment,只需要通过FragmentManager的findFragmentByXXX()方法将Fragment取出,即可恢复使用。

3.状态信息的存储和恢复原理

在页面销毁后重建并恢复数据,在节省内存的同时又保证了用户体验的连续,这是Android一个非常好的设计。那页面的状态信息是存储在哪里了呢?在View和Fragment的状态信息保存和恢复中,可以状态信息都是存储在Parcelable类型的变量里了,这表明状态信息应该是保存在内存中了。这也比较容易理解,onSaveInstanceState()和onRestoreInstanceState()方法都是在UI线程中实现的,如果做IO存储操作,需要解决线程同步、文件读写同步等问题,麻烦且容易造成卡顿。但如果存储在内存中,进程被杀死,数据是不是就不存在了呢?看个关闭权限重启进程的例子,首先进入到设置用户名页面,将tc_android更改为tc,然后到权限设置页面关闭一个权限,导致应用进程重启,然后重新打开app,让页面恢复,发现显示的仍然是tc,而非tc_android!

对于这个问题,Android其实处理得很巧妙,状态信息确实是存储在内存中,只不过不是在应用所在进程的内存中,而是在ActivityManagerService所在的进程中,看下PendingTransactionActions.StopInfo类,

    public static class StopInfo implements Runnable {        private Bundle mState;        private PersistableBundle mPersistentState;        @Override        public void run() {            // Tell activity manager we have been stopped.            try {                ActivityManager.getService().activityStopped(                        mActivity.token, mState, mPersistentState, mDescription);            } catch (RemoteException ex) {                // Dump statistics about bundle to help developers debug                final LogWriter writer = new LogWriter(Log.WARN, TAG);                final IndentingPrintWriter pw = new IndentingPrintWriter(writer, "  ");                pw.println("Bundle stats:");                Bundle.dumpStats(pw, mState);                pw.println("PersistableBundle stats:");                Bundle.dumpStats(pw, mPersistentState);                if (ex instanceof TransactionTooLargeException                        && mActivity.packageInfo.getTargetSdkVersion() < Build.VERSION_CODES.N) {                    Log.e(TAG, "App sent too much data in instance state, so it was ignored", ex);                    return;                }                throw ex.rethrowFromSystemServer();            }        }    }

主要通过Binder跨进程调用了ActivityManagerService的activityStopped()方法,其中mState参数就是Activity的onSaveInstanceState()方法的参数,看下ActivityManagerService的activityStopped()方法。

    @Override    public final void activityStopped(IBinder token, Bundle icicle,            PersistableBundle persistentState, CharSequence description) {        synchronized (this) {            final ActivityRecord r = ActivityRecord.isInStackLocked(token);            if (r != null) {                r.activityStoppedLocked(icicle, persistentState, description);            }        }    }

调用了ActivityRecord的activityStoppedLocked()方法,

    final void activityStoppedLocked(Bundle newIcicle, PersistableBundle newPersistentState,            CharSequence description) {        final ActivityStack stack = getStack();        if (newPersistentState != null) {            persistentState = newPersistentState;            service.notifyTaskPersisterLocked(task, false);        }                if (newIcicle != null) {            // If icicle is null, this is happening due to a timeout, so we haven't really saved            // the state.            icicle = newIcicle;            haveState = true;            launchCount = 0;            updateTaskDescription(description);        }        ... // 省略    }

可以看到,将newIcicle变量保存到了ActivityRecord的icicle变量中。这样ActivityManagerService维护着ActivityRecord列表,ActivityRecord维护着Bundle 变量,即使应用所在的进程被杀死了,页面的状态信息也还会在内存中存储,如果页面重建,就可以把数据从ActivityManagerService进程传递回应用进程,然后进行恢复,保证了效率和流程的简化。

4.TransactionTooLargeException

当然,所有的方案一定有一定的弊端。Binder进行数据传输,一个让人头痛的问题是,数据量过大时,会出现TransactionTooLargeException。这个在上面的StopInfo类中的run()方法中也可以看到。

 try {                ActivityManager.getService().activityStopped(                        mActivity.token, mState, mPersistentState, mDescription);            } catch (RemoteException ex) {                // Dump statistics about bundle to help developers debug                final LogWriter writer = new LogWriter(Log.WARN, TAG);                final IndentingPrintWriter pw = new IndentingPrintWriter(writer, "  ");                pw.println("Bundle stats:");                Bundle.dumpStats(pw, mState);                pw.println("PersistableBundle stats:");                Bundle.dumpStats(pw, mPersistentState);                if (ex instanceof TransactionTooLargeException                        && mActivity.packageInfo.getTargetSdkVersion() < Build.VERSION_CODES.N) {                    Log.e(TAG, "App sent too much data in instance state, so it was ignored", ex);                    return;                }                throw ex.rethrowFromSystemServer();            }

如果跨进程调用出现异常,且异常类型为TransactionTooLargeException,并且当应用的targetSdkVersion小于24,会丢弃该异常。否则,会抛出该异常。
对于很多应用而言,列表数据也要进行保持,如果列表数据量过大,且保存到Bundle中,那么在targetSdkVersion大于等于24时,就有可能因为TransactionTooLargeException而导致崩溃,当然targetSdkVersion小于24时,也会导致数据保持失败的问题。那应该如何处理呢?
当前的方案是通过静态变量来实现的,比如FeedListActivity中声明一个static的HashMap sMap变量,每个FeedListActivity都存储一个特定的key值,比如页面正常启动时的System.naoTime(),然后在onSaveInstanceState()方法中,保持这个key值,同时把列表的值存入到sMap中。当onDestroy()调用时,如果isFinishing()为true,则从sMap中将该列表清除,防止内存泄漏。如果isFinishing()为false,则说明是系统销毁页面,还有可能重建,就不做处理。等页面重建时,从bundle中取出保持的key值,再通过key从sMap中取出列表值,进行显示,同时从sMap中清除该值,防止内存泄漏。大致效果如下,冒昧得直接使用微信做效果演示:

可以看到页面销毁前显示的是泓洋的公众号,经过不保留活动,重新打开微信,显示的仍然是泓洋的公众号。
但这种方法有个问题,就是如果是关闭应用权限导致的页面销毁和重建,进程也销毁重建了,静态变量的值也没有了,这个怎么办呢?仍然看下微信的做法。

可以看到,微信一开始显示钛师傅的公众号,然后关闭权限后重新打开,会启动欢迎页,之后虽然跳转到订阅号消息页,但是回到了顶部,且只展示第一页的数据,因此此种情况下,微信应该也只是放弃了数据恢复,重新请求了第一页的数据进行显示。
不知道有没有其他的处理方法?

三、参考文章

  1. Activity的非正常销毁
  2. Android 关于Activity的销毁和重建

更多相关文章

  1. Android 4.4 Dialog 被状态栏遮挡的解决方法
  2. android系统裁剪方法
  3. DIY osc android 客户端 之 方法论
  4. Android webview和js互相调用实现方法
  5. 深入解析android log的分析方法(1)

随机推荐

  1. Android录屏命令、Android录Gif、Android
  2. android的大好时光结束进行时
  3. Google:Android正在走出碎片化泥沼
  4. Android(安卓)知识图谱:该如何入门Android
  5. Android音乐播放器系列讲解之一
  6. 我的Android相关文章目录
  7. android切换效果、Flutter信息类App、仿
  8. Android(安卓)与 Unity 交互一
  9. Java事件模型与Android事件模型的比较
  10. Android音乐播放器系列讲解之一