1. 开发架构 是什么?
我们先来理解开发架构的本质是什么,维基百科对软件架构的描述如下:
软件架构是一个系统的草图。软件架构描述的对象是直接构成系统的抽象组件。各个组件之间的连接则明确和相对细致地描述组件之间的通讯。
- 针对的是一个完整系统,此系统可以实现某种功能。
- 系统包含多个模块,模块间有一些关系和连接。
- 架构是实现此系统的实施描述:模块责任、模块间的连接。
为啥要做开发架构设计呢?
- 模块化责任具体化,使得每个模块专注自己内部。
- 模块间的关联简单化,减少耦合。
- 易于使用、维护性好
- 提高开发效率
架构模式最终都是 服务于开发者。如果代码职责和逻辑混乱,维护成本就会相应地上升。
宏观上来说,开发架构是一种思想,每个领域都有一些成熟的架构模式,选择适合自己项目即可。
2. Android开发中的架构
具体到Android开发中,开发架构就是描述 视图层、逻辑层、数据层 三者之间的关系和实施:
- 视图层:用户界面,即界面的展示、以及交互事件的响应。
- 逻辑层:为了实现系统功能而进行的必要逻辑。
- 数据层:数据的获取和存储,含本地、server。
正常的开发流程中,开始写代码之前 都会有架构设计这一过程。这就需要你选择使用何种架构模式了。
我的Android开发之路完整地经过了 MVC、MVP、MVVM,相信很多开发者和我一样都是这样一个过程,先来回顾下三者。
2.1MVC
MVC,Model-View-Controller,职责分类如下:
- Model,模型层,即数据模型,用于获取和存储数据。
- View,视图层,即xml布局
- Controller,控制层,负责业务逻辑。
View层 接收到用户操作事件,通知到 Controller 进行对应的逻辑处理,然后通知 Model去获取/更新数据,Model 再把新的数据 通知到 View 更新界面。这就是一个完整 MVC 的数据流向。
但在Android中,因为xml布局能力很弱,View的很多操作是在Activity/Fragment中的,而业务逻辑同样也是写在Activity/Fragment中,因此Android 中的 MVC 更像下面的这种结构:
所以,MVC 的问题点 如下:
- Activity/Fragment 责任不明,同时负责View、Controller,就会导致其中代码量大,不满足单一职责。
- Model耦合View,View 的修改会导致 Controller 和 Model 都进行改动,不满足最少知识原则。
2.2MVP
MVP,Model-View-Presenter,职责分类如下:
- Model,模型层,即数据模型,用于获取和存储数据。
- View,视图层,即Activity/Fragment
- Presenter,控制层,负责业务逻辑。
MVP解决了MVC的问题:
- View责任明确,逻辑不再写在Activity中,而是在Presenter中;
- Model不再持有View。
View层 接收到用户操作事件,通知到Presenter,Presenter进行逻辑处理,然后通知Model更新数据,Model 把更新的数据给到Presenter,Presenter再通知到 View 更新界面。
MVP的实现思路:
- UI逻辑抽象成IView接口,由具体的Activity实现类来完成。且调用Presenter进行逻辑操作。
- 业务逻辑抽象成IPresenter接口,由具体的Presenter实现类来完成。逻辑操作完成后调用IView接口方法刷新UI。
MVP 本质是面向接口编程,实现了依赖倒置原则。MVP解决了View层责任不明的问题,但并没有解决代码耦合的问题,View和Presenter之间相互持有。
所以 MVP 有问题点 如下:
- 会引入大量的IView、IPresenter接口,增加实现的复杂度。
- View和Presenter相互持有,形成耦合。
2.3 MVVM
MVVM,Model-View-ViewModel,职责分类如下:
- Model,模型层,即数据模型,用于获取和存储数据。
- View,视图,即Activity/Fragment
- ViewModel,视图模型,负责业务逻辑。
注意,MVVM这里的ViewModel就是一个名称,可以理解为MVP中的Presenter。不等同于 ViewModel组件 ,Jetpack ViewModel组件是 对 MVVM的ViewModel 的具体实施方案。
MVVM 的本质是 数据驱动,把解耦做的更彻底,viewModel不持有view 。
View 产生事件,使用 ViewModel进行逻辑处理后,通知Model更新数据,Model把更新的数据给ViewModel,ViewModel自动通知View更新界面,而不是主动调用View的方法。
3.MVVM 的实现 - Jetpack MVVM
前面提到,架构模式选择适合自己项目的即可。话虽如此,但Google官方推荐的架构模式 是适合大多数情况,是非常值得我们学习和实践的。
好了,下面我们就来详细介绍 Jetpack MVVM 架构。
3.1 Jetpack MVVM 理解
Jetpack MVVM 是 MVVM 模式在 Android 开发中的一个具体实现,是 Android中 Google 官方提供并推荐的 MVVM实现方式。
不仅通过数据驱动完成彻底解耦,还兼顾了 Android 页面开发中其他不可预期的错误,例如Lifecycle 能在妥善处理 页面生命周期 避免view空指针问题,ViewModel使得UI发生重建时 无需重新向后台请求数据,节省了开销,让视图重建时更快展示数据。
首先,请查看下图,该图显示了所有模块应如何彼此交互:
!https://pic1.zhimg.com/80/v2-1ac2c700ee10cb05f61125c99e395754_720w.webp
各模块对应MVVM架构:
- View层:Activity/Fragment
- ViewModel层:Jetpack ViewModel + Jetpack LivaData
- Model层:Repository仓库,包含 本地持久性数据 和 服务端数据
View层 包含了我们平时写的Activity/Fragment/布局文件等与界面相关的东西。
ViewModel层 用于持有和UI元素相关的数据,以保证这些数据在屏幕旋转时不会丢失,并且还要提供接口给View层调用以及和仓库层进行通信。
仓库层 要做的主要工作是判断调用方请求的数据应该是从本地数据源中获取还是从网络数据源中获取,并将获取到的数据返回给调用方。本地数据源可以使用数据库、SharedPreferences等持久化技术来实现,而网络数据源则通常使用Retrofit访问服务器提供的Webservice接口来实现。
另外,图中所有的箭头都是单向的,例如View层指向了ViewModel层,表示View层会持有ViewModel层的引用,但是反过来ViewModel层却不能持有View层的引用。除此之外,引用也不能跨层持有,比如View层不能持有仓库层的引用,谨记每一层的组件都只能与它相邻层的组件进行交互。
这种设计打造了一致且愉快的用户体验。无论用户上次使用应用是在几分钟前还是几天之前,现在回到应用时都会立即看到应用在本地保留的数据。如果此数据已过期,则应用的Repository将开始在后台更新数据。
4**.MVVM 实战**
实战项目基于Jetpack套件实现了MVVM架构:
- View层:ViewBinding
- ViewModel层:ViewModel + LivaData
- Model层:Flow
4**.1 View层**
View层为什么使用ViewBinding而不是DataBinding呢?
- ViewBinding比Databinding使用简便、效率更高;
- ViewBinding对XML无侵入,使用场景更灵活;
- ViewBinding的学习使用成本更低,没有数据绑定的复杂语法;
- Google官方推荐;
在AndroidStudio配置中启用ViewBinding后,编译器在编译过程中会将xml布局转成对应的binding类:
在binding类中会包含XML布局中的对应控件和根布局引用:
那么,这些控件又是如何完成初始化,实现通过binding.ivLogo.setOnLongClickListener
这样方便调用而不报错的呢?
这就是ViewBinding的核心方法*public static* MainActivityAboutBinding bind(@NonNull View rootView)
所要实现的功能了,这个函数并不是接口继承而来,而是针对每一个XML布局动态生成的:
可以看到在bind函数中进行了rootView.findViewById
操作将对应的控件进行了初始化绑定,并且能看到rootView的初始化也是通过inflater.inflate(R.layout.main_activity_about, parent, *false*)
的方式实现,简而言之,binding类帮我们完成了模板代码的编写!
那么项目中是如何使用的呢?以Activity为例简单说明一下,fragment、dialog和adapter等类中的使用方式相差不大。
利用kotlin的语法特性声明了binding及其获取方式:
在onCreate
函数中对binding进行初始化赋值,具体类型由子类控制,这里以AboutActivity
为例:
在对应的销毁回调中将binding置为null,即binding的可用周期限制在onCreate
到onDestroy
之间。避免不必要的内存泄露(如果非法调用会空指针异常):
至此,View层的搭建就基本完成了!
4**.2** ViewModel层
4.2.1 ViewModel创建、绑定和销毁
ViewModel层采用了Jetpack中的ViewModel组件和LiveData组件构建,其中在ViewModel中创建并操作LiveData。
项目中ViewModel的创建和binding类似,以Activity为例:
具体的ViewModel类型由子类决定:
而ViewModel的销毁则与binding不同,是由系统控制,在系统进程资源被回收时一同销毁回收。
而ViewModel的观察者则是通过感知声明周期进行自动清除:
最终回调到了ViewModel的clear方法:
4.2.2 ViewModel的数据缓存能力
由4.2.1开头可知,ViewModel是通过_viewModel = ViewModelProvider(*this*)[viewModelClass]
获取到的,其中的核心便是ViewModelProvider
的get(key: String, modelClass: Class<T>)
方法了:
通过ViewModelStore
进行ViewModel的创建保存和获取,那这个ViewModelStore
又是什么呢?
看源码可知其实就是一个map包装类:
那么ViewModelProvider
又是如何拿到ViewModelStore
的呢?查看构造函数可知:
ViewModelStore
是通过ViewModelStoreOwner
获取到的,这是一个接口,其子类包括了Activity和Fragment:
以Activity为例,查看是如何获取对应的ViewModelStore
的:
可以看到ViewModelStore
是创建并保存在NonConfigurationInstances
对象中的,而这个类是Activity的静态内部类。
那么ViewModel的生命周期是如何实现贯穿Activity生命周期的呢?即使是Activity的销毁和重建场景也能正确的保存和恢复数据?
跟踪源码可知,NonConfigurationInstances
类对象的保存及获取由系统自行调用对应函数进行实现。调用时机为系统配置修改后等行为导致的Activity被销毁之前,并将保存的值传递到新的重启Activity对象中。整个过程具体如何实现涉及到AMS等FrameWork源代码,就不详细展开跟踪!其实AMS里最终是用的ThreadLocal
进行保存并恢复的。
子类具体实现:
但是在activity-1.2.3以上版本的库中,这种方式已经被警告废弃掉了!!
只不过这个废弃标识是为了让用户不要使用重写该函数的方式来保存一些自定义的*non-config state
* 数据 ,因为处理不当会导致ViewModelStore的保存和恢复出现异常!!用户如果需要保存一些non-config state
数据,官方建议使用ViewModel!因为通过上面的分析知道ViewModel已经通过这种方式实现了*non-config state
* 数据保存恢复功能,直接使用即可!
在这里插入图片描述
那么新创建的Activity又是如何获取到之前的NonConfigurationInstance
对象的呢?
答案是AMS!通过AMS启动新的Activity实例时attach传递过来!
总之,ViewModel的缓存复用系统已经帮我们搞定了!接着继续看项目实战~
4.2.3 LiveData的绑定
从上文可知,View层已经绑定了ViewModel,但是ViewModel又是如何通过修改LiveData达到更改View的显示呢?
答案便是观察者模式。其核心便是LiveData,LiveData会保存所有的观察者,并在数据修改且时机合适的情况下通知观察者数据变更。
符合条件的观察者会执行onChanged(T t)
函数,通知观察对象进行更新数据。这个观察者对象又是怎么来的?
先看项目中是如何使用的:
可以看到项目中使用了扩展函数
LifecycleOwner.observeLiveData( liveData: LiveData<T>, crossinline action: (t: T) -> Unit)
对LiveData的使用进行了封装,使用lamda表达式的方式传入了所需要的观察者对象,其中observe
的函数原型如下图所示:
而Observer<? super T> observer
本身是一个接口,提供了一个onChanged(T t)
函数通知观察者更新:
接着看LiveData是如何添加观察者的,可以分成以下几步:
- 生命周期组件销毁则结束观察者添加;
- 包装生命周期组件和观察者,让观察者能感知对应的生命周期,在销毁的时候移除观察者,在初次绑定并且生命周期组件处于“激活(能够Active观察者)”状态的时候触发观察者更新通知;
- 确保观察者唯一且一个观察者只能观察一个生命周期组件后添加到LiveData保存的观察者集合及生命周期组件的观察者集合中;
如果是第一次激活状态,则会触发LiveData的结果值分发流程!至此,我们的LiveData数据更新流程已经完成了一半,实现了绑定LiveData到对应的Activity或者Fragment,并在Activity或者Fragment的生命周期大于*State.STARTED
* 的时候进行初始化通知更新!当页面销毁时也能自动的移除掉对应的观察者对象,避免不必要的内存泄露!
4.2.4 LiveData实时更新修改
通过上文我们知道LiveData绑定后会在生命周期组件初次“激活”时执行一次观察更新流程,那么项目实时运行时动态更新LiveData值能够持续触发对应的观察更新流程吗?
答案是肯定的。
先看项目中对LiveData的值是如何修改的:
_logoutSuccess.*value* = true
是Kotlin对set函数的简化写法,函数原型如下:
更新了保存的值mData
后,依旧是进入了4.2.3最后的dispatchingValue(@Nullable ObserverWrapper initiator)
方法中,并且这次的参数是null
,意味着将对所有观察当前LiveData的Active状态的观察者进行更新通知。
需要注意的是setValue(T value)
只能在主线程中调用,如果要在子线程更新LiveData,可以使用LiveData的postValue(T value)
函数:
可以看到postValue(T value)
其实是post了一个Runnable任务到主线程执行,最终还是调用的setValue(T value)
函数来触发观察者数据更新。
4**.3** Model层
上面两步已经完成了ViewModel修改数据后通知View层变化了,那么ViewModel如何获取数据并修改LiveData呢?
项目中使用的ViewModel的扩展函数来实现ViewModel层对Model层数据的获取。
关于这一步相关的内容在分享一中有过详细分析(包括了Flow的原理解析),感兴趣可以看看分享一,这里不过多赘述。