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

一、页面销毁和重建

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的数据恢复

先看一个未做任何数据恢复的页面销毁和重建的效果,
TextView自动恢复数据
在这个例子中并没有保存和恢复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()方法。

    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<String, ArrayList> sMap变量,每个FeedListActivity都存储一个特定的key值,比如页面正常启动时的System.naoTime(),然后在onSaveInstanceState()方法中,保持这个key值,同时把列表的值存入到sMap中。当onDestroy()调用时,如果isFinishing()为true,则从sMap中将该列表清除,防止内存泄漏。如果isFinishing()为false,则说明是系统销毁页面,还有可能重建,就不做处理。等页面重建时,从bundle中取出保持的key值,再通过key从sMap中取出列表值,进行显示,同时从sMap中清除该值,防止内存泄漏。大致效果如下,冒昧得直接使用微信做效果演示:
在这里插入图片描述
可以看到页面销毁前显示的是泓洋的公众号,经过不保留活动,重新打开微信,显示的仍然是泓洋的公众号。
但这种方法有个问题,就是如果是关闭应用权限导致的页面销毁和重建,进程也销毁重建了,静态变量的值也没有了,这个怎么办呢?仍然看下微信的做法。
在这里插入图片描述
可以看到,微信一开始显示钛师傅的公众号,然后关闭权限后重新打开,会启动欢迎页,之后虽然跳转到订阅号消息页,但是回到了顶部,且只展示第一页的数据,因此此种情况下,微信应该也只是放弃了数据恢复,重新请求了第一页的数据进行显示。
不知道有没有其他的处理方法?

三、参考文章

  1. Activity的非正常销毁
  2. Android 关于Activity的销毁和重建
  • 7
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值