浅谈MVVM模式与ViewModel及应用

一、什么是ViewModel

配置变更引起Activity的销毁重建是非常常见的业务场景,界面的数据的存储以及用户交互状态的保存成了提升用户体验的关键。ViewModel是Android Jetpack组件中提供的以注重生命周期的方式存储和管理界面相关数据。

Android官方对ViewModel的介绍是

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

在ViewModel出现之前,一般通过onSaveInstanceState()保存页面中的轻量关键数据,在页面onCreate()时机再将数据取出恢复界面,但这种方案仍然面临很多问题:

  • Bundle数据大小问题 :一般来说,onSaveInstanceState保存的数据大小限制可以较大,但并不建议保存过大的数据。这是因为保存的数据会持久存储在磁盘上,以备Activity被销毁后重新创建时使用。如果数据太大,会对磁盘存储和恢复过程造成额外的开销,并且可能会导致性能问题。
  • 异步操作管理:一般的页面中都会存在网络等耗时操作,当这些耗时的异步操作尚未完成,Activity出现销毁重建,那对这些任务的调度管理及资源释放问题都是业务中需要维护的部分
  • 用户操作状态的保存:以往的MVC和MVP模式中,用户的操作,如:文本编辑、内容勾选等逻辑可能都是直接编写在Activity或Fragment中的,当面临销毁的场景就不得不梳理这些逻辑及变量以用于界面恢复。 在ViewModel出现后,我们可以将界面中的业务逻辑及数据都委托存储在这个类中,即使在Activity被销毁重建,仍可轻松恢复界面数据及用户状态。

二、从MVVM架构看ViewModel的角色定位

2.1 MVVM模式的分析与理解

MVVM是一种用于开发用户界面的软件设计架构模式。使用数据绑定技术,在UI层和业务逻辑之间进行分离的架构模式。

  • Model:程序数据及纯粹的业务逻辑层;
  • View:用户可视化界面,一般负责UI样式及动画、界面渲染等工作;
  • ViewModel:ViewModel是连接View层和Model层。
    • 对View层,它提供View层需要的数据和行为,必要时将行为逻辑传递到Model层
    • 对Model层,它获取数据并转换为View层可以直接使用的数据形式

MVVM设计模式以其对View层和Model层的高度的解耦设计、响应式编程思想在应用开发中流行。对比MVP模式,它的优越性表现在

  • View层和Model层清晰的职责界定也使得代码的可测试性更强;
  • 数据绑定机制使得数据自动更新到UI,代码逻辑更为简单直接,减少开发的手动维护麻烦

2.2 Jetpack中的ViewModel组件与VM层的关系

Android Jetpack库提供的ViewModel组件意在承担起MVVM中VM层的职责,在界面开发中扮演VM层的角色。然而在Jetpack库中的ViewModel组件并无直接提供数据绑定的能力,这部分能力需要配合LiveData、Flow库、DataBinding来完成响应式UI部分的能力。

由于在MVVM架构中,VM层的职责便是承担起连接View层和Model层的桥梁,同时为界面层提供所需的数据及行为逻辑,理所当然的是它便需要有保存界面数据的职责。在Android界面有一个典型的业务场景:配置变更带来UI的销毁重建。 官方在ViewModel的组件设计中也考虑了这个业务场景:ViewModel有独立于Activity的生命周期,下图是官方提供的关于ViewModel与Activity的生命周期图。

ViewModel的这种设计至少有两个要点:

  • 关注点分离:ViewModel独立于Activity生命周期,开发者可以更好地分离代码开发中的关注点,Activity专注于UI的开发,数据及行为放心地交给ViewModel;
  • VM的UI无感知 :由于ViewModel的生命周期不与Activity强绑定且长于Activity,若在VM层持有View层数据可能导致内存泄漏的风险。

三、ViewModel的开发实践

3.1 数据与UI的分离与协作

在应用的配置发生变更时,在Activity和Fragment被销毁重建时,系统会保障与之关联的ViewModel仍然存活而不受影响,从而分离开发者在APP功能开发的关注点,也反向推动了开发者在做ViewModel层开发和UI层开发时更能明晰分层的价值和意义,使得架构的设计指责更明确和清晰,使得代码更好维护。 例如,界面有两个可编辑文本editText_1editText_2,若当前界面支持横竖屏切换,ViewModel层由系统天然支持的能力则会让开发者将其值存放在ViewModel,便无需将这两个值另外存放在别处。

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

    private val viewModel by viewModels<MainViewModel>()
    
    fun onCreate(bundle: Bundle) {
        // 在Activity层通过观测VM层的数据对象对UI进行修改,同样也适用初始化
        viewModel.editValue1.observe(this) {
            editTextView_1.text = it
        }
        viewModel.editValue2.observe(this) {
            editTextView_2.text = it
        }
    }
}

class MainViewModel: ViewModel() {
    // 这里使用了后备属性,来控制外部对LiveData数据的访问修改
    /**
     * 数1
     */
    private val _numOneLiveData = MutableLiveData<Int>()
    val editValue1: LiveData<Int> = _numOneLiveData

    /**
     * 数2
     */
    private val _numTwoLiveData = MutableLiveData<Int>()
    val editValue2: LiveData<Int> = _numTwoLiveData
}

3.2 ViewModel的逻辑代理与UI响应更新

在经典的设计中,我们一般把ViewModel作为界面数据和逻辑的承载者,我们会把和界面相关的数据及逻辑实现在ViewModel层中。页面中并不直接修改和操作界面数据,而是将界面的相关的操作委托到ViewModel中实现,界面只监听ViewModel中的可观测数据。例如下面的例子:

  • 关于界面数据的具体操作封装在ViewModel中
  • 在MainActivity界面的增减Button则通过ViewModel提供的操作函数而直接调用
  • 数据的变更会直接响应到editValueX这个可观测数据对象上,从而在Activity层响应变更而变化UI
MainViewModel 复制代码
class MainViewModel: ViewModel() {

    /**
     * 提供具体的逻辑函数:增
     */
    fun inc(type: Type) {
        when (type) {
            is Type.One -> {
                _numOneLiveData.value = (editValue1.value?:0) + 1
            }
            is Type.Two -> {
                _numTwoLiveData.value = (editValue2.value?:0) + 1
            }
        }
    }
    
    /**
     * 提供具体的逻辑函数:减
     * @type Type类型是自己定义的枚举类,用于区分界面上的两个不同的操作文本框
     */
    fun des(type: Type) {
        when (type) {
            is Type.One -> {
                _numOneLiveData.value = (editValue1.value?:0) - 1
            }
            is Type.Two -> {
                _numTwoLiveData.value = (editValue2.value?:0) - 1
            }
        }
    }
}
  • 在MainActivity界面的增减Button则通过ViewModel提供的操作函数而直接调用
kotlin 复制代码
class MainActivity: ComponentActivity() {

    ····
    
    @Composable
    fun ValueTextLayout() {
        val vm: MainViewModel = viewModel()
        // 将数据转为与compose配合使用的State<T>类型的数据
        val valueState = vm.editValue1.observeAsState(0)
        Row(...) {
            Text(text = "【数值】: ", fontWeight = FontWeight.Bold)
            // 当LiveData类型的editValue1内容发生变化时,会实时响应到Text文本内容中
            Text(text = " ${valueState.value}")
        }
    }
    
    ···
}
  • 数据的变更会直接响应到editValueX这个可观测数据对象上,从而在Activity层响应变更而变化UI。(这里使用Compose来实现,和普通的Layout布局实现是一样的:按钮的Click事件直接调用ViewModel的逻辑)
kotlin 复制代码
class MainActivity: ComponentActivity() {

    ····
    
    @Composable
    fun LinearLayout(type: Type) {
        val viewMode: MainViewModel = viewModel()
        // 定义一个名为LinearLayout的Composable函数
        Column(...) {
            Row(...) {
                // button的Click事件直接委托给viewModel的inc接口
                Button(onClick = {**viewMode.inc(type)**}, modifier = ...) {
                    Text(text = "ADD") 
                }
                // button的Click事件直接委托给viewModel的des接口
                Button(onClick = {**viewMode.des(type)**}, modifier = ...) {
                    Text(text = "DEL") 
                }
            }
        }
    }
    ····
}

四、ViewModel的源码分析

ViewModel由框架提供,自然由框架创建,并也由框架销毁。 通过LiveData合作,绑定LifecycleOwners,或者由viewModelScope完成数据的生命周期管理及数据的分发时机。

4.1 ViewModel的创建

我们的分析入口是kotlin文件的ViewModel的创建,其他创建方式底层原理相同。

我们从代码中创建ViewModel一般委托给ComponentActivity的拓展函数viewModels来创建,viewModels函数接受一个自定义的ViewModel的工厂参数,一般情况下使用的是默认的defaultViewModelProviderFactory内置的工厂实现,而最终返回的是一个ViewModelLazy的类。

kotlin 复制代码
@MainThread
public inline fun <reified VM : ViewModel> ComponentActivity.viewModels(
    noinline factoryProducer: (() -> Factory)? = null
): Lazy<VM> {
    val factoryPromise = factoryProducer ?: {
        defaultViewModelProviderFactory
    }

    return ViewModelLazy(VM::class, { viewModelStore }, factoryPromise)
}

ViewModelLazy实现了Lazy接口。类的实例化接收三个参数:

  • viewModelClass:ViewModel的KClass对象,用于创建ViewModel实例;
  • storeProducer:用于存储和获取当前界面实例关联的ViewModel,避免重复创建和方便ViewModel的复用;
  • factoryProducer:ViewModel实例创建的工厂;
kotlin 复制代码
public class ViewModelLazy<VM : ViewModel> (
    private val viewModelClass: KClass<VM>,
    private val storeProducer: () -> ViewModelStore,
    private val factoryProducer: () -> ViewModelProvider.Factory
) : Lazy<VM> {
    private var cached: VM? = null

    override val value: VM
        get() {
            val viewModel = cached
            return if (viewModel == null) {
                val factory = factoryProducer()
                val store = storeProducer()
                ViewModelProvider(store, factory).get(viewModelClass.java).also {
                    cached = it
                }
            } else {
                viewModel
            }
        }

    override fun isInitialized(): Boolean = cached != null
}

顾名思义,ViewModelProvider是ViewModel的提供者,它封装了ViewModel对象的获取逻辑,在代码中我们可以看到ViewModel被创建出来后,是被保存在ViewModelStore中的。ViewModel的获取逻辑见如下源码及源码中的注释:

kotlin 复制代码
ViewModelProvider(store, factory).get(viewModelClass.java).also { cached = it }
less 复制代码
public class ViewModelProvider {

    ...
    @NonNull
    @MainThread
    public <T extends ViewModel> T get(@NonNull Class<T> modelClass) {
        String canonicalName = modelClass.getCanonicalName();
        if (canonicalName == null) {
            throw new IllegalArgumentException("Local and anonymous classes can not be ViewModels");
        }
        return get(DEFAULT_KEY + ":" + canonicalName, modelClass);
    }
    
    @MainThread
    public <T extends ViewModel> T get(@NonNull String key, @NonNull Class<T> modelClass) {
        ViewModel viewModel = mViewModelStore.get(key);

        // **1. 当前mViewModelStore中对应的Key值存放的是当前要创建的ViewModel类型,则返回。**
        if (modelClass.isInstance(viewModel)) {
            // 1.1 判断是否是OnRequeryFactory类型的工厂,若是,则回调onRequery函数
            if (mFactory instanceof OnRequeryFactory) {
                ((OnRequeryFactory) mFactory).onRequery(viewModel);
            }
            return (T) viewModel;
        } else {
            //noinspection StatementWithEmptyBody
            if (viewModel != null) {
                // TODO: log a warning.
            }
        }
        // **2. 若在mViewModelStore中没有ViewModel的缓存,则需要创建一个新的ViewModel出去**
        if (mFactory instanceof KeyedFactory) {
            // 2.1 用带Key的参数创建ViewModel
            viewModel = ((KeyedFactory) mFactory).create(key, modelClass);
        } else {
            // 2.2 用普通的工厂创建的借口创建
            viewModel = mFactory.create(modelClass);
        }
        
        // **3. 将ViewModel存回mViewModelStore中**
        mViewModelStore.put(key, viewModel);
        return (T) viewModel;
    }
}

在上述源码中,我们可以看到源码对两个特殊的工厂做了处理:OnRequeryFactoryKeyedFactory

  • OnRequeryFactory:是一个包含onRequery的接口,在ViewModel对象被从缓存获取后回调的接口。这个回调的attachHandleIfNeeded函数与页面的数据恢复和保存相关,后面再做分析。
scala 复制代码
public final class SavedStateViewModelFactory extends ViewModelProvider.KeyedFactory {
    ...
    
    @Override
    void onRequery(@NonNull ViewModel viewModel) {
        attachHandleIfNeeded(viewModel, mSavedStateRegistry, mLifecycle);
    }
}
  • KeyedFactory:创建ViewModel会通过这个Key值得到用于恢复界面的Bundle,以及通过它来创建SavedStateHandle,用在配合SavedStateHandle的场景。
less 复制代码
@Override
public final <T extends ViewModel> T create(@NonNull String key, @NonNull Class<T> modelClass) {
    SavedStateHandleController controller = SavedStateHandleController.create(
            mSavedStateRegistry, mLifecycle, key, mDefaultArgs);
    T viewmodel = create(key, modelClass, controller.getHandle());
    viewmodel.setTagIfAbsent(TAG_SAVED_STATE_HANDLE_CONTROLLER, controller);
    return viewmodel;
}
ini 复制代码
static SavedStateHandleController create(SavedStateRegistry registry, Lifecycle lifecycle,
        String key, Bundle defaultArgs) {
    Bundle restoredState = registry.consumeRestoredStateForKey(key);
    SavedStateHandle handle = SavedStateHandle.createHandle(restoredState, defaultArgs);
    SavedStateHandleController controller = new SavedStateHandleController(key, handle);
    controller.attachToLifecycle(registry, lifecycle);
    tryToAddRecreator(registry, lifecycle);
    return controller;
}

4.2 ViewModel的销毁

Activity中的判断:通过isChangingConfigurations来判断当前变更是否为配置引起的销毁,若非配置变更引起的销毁,则回调ViewModelStore的clear,进而回调到viewModel的onClear函数进行清理。

less 复制代码
public class ComponentActivity extends androidx.core.app.ComponentActivity implements
        ContextAware,
        LifecycleOwner,
        ViewModelStoreOwner,
        HasDefaultViewModelProviderFactory,
        SavedStateRegistryOwner,
        OnBackPressedDispatcherOwner,
        ActivityResultRegistryOwner,
        ActivityResultCaller {

    public ComponentActivity() {
        ...
        getLifecycle().addObserver(new LifecycleEventObserver() {
            @Override
            public void onStateChanged(@NonNull LifecycleOwner source,
                @NonNull Lifecycle.Event event) {
                if (event == Lifecycle.Event.ON_DESTROY) {
                    // 1. 当Activity发生销毁回调时,判断是否由于配置变更引起
                    mContextAwareHelper.clearAvailableContext();
                    // And clear the ViewModelStore
                    if (!isChangingConfigurations()) {
                        getViewModelStore().clear();
                    }
                }
            }
        });
       ... 
    }
    
    ...
}

在Fragment的销毁基本同理,不同的是Fragment的ViewModel的销毁在FragmentStateManager中处理

4.4 ViewModel独立于Activity生命周期存活的原理

4.4.1 Activity销毁时系统如何保存ViewModel实例

当Activity由于配置变更而导致销毁时,Android系统会使用ActivityRelaunchItem来执行重启行为

进而调用到ActivityThread中的函数

graph TD ActivityRelaunchItem#execute --> ActivityThread#handleRelaunchActivity --> ActivityThread#handleRelaunchActivityInner --> ActivityThread#handleDestroyActivity --> ActivityThread#performDestroyActivity --> Activity#retainNonConfigurationInstances

在上述代码中我们可以看到在Activity即将被销毁时,会通过Activity的retainNonConfigurationInstances()NonConfigurationInstances对象保存到ActivityClientRecord中,而NonConfigurationInstances的即ViewModelStore的持有者。所以当前Activity的ViewModel也被间接地保存了下来。

dart 复制代码
static final class NonConfigurationInstances {
    Object custom;
    // 在配置变更后Activity被重新创建后ViewModel其实是存在这里的
    // 所以并没有跟随Activity实例的销毁而销毁。
    ViewModelStore viewModelStore;
}

4.4.2 配置变更后的新的Activity实例如何拿到ViewModel?

4.4.2.1 ViewModelStore的获取与创建

从上个部分,我们可以知道,ViewModel的创建出来后是被保存在ViewModelStore中的。而当Activity在尝试获取ViewModel时,也是通过ComponentActivity中的mViewModelStore尝试获取ViewModel,而ComponentActivity的mViewModelStore的初始化则是ensureViewModelStore()函数来完成,其中NonConfigurationInstances即是缓存了上个Activity中的ViewModelStore的实际对象。

typescript 复制代码
public class ComponentActivity {
    
    ...
    
    @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.");
        }
        // 在这里确保mViewModelStore被赋值
        ensureViewModelStore();
        return mViewModelStore;
    }

    @SuppressWarnings("WeakerAccess") /* synthetic access */
    void ensureViewModelStore() {
        if (mViewModelStore == null) {
            // 从getLastNonConfigurationInstance()尝试获取历史的非配置实例
            // 若是因为配置销毁而重建的Activity,且初始化过ViewModel,
            // 则能在这里读取到上个Activity使用的ViewModel对象
            NonConfigurationInstances nc =
                    (NonConfigurationInstances) getLastNonConfigurationInstance();
            if (nc != null) {
                // Restore the ViewModelStore from NonConfigurationInstances
                mViewModelStore = nc.viewModelStore;
            }
            // 若历史配置中未能获取到mViewModelStore实例
            // 则表示需要新建一个mViewModel实例
            if (mViewModelStore == null) {
                mViewModelStore = new ViewModelStore();
            }
        }
    }
}

4.4.2.2 NonConfigurationInstances的传递

在ActivityThread启动Activity时,会将destroy阶段保存下来的lastNonConfigurationInstances作为参数传递给新Activity

新创建的 Activity 实例则通过NonConfigurationInstances来间接获得ViewModelStore

五、后记

仓促完成的文稿,源码部分后续再整理下,完善一些流程图和类图可能会更清晰些,等有空再来补充,先记录发布。

相关推荐
我命由我1234516 天前
Android 解绑服务问题:java.lang.IllegalArgumentException: Service not registered
android·java·开发语言·java-ee·安卓·android jetpack·android-studio
我命由我1234518 天前
MQTT - Android MQTT 编码实战(MQTT 客户端创建、MQTT 客户端事件、MQTT 客户端连接配置、MQTT 客户端主题)
android·java·java-ee·android studio·android jetpack·android-studio·android runtime
前行的小黑炭18 天前
Android LiveData源码分析:为什么他刷新数据比Handler好,能更节省资源,解决内存泄漏的隐患;
android·kotlin·android jetpack
_一条咸鱼_18 天前
深度剖析:Java PriorityQueue 使用原理大揭秘
android·面试·android jetpack
_一条咸鱼_18 天前
揭秘 Java PriorityBlockingQueue:从源码洞悉其使用原理
android·面试·android jetpack
_一条咸鱼_18 天前
深度揭秘:Java LinkedList 源码级使用原理剖析
android·面试·android jetpack
_一条咸鱼_18 天前
深入剖析 Java LinkedBlockingQueue:源码级别的全面解读
android·面试·android jetpack
_一条咸鱼_18 天前
探秘 Java DelayQueue:源码级剖析其使用原理
android·面试·android jetpack
_一条咸鱼_18 天前
揭秘 Java ArrayDeque:从源码到原理的深度剖析
android·面试·android jetpack
_一条咸鱼_18 天前
深入剖析!Android WebView使用原理全解析:从源码底层到实战应用
android·面试·android jetpack