[Framework] ViewModel 如何做到 Activity 重启不丢失数据

[Framework] ViewModel 如何做到 Activity 重启不丢失数据

Android 屏幕旋转和手机配置改变都有可能造成 Activity 重启,在这个过程中旧的 Activity 数据就需要保存,然后需要把旧的数据传递给新的 Activity,然后恢复重启前的状态,开发中处理这种重启的情况非常烦人😡,新手开发经常不会处理,然后导致状态丢失,严重的还会有一些崩溃,Android 官方自己也发现了这个问题然后提供了 ViewModel,并告诉大家 Activity 中状态相关的数据都可以放在里面,并保证屏幕旋转等重启不会导致它的数据丢失。好了这就引出了我们今天的主题,我们来看看 ViewModel 是如何做到 Activity 重启数据不丢失的。

ViewModel 是如何保存数据的

我们一般像如下方式使用 ViewModel

kotlin 复制代码
class MainActivity : ComponentActivity() {

    val myViewModel: MyViewModel by lazy {
        ViewModelProvider(this).get(MyViewModel::class.java)
    }
    
    // ...
}

首先创建一个 ViewModelProvider 实例,构造函数中需要传递一个 ViewModelStoreOwner 对象,好巧不巧,我们使用的 jitpack 中的 ActivityFragment 他们都是 ViewModelStoreOwner,后续我们也都用 Activity 作为分析源码的标准。然后通过 ViewModelProvider#get() 方法来获取我们想要的 ViewModel 实例。

首先看看 ViewModelProvider 的构造函数:

Kotlin 复制代码
public constructor(
    owner: ViewModelStoreOwner
) : this(owner.viewModelStore, defaultFactory(owner), defaultCreationExtras(owner))


public constructor(owner: ViewModelStoreOwner, factory: Factory) : this(
    owner.viewModelStore,
    factory,
    defaultCreationExtras(owner)
)


@JvmOverloads
constructor(
    private val store: ViewModelStore,
    private val factory: Factory,
    private val defaultCreationExtras: CreationExtras = CreationExtras.Empty,
) 

朴实无华的构造函数,会直接通过我们的 ViewModelStoreOwner 来直接获取 ViewModelStore,然后创建一个 defaultFactory 他就是我们用来生成 ViewModel 实例的仓库,后续我们再看看它。忽略掉 defaultCreationExtras 变量。

然后再看看 ViewModelProvider#get() 方法:

Kotlin 复制代码
@MainThread
public open operator fun <T : ViewModel> get(modelClass: Class<T>): T {
    val canonicalName = modelClass.canonicalName
        ?: throw IllegalArgumentException("Local and anonymous classes can not be ViewModels")
    return get("$DEFAULT_KEY:$canonicalName", modelClass)
}

@Suppress("UNCHECKED_CAST")
@MainThread
public open operator fun <T : ViewModel> get(key: String, modelClass: Class<T>): T {
    val viewModel = store[key]
    if (modelClass.isInstance(viewModel)) {
        (factory as? OnRequeryFactory)?.onRequery(viewModel!!)
        return viewModel as T
    } else {
        @Suppress("ControlFlowWithEmptyBody")
        if (viewModel != null) {
            // TODO: log a warning.
        }
    }
    val extras = MutableCreationExtras(defaultCreationExtras)
    extras[VIEW_MODEL_KEY] = key
    // AGP has some desugaring issues associated with compileOnly dependencies so we need to
    // fall back to the other create method to keep from crashing.
    return try {
        factory.create(modelClass, extras)
    } catch (e: AbstractMethodError) {
        factory.create(modelClass)
    }.also { store.put(key, it) }
}

默认的 key 的构建方式是 DEFAULT_KEY + ViewModel 的类名,直接从 ViewModelStore 中根据这个 key 去查找这个对象,同时还会判断它的类型,如果都没有问题会通知 OnRequeryFactory#onRequery() 方法,然后直接返回;如果从 ViewModelStore 中查询失败了,就通过 Factory 创建一个然后储存到 ViewModelStore 中,然后再返回。

我们再看看 AndroidViewModelFactory 是如何创建 ViewModel 的。

Kotlin 复制代码
@Suppress("DocumentExceptions")
override fun <T : ViewModel> create(modelClass: Class<T>): T {
    return if (application == null) {
        throw UnsupportedOperationException(
            "AndroidViewModelFactory constructed " +
                "with empty constructor works only with " +
                "create(modelClass: Class<T>, extras: CreationExtras)."
        )
    } else {
        create(modelClass, application)
    }
}


@Suppress("DocumentExceptions")
private fun <T : ViewModel> create(modelClass: Class<T>, app: Application): T {
    return if (AndroidViewModel::class.java.isAssignableFrom(modelClass)) {
        try {
            modelClass.getConstructor(Application::class.java).newInstance(app)
        } catch (e: NoSuchMethodException) {
            throw RuntimeException("Cannot create an instance of $modelClass", e)
        } catch (e: IllegalAccessException) {
            throw RuntimeException("Cannot create an instance of $modelClass", e)
        } catch (e: InstantiationException) {
            throw RuntimeException("Cannot create an instance of $modelClass", e)
        } catch (e: InvocationTargetException) {
            throw RuntimeException("Cannot create an instance of $modelClass", e)
        }
    } else super.create(modelClass)
}

override fun <T : ViewModel> create(modelClass: Class<T>): T {
    return try {
        modelClass.getDeclaredConstructor().newInstance()
    } catch (e: NoSuchMethodException) {
        throw RuntimeException("Cannot create an instance of $modelClass", e)
    } catch (e: InstantiationException) {
        throw RuntimeException("Cannot create an instance of $modelClass", e)
    } catch (e: IllegalAccessException) {
        throw RuntimeException("Cannot create an instance of $modelClass", e)
    }
}

创建 ViewModel 都是通过反射的方式创建,支持无参数和只有一个 Application 的构造函数的 ViewModel

我们再来看看 ComponentActivity(它实现了 ViewModelStoreOwner 接口) 中是如何来创建 ViewModelStore 的:

Java 复制代码
@NonNull
@Override
public ViewModelStore getViewModelStore() {
    if (getApplication() == null) {
        throw new IllegalStateException("Your activity is not yet attached to the "
                + "Application instance. You can't request ViewModel before onCreate call.");
    }
    ensureViewModelStore();
    return mViewModelStore;
}


void ensureViewModelStore() {
    if (mViewModelStore == null) {
        NonConfigurationInstances nc =
                (NonConfigurationInstances) getLastNonConfigurationInstance();
        if (nc != null) {
            // Restore the ViewModelStore from NonConfigurationInstances
            mViewModelStore = nc.viewModelStore;
        }
        if (mViewModelStore == null) {
            mViewModelStore = new ViewModelStore();
        }
    }
}

OK 我们的主角出现了,通过 getLastNonConfigurationInstance() 方法来获取 ViewModelStore 的,如果没有获取到就创建一个。他也是 Activity 中的标准方法哦,他就相当于 onRestoreInstanceState() (也就是用来获取上次保存的数据),而对应的 onSaveInstanceState() (保存数据) 方法就是 onRetainNonConfigurationInstance(),他们在某些方面和 onSaveInstanceState() / onRestoreInstanceState() 确实很像(可以参考我的另外一篇文章: 关于 Activity#onSaveInstanceState() 的笔记)。

我们来看看 ComponentActivity 对应的 onRetainNonConfigurationInstance()getLastNonConfigurationInstance() 的实现:

onRetainNonConfigurationInstance():

Java 复制代码
public final Object onRetainNonConfigurationInstance() {
    // Maintain backward compatibility.
    Object custom = onRetainCustomNonConfigurationInstance();

    ViewModelStore viewModelStore = mViewModelStore;
    if (viewModelStore == null) {
        // No one called getViewModelStore(), so see if there was an existing
        // ViewModelStore from our last NonConfigurationInstance
        NonConfigurationInstances nc =
                (NonConfigurationInstances) getLastNonConfigurationInstance();
        if (nc != null) {
            viewModelStore = nc.viewModelStore;
        }
    }

    if (viewModelStore == null && custom == null) {
        return null;
    }

    NonConfigurationInstances nci = new NonConfigurationInstances();
    nci.custom = custom;
    nci.viewModelStore = viewModelStore;
    return nci;
}

他这里还添加了一个 onRetainCustomNonConfigurationInstance() 方法来保存自定义的数据,因为 onRetainNonConfigurationInstance() 已经被 final 修饰了,不让重写,自定义的数据和 ViewModelStore 都会被存放在 NonConfigurationInstances() 中。

getLastNonConfigurationInstance() 直接是 Activity 的默认实现:

Java 复制代码
public Object getLastNonConfigurationInstance() {
    return mLastNonConfigurationInstances != null
            ? mLastNonConfigurationInstances.activity : null;
}

官方文档

onRetainNonConfigurationInstance() 的官方文档:

Called by the system, as part of destroying an activity due to a configuration change, when it is known that a new instance will immediately be created for the new configuration. You can return any object you like here, including the activity instance itself, which can later be retrieved by calling getLastNonConfigurationInstance() in the new activity instance. If you are targeting Build.VERSION_CODES.HONEYCOMB or later, consider instead using a Fragment with Fragment.setRetainInstance(boolean.

This function is called purely as an optimization, and you must not rely on it being called. When it is called, a number of guarantees will be made to help optimize configuration switching:
The function will be called between onStop and onDestroy.
A new instance of the activity will always be immediately created after this one's onDestroy() is called. In particular, no messages will be dispatched during this time (when the returned object does not have an activity to be associated with).
The object you return here will always be available from the getLastNonConfigurationInstance() method of the following activity instance as described there.
These guarantees are designed so that an activity can use this API to propagate extensive state from the old to new activity instance, from loaded bitmaps, to network connections, to evenly actively running threads. Note that you should not propagate any data that may change based on the configuration, including any data loaded from resources such as strings, layouts, or drawables.

The guarantee of no message handling during the switch to the next activity simplifies use with active objects. For example if your retained state is an android.os.AsyncTask you are guaranteed that its call back functions (like android.os.AsyncTask.onPostExecute) will not be called from the call here until you execute the next instance's onCreate(Bundle). (Note however that there is of course no such guarantee for android.os.AsyncTask.doInBackground since that is running in a separate thread.)

Note: For most cases you should use the Fragment API Fragment.setRetainInstance(boolean) instead; this is also available on older platforms through the Android support libraries.

Returns: any Object holding the desired state to propagate to the next activity instance

我来说说我对文档的理解:

该方法被系统调用,由于配置改变需要销毁当前 Activity,然后立马会创建一个新配置的 Activity,你就可以通过这个方法来保存销毁的 Activity 的数据,你可以返回任何对象来保存数据,在新的 Activity 中可以通过 getLastNonConfigurationInstance() 方法来获取旧 Activity 中保存的数据 (注意对比和 onSaveInstanceState() 方法的描述)。

该方法只是纯粹的优化,你不能依赖它的调用,当它被调用时会有以下几点保证帮助你优化配置切换:

  • 该方法会在 onStoponDestroy 之间调用。

  • 旧的 Activity onDestroy() 方法被调用后,新的 Activity 会被立即创建。

  • 旧的 Activity 返回的数据,在新的 Activity 可以通过 getLastNonConfigurationInstance() 获取。

该 API 是设计用来从旧的 Activity 向新的 Activity 来传递状态(针对配置改变这种情况),从 bitmap 到网络连接再到运行的线程,都是可以的。但是你不能传递和配置改变相关的数据,其中包括从 resources 中获取的数据,比如 string, layoutdrawable 等。(这也很好理解本来 string 这些都依赖配置,比如你切换语言了,你还用之前语言对应的 string 这肯定不对,这也解决了我的一个疑问为什么叫 onRetainNonConfigurationInstance,哈哈哈哈😂)

最后还说到 Framget 可以通过 setRetainInstance() 方法来保存对应的实例。

然后再看看 getLastNonConfigurationInstance 的文档描述:

Retrieve the non-configuration instance data that was previously returned by onRetainNonConfigurationInstance(). This will be available from the initial onCreate and onStart calls to the new instance, allowing you to extract any useful dynamic state from the previous instance.

Note that the data you retrieve here should only be used as an optimization for handling configuration changes. You should always be able to handle getting a null pointer back, and an activity must still be able to restore itself to its previous state (through the normal onSaveInstanceState(Bundle) mechanism) even if this function returns null.

Note: For most cases you should use the Fragment API Fragment.setRetainInstance(boolean) instead; this is also available on older platforms through the Android support libraries.

Returns:

the object previously returned by onRetainNonConfigurationInstance()

都没什么好看的了,onRetainNonConfigurationInstatce() 方法中已经讲得很清楚了,我不想再翻译了😂 (只是因为强迫症贴出来)

从源码来看看 Activity 重启过程

源码阅读基于 Android 9

关于 Activity#onSaveInstanceState() 的笔记)这篇文章中我有讲到 AMS 控制 ActivityonStop 生命流程,AMS 会通过 binder 向应用进程发送一个 Transaction,这个 Transaction 中会包含一个和对应生命周期的对象叫 StopActivityItem,应用进程收到这个任务后就会在主线程执行相关的生命周期,具体的执行流程如果不清楚的话,可以看看前面提到的文章。

重启过程也是和 onStop 生命周期类似的,只是对应的执行生命周期的对象是 ActivityRelaunchItem,我们也是从 ActivityRelaunchItem#execute() 方法当入口函数来分析:

Java 复制代码
@Override
public void execute(ClientTransactionHandler client, IBinder token,
PendingTransactionActions pendingActions) {
    if (mActivityClientRecord == null) {
        if (DEBUG_ORDER) Slog.d(TAG, "Activity relaunch cancelled");
        return;
    }
    Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "activityRestart");
    client.handleRelaunchActivity(mActivityClientRecord, pendingActions);
    Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
}

client 就是对应的 ActivityThread 对象,这里直接调用了它的 handleRelauchActivity() 方法。

Java 复制代码
@Override
public void handleRelaunchActivity(ActivityClientRecord tmp,
PendingTransactionActions pendingActions) {
    // ...

    handleRelaunchActivityInner(r, configChanges, tmp.pendingResults, tmp.pendingIntents,
        pendingActions, tmp.startsNotResumed, tmp.overrideConfig, "handleRelaunchActivity");
    
    // ...
}

handleRelaunchActivity() 中又调用了一个关键方法 handleRelaunchActivityInner() 方法。

Java 复制代码
private void handleRelaunchActivityInner(ActivityClientRecord r, int configChanges,
List<ResultInfo> pendingResults, List<ReferrerIntent> pendingIntents,
PendingTransactionActions pendingActions, boolean startsNotResumed,
Configuration overrideConfig, String reason) {
    // ...

    handleDestroyActivity(r.token, false, configChanges, true, reason);

    // ...

    handleLaunchActivity(r, pendingActions, customIntent);
}

这个地方就很关键了,首先会调用 handleDestroyActivity() 方法来销毁当前的 Activity,注意它这里传递的 getNonConfigInstance 参数为 true,对,这就是控制 onRetainNonConfigurationInstance() 方法的回调,如果是正常销毁这个参数是 false;然后接着就是通过 handleLaunchActivity() 方法立马启动新的 Activity

重启 Activity 和其他正常情况创建 Activity 和销毁 Activity 还是有一些区别。正常的销毁 ActivityonPauseonStop() 都需要单独等待 AMS 发送过来的 Transaction,新的 Activity 创建也是要单独等待 AMS 发送过来的 Transaction;而在重启流程中 handleDestroyActivity 会直接调用销毁的 ActivityonPause()onStop()onDestroy() 生命周期,然后再创建新的 Activity 然后调用 onCreate()onStart()onResume() 生命周期。

我在 Activity#onDestroy() 延迟回调的文章中有提到 Activity 的销毁过程,感兴趣可以看看:Activity onDestroy 生命周期延迟回调原理

我们先看旧 Acitivity 销毁方法 handleDestroyActivity() 的源码:

Java 复制代码
@Override
public void handleDestroyActivity(IBinder token, boolean finishing, int configChanges,
boolean getNonConfigInstance, String reason) {
    ActivityClientRecord r = performDestroyActivity(token, finishing,
    configChanges, getNonConfigInstance, reason);
    // ...
}

接着看 performDestroyActivity() 方法:

Java 复制代码
ActivityClientRecord performDestroyActivity(IBinder token, boolean finishing,
int configChanges, boolean getNonConfigInstance, String reason) {
    ActivityClientRecord r = mActivities.get(token);
    Class<? extends Activity> activityClass = null;
    if (localLOGV) Slog.v(TAG, "Performing finish of " + r);
    if (r != null) {
        activityClass = r.activity.getClass();
        r.activity.mConfigChangeFlags |= configChanges;
        if (finishing) {
            r.activity.mFinished = true;
        }

        performPauseActivityIfNeeded(r, "destroy");

        if (!r.stopped) {
            callActivityOnStop(r, false /* saveState */, "destroy");
        }
        if (getNonConfigInstance) {
            try {
                r.lastNonConfigurationInstances
                = r.activity.retainNonConfigurationInstances();
            } catch (Exception e) {
                // ...
            }
        }
        try {
            r.activity.mCalled = false;
            mInstrumentation.callActivityOnDestroy(r.activity);
            // ...
        } catch (SuperNotCalledException e) {
            throw e;
        } catch (Exception e) {
            // ...
        }
        r.setState(ON_DESTROY);
    }
    // ...
    return r;
}

如果没有执行 onPause() 就先执行,如果没有执行 onStop() 就先执行,前面说到我们的这种情况下 getNonConfigInstancetrue,然后会调用 Activity#retainNonConfigurationInstances() 方法,最终返回的对象就包含我们在 onRetainNonConfigurationInstances() 返回的对象,然后存放在 ActivityClientRecord 中的 lastNonConfigurationinstances 变量中,最后执行 onDestroy() 流程。

我们继续看看创建新的 ActivityhandleLaunchActivity() 方法:

Java 复制代码
@Override
public Activity handleLaunchActivity(ActivityClientRecord r,
PendingTransactionActions pendingActions, Intent customIntent) {
    // ...

    final Activity a = performLaunchActivity(r, customIntent);

    // ...

    return a;
}

然后我们继续看 performLaunchActivity() 方法:

Java 复制代码
private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) {
    // ...
    try {
        java.lang.ClassLoader cl = appContext.getClassLoader();
        activity = mInstrumentation.newActivity(
            cl, component.getClassName(), r.intent);
        StrictMode.incrementExpectedActivityCount(activity.getClass());
        r.intent.setExtrasClassLoader(cl);
        r.intent.prepareToEnterProcess();
        if (r.state != null) {
            r.state.setClassLoader(cl);
        }
    } catch (Exception e) {
        // ...
    }

    try {
        Application app = r.packageInfo.makeApplication(false, mInstrumentation);
        
        // ...

        if (activity != null) {
            // ...
            activity.attach(appContext, this, getInstrumentation(), r.token,
                r.ident, app, r.intent, r.activityInfo, title, r.parent,
                r.embeddedID, r.lastNonConfigurationInstances, config,
                r.referrer, r.voiceInteractor, window, r.configCallback);

            if (customIntent != null) {
                activity.mIntent = customIntent;
            }
            r.lastNonConfigurationInstances = null;
            // ...
            if (r.isPersistable()) {
                mInstrumentation.callActivityOnCreate(activity, r.state, r.persistentState);
            } else {
                mInstrumentation.callActivityOnCreate(activity, r.state);
            }
            if (!activity.mCalled) {
                // ...
            }
            r.activity = activity;
        }
        r.setState(ON_CREATE);

        mActivities.put(r.token, r);

    } catch (SuperNotCalledException e) {
        throw e;

    } catch (Exception e) {
        // ...
    }

    return activity;
}

首先通过反射创建一个 Activity 对象,然后调用 attach() 方法,在这个方法中就会传递前面在 ActivityClientRecord.lastNonConfigurationInstances 赋值的对象,然后执行 onCreate() 生命周期。

我们看看 Activityattach() 方法:

Java 复制代码
final void attach(Context context, ActivityThread aThread,
Instrumentation instr, IBinder token, int ident,
Application application, Intent intent, ActivityInfo info,
CharSequence title, Activity parent, String id,
NonConfigurationInstances lastNonConfigurationInstances,
Configuration config, String referrer, IVoiceInteractor voiceInteractor,
Window window, ActivityConfigCallback activityConfigCallback) {
    // ...
    mLastNonConfigurationInstances = lastNonConfigurationInstances;
    // ...
}

public Object getLastNonConfigurationInstance() {
    return mLastNonConfigurationInstances != null
    ? mLastNonConfigurationInstances.activity : null;
}

上面的代码朴实无华,不想多解释。

总结

ViewModel 能够保存重建 Activity 的数据是因为使用了 onRetainNonConfigurationInstances() 这个接口来保存数据,主要的工作逻辑就是 AMS 通知应用进程重建后,应用进程会销毁当前的 Activity 同时通过 onRetainNonConfigurationInstances() 接口来获取需要保存的对象,然后把他赋值到 ActivityClientRecord 中,销毁旧的 Activity 后会立即重建新的 Activity,在新的 Activityattach() 把之前保存的数据传递过去,这样新的 Activity 就拿到了上次的数据。

它和 onSaveInstanceState() 方法也有很多相似与不同,我们再来对比一下他们。

触发时机不同

onRetainNonConfigurationInstantces() 明确了是由于配置改变 导致的 Activity 会被立即销毁 ,然后立即重建的时候才会触发而且是一定会触发 ,比如旋转屏幕、屏幕大小改变还有设置中语言等等改变时。
onSaveInstantceState()Activity 有可能被回收但后续需要恢复的情况下会触发 ,官方有一个很好例子来说明 A 启动 B 后,由于系统资源不足就会回收 A,但是有可能后续还要使用 A,然后就会触发 AonSaveInstantceState() 方法来保存数据,当 B 退出后,就需要重建 A,然后把上次的数据传递给新的 A

onSaveInstantceState() 的触发范围比 onRetainNonConfigurationInstantces() 要广,配置改变引起的重建 onSaveInstantceState()onRetainNonConfigurationInstantces() 都会触发(这个我有测试); 当系统资源不足引起回收只有 onSaveInstantceState() 会触发(这点我没有验证,有可能是错的,你可以自己去试试看)。

数据保存的方式不同

onRetainNonConfigurationInstantces() 直接把对象保存在当前进程的 ActivityClientRecord 中,回收时和恢复时都是同一个对象。
onSaveInstanceState() 需要通过 binder 跨进程通信保存在系统进程 AMSActivityRecorder 中,回收前和恢复时不是同一个对象,但是数据是一样的。

这也解释了为什么 onSaveInstanceState() 中存储的对象是需要支持序列化的,而 onRetainNoConfigurationInstantces() 是可以保存任意对象的。

相关推荐
wk灬丨9 分钟前
Android Kotlin Flow 冷流 热流
android·kotlin·flow
千雅爸爸10 分钟前
Android MVVM demo(使用DataBinding,LiveData,Fresco,RecyclerView,Room,ViewModel 完成)
android
晨曦_子画1 小时前
编程语言之战:AI 之后的 Kotlin 与 Java
android·java·开发语言·人工智能·kotlin
孤客网络科技工作室1 小时前
AJAX 全面教程:从基础到高级
android·ajax·okhttp
Mr Lee_2 小时前
android 配置鼠标右键快捷对apk进行反编译
android
顾北川_野3 小时前
Android CALL关于电话音频和紧急电话设置和获取
android·音视频
&岁月不待人&3 小时前
Kotlin by lazy和lateinit的使用及区别
android·开发语言·kotlin
Winston Wood5 小时前
Android Parcelable和Serializable的区别与联系
android·序列化
清风徐来辽5 小时前
Android 项目模型配置管理
android
帅得不敢出门5 小时前
Gradle命令编译Android Studio工程项目并签名
android·ide·android studio·gradlew