Loader 是 Android 3.0 HoneyComb 中随 Fragment 一同引入的一个异步数据加载机制,旨在给开发者带来更便利的异步数据库查询方式,当然我们可以自己扩展 Loader 类来实现更灵活的数据加载功能。
本文不会介绍 Loader 的使用方法,这里我们来分析一下它是如何做到状态保留的。首先我们来看看不用它时异步加载数据是怎样的一种情景:
如果不做特殊处理的话,每次屏幕旋转或进入退出多窗口模式(窗口布局改变)时 Activity 就会被销毁重启,那么这时就需要我们手动保存 Activity 中一些重要的状态,比如用户正在浏览的内容 Id,键入的文字,大部分 Widgets 的状态能够自动保存。但即使这样,我们并不能将大量的实体数据存在状态 Bundle 里,因为其序列化后的 Parcel 对象在 Binder 中的传输大小是有限制的,很容易出现问题。为了解决这个问题,我们通常会使用全局对象,包括单例和静态成员变量,使用一个不会随 Activity 销毁的控制器来集中存储这些实体数据。再者,直接设置旋转屏幕时不重建 Activity,我认为这是最差的一种方法,状态管理一定是要做的,而不能通过这种方法避免对状态的维护。
然而如果我们使用 Loader 来加载数据,你就会发现当 Activity 或 Fragment 因为突发情况被重建时,刚才加载的数据仍然存在,LoaderCallbacks 能立即调用 onLoadFinished 方法来提交数据。没有任何手动维护状态的过程,这是怎么做到的呢?
下面我们就来一步一步分析一下。
首先我们在 onLoadFinished 处下一个断点,来跟踪它是如何被调用的:
这是在加载结束后,旋转屏幕导致 Fragment 重建,紧接着 onLoadFinished 方法被调用的调用栈。下层基本就是 Activity 声明周期的处理,我们直接跳过,来看 FragmentHostCallback #reportLoaderStart 这个方法:
void reportLoaderStart() {
if (mAllLoaderManagers != null) {
final int N = mAllLoaderManagers.size();
LoaderManagerImpl loaders[] = new LoaderManagerImpl[N];
for (int i=N-1; i>=0; i--) {
loaders[i] = (LoaderManagerImpl) mAllLoaderManagers.valueAt(i);
}
for (int i=0; i
LoaderManagerImpl lm = loaders[i];
lm.finishRetain();
lm.doReportStart();
}
}
}
这里我们能看到,mAllLoaderManagers 这个数组一定盛放了之前保留下来的 LoaderManager 对象,对它调用 finishRetain 就会导致其管理的 Loader 对象触发 onLoadFinished 来进行状态恢复。所以下面我们就应该关心一下 mAllLoaderManagers 是怎么来的了。根据命名它显然不是一个静态变量,所以我们直接检索对其赋值的语句,能够找到:
void restoreLoaderNonConfig(SimpleArrayMap loaderManagers) {
mAllLoaderManagers = loaderManagers;
}
OK,那么被保留的 LoaderManagers 一定是在这里被交至 FragmentHostCallback 对象的。
下一步我们就可以为这个方法打一个断点,继续跟踪是谁触发的它。
可以发现 Activity 一被创建,保留的对象就会被恢复,我们来看看具体实现:
NonConfigurationInstances nc =
(NonConfigurationInstances) getLastNonConfigurationInstance();
if (nc != null) {
mFragments.restoreLoaderNonConfig(nc.loaders);
}
这是 FragmentActivity#onCreate 的一段代码,getLastNonConfigurationInstance 方法拿到了之前保留的对象。这个方法直接返回了 Activity 中的 mLastNonConfigurationInstances 成员变量。Well,它又是在哪被赋值的呢?通过上面的方法,可以找到,在 attach 这个方法中,它被当做一个参数传了进来。
到这里,我们一直在跟踪变量,是不是很无聊,接下来终于轮到与 AMS 交互了。
这里我们变一下分析方法,冒泡式的寻找基层 caller 不容易跟踪最终的对象是被哪个参数生成的。所以这里我索性直接在 ActivityThread$H 这个 Handler 类的 handleMessage 上打个断点,这是 AMS 通过 Binder 通讯后本地对象处理的入口。
上面我们分析出,mLastNonConfigurationInstances 是由参数传递来的,而这个参数正是 ActivityClientRecord 这个类,在 Handler 的处理方法中,我们也能看到正在处理的 Message 的 obj 就是这个 ActivityClientRecord对象,然而里面的 lastNonConfigurationInstances 成员变量是 null?这当然正常,因为 AMS 与本地通讯肯定是不能携带本地对象的嘛,所以这个对象肯定是保存在本地,然后被某一步查询出来插进 record 中的。
继续查找发现 record 对象中途竟然被掉包,在 handleRelaunchActivity 这个方法中,ActivityThread 通过一个名为 mActivities 的 Map,以 token 为键查出了一个新的 record 对象!token 是什么呢?它其实就是 AMS 用于标识一个 Activity 所创建的一个 Binder,由于 Binder 可以通过 writeStrongBinder 方法被写进 Parcel 中从而在 Binder 上传输,所以这里 AMS 就可以将之前需要重启的 Activity 的 token 放在 record 中传给本地应用了。然而掉包后的 record 对象仍然没有 lastNonConfigurationInstances 这个对象,依然是 null。
好吧,掐头去尾,这个 lastNonConfigurationInstances 到底是什么时候被赋值的呢?这里我只好用二分法来锁定它被赋值的时机。最终终于锁定到了 performDestroyActivity 这个方法:
if (getNonConfigInstance) {
try {
r.lastNonConfigurationInstances
= r.activity.retainNonConfigurationInstances();
} catch (Exception e) {
if (!mInstrumentation.onException(r.activity, e)) {
throw new RuntimeException(
"Unable to retain activity "
+ r.intent.getComponent().toShortString()
+ ": " + e.toString(), e);
}
}
}
Activity 的 retainNonConfigurationInstances 方法将一系列状态整理后返回,最后赋给了 record。
Recap
至此我们再理顺一下思路:
Activity 进入 Relaunch 阶段首先会进入 Destroy 的生命周期,然而这里有一个特殊的 flag 指明:我们需要把一个对象传递给下一个 Activity 实例,虽然 Activity 对象不是同一个了,但其中的某些东西是可以移花接木顺延过来的(其中就包括 LoaderManager),这样,尽管上一个 Activity 被 Destroy 阶段销毁了,但是其中一部分数据直接通过 attach 方法被拿到新的 Activity 里了。所以,也仅限于 Relaunch,其他的自然销毁默认情况下是仍然不会保留那部分数据的。
这里可以大胆猜测一下 Android 之所以这么做的原因,为什么会出现 Relaunch?几种情况,比如屏幕旋转,系统配置改变,需要重新 setTheme 等等,销毁 Activity 再建一个是最安全的方法,不会出现一些由于状态改变而造成的非预期状况,然而对于转屏而言,整个 Activity 被丢掉了显然有些可惜,所以系统需要保留状态,状态又分为两种:一种是可以写进 Parcel 的,这种状态在由于内存紧张而造成资源回收时也能得以保留;而另一种就是通过 NonConfigurationInstances 这种方法保存,它在进程退出时就会直接销毁,但是进程都退出了,Relaunch 是肯定不会走的了,所以这类状态就是应用有意重启时想保留的状态,不做任何序列化工作,直接存储对象,能够保留最完整的内存结构。
完。