关于ViewModel的这几个问题,你都知道吗?

5,247 阅读10分钟

前言

作为一名Android开发者,如果你熟悉MVVM架构,熟悉Jetpack组件,那么相信你肯定使用过ViewModel

正如它的名字一样,它是Google推出的一个类,方便我们实现MVVM架构中的ViewModel层。我们在其中处理View层所需的数据,然后在特定条件下通知View层进行UI更新

正如官方所介绍:

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

我们抓一下这句话的重点:

  1. 注重生命周期的方式:会在合适的时间进行自我回收,防止出现内存泄漏。
  2. 存储和管理界面相关的数据:符合MVVM架构ViewModel层的理念。
  3. 在发生屏幕旋转等配置更改后继续留存数据:为什么要这么设计?怎么做到的。

接下来,就让我们带着问题,深入学习一下ViewModel类。

使用方法

在阅读源码前,让我们先来简单回顾一下ViewModel的使用方法。

class MainViewModel(private val repository: MainRepo) : ViewModel() {

    private val _textMld = MutableLiveData<String>()
    val textLd: LiveData<String> = _textMld
    
    fun getTextInfo() {
        viewModelScope.launch {
            withContext(Dispatchers.IO) {
                //做异步网络请求工作,获取到textData
                repository.getTextInfo()
            }.apply {
                _textMld.postValue(textData)
            }
        }
    }
}

class MainActivity : AppCompatActivity() {

    fun setVmFactory(): ViewModelProvider.Factory {
        return object : ViewModelProvider.Factory {
            override fun <T : ViewModel> create(modelClass: Class<T>): T {
                return MainViewModel(MainRepo()) as T
            }
        }
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        val binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)
        
        val vm = ViewModelProvider(this, setVmFactory())[MainViewModel::class.java]
        vm.textLd.observe(this, Observer { 
            binding.textTv.text = it
        })
    }
}

方法步骤可以简单分为两步,分别为:

  1. 继承ViewModel类实现自定义ViewModel,如:MainViewModel。
  2. 通过ViewModelProvider实例化ViewModel

源码

现在,根据上方所介绍的使用方法,我们进一步来看一下ViewModel的源码。

public abstract class ViewModel {
    ....

    @SuppressWarnings("WeakerAccess")
    protected void onCleared() {
    }

    @MainThread
    final void clear() {
    	
        ....

        onCleared();
    }

    ....
}

ViewModel 是一个抽象类,提供 onCleared()方法供我们在ViewModel销毁前做一下清除工作。

接下来,我们来看看是如何通过ViewModelProvider来实例化ViewModel对象的。

其实就是两步:

  1. 实例化一个 ViewModelProvider 对象。
  2. 调用 ViewModelProvider.get()方法来得到一个ViewModel对象。

先来看看它的构造函数:

public open class ViewModelProvider(
    private val store: ViewModelStore,
    private val factory: Factory
) {

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

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

......

}

通过 ViewModelProvider 的构造函数可以看出,一共有两个参数:ViewModelStoreFactory

分别来看看这两个参数代表着什么意思。

public class ViewModelStore {

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

    final void put(String key, ViewModel viewModel) {
	//所以这里key需要注意,不要使用相同的key,否则后创建的VM会替换到老的VM
        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());
    }

    public final void clear() {
        for (ViewModel vm : mMap.values()) {
            //调用ViewModel的clear方法,表示不再使用
            vm.clear();
        }
	//清除集合中的所有ViewModel
        mMap.clear();
    }
}

ViewModelStore:正如其名,就是用来存储ViewModel对象的,通过内部维护一个HashMap来实现对ViewMoel对象的存储与管理工作。

再来看看上面所介绍的 Factory

而Factory则是工厂接口的,用来实例化ViewModel,具体实现可以参考使用方法中所介绍的 setVmFactory() 方法,用来实例化 MainViewModel。

public interface Factory {

    public fun <T : ViewModel> create(modelClass: Class<T>): T

}

接下来,我们来看看 ViewModelProvider 中的 get() 方法。

public open operator fun <T : ViewModel> get(modelClass: Class<T>): T {
    val canonicalName = modelClass.canonicalName
    return get("$DEFAULT_KEY:$canonicalName", modelClass)
}

public open operator fun <T : ViewModel> get(key: String, modelClass: Class<T>): T {
    //1.根据key从ViewModelStore中取出ViewModel
    var viewModel = store[key]

    //2.通过工厂方法来实例化viewModel
    if (modelClass.isInstance(viewModel)) {
        (factory as? OnRequeryFactory)?.onRequery(viewModel)
        return viewModel as T
    }
    
    viewModel = if (factory is KeyedFactory) {
        factory.create(key, modelClass)
    } else {
        factory.create(modelClass)
    }

    //3.将实例化的ViewModel放入ViewModelStore
    store.put(key, viewModel)
    return viewModel
}

概括一下 get() 方法:

  1. 根据传入的形参Class.canonicalName做为key,从ViewModelStore中取出ViewModel。
  2. 再通过工厂方法来实例化ViewModel。
  3. 最后将实例化后的ViewModel放入ViewModelStore中,并返回。

OK,根据使用方法来分析源码的话,我们好像已经分析完了😅。本篇文章到此结束?

这还远远不够!

不知你有没有想过它为什么会取名叫做ViewModel呢?它跟MVVM架构模式中的ViewModel是怎么样的一种关系呢?它又是怎么感知到生命周期的呢?为什么要设计成屏幕旋转后继续留存ViewModel呢?

接下来,让我们带着问题,进一步进行学习思考。

为什么要叫做ViewModel

这其实就是Google设计这个类的初衷问题了。我们知道在Android的架构模式中,从最初始的MVC,到MVP,Google其实没有专门为了这架构模式而设计一些类来支持提供我们开发者使用,这也间接导致开发者会拥有一套属于自己的设计风格。直到MVVM架构模式的出现,Google为了减小开发者架构难度,提升开发效率,为我们新设计了一些类来支持MVVM架构模式,其中就有ViewModel这个类,用来处理MVVM模式中ViewModel层所负责的工作。这也就是为什么它会被命名叫做ViewModel

ViewModel是如何感知到生命周期的

正如官网所介绍的,ViewModel是有生命周期的,如下图所示:

viewmodel-lifecycle.png

不知你有没有思考过ViewModel是如何感知到生命周期的呢?

如果你熟悉 Jetpack 的话,你肯定会脱口而出答案:Lifecycle。事实也证明的确是通过 Lifecycle 来感知到生命周期的。

其实官方文档也介绍了:

ViewModel 对象存在的时间范围是获取 ViewModel 时传递给 ViewModelProviderLifecycle。ViewModel 将一直留在内存中,直到限定其存在时间范围的 Lifecycle 永久消失.

那接下来就让我们来进一步看看ViewModel是如何拿到Lifecycle来感知到生命周期的。

还记得使用方法中所介绍的使用ViewModelProvider来实例化ViewModel吗?

val viewModel = ViewModelProvider(this, setVmFactory())[getViewModelClass()]

关于 ViewModelProvider 的构造函数,我们之前已经介绍过了,分别是ViewModelStoreFactory。回到调用方法里,这里的this就是指当前所在的Activity,也对应着构造函数中的ViewModelStore

Activity 对应 ViewModelStore ?这是什么对应关系?

这时,我扫了一眼 ViewModelStore 类注释,发现了一句话:

Use {@link ViewModelStoreOwner#getViewModelStore()} to retrieve a {@code ViewModelStore} for activities and fragments.

通过 ViewModelStoreOwner#getViewModelStore() 方法来给 Activity、Fragment 获取到 ViewModelStore

public interface ViewModelStoreOwner {
    /**
     * 返回 ViewModelStore
     */
    @NonNull
    ViewModelStore getViewModelStore();
}

到这一步,应该想到了吧,我们的 Activity | Fragment 实现了ViewModelStoreOwner 接口,并实现了getViewModelStore()方法来得到 ViewModelStore

果然,Activity的父类ComponentActivity 类实现了ViewModelStoreOwner 接口并实现了  getViewModelStore() 接口方法。

ComponnetActivity.kt

public class ComponentActivity extends androidx.core.app.ComponentActivity implements
        LifecycleOwner,
        ViewModelStoreOwner
	...... {

	public ViewModelStore getViewModelStore() {
            //检查一下当前 mViewModelStore 是否为空
	    if (mViewModelStore == null) {
		//检索一下是否有先前保存的非配置实例数据
	        NonConfigurationInstances nc =
	                (NonConfigurationInstances) getLastNonConfigurationInstance();
	        if (nc != null) {
                    //从先前保存的非配置实例数据中取出 ViewModelStore
	            mViewModelStore = nc.viewModelStore;
	        }
	        if (mViewModelStore == null) {
                    //先前没有保存过非配置实例数据,则新建一个 ViewModelStore
	            mViewModelStore = new ViewModelStore();
	        }
	    }

	    return mViewModelStore;
	}

}

简单概括一下:在获取 ViewModelStore 这一步中,会先去检索一下是否有先前保存的非配置实例数据,如果有保存,则取出其中的 ViewModelStore; 反之,如果之前没有保存过,则新创建一个 ViewModelStore

所以,在 ViewModelProvider 的构造函数中通过调用 owner.viewModelStore 方法来获取到 viewModelStore,这一步其实就是取在 Activity | FragmentgetViewModelStore() 方法中创建的 ViewModelStore,也就是我们刚刚所介绍的。

到了这,好像也没有看出跟 Lifecycle 有任何关系啊。

接着我们进一步看看 viewModelStoreLifecycle 之间的调用关系。

public ComponentActivity() {
    ......

    getLifecycle().addObserver(new LifecycleEventObserver() {
        @Override
        public void onStateChanged(@NonNull LifecycleOwner source, @NonNull Lifecycle.Event event) {

            if (event == Lifecycle.Event.ON_DESTROY) {
                //检查一下是否发生配置变更
                if (!isChangingConfigurations()) {
                    //调用ViewModelStore中的clear()方法来清除其ViewModel
                    getViewModelStore().clear();
                }
            }
        }
    });

    ......

}

通过Lifecycle 来感知Activity的生命周期,当Activity处于销毁状态时,检查一下是否发生配置变更,如果未发生配置变更,则调用clear()方法来清除ViewModelStore及其保存的ViewModel

关于配置变更,我们放到文章后面来分析,这里先不展开。

总结一下ViewModel是如何感知到生命周期的。

首先我们知道ViewModel是存储在ViewModelStore中的,当我们进入Activity时,会自动创建一个ViewModelStore,当我们在onCreate()方法中调用ViewModelProvider.get()方法时,就会将创建的ViewModel存到该ViewModelStore中。通过Lifecycle来得知Activity的生命周期,当Activity处于销毁时,检查一下是否发生配置变更,如果未发生配置变更,则调用clear()方法来清除ViewModelStore及其保存的ViewModel

为什么屏幕旋转后ViewModel数据可以继续留存

当我们没有为Activity设置 configChanges 属性时,Activity会在我们旋转屏幕时发生重建,而此时ViewModel会继续留存这些数据。

这是如何做到的呢?

正如上文中所提到的,当Activity处于销毁时,会去检查一下是否发生配置变更,如果未发生配置变更,则调用clear()方法来清除ViewModelStore及其保存的ViewModel

那这么看的话,想必我们在旋转屏幕时,是发生了配置变更的,因为正是发生了配置变更才让我们的ViewModel没有被清除掉,得以留存这些状态数据。

当Activity因为配置更改(如:旋转屏幕)而被销毁重建时,系统会立即调用onRetainNonConfigurationInstance()方法。

ComponentActivity.java

public final Object onRetainNonConfigurationInstance() {
    // Maintain backward compatibility.
    Object custom = onRetainCustomNonConfigurationInstance();

    ViewModelStore viewModelStore = mViewModelStore;
    if (viewModelStore == null) {
        //获取先前创建的非配置实例数据
        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;
}

static final class NonConfigurationInstances {
    Object custom;
    ViewModelStore viewModelStore;
}

会先通过 getLastNonConfigurationInstance() 方法来获取先前创建的非配置实例数据,如果先前没有创建过非配置实例数据,则新创建一个,并返回。

Activity.java

NonConfigurationInstances mLastNonConfigurationInstances;

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

final void attach(NonConfigurationInstances lastNonConfigurationInstances ...) {
    ...
    mLastNonConfigurationInstances = lastNonConfigurationInstances;
    ...
}

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

可以看出,mLastNonConfigurationInstances 是在Activity的 attach() 方法中被赋值的,所以我们需要往上看Activity的启动流程。

ActivityThread.java

private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) {
	......
    Activity activity = null;
    activity.attach(r.lastNonConfigurationInstances ...);
    r.lastNonConfigurationInstances = null;


	......
    return activity;
}

也就是说最初始的非配置实例是在ActivityThread中的ActivityClientRecord所创建的,当Activity发生配置变更而重建时ActivityClientRecord是不受影响的,所以这样当屏幕旋转Activity重建(配置变更)时,先是onRetainNonConfigurationInstance() 方法会被调用返回一个包含当前ViewModelStore的非配置实例对象,然后后续通过getLastNonConfigurationInstance()方法来获取到该非配置实例,所以保存在其ViewModelStore中的ViewModel是不是被清除的。

关于配置变更,这里补充一点。

  • 旋转屏幕其实就是一次配置变更,isChangingConfigurations = true。
  • 当跳转到另外一个Activity时(Activity正常退出或系统杀死)配置未变更,isChangingConfigurations = false。

为什么要设计成配置变更后ViewModel依然存在

现在我们知道当Activity发生配置变更重建的时,ViewModel是不会被清除的,所以他保存的数据依然存在。

不知你有没有想过,为什么要这么设计呢?

我们知道ViewModel类负责处理的是MVVM架构模式ViewModel层的工作,所以ViewModel保留的是UI状态数据。当你屏幕旋转时,Activity会发生重建工作,将我们的xml布局从portrait切换到了landscape,但其实布局中展示的数据是一样的。所以,我们完全不需要再去Model层重新获取一下数据,直接复用ViewModel中保留的数据即可,从而节省系统开销。

另外,在ViewModel出现之前,当我们想要在Activity发生销毁重建时保留数据需要通过覆写 onSaveInstanceState() 方法来实现,该方法通过Bundle来将数据序列化保存在磁盘中,实现步骤相对复杂且又有大小限制。相对比,ViewModel将数据保存在内存中,读写速度更快,而且又没有大小限制,所以是一种很好的替代方案。

总结

本篇文章,我们通过ViewModel的使用方法来学习了ViewModel的源码,又对ViewModel的设计进行了进一步思考,就 “它为什么会取名叫做ViewModel?它跟MVVM中的ViewModel是怎么样的一种关系?他又是怎么感知到生命周期的?为什么要设计成屏幕旋转后继续留存ViewModel?” 这几个问题进行了进一步的学习,相信你现在对ViewModel有了进一步的认识与理解。

到此本篇文章就结束啦,如果你有任何疑问或者不同的想法,欢迎在评论区留言与我一起探讨。

其实分享文章的最大目的正是等待着有人指出我的错误,如果你发现哪里有错误,请毫无保留的指出即可,虚心请教。

另外,如果你觉得文章不错,对你有所帮助,请帮我点个赞,就当鼓励,谢谢~。