文章目录
- 简介
- ViewModel的基本使用
- ViewModel原理全面剖析
- 其他杂谈
简介
为了更好地将职能划分清楚,Android为我们提供了ViewModel类,专门用于存放应用程序页面所需的数据。ViewModel可以这样理解:介于View(视图)和Model(数据模型)之间的东西。起到桥梁的作用,使视图和数据不仅能够分开,还能保持通信。
ViewModel生命特征
通常会在调用Activity对象的onCreate()方法时请求ViewModel对象。但是系统可能会在activity存在期间多次调用activity的onCreate(),重走生命周期(比如设备旋转等配置变化)。但是ViewModel存在的时间范围时从首次请求ViewModel直到Activity完成并销毁(此时ViewModel会执行onCleared方法)。比如activity生命周期如下所示,中间出现设备旋转。ViewModel会直到Activity真正销毁的时候,去销毁。
ViewModel是一个抽象类,其中只有一个onCleared()方法。当ViewModel不再被需要,即与之相关的Activity都被销毁时,该方法会被系统调用。我们可以在该方法中执行一些资源释放的相关操作。注意,由于屏幕旋转而导致的Activity重建,并不会调用该方法。
依赖
buildscript {
//android lifecycle version
ext.lifecycle_version = "2.3.0"
}
// ViewModel
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version"
ViewModel的基本使用
前面提到,ViewModel最重要的作用是将视图与数据分离,并独立于Activity的重建。为了验证这一点,我们在ViewModel中创建一个计时器,每隔1s,通过通知给activity。activity进行计时时间,并测试activity旋转后数据是否会重新计时。写一个这么的小实验,顺便展示一下ViewModel的基本使用方式。
1.写一个继承自ViewModel的类,将其命名为TimingViewModel。
class TimingViewModel : ViewModel() {
override fun onCleared() {
super.onCleared()
// TODO: 2021/9/4 可以做资源释放操作
}
}
**2.**写一个startTiming方法用于进行倒计时,并且回调给主界面刷新UI。完整代码如下:
class TimingViewModel : ViewModel() {
@Volatile
var currentNum = 0
/**
* 回调主界面block
*/
private lateinit var block: (Int) -> Unit
/**
* 倒计时
* @param block 倒计时回调 调度到主线程执行
*/
fun startTiming(block: (Int) -> Unit) {
//block 赋值
this.block = block
//使用协程调度
viewModelScope.launch(Dispatchers.Default) {
//防止屏幕旋转多次调用
if (currentNum <= 0) {
while (true) {
delay(1000)
++currentNum
//通知界面刷新
withContext(Dispatchers.Main) {
this@TimingViewModel.block.invoke(currentNum)
}
}
}
}
}
override fun onCleared() {
super.onCleared()
// TODO: 2021/9/4 可以做资源释放操作
}
}
注意:currentNum和block必须在ViewModel中最为成员变量持有,这样在activity因为配置重建之后,会重新调用onCreate进而调用startTiming方法。利用ViewModel生命周期不变的特性,currentNum的值就不会被刷新。block可以被赋值为新传入的block。
3.创建TimingActivity,使用ViewModelProvider来实例化ViewModel(这样才可以保证ViewModel生命周期在activit变化的时候保持不变,因为ViewModelProvider会判断ViewModel是否存在,若存在则直接返回,否则它会创建一个ViewModel。)。然后调用startTiming且更新UI。
class TimingActivity : AppCompatActivity() {
lateinit var binding: ActivityCommonBinding
lateinit var timingViewModel: TimingViewModel
@SuppressLint("SetTextI18n")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityCommonBinding.inflate(layoutInflater)
setContentView(binding.root)
//初始化ViewModel
timingViewModel = ViewModelProvider(this).get(TimingViewModel::class.java)
//调用开始计时,并且更新界面
timingViewModel.startTiming { currentTime ->
binding.tvContent.text = "currentTime : $currentTime"
}
}
}
运行程序并旋转屏幕。结果如下所示:旋转屏幕导致Activity重建时,计时器并没有停止。这意味着,横/竖屏状态下的Activity所对应的ViewModel是同一个,它并没有被销毁,所持有的数据也一直都存在着。
ViewModel原理全面剖析
在页面中通过ViewModelProvider来实例化ViewModel的。
timingViewModel = ViewModelProvider(this).get(TimingViewModel::class.java)
看一下ViewModelProvider的构造函数:
public ViewModelProvider(@NonNull ViewModelStoreOwner owner) {
this(owner.getViewModelStore(), owner instanceof HasDefaultViewModelProviderFactory
? ((HasDefaultViewModelProviderFactory) owner).getDefaultViewModelProviderFactory()
: NewInstanceFactory.getInstance());
}
ViewModelProvider接收一个ViewModelStoreOwner对象作为参数。在以上示例代码中该参数是this。所以Activity在哪里肯定继承了ViewModelStoreOwner或者他的子类。找一找,最终发现在ComponentActivity中继承了ViewModelStoreOwner。实现了getViewModelStore方法(getViewModelStore方法是ViewModelStoreOwner接口内唯一的方法,可以自行找源码看)。
public class ComponentActivity extends androidx.core.app.ComponentActivity implements
LifecycleOwner,
ViewModelStoreOwner,
SavedStateRegistryOwner,
OnBackPressedDispatcherOwner {
...
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.");
}
if (mViewModelStore == null) {
NonConfigurationInstances nc =
(NonConfigurationInstances) getLastNonConfigurationInstance();
if (nc != null) {
// Restore the ViewModelStore from NonConfigurationInstances
mViewModelStore = nc.viewModelStore;
}
if (mViewModelStore == null) {
mViewModelStore = new ViewModelStore();
}
}
return mViewModelStore;
}
...
}
分析一下这个方法,如果getApplication为null的话直接抛出异常了,所以不能在onCreate之前调用ViewModelProvider去实例化ViewModel(推荐在onCreate里面实例化)。下面的逻辑分析。getLastNonConfigurationInstance返回的是onRetainNonConfigurationInstance的返回值。而onRetainNonConfigurationInstance方法会在配置发生变化的时候调用保存信息。这里返回了ViewModelStore,在ComponentActivity里面的onRetainNonConfigurationInstance方法里面在配置发生变化activity发生变化销毁之前会保存ViewModelStore(代码较为简单可以自行查看,就不贴出来了)。所以取ViewModelStore的时候,首先判断getLastNonConfigurationInstance是否存在ViewModelStore,如果存在直接拿取,否则new 一个新的ViewModelStore。这样就保证了ViewModelStore不会因为activity配置改变生命周期变化而销毁,而且保持是同一个对象!
接口方法getViewModelStore()就返回了ViewModelStore。说了半天的ViewModelStore,看看ViewModelStore到底是个啥。
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();
}
}
从ViewModelStore的源码可以看出,ViewModel实际上是以**HashMap<String,ViewModel>**的形式被缓存起来了。提供get、put、clear相关的方法。ViewModel与页面之间没有直接的关联,它们通过ViewModelProvider进行关联。接下来就看一下,ViewModelProvider的get方法吧。
public <T extends ViewModel> T get(@NonNull String key, @NonNull Class<T> modelClass){
//默认的get方法传进来的key 是定义好的一个常量 无需在意
ViewModel viewModel = mViewModelStore.get(key);
if (modelClass.isInstance(viewModel)) {
...
//如果 ViewModelStore里面存在当前的 viewmodel 则直接返回。
return (T) viewModel;
} else {
...
}
if (mFactory instanceof KeyedFactory) {
viewModel = ((KeyedFactory) mFactory).create(key, modelClass);
} else {
//否则则直接创建一个新的对应,内部其实调用了 newInstance方法.
viewModel = mFactory.create(modelClass);
}
mViewModelStore.put(key, viewModel);
return (T) viewModel;
}
ViewModelProvider检查该ViewModel是否已经存在于缓存中,若存在,则直接返回,若不存在,则实例化一个。
那还有一个问题,ViewModel是如何知道activity真正销毁了呢,进而调用onCleared方法呢?其实在ComponentActivity的构造方法里面对activity生命周期做了监听,如果activity走到了OnDestroy且配置没有发生改变时,调用ViewModelStore的clear方法。代码如下:
public ComponentActivity() {
Lifecycle lifecycle = getLifecycle();
...
getLifecycle().addObserver(new LifecycleEventObserver() {
@Override
public void onStateChanged(@NonNull LifecycleOwner source,
@NonNull Lifecycle.Event event) {
if (event == Lifecycle.Event.ON_DESTROY) {
if (!isChangingConfigurations()) {
getViewModelStore().clear();
}
}
}
});
...
}
总结一下:
ViewModelProvider检查该ViewModel是否已经存在于缓存中,若存在,则直接返回,若不存在,则实例化一个,而储存ViewModel的集合ViewModelStore在activity中通过onRetainNonConfigurationInstance进行保存调用,通过Lifecycle判断当activity真正销毁的时候ViewModelStore的clear方法清空ViewModel回调onCleared方法。因此,Activity由于配置变化导致的销毁重建并不会影响ViewModel,ViewModel是独立于页面而存在的。所以,在使用ViewModel时,需要特别注意,不要向ViewModel中传入任何类型的Context或带有Context引用的对象,这可能会导致页面无法被销毁,从而引发内存泄漏!!!。
其他杂谈
AndroidViewModel
在使用ViewModel时,不能将任何类型的Context或含有Context引用的对象传入ViewModel,因为这可能会导致内存泄漏。但是如果你真的需要在ViewModel中使用Context,该怎么办呢?可以使用AndroidViewModel类,它继承自ViewModel,并接收Application作为Context。
ViewModel与onSaveInstanceState()的区别
onSaveInstanceState()方法只能保存少量的、能支持序列化的数据。但是onSaveInstanceState()
ViewModel能支持页面中所有的数据。但是,ViewModel不支持数据的持久化,当界面被彻底销毁时,ViewModel及其持有的数据就不存在了。