深入 Android 配置变更全链路解析:从 Activity 销毁重建到 ViewModel 跨旋转存活的技术内幕

做组件化改造时遇到过一个诡异现象:横屏状态下进入某个二级页面,竖屏退出后返回首页,ViewModel 里的数据全丢了。排查一圈发现,不是 ViewModel 的问题,而是我对配置变更(Configuration Change)的重建链路理解有盲区。

这个盲区藏在 ActivityThread 的源码里,一行关键代码串联起了销毁和重建的完整链路。

触发:谁决定重建 Activity

ActivityThread.handleRelaunchActivity 是整个流程的入口。系统检测到配置变化(旋转、语言切换、深色模式等)后,通过 Binder 调用将 RELAUNCH_ACTIVITY 消息投递到主线程:

// ActivityThread.java
public void handleRelaunchActivity(ActivityClientRecord tmp, ...) {
    // 如果 Activity 声明了 android:configChanges,走这里
    if (tmp.activity != null && !tmp.activity.mFinished) {
        handleRelaunchActivityInner(r, configChanges, tmp.pendingResults, ...);
    }
}

关键判断在 handleRelaunchActivityInner 里:如果该 Activity 在 AndroidManifest 中声明了对应的 configChanges(如 orientation|screenSize),则走 handleConfigurationChanged 回调,不重建。否则,进入重建流程——先销毁,再创建。

这是 Android 一直以来的设计逻辑:配置变化时,系统默认你需要重新加载所有资源(布局、图片、字符串),最直接的做法就是从头走一遍生命周期。

销毁时的”遗产”保留

重建流程分两步:handleDestroyActivityhandleLaunchActivity。核心机制藏在销毁阶段的这个方法里:

// ActivityThread.java - performDestroy
NonConfigurationInstances retainNonConfigurationInstances() {
    Object activity = onRetainNonConfigurationInstance();  // ①
    HashMap<String, Object> children = onRetainNonConfigurationChildInstances();
    FragmentManagerNonConfig fragments = mFragments.retainNestedNonConfig();
    // ... 聚集所有需要跨重建保留的对象
    return new NonConfigurationInstances(activity, children, fragments, ...);
}

onRetainNonConfigurationInstance() 是一个模板方法,Activity 子类可以重写它,返回一个对象。这个对象会被系统保存下来,在新 Activity 创建时原样传回去。

AndroidX 的 ComponentActivity 利用这个机制做了关键操作:

// ComponentActivity.java (简化)
public final Object onRetainNonConfigurationInstance() {
    Object custom = onRetainCustomNonConfigurationInstance();
    ViewModelStore viewModelStore = mViewModelStore;  // ②
    if (viewModelStore == null) {
        // 没有 ViewModel 就不需要保留
        return custom;
    }
    return new NonConfigurationInstances(custom, viewModelStore);
}

mViewModelStore 是一个 HashMap<String, ViewModel> 容器,持有当前 Activity 内所有通过 ViewModelProvider 创建的 ViewModel 实例。销毁时,这个容器被整体打包进 NonConfigurationInstances 对象,存入 ActivityClientRecord,等待重建时取用。

onRetainCustomNonConfigurationInstance() 是对开发者的扩展点——你可以在 Activity 里重写这个方法返回任意对象,实现自定义的状态保留。

新 Activity 如何继承”遗产”

销毁完成后,handleLaunchActivity 开始创建新 Activity。在 performLaunchActivity 中会调用 Activity.attach,传入上一步保留的 NonConfigurationInstances

// Activity.java
final void attach(Context context, ActivityThread aThread, ...,
                  NonConfigurationInstances lastNonConfigurationInstances) {
    // ...
    mLastNonConfigurationInstances = lastNonConfigurationInstances;  // ③
}

mLastNonConfigurationInstances 被存为 Activity 的成员变量。之后在 Activity.onCreate 中,ViewModelProvider 构建时通过 getLastNonConfigurationInstance() 拿到旧的 ViewModelStore

// ComponentActivity.getViewModelStore()
public ViewModelStore getViewModelStore() {
    if (mViewModelStore == null) {
        NonConfigurationInstances nc =
            (NonConfigurationInstances) getLastNonConfigurationInstance();  // ④
        if (nc != null) {
            mViewModelStore = nc.viewModelStore;  // 直接复用
        }
        if (mViewModelStore == null) {
            mViewModelStore = new ViewModelStore();
        }
    }
    return mViewModelStore;
}

如果拿到的 nc.viewModelStore 不为空,就直接复用旧的 Store。ViewModel 从头到尾没有被销毁,只是换了一个宿主 Activity。

ViewModel 与 SavedStateHandle 的边界

很多人把 ViewModel 的存活和 onSaveInstanceState 混为一谈。两者是两条完全不同的通道:

  • ViewModel 保留:依赖 onRetainNonConfigurationInstance,数据在内存中,进程被杀后丢失
  • SavedStateHandle:依赖 onSaveInstanceState,序列化到 Bundle,走跨进程持久化

实际项目中踩过一个坑:在 ViewModel 里把 UI 状态都存到了 SavedStateHandle,旋转后发现数据还在,就以为 ViewModel 本身也能抗进程杀死。结果低内存设备杀掉后台进程后,ViewModel 数据全丢——SavedStateHandle 恢复了,但两套数据的同步逻辑没写好,导致 UI 展示不一致。

正确的分工是:ViewModel 存运行时状态(已加载的列表数据、当前选中项),SavedStateHandle 存可序列化的关键状态(编辑框文本、筛选条件)。后者作为前者的兜底,进程重建时 ViewModel 从 SavedStateHandle 重新初始化。

自定义配置变更处理

如果不想 Activity 重建,最简单的方法是在 Manifest 声明 configChanges

<activity android:name=".VideoPlayerActivity"
    android:configChanges="orientation|screenSize|keyboardHidden" />

然后重写 onConfigurationChanged 自行处理 UI 适配。这种方式的代价是系统不再自动切换资源。如果你依赖 res/layout-land 之类的资源限定符,声明后需要手动处理布局切换。

我倾向于另一种方案:利用 ViewModel + 重建机制,把重建当作”免费的状态重置”。旋转后重新 inflate 布局,自动适配横竖屏资源目录,ViewModel 保证数据不丢失。代码量更少,也更贴合框架的设计意图。

Fragment 这里容易出问题:配置变更期间 Fragment 默认也会被销毁重建。如果用 setRetainInstance(true)(已废弃)或在 FragmentFactory 中做保留逻辑,Fragment 内部状态和 ViewModel 的一致性需要单独保证——保留 Fragment 但重建 View 时,onDestroyViewonCreateView 之间的状态同步是常见 bug 来源。

回到开头那个问题。根因是二级页面声明了 configChanges="orientation" 不走重建,而首页没有声明。横屏进二级、竖屏退出后,首页缺少重建触发,没有重新 inflate 资源。ViewModel 中的数据键对了,但视图状态没跟上。修法很简单:统一 configChanges 策略,或者在 onResume 里手动检查资源状态。