一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第3天,点击查看活动详情。
0x1、引言
来来来,继续学穿Jetpack,本节带来组件 → ViewModel 视图模型的解读!叫 视图数据 可能更贴切,有人也叫 视图状态,都一个意思,怎么称呼看你自己喜欢~
ViewModel 一言以蔽之
ViewModel 将 视图数据 从 视图控制器 中分离,并实现了 数据管理 的:一致性、数据共享(跨页面通信) 及 作用域可控。
① 视图数据与控制器分离
视图控制器
一般代指Activity和Fragment,它们通过在屏幕上绘制View,捕获用户事件,处理用户与互动界面相关的操作来 控制界面。
视图数据
就是你用来对控件setXxx()的数据源,它和与它相关的决策逻辑 (或者说管理) 不应该放到视图控制器中。
ViewModel所做的事,就是用 模版方法模式 进行封装,隐藏一些具体细节,提供简洁的API供我们使用。给了我们一种它们好像真的分离了的错觉,实际上还是与视图控制器紧密相连,ViewModel依旧被对应的Activity、Fragment所持有。
最直观的体现::页面配置变更,引起页面销毁重建,ViewModel中的数据不会因此而丢失。
写个简单的例子更直观,先不用ViewModel:
class TestActivity : AppCompatActivity() {
private var mCount: Int = 0
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_test)
tv_content.text = "${mCount}"
}
bt_test.setOnClickListener { tv_content.text = "${++mCount}" }
}
操作:点击按钮mCount会自增1,旋转手机触发屏幕翻转,发现数字又从0开始了:
接着用上ViewModel,定义一个类继承ViewModel,把mCount丢到里面:
class TestViewModel: ViewModel() { var mCount = 0 }
改动下原代码:
class TestActivity : AppCompatActivity() {
private lateinit var testViewModel: TestViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_test)
testViewModel = ViewModelProvider(this).get(TestViewModel::class.java)
tv_content.text = "${testViewModel.mCount}"
}
bt_test.setOnClickListener { tv_content.text = "${++testViewModel.mCount}" }
}
执行同样的操作,无论怎么翻转,数字都不会因页面重建而丢失(表现为从0开始)。
结论:把视图数据放到ViewModel里,就不会受页面配置变化销毁重建的影响。
② 一致性
对了,上面说到的配置变更,除了横竖屏切换外,还有这些:
分辨率调整、权限变更、系统字体样式变更、系统语言切换、多窗口设定、系统导航方式变更等。
在以前,为了避免这种 页面配置变更引起的页面销毁重建 导致的 视图数据丢失 问题,需要我们在 onSaveInstanceState() 和 onRestoreInstanceState() 手动编写数据保存和恢复的语句。
页面一多、要保存恢复的数据一多、加之多人协作,就很容易出现 结果不一致的问题,比如:某人在编写存数据相关的代码,漏掉了某个数据,导致拿时没拿到正确的数据。
结论:使用ViewModel,你只管把数据丢里面就行,无需关心具体如何存取,间接保证了结果一致性。
扩展一下:有了ViewModel就不需要onSaveInstanceState()了?
答:非也非也,具体用哪个还得权衡数据复杂度、访问速度及生命周期,建议 混合使用,分而治之。
怎么说?除了这两种存储恢复数据的方式外,还有一种 持久化存储,官方文档 《保存界面状态》 提供了一个维度参考表:
简要说下笔者的看法:
ViewModel
- 数据存内存中,读取更快,和Activity(或其他生命周期所有者)关联,配置更改期间保留在内存中,系统会自动将ViewModel与发生配置更改后产生的新Activity实例关联;
- 关联组件 (Activity或Fragment) 退出时,系统会自动销毁ViewModel,进程终止也会销毁;
- 适用于:配置更改后数据需要继续存在的场景,支持复杂对象。
onSaveInstanceState()
- 用Bundle存储数据以便于跨进程传递,存储上限受限于Binder(1M),请勿用于存储大量数据(如Bitmap),也不要存需要冗长序列化和反序列化操作的复杂数据结构;
- onSaveInstanceState()会将数据序列化到磁盘,如果序列化对象很复制,序列化时会占用大量内存,可能丢帧和视觉卡顿;
- 适用于:配置更改后少量数据、Activity异常关闭,进程被终止后重新打开 需要恢复的场景。
持久性存储
如果 数据的恢复非常重要、存储数据非常大、数据需要长期存储 的场景,可以考虑持久化存储,比如存数据库中。建议策略:间歇性提前自动把临时数据从内存中备份到硬盘中。当然,持久性存储不局限于本地,网络亦可。
③ 数据共享 (跨页面通信)
日常开发中,Activity和Fragment通信,Fragment与Fragment通信的场景非常常见,常见的做法下述几种:
-
- 依托于宿主Activity,定义一堆公共的访问Fragment的方法,setArguments() 或者 目标Fragment()预留回调;
-
- Fragment中调 getActivity() 获得宿主Activity,强转后获取FragmentManager实例,通过findFragmentById()或者ByTag()拿到目标Fragment实例,调用setArguments()传参;
-
- 利用Fragment Result的API,使用公共FragmentManager,充当传递数据的中心存储,setFragmentResult() 和 setFragmentResultListener();
-
- EventBus、广播等
各有利弊,而采用ViewModel,只需 指定作用域,即可轻松实现跨页面通信。写个烂大街的经典例子:点击列表Fragment,更新右侧内容Fragment,预期效果如下:
接着写代码实现一波,先是左侧列表项的布局 (item_list.xml):
<?xml version="1.0" encoding="utf-8"?>
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/tv_choose"
android:layout_width="match_parent"
android:layout_height="60dp"
android:background="@android:color/holo_green_light"
android:gravity="center" />
接着到列表适配器类 (ListAdapter.kt)
class ListAdapter(data: ArrayList<String>): RecyclerView.Adapter<ListAdapter.ViewHolder>() {
private var mData = data
private var mClickListener: ItemClickListener? = null
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
return ViewHolder(LayoutInflater.from(parent.context).inflate(R.layout.item_list, parent ,false))
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.chooseTv?.let {
it.text = mData[position]
it.setOnClickListener { mClickListener?.onItemClick(mData[position]) }
}
}
override fun getItemCount() = mData.size
fun setOnItemClickListener(listener: ItemClickListener) {
this.mClickListener = listener
}
inner class ViewHolder(itemView: View): RecyclerView.ViewHolder(itemView) {
var chooseTv: TextView? = null
init { chooseTv = itemView.findViewById(R.id.tv_choose) }
}
interface ItemClickListener { fun onItemClick(choose: String) }
}
设置文本,预留点击接口而已,再接着到列表Fragment的布局,直接一个RecyclerView (fragment_list.xml)
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="150dp"
android:layout_height="match_parent">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rv_list"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
</FrameLayout>
接着自定义ViewHolder,维护一个选中的值,配置LiveData:
class SharedViewModel: ViewModel() {
private val mSelectData = MutableLiveData<String>()
fun select(data: String) { mSelectData.value = data }
fun getSelected() = mSelectData
}
再接着把ListFragment也写出来,获取宿主Activity作用域的SharedViewModel实例,点击时更新值:
class ListFragment(data: ArrayList<String>): Fragment(R.layout.fragment_list) {
private var mData = data
// 定义SharedViewModel变量
private var mModel: SharedViewModel? = null
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
// 获得宿主Activity作用域内的SharedViewModel实例
mModel = ViewModelProvider(requireActivity()).get(SharedViewModel::class.java)
view.findViewById<RecyclerView>(R.id.rv_list).apply {
adapter = ListAdapter(mData).also {
it.setOnItemClickListener(object : ListAdapter.ItemClickListener {
override fun onItemClick(choose: String) {
// 更新ViewModel中的mSelectData
mModel?.select(choose)
}
})
}
layoutManager = LinearLayoutManager(activity)
}
}
}
在接着到右侧Fragment,xml里就一个简单的TextView:(fragment_content.xml):
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@android:color/holo_blue_light">
<TextView
android:id="@+id/tv_content"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"/>
</LinearLayout>
补齐ContentFragment:
class ContentFragment : Fragment(R.layout.fragment_content) {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
// 获取宿主Activity作用域的SharedViewModel实例,然后监听数据变化
ViewModelProvider(requireActivity()).get(SharedViewModel::class.java).getSelected()
.observe(viewLifecycleOwner) {
view.findViewById<TextView>(R.id.tv_content).text = "您翻牌了:${it}"
}
}
}
紧着是测试Activity的xml (activity_vm_test.xml):
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="horizontal">
<FrameLayout
android:id="@+id/fly_choose"
android:layout_width="wrap_content"
android:layout_height="match_parent" />
<FrameLayout
android:id="@+id/fly_content"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"/>
</LinearLayout>
最后上测试Activity:
class VMTestActivity: AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_vm_test)
val mData = arrayListOf("XX","XX","XX","XX","XXX", "XX", "XX", "XX")
supportFragmentManager.apply {
beginTransaction().replace(R.id.fly_choose, ListFragment(mData)).commit()
beginTransaction().replace(R.id.fly_content, ContentFragment()).commit()
}
}
}
别看这里代码好像很多的样子,只是为了便于读者copy运行体验而已,核心就这四点:
- ① 自定义定义ViewModel,里面放通信的数据,提供set()和get()方法;
- ② ViewModelProvider(requireActivity()).get(SharedViewModel::class.java) 获取一个 作用域为宿主Activity 的ViewModel实例;
- ③ 发消息:调用ViewModel实例的set()方法更新数据;
- ④ 收消息:调用ViewModel实例的get()方法获得一个LiveData,然后observe() 监听数据变化。
通过这样的方式,间接实现了跨页面通信,宿主Activity还蒙在鼓里 (无代码入侵),两个Fragment就偷偷完成了py交易,妙啊!!!
④ 作用域可控
上面两个Fragment轻松实现数据共享的例子,得益于ViewModel的 作用域可控,在创建 ViewModelProvider 时注入不同的 ViewModelStoreOwner 来反映作用域。
如果换成传入 Fragment实例本身,作用域就 仅限于此Fragment,当此Fragment销毁时,对应的ViewModel实例也会被销毁。
扩展一下:ViewModel是如何实现作用域可控的?
看一波源码探探原理,先跟下 ViewModelProvider 的构造方法:
初始化了一个Factory和ViewModelStore,先看第一个参数,调用了 owner.getViewModelStore()
此方法返回一个 ViewModelStore,跟下:
吼,内部 维护一个ViewModel的集合,还提供一个clear()方法,遍历回调ViewModel的clear()方法,并清空集合。
所以包含关系是:ViewModelProvider → ViewModelStore → ViewModel
接着看第二个参数,根据传入的ViewModelStoreOwner不同,使用不同的工厂实例:
- 判断条件:owner是否为
HasDefaultViewModelProviderFactory类型? - 是:强转后调用
getDefaultViewModelProviderFactory()获取一个ViewModelProvider.Factory实例; - 否:调用
NewInstanceFactory.getInstance()获取一个NewInstanceFactory实例;
构造方法中初始化了Factory和ViewModelStore实例,继续往下走,跟下 get() 方法:
在此拼接了一个 key,继续跟另一个 get() 方法:
这里执行的操作非常简单明了:
- ① 拼接key 区分不同作用域的ViewModel实例,规则:固定字符串+ViewModel完整类名;
- ② 尝试从 缓存Map 中根据key获取ViewModel实例;
- ③ 拿到:直接返回
- ④ 拿不到:往下走,通过工厂创建新ViewModel实例,保存到缓存Map中,然后返回;
到此好像还没get√到具体怎么实现作用域可控?
把关注点拉回 owner.getViewModelStore() 上,这个owner是 ViewModelStoreOwner 类型的,而我们上面传入的是 Activity实例,可以推测Actitivty绝壁实现了这个接口。定位一波:
跟下 getViewModelStore()
2333,原来是 ComponentActivity 内部自己维护了一个ViewModelStore。
啧啧,再来看看Fragment又是怎么玩的,跟下 Fragment.getViewModelStore():
跟下 FragmentManager.getViewModelStore():
Fragment 内部持有一个 FragmentManagerViewModel 实例,点进去它的 getViewModelStore() 方法:
好家伙,我悟了:Fragment → FragmentManager → FragmentManagerViewModel → mViewModelStores集合
所以:ViewModel的作用域可控 = 工厂模式 + 缓存集合(特定key规则)。
关于ViewModel的特点大概了解到这,接着过下基本用法~
0x2、ViewModel 基本用法
老规矩,官方文档双手奉上:《ViewModel 概览》,以官方文档和源码为准~
① 依赖组件
ViewModel基本配合LiveData使用,更多依赖可以选择可参见:Lifecycle
def lifecycle_version = "2.4.1"
// Java项目
implementation "androidx.lifecycle:lifecycle-viewmodel:$lifecycle_version"
// Kotlin项目
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version"
咳,如果你启用了DataBinding,可以不用另外依赖,不然会发现两个版本的ViewModel:
不信的话,自己命令行键入:gradlew :app:dependencies > dependencies.txt 扫一波就知道了
② 实现ViewModel
上面例子已经写得很明显了,就不重复了,这里提两点:
- 如果依赖了
activity-ktx模块,可以使用by viewModels()委托初始化ViewModel。
implementation 'androidx.activity:activity-ktx:1.4.0'
val model: MyViewModel by viewModels()
// Tips:如果依赖了fragment-ktx模块,可以在Fragment中商用activityViewModels() 委托初始化
// 宿主Activity的ViewModel
implementation 'androidx.fragment:fragment-ktx:1.4.1'
private val model: SharedViewModel by activityViewModels()
当所有者Activity关闭时,会调用ViewModel对象的onCleared() 方法,以便它可以清理资源。
- ViewModel绝不能引用View、Lifecycle或可能存储对Activity上下文的引用的类,内存泄露警告!!!
③ ViewModel的生命周期
ViewModel将一直存在与内存中,直到限定其时间访问的Lifecycle永久消失:对于Activity,是在Activity finish时,对于Fragment,是在Fragment移除时。基本用法就这些,协程搭配Jetpack组件使用,后面会专门讲~
0x3、面试题:ViewModel自动保存和恢复的原理
ViewModel好像就这个能问了,简单的探一探,使用 ViewModelProvider 实例化ViewModel时,传入 ViewModelStoreOwner 对象作为参数,Activity、Fragment自然实现了这个接口。跟下:ComponentActivity.getViewModelStore()
跟下 ensureViewModelStore:
点开 NonConfigurationInstances,可以看到 ViewModelStore 对象被缓存在这里:
跟下 getLastNonConfigurationInstance()
就是Activity除了提供 onSaveInstanceState() 和 onRestoreInstanceState() 外,还另外提供了两个方法 onRetainNonConfigurationInstance() 和 getLastNonConfigurationInstance() 专门处理配置更改。
跟下 onRetainNonConfigurationInstance():
就是在配置更改销毁重建过程中,先调用 onRetainNonConfigurationInstance() 保存 旧Activity中的ViewModelStore实例,重建后通过 getLastNonConfigurationInstance() 获取到之前保存的ViewModelStore实例。
知道怎么保存和恢复,接着就是确定 调用时机 ,跟下 ActivityThread.performDestroyActivity() 它是 Activity销毁 调用的核心实现:
跟下 Activity.retainNonConfigurationInstances()
知道保存数据的方法是在这里调用的,接着看获取数据的方法又是在哪调用的,跟下 Activity.handleLaunchActivity(),它是 Activity启动 的重要步骤:
还记得销毁处的代码吗:
销毁时,先存到 ActivityClientRecord.lastNonConfigurationInstances 中,然后在Activity启动时,通过 attach() 方法传递给新Activity。
到此就一清二楚了,onSaveInstanceState() 相关的也可以在 ActivityThread() 中找到踪迹,如:
就不去跟了,除了前面说的存数据的颗粒度大小不同外,两者还存在下述区别:
onSaveInstanceState()的数据最终存储到ActivityManagerService的ActivityRecord中,即 系统进程,所以APP被杀后还能恢复;onRetainNonConfigurationInstance()数据是存储到ActivityClientRecord中,即 应用自身进程中 ,所以APP被杀后无法恢复。
另外,再送一个问题:ViewModelStore的onCleard()何时会被调用?
关于原理相关的就只了解到这吧,Activity销毁重建完整逻辑可是个大块头,就不展开讲了~
0x4、加餐:ViewModel-State组件
上面说过ViewModel仅对页面变更,Activity销毁后打开重建只能用onSaveInstanceState(),写个简单例子验证下:
class VMFirstActivity: AppCompatActivity() {
companion object {
const val COUNT_TAG = "count"
}
private var mSaveInstanceCount = 0
private val mModel: VMViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_wm_first)
// 判断是否有保存的数据,有取值
savedInstanceState?.let { mSaveInstanceCount = it.getInt(COUNT_TAG) }
tv_vm_content.text = "ViewModel保存的数据:${mModel.mCount}"
tv_on_save_content.text = "onSaveInstanceState()保存的数据:${mSaveInstanceCount}"
bt_test.setOnClickListener {
tv_vm_content.text = "ViewModel保存的数据:${++mModel.mCount}"
tv_on_save_content.text = "onSaveInstanceState()保存的数据:${++mSaveInstanceCount}"
}
}
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
outState.putInt(COUNT_TAG, mSaveInstanceCount)
}
}
class VMViewModel: ViewModel() { var mCount = 0 }
运行后点击按钮自增多次,然后旋转手机引起Activity销毁重建,效果一致:
接着试下杀掉进程,home键退到后台,键入下述命令:
adb shell am kill 应用包名
点击桌面图标重新打开:
果然,ViewModel中的数据丢失了,如果数据比较重要,而且量不大,可以在onCreate()拿到savedInstanceState时也重置一下值。
savedInstanceState?.let {
mSaveInstanceCount = it.getInt(COUNT_TAG)
mModel.mCount = it.getInt(COUNT_TAG)
}
然后就可以了,当然,这样搞法有点冗余,如果能判断是配置变更引起的重建,还是异常销毁引起的重建就好了,笔者暂时没找到判定的API,只能这样了。有知道的小伙伴欢迎在评论区告知~
对于这种场景,Jetpack其实还给我们提供了一个模块:SaveState,activity库内部默认引入了这个组件,不需要另外依赖,当然你要依赖特定版本也是可以的:
implementation "androidx.savedstate:savedstate:1.0.0"
用法非常简单,ViewModel的构造方法,传入一个SavedStateHandle参数,然后用这个参数读取数据即可:
class VMViewModel(private val state: SavedStateHandle): ViewModel() {
private val countTag = "count"
fun setValue(value: Int) = state.set(countTag, value)
fun getValue() = state.get<Int>(countTag)
}
把原先onSaveInstanceState相关的代码干掉后:
class VMFirstActivity: AppCompatActivity() {
private val mModel: VMViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_wm_first)
// 如果为空,要先设置一个初始值,不然一直都是null
if(mModel.getValue() == null) mModel.setValue(0)
tv_vm_content.text = "ViewModel保存的数据:${mModel.getValue()}"
bt_test.setOnClickListener {
// 需要判空,然后在更新值
mModel.getValue()?.plus(1)?.let { mModel.setValue(it) }
tv_vm_content.text = "ViewModel保存的数据:${mModel.getValue()}"
}
}
}
清爽多了,除了可以调用 get() 方法外,还可以调用 getLiveData() 来获取LiveData类型的值。代码就更精简了:
非常简单,不过要注意下:
存数据跟Bundle一样,存对象要序列化,然后也适合保存轻量级的数据!!!
原理的话,还是利用的 onSaveInstanceState(),每个ViewModel的数据单独存在一个Bundle中,再合并成一个整体,放到outBundle,所以它同样不能存超过1M的数据。
具体源码和流程就不去刨了,感兴趣的可自行查阅下述两篇文章:
0x5、小结
本节过了下ViewModel的用法,对它的特点:视图数据与控制器、数据管理的一致性、数据共享、作用域可控进行了详解的解读,并配以简单例子帮助理解,还从源码层面讲解了ViewModel自动保存和恢复的原理,最后还提了一嘴ViewModel-State组件的使用。基本上算是面面俱到了,相信看完的读者用起ViewModel来也是水到渠成了。
有问题或者建议欢迎在评论区提出,肝文不易,如果本文有帮到你的话,可以给个三连,谢谢~
参考文献: