Android进阶宝典 -- Jectpack篇(ViewModel数据持久化原理)

在ViewModel的官方文档中,简明扼要地概括了ViewModel的作用

ViewModel 类旨在以注重生命周期的方式存储和管理界面相关的数据。ViewModel 类让数据可在发生屏幕旋转等配置更改后继续留存。

首先关键词【生命周期】,也就是说ViewModel能够感知组件的生命周期;在上一章中介绍了关于LiveData的使用,因为LiveData通常持有界面相关的数据,因此ViewModel就是用来管理LiveData。

除此之外,我们知道在屏幕旋转的时候,如果不做设置那么Activity或者Fragment会重新走一遍生命周期,销毁页面后重建,那么之前页面保存的数据也将销毁并保存在Bundle中,待页面重建之后重新从Bundle中取出数据,这种处理方式其实是不稳定的,而且这部分的逻辑需要我们自己写,Bundle能存储的数据量有限;但是ViewModel是能够在页面旋转之后,依然保留旋转前所有的页面数据。

那么为什么ViewModel拥有如此之多的特性,我们从源码了解ViewModel

1 ViewModel基本使用

public class MyViewModel extends ViewModel {
    MutableLiveData<String> data = new MutableLiveData<>();
}
复制代码

对于ViewModel的使用,因为我们自己创建的类也是一个对象,我们可以通过new的方式创建一个对象,但是这样创建出来的ViewModel仅仅只是Java或者Kotlin的对象,并不具备ViewModel的其他特性,因此在官方文档中,创建ViewModel通常采用ViewModelProvider的方式来创建

viewModel = new ViewModelProvider(this).get(MyViewModel.class);
复制代码

我们可以看下源码,现在jectpack组件好多组件源码已经是在用kotlin写了,这里我举例子尽量使用Java语言来写,因为可能有些伙伴不熟悉kotlin,但是本章的源码可能大部分都是kotlin

public constructor(
    owner: ViewModelStoreOwner
) : this(owner.viewModelStore, defaultFactory(owner))

public constructor(owner: ViewModelStoreOwner, factory: Factory) : this(
    owner.viewModelStore,
    factory
)
复制代码

ViewModelProvider的构造方法,一般是需要传入ViewModelStoreOwner和Factory,ViewModelStoreOwner就是我们的组件Activity或者Fragment,因为都实现了这个接口;如果像示例那样只传入ViewModelStoreOwner,那么就会使用默认的defaultFactory。

@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)
}
复制代码

然后调用get方法,传入我们自定义的ViewModel,这里会拿到ViewModel的全类名(包名 + 类名)作为key,从store中取出ViewModel

public open operator fun <T : ViewModel> get(key: String, modelClass: Class<T>): T {
    var viewModel = store[key]
    1️⃣
    if (modelClass.isInstance(viewModel)) {
        (factory as? OnRequeryFactory)?.onRequery(viewModel)
        return viewModel as T
    } else {
        @Suppress("ControlFlowWithEmptyBody")
        if (viewModel != null) {
            // TODO: log a warning.
        }
    }
    2️⃣
    viewModel = if (factory is KeyedFactory) {
        factory.create(key, modelClass)
    } else {
        factory.create(modelClass)
    }
    store.put(key, viewModel)
    return viewModel
}
复制代码

store是啥?就是我们传入的组件ViewModelStoreOwner,通过getViewModelStore方法获取到的ViewModelStore对象,从源码中我们能够看到,在ViewModelStore内部是保存了一个HashMap,key就是ViewModel的全类名,所以在调用get方法时,从store拿到的ViewModel就是我们要创建的这个实例。

public interface ViewModelStoreOwner {
    /**
     * Returns owned {@link ViewModelStore}
     *
     * @return a {@code ViewModelStore}
     */
    @NonNull
    ViewModelStore getViewModelStore();
}
复制代码
public class ViewModelStore {

    private final HashMap<String, ViewModel> mMap = new HashMap<>();

    final void put(String key, ViewModel viewModel) {
        ViewModel oldViewModel = mMap.put(key, viewModel);
        if (oldViewModel != null) {
            oldViewModel.onCleared();
        }
    }

    final ViewModel get(String key) {
        return mMap.get(key);
    }

    Set<String> keys() {
        return new HashSet<>(mMap.keySet());
    }

    /**
     *  Clears internal storage and notifies ViewModels that they are no longer used.
     */
    public final void clear() {
        for (ViewModel vm : mMap.values()) {
            vm.clear();
        }
        mMap.clear();
    }
}
复制代码

因为第一次进来,那么肯定拿不到是空的,那么第1️⃣步其实就是就有判空的操作,如果是空,那么isInstance就返回false,直接进入第2️⃣步。

2️⃣:这里其实会根据我们传入的factory类型来判断,通过前面知道,如果不传,那么就使用默认的defaultFactory,这里会判断如果当前组件是否是HasDefaultViewModelProviderFactory,从ComponentActivity的源码中可以看到,是实现了这个接口,那么最终使用的就是SavedStateViewModelFactory

internal fun defaultFactory(owner: ViewModelStoreOwner): Factory =
    if (owner is HasDefaultViewModelProviderFactory)
        owner.defaultViewModelProviderFactory else instance
复制代码

所以这里最终调用的就是SavedStateViewModelFactory的create方法,其实创建的方式就是通过反射newInstance创建实例。

@NonNull
@Override
public <T extends ViewModel> T create(@NonNull Class<T> modelClass) {
    // ViewModelProvider calls correct create that support same modelClass with different keys
    // If a developer manually calls this method, there is no "key" in picture, so factory
    // simply uses classname internally as as key.
    String canonicalName = modelClass.getCanonicalName();
    if (canonicalName == null) {
        throw new IllegalArgumentException("Local and anonymous classes can not be ViewModels");
    }
    return create(canonicalName, modelClass);
}
复制代码
@NonNull
@Override
public <T extends ViewModel> T create(@NonNull String key, @NonNull Class<T> modelClass) {
    boolean isAndroidViewModel = AndroidViewModel.class.isAssignableFrom(modelClass);

    Constructor<T> constructor;
    if (isAndroidViewModel && mApplication != null) {
        constructor = findMatchingConstructor(modelClass, ANDROID_VIEWMODEL_SIGNATURE);
    } else {
        constructor = findMatchingConstructor(modelClass, VIEWMODEL_SIGNATURE);
    }
    // doesn't need SavedStateHandle
    if (constructor == null) {
        return mFactory.create(modelClass);
    }

    SavedStateHandleController controller = SavedStateHandleController.create(
            mSavedStateRegistry, mLifecycle, key, mDefaultArgs);
    try {
        T viewmodel;
        if (isAndroidViewModel && mApplication != null) {
            viewmodel = constructor.newInstance(mApplication, controller.getHandle());
        } else {
            viewmodel = constructor.newInstance(controller.getHandle());
        }

        viewmodel.setTagIfAbsent(TAG_SAVED_STATE_HANDLE_CONTROLLER, controller);
        return viewmodel;
    } catch (IllegalAccessException e) {
        throw new RuntimeException("Failed to access " + modelClass, e);
    } catch (InstantiationException e) {
        throw new RuntimeException("A " + modelClass + " cannot be instantiated.", e);
    } catch (InvocationTargetException e) {
        throw new RuntimeException("An exception happened in constructor of "
                + modelClass, e.getCause());
    }
}
复制代码

2 数据持久化

从源码当中我们可以得知,当我们每次获取ViewModel时,除了第一次之外,其实每次都是拿到第一次创建的ViewModel对象,这也是引言中说到的当屏幕旋转时数据不会丢失,就是因为将数据保存到ViewModel中,虽然页面被销毁了,但是后续onCreate拿到的ViewModel依然是第一次创建的ViewModel,数据没有丢失。

其实这个是可以验证的。

@Override
protected void onDestroy() {
    super.onDestroy();
    Log.e("TAG","onDestroy --- "+viewModel.hashCode());
}
复制代码

当屏幕旋转页面销毁的时候,打印ViewModel的hashcode,可以看到获取到的ViewModel是一样的。

2022-06-05 15:41:12.682 18126-18126/com.t.demo02 E/TAG: onDestroy --- 179372618
2022-06-05 15:41:14.652 18126-18126/com.t.demo02 E/TAG: onDestroy --- 179372618
2022-06-05 15:41:23.582 18126-18126/com.t.demo02 E/TAG: onDestroy --- 179372618
2022-06-05 15:41:24.823 18126-18126/com.t.demo02 E/TAG: onDestroy --- 179372618
复制代码

因为我们可能不止这一个组件会用到ViewModel,那么每次创建ViewModel都会从store里面去拿,前面我们也提到过就是store是通过传入的ViewModelStoreOwner,也就是组件获取的,那么我们现在从组件层去看下,这个ViewModelStore.

进入到ComponentActivity,因为这个Activity实现了ViewModelStoreOwner接口,我们看下实现

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

@SuppressWarnings("WeakerAccess") /* synthetic access */
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();
        }
    }
}
复制代码

我们可以看到,mViewModelStore是ComponentActivity持有的一个成员变量,在获取ViewModel时拿到的那个store其实就是mViewModelStore。

屏幕旋转页面都销毁了,按道理说ViewModelStore也被回收了,那为什么为还能拿到之前的ViewModel❓

原因就在源码里了,我们接着看源码---在页面销毁重建之后,我们会重新调用下面这行代码从store里拿ViewModel

viewModel = new ViewModelProvider(this).get(MyViewModel.class);
复制代码

在此之前,需要调用ComponentActivity的getViewModelStore方法获取ViewModelStore,在返回mViewModelStore之前,调用了ensureViewModelStore方法。

这个时候,mViewModelStore确实在之前被回收了,所以mViewModelStore是空的,接下来核心代码了,我们看到getLastNonConfigurationInstance这个方法,有没有第一时间就感觉,这个肯定跟屏幕旋转相关!!!(有没有这个想法)

2.1 getLastNonConfigurationInstance

通过getLastNonConfigurationInstance方法,最终拿到了一个NonConfigurationInstances实例,然后从这个实例中,拿到了ViewModelStore,看注释,就是从NonConfigurationInstances中恢复ViewModelStore,也就是说,在上次页面销毁之后,ViewModelStore会被存储在NonConfigurationInstances中,然后再次调用getViewModelStore之后,拿到的mViewModelStore就是页面销毁之前的HashMap,里面存储之前页面的ViewModel,是不是这样的呢?这里只是猜想,我们需要验证

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

getLastNonConfigurationInstance方法中会判断mLastNonConfigurationInstances是否为空,如果不为空,那么就返回mLastNonConfigurationInstances的activity属性,首先看下这个是在哪赋值的?

static final class NonConfigurationInstances {
    Object activity;
    HashMap<String, Object> children;
    FragmentManagerNonConfig fragments;
    ArrayMap<String, LoaderManager> loaders;
    VoiceInteractor voiceInteractor;
}
复制代码

2.2 NonConfigurationInstances

NonConfigurationInstances是Activty类中的一个静态内部类,其中activity是其内部的一个成员变量。

Activity.java -- attach method 

@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
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, IBinder assistToken,
        IBinder shareableActivityToken) {
    attachBaseContext(context);

    mFragments.attachHost(null /*parent*/);

    mWindow = new PhoneWindow(this, window, activityConfigCallback);
    mWindow.setWindowControllerCallback(mWindowControllerCallback);
    mWindow.setCallback(this);
    mWindow.setOnWindowDismissedCallback(this);
    mWindow.getLayoutInflater().setPrivateFactory(this);
    if (info.softInputMode != WindowManager.LayoutParams.SOFT_INPUT_STATE_UNSPECIFIED) {
        mWindow.setSoftInputMode(info.softInputMode);
    }
    if (info.uiOptions != 0) {
        mWindow.setUiOptions(info.uiOptions);
    }
    mUiThread = Thread.currentThread();

    mMainThread = aThread;
    mInstrumentation = instr;
    mToken = token;
    mAssistToken = assistToken;
    mShareableActivityToken = shareableActivityToken;
    mIdent = ident;
    mApplication = application;
    mIntent = intent;
    mReferrer = referrer;
    mComponent = intent.getComponent();
    mActivityInfo = info;
    mTitle = title;
    mParent = parent;
    mEmbeddedID = id;
    mLastNonConfigurationInstances = lastNonConfigurationInstances;
    if (voiceInteractor != null) {
        if (lastNonConfigurationInstances != null) {
            mVoiceInteractor = lastNonConfigurationInstances.voiceInteractor;
        } else {
            mVoiceInteractor = new VoiceInteractor(voiceInteractor, this, this,
                    Looper.myLooper());
        }
    }

    mWindow.setWindowManager(
            (WindowManager)context.getSystemService(Context.WINDOW_SERVICE),
            mToken, mComponent.flattenToString(),
            (info.flags & ActivityInfo.FLAG_HARDWARE_ACCELERATED) != 0);
    if (mParent != null) {
        mWindow.setContainer(mParent.getWindow());
    }
    mWindowManager = mWindow.getWindowManager();
    mCurrentConfig = config;

    mWindow.setColorMode(info.colorMode);
    mWindow.setPreferMinimalPostProcessing(
            (info.flags & ActivityInfo.FLAG_PREFER_MINIMAL_POST_PROCESSING) != 0);

    setAutofillOptions(application.getAutofillOptions());
    setContentCaptureOptions(application.getContentCaptureOptions());
}
复制代码

当调用Activity的attch方法时,会给mLastNonConfigurationInstances赋值,首先我们先关注下时序问题,就是正常的流程中,我们是先销毁了页面,然后再重建,那么当页面销毁时,是如何操作NonConfigurationInstances的❓

这里我们需要知道一点很重要,就是当页面旋转之后会回调哪个方法,我们知道在页面销毁之前,会调用onSaveInstanceState方法,其实在onDestory之前,还会回调一个方法就是onRetainNonConfigurationInstance

ComponentActivity.java -- onRetainNonConfigurationInstance

public final Object onRetainNonConfigurationInstance() {
    // Maintain backward compatibility.
    Object custom = onRetainCustomNonConfigurationInstance();
    1️⃣
    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;
        }
    }
    2️⃣
    if (viewModelStore == null && custom == null) {
        return null;
    }
    3️⃣
    NonConfigurationInstances nci = new NonConfigurationInstances();
    nci.custom = custom;
    nci.viewModelStore = viewModelStore;
    return nci;
}
复制代码

1️⃣ 2️⃣:这里要拿到页面销毁之前的ViewModelStore,如果都是空的就return,其实这里我们不需要关注,都是空的了,没有研究的价值了,主要看不是空的情况下,是如何恢复的。

3️⃣:我们可以看到,这里首先创建了一个NonConfigurationInstances对象,custom是我们可以自定义的保存状态,本身返回就是null,可以重新自定义,关键在于,是把viewModelStore赋值给了NonConfigurationInstances,最终返回了NonConfigurationInstances实例。

2.3 页面销毁重建流程

OK,到现在,我们已经把viewModelStore保存在NonConfigurationInstances实例中,注意这是onDestory之前,现在我们要销毁页面了,页面销毁,ActivityThread会调用performDestroyActivity方法,我们只关注核心代码

ActivityThread.java -- performDestroyActivity method

void performDestroyActivity(ActivityClientRecord r, boolean finishing,
        int configChanges, boolean getNonConfigInstance, String reason) {
    Class<? extends Activity> activityClass = null;
    if (localLOGV) Slog.v(TAG, "Performing finish of " + r);
    activityClass = r.activity.getClass();
    r.activity.mConfigChangeFlags |= configChanges;
    if (finishing) {
        r.activity.mFinished = true;
    }

    performPauseActivityIfNeeded(r, "destroy");

    if (getNonConfigInstance) {
        try {
            1️⃣
            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);
            }
        }
    }
    try {
        r.activity.mCalled = false;
        mInstrumentation.callActivityOnDestroy(r.activity);
        if (!r.activity.mCalled) {
            throw new SuperNotCalledException("Activity " + safeToComponentShortString(r.intent)
                    + " did not call through to super.onDestroy()");
        }
        if (r.window != null) {
            r.window.closeAllPanels();
        }
    }
    ......
}
复制代码

1️⃣:关键就在这里,当调用performDestroyActivity方法时,就会调用Activity的retainNonConfigurationInstances方法,我们去看下具体做了什么。

Activity.java -- retainNonConfigurationInstances method

NonConfigurationInstances retainNonConfigurationInstances() {
    1️⃣
    Object activity = onRetainNonConfigurationInstance();
    HashMap<String, Object> children = onRetainNonConfigurationChildInstances();
    FragmentManagerNonConfig fragments = mFragments.retainNestedNonConfig();

    // We're already stopped but we've been asked to retain.
    // Our fragments are taken care of but we need to mark the loaders for retention.
    // In order to do this correctly we need to restart the loaders first before
    // handing them off to the next activity.
    mFragments.doLoaderStart();
    mFragments.doLoaderStop(true);
    ArrayMap<String, LoaderManager> loaders = mFragments.retainLoaderNonConfig();

    if (activity == null && children == null && fragments == null && loaders == null
            && mVoiceInteractor == null) {
        return null;
    }
    2️⃣
    NonConfigurationInstances nci = new NonConfigurationInstances();
    nci.activity = activity;
    nci.children = children;
    nci.fragments = fragments;
    nci.loaders = loaders;
    if (mVoiceInteractor != null) {
        mVoiceInteractor.retainInstance();
        nci.voiceInteractor = mVoiceInteractor;
    }
    return nci;
}
复制代码

1️⃣:首先调用onRetainNonConfigurationInstance方法,这个方法是不是很熟悉,这个方法在1.2小节中,就是将ViewModelStore封装到了NonConfigurationInstances中,所以这里的activity其实就是ComponentActivity中的NonConfigurationInstances类,这里记一下,后边会用到。

2️⃣:这里就不用多介绍了,就是又封装了一个NonConfigurationInstances对象,把activity封装进去

然后将封装好的NonConfigurationInstances实例,给ActivityClientRecord的lastNonConfigurationInstances属性赋值,然后页面被销毁了

页面被销毁之后,就立刻进行重建的过程,还是得在ActivityThread中,调用performLaunchActivity方法

ActivityThread.java -- performLaunchActivity method

private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) {

    try {
        
        if (activity != null) {
            CharSequence title = r.activityInfo.loadLabel(appContext.getPackageManager());
            Configuration config =
                    new Configuration(mConfigurationController.getCompatConfiguration());
            if (r.overrideConfig != null) {
                config.updateFrom(r.overrideConfig);
            }
            ......
            1️⃣
            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,
                    r.assistToken, r.shareableActivityToken);

            if (customIntent != null) {
                activity.mIntent = customIntent;
            }
            2️⃣
            r.lastNonConfigurationInstances = null;
            ......
            
            if (r.isPersistable()) {
                3️⃣
                mInstrumentation.callActivityOnCreate(activity, r.state, r.persistentState);
            } else {
                mInstrumentation.callActivityOnCreate(activity, r.state);
            }
            4️⃣
            r.activity = activity;
            mLastReportedWindowingMode.put(activity.getActivityToken(),
                    config.windowConfiguration.getWindowingMode());
        }
        r.setState(ON_CREATE);

        synchronized (mResourcesManager) {
            mActivities.put(r.token, r);
        }

    } catch (SuperNotCalledException e) {
        throw e;

    } catch (Exception e) {
        if (!mInstrumentation.onException(activity, e)) {
            throw new RuntimeException(
                "Unable to start activity " + component
                + ": " + e.toString(), e);
        }
    }

    return activity;
}
复制代码

1️⃣:在performLaunchActivity方法中,调用了attach方法,这里我们可以看到,ActivityClientRecord的lastNonConfigurationInstances被传进去,这个就是包含销毁前ViewModelStore的实例对象NonConfigurationInstances

所以我们回到前面,在attach方法中,Activity的mLastNonConfigurationInstances属性,就是这个值!!!!最终得到了考证,我们的猜想是没错的

总结:

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

当调用getLastNonConfigurationInstance方法的时候,拿到的mLastNonConfigurationInstances的activity属性,就是NonConfigurationInstances,最终拿到的ViewModelStore就是销毁之前的,所以这也就是页面能够保留之前的数据的主要原因。

3 Fragment之间数据共享

其实在了解了ViewModel的使用原理之后,对于数据共享,其实就变得很简单了。

我们依靠什么来做到的数据共享,如果每个页面之间是互相隔离的,那么每个页面都有一个单独的ViewModel

image.png

这样其实也是正常的,通常每个页面的业务逻辑是不同的,但是你们会不会遇到下面这个场景,就是每个页面都需要一些公共数据,比如用户信息,比如配置信息等,在一个页面更新之后,其他页面也需要做相应的更新,这就需要涉及到了数据的共享。

image.png

因为所有的Fragment都需要依附于Activity主体而不能单独存在,因此Activity作为所有Fragment的宿主,其持有的ViewModel是能够被其他Fragment获取到的,而且通过前面的讲解我们知道获取到的ViewModel实例其实是一样的,因此在不同的页面修改数据,其他页面也是能够感知到的,以此来实现了数据共享。

其实Jetcpack推出的组件中,有一个Navigation,这个我后续会详细地讲解这个组件的使用,尤其是单Activity和多Fragment架构,我认为最后也会成为主流,那么Fragment之间数据共享必将成为主旋律。

猜你喜欢

转载自juejin.im/post/7107860512108445727
今日推荐