Jetpack:ViewModel使用指南,实现原理详细解析!

11 篇文章 0 订阅


简介

为了更好地将职能划分清楚,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及其持有的数据就不存在了。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

pumpkin的玄学

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值