转眼一年又过去大半了,在2022年,初定了大多计划,搬家,换公司,很多事情都一托再拖。
这里分享一篇我在公司内部做的分享文章吧,删除了部分对公司内部代码的探讨。
公司中的项目运用到了大量的组件封装。有的是对第三方组件进行二次封装,有的是从零开始设计,如何设计一个可扩展性高,容易使用的组件呢?可以参考参考google开发者是如何设计组件的。
我们以ViewModel组件为例,试图从源码中,窥探作者是如何设计ViewModel组件的。如果是我们,我们会怎么设计ViewModel组件呢?
首先,需要明确我们的组件需要什么功能:
1、这个组件专门处理业务的数据逻辑
2、给Activity或者Fragment用的,希望同一个Activity下的fragment可以利用这个VeiwModel交互传递数据。
3、配置发生变化,导致View销毁重建的时候,保存这个ViewModel。数据不丢失。
4、ViewModel虽然不能持有Acitvity、fragment这种上下文,但是允许给一个Application。
5、进程意外杀死重建的时候,需要保存数据
整理了需求之后,作者开始了头脑风暴,一起看看作者是如何思考的。
1、用于逻辑处理的类ViewModel。先搞个抽象父类,模板模式封装公共代码,逻辑交给子类。到时候用户继承我们的抽象类就好了。
2、保存这个功能要如何处理?我们希望在Activity或者Fragment中使用。并且到时候,一个Activity,可能有多个ViewModel。作者创建了一个新的类:ViewModelStore。专门用来保存一个界面的多个ViewModel。而VeiwModel负责处理业务逻辑,ViewModel的存储交给ViewModelStore.符合单一职责原则。
3、有了ViewModelStore保存我们的ViewModel了,那配置发生变化的时候,只需要存VeiwModelStore就好了。不是配置发生变化导致的界面销毁,就需要清空ViewModelStore中的ViewModel了。什么时候该保存,什么时候该清空,那就不是ViewModelStore这个类需要关心的事情了。因为ViewModelStore的责任只是用来保存ViewModel的。谁该关心呢?作者认为,谁持有,谁关心~作者创建了一个接口类ViewModelStoreOwner用来声明职责,给个方法,返回一个ViewModelStore,实现了这个接口,就会持有ViewModelStore了。那就由他去操心保存和清空ViewModelStore的功能吧。
可见,职责声明的场景,用接口来表示。
3.1、作者让ComponentActivity实现了接口,并在构造函数中监听生命周期回调。当生命周期走向onDestroy的时候,判断当前是不是因为配置变化导致的销毁Activity,不是的话,就清空ViewModelStore中的数据。
3.2、那具体怎么保存数据呢?当因为配置发生变化的时候,Acitvity的生命周期被安排走向尽头。在ActivityThread中,调用activity的retainNonConfigurationInstances。该方法返回一个NonConfigurationInstances对象(可以认为包装类),把对象用变量lastNonConfigutationInstances保存在ActivityCliendRecord中。在该方法中
- NonConfigurationInstances retainNonConfigurationInstances() {
- //该方法是空实现,用于保存子类的对象
- Object activity = onRetainNonConfigurationInstance();
-
- //...忽略代码
-
- NonConfigurationInstances nci = new NonConfigurationInstances();
- nci.activity = activity;
-
- //...忽略代码
- return nci;
- }
所以ComponentActivity实现onRetainNonConfigurationInstance并返回ViewModelStore对象即可。
不过,作者明白,只有一些刁民,喜欢瞎搞事情。
为了防止这些刁民,作者在ComponentAcitvity中把这个onRetainNonConfigurationInstance方法写了final,防止他们重写了方法,覆盖了我们的代码。这个方法就用来给我们保存数据的.
但是做人做事都不能赶尽杀绝啊,毕竟大部分的用户都是遵纪守法的好公民,这个方法只保存我们的ViewModelStore,也太局限了。于是,作者就创建了一个空函数,用户实现这个方法,返回他们想要正常保存的数据,到时候,连带帮用户的数据一起存起来不就好了。所以到时候补一个用户实现这个方法,配置发生变化的时候,顺带帮他们一起保存了。既然要存的有2种数据,一种是用户的,另一种是ViewModelStore。那就需要创建一个包装类了,把这俩包装起来。于是ComponentActivity中,就需要创建一个静态内部类了。
这种实现父类给的方法,但是不赶紧杀绝的做法很赞,在我们的项目中,也发现了类似的写法。此处省略一万字。。。。。
当Activity重新创建的时候,再来个方法返回我们之前保存的数据就好了~
在Activtiy的attach函数中,会把之前的配置传回来。
这样子类在就可以通过这个对象获取之前我们存储的ViewModelStore了。
如此一来,保存数据和获取数据的方案都有了~
到目前为止,我们已经有一个专门处理逻辑的类ViewModel,一个专门保存ViewModel的类ViewModelStore,以及利用接口ViewModelStoreOwner去声明相关职责了。保存清空的思路也都有了。
4、下面就需要考虑如何去创建了
用户是可以自由定义ViewModel的具体的类的,只需要继承我们的ViewModel就好了,并且ViewModel的构造函数中,可能有用户自己的参数。也可能没有参数或者是有一个Application的参数。这种情况下,作者想到了利用工厂模式,为每一个ViewModel提供一个工厂类,如果有构造函数有自定义的参数,那就自己搞个工厂类去创建,没有参数或这有一个Application参数的时候,由我们提供的工厂创建就好了。
工厂类接口很简单:
作者为没有参数的构造函数和参数只有一个Applicaiton的构造函数,创建一个默认的工厂类NewInstanceFactory和AndridViewModelFactory
使用反射直接获取具体的类对象。带有Application参数的工厂类也是一样的,只是多了一个参数而已。问题不大:AndroidViewModelFactory
5、创建的方法已经有了。下面就是思考如何获取拿到ViewModel实例对象了。为什么要这么考虑呢?利用工厂模式不是已经可以获取到实例对象了?
因为我们有存储机制,当配置变化的时候,不再是通过factory获取了,而是从ViewModelStore中获取。
先整理一下获取ViewModel需要的相关逻辑:
(1)获取到对应的工厂实例对象
(2)从viewModelStore中获取ViewModel,或者利用工厂去创建一个ViewModel。
(3)创建之后,同时需要保存ViewModel。
这三部就是创建的流程需要涉及的相关逻辑
对于用户而言,他不应该关心如何保存我们的ViewMdel,什么时候从ViewModelStore中获取,什么时候创建,这些都应该是用户无感知的。这遵循迪米特原则:最少知道原则,用户在调用ViewModel组件的时候,不应该知道这个类的内部细节,以及内部调用逻辑。比如说外观者模式,或者中介者模式就符合迪米特原则。外观者,封装了组件内部实现细节,并提供统一的接口或者函数给外部。也降低了使用成本。这种最常见的就是Glide。简单的一行内码,就可以整合了内部千军万马的组件调用逻辑。
作者创建了一个新的类:ViewModelProvider。对外提供一个get函数,利用传入的具体的class,使用对应的工厂创建ViewModel。内部逻辑怎么调用,与用户无关。
这个ViewModelProvider做了什么事情呢?
1、根据不同的情况,拿到对应的工厂类。
2、从缓存中获取ViewModel,拿不到的时候,利用工厂类创建ViewModel。
3、把ViewModel保存到ViewModelStore中。
小结:创建的流程,使用了工厂模式和外观者模式,把创建的流程和业务代码隔离开,并且提供统一的对外访问函数get,降低用户使用成本。
5.1、具体看看这个ViewModelProvider是怎么做的,
ViewModelStore肯定是从外部(View层)带进来了,View层的ComponentActivtiy和Fragment都实现了ViewModelStoreOwner的接口,利用这个接口,就可以拿到了ViewModelStore了。另外还需要根据具体情况获取到默认的Factory。提供默认Factory这个职责,作者又创建一个接口类,HasDefaultViewModelProviderFactory,这个接口也只有一个方法,就是用来返回默认的Factory的。ComponentActivity去负责创建这个默认Factory在合适不过了。毕竟还要获取到application。这个接口类和Owner接口类搭配使用。
可以发现,对于每一个不同的功能职责声明,都有一个独立的接口去负责,并不会因为这些功能都和VeiwModel相关,也都用在一起,而把不同功能的接口整合在一起。就是典型的接口隔离的思想。
到这里,基本的逻辑已经完善了,创建了几个单一职责的类:ViewModel、ViewModelStore,外观者模式类:ViewModelProder、工厂模式涉及的具体的Factory。每一个类都有各自的功能。创建了声明职责的HasDefaultViewModelProviderFactory、ViewModelStoreOwner接口,遵循接口隔离原则。
继续看:
6、ViewModel只能保存因为配置发生变化导致界面重建的数据,如果因为系统资源限制等,导致回来的时候进程重新创建了,我们现有的逻辑是保存数据走onSaveInstanceState的方法。这个只能在View层处理了,这不就是解耦的不够彻底了。于是,就有一个新的类SaveStateHandle
保存这块数据,底层逻辑肯定还是需要调用OnSaveInstanceState,但是ViewModel和Activity是完全解耦的,如何交互呢?
作者想到使用第三者(一个新的类saveStateRegistry)来建立两者的沟通,这就有一些中介者味道了,比如我们非常熟悉的MVC架构,controller就是M和V的中介者。
通过第三者,在onCreate的时候,拿到数据,在onSaveInstanceState的时候,把数据存起来
mSaveStateRegistry这个类和ViewModelStore一样,就是用来做数据存储的,从onCreate中获取数据,把数据用map保存,当需要调用onSaveInstanceState的时候,从外部拿到bundle,把我们的map都存到这个bundle中。
这个mSaveStateRegistry就像是一个model,提供数据的。当然,作者并不直接像上面这样,在onCreate中直接调用mSaveStateRegistry,在调用mSaveStateRegsitry.performStore方法得时候,增加了一些逻辑判断。而逻辑处理的逻辑不应该写在Activity中,更不应该写在数据存储类里面,作者使用了mvc模式,增加一个类作为控制层SaveStateRegistryController,负责处理SaveStateRegistry存储数据时候的额外逻辑。V是Activity,M就是SaveStateRegistry。最终:
下面就是要思考如何把我们的ViewModel和这个SaveStateRegistry建立关系了。作者并没有直接让SaveStateRegistry持有ViewModel。这点和ViewModelStore不同。通过创建了一个接口类SavedStateProvider,这个接口的唯一方法就是返回一个bundle。SaveStateRegistry使用map存储这个接口。而ViewModel间接持有这个接口的实例对象。从而实现了ViewModel和SaveStateRegistry的解耦。
另外,对于ViewModel而言,也需要一个数据类,作者创建了一个新的数据类:SaveStateHandle。目的是通过该类存储我们希望进程意外杀死重建的时候的数据。同样的,SaveStateHandle是对数据的存储操作,该类就持有SaveStateProvider的接口实例对象。作者同样需要做一些额外的操作,比如把SaveStateHandle中的SaveStateProvider对象注册到SaveStateRegistry中,并监听生命周期,保证只注册一次。这些额外的逻辑操作同理交给SaveStatehandleController处理。
所以,ViewModel会持有SaveStateHandle,SaveStateHandle是数据类型,是Model,由控制类SaveStateHandleController提供,逻辑也有它负责。同样是MVC模式。
题外话:ViewModel和Activity的关系很淡泊,ViewModel持有SaveStateHandle对象,SaveStateHandle持有一个SaveStateProvider对象,这个对象被注册到SaveStateRegistry中,SaveStateRegistry不会直接和View层交互,而是通过SaveStateRegistryController。旁系三代以外就可以结婚了。这俩隔离这么远,可以说是妥妥的没啥联系了。
SaveStateHandle怎么和ViewModel建立联系呢?
现在只要在创建ViewModel的时候,把这个SaveStateHandler传给ViewModel即可。
如果是我的话,我会在ViewModel抽象父类中,创建这个成员变量。但作者的思路不同,他对构造函数进行解析,如果构造函数的参数只有SaveStateHandle这个类,或者是SaveStateHandle和Application的类型,在创建的时候,就把这个SaveStateHandle传输传入。这样用户就可以创建一个次构,来获取这个SaveStateHandle了。不过这种做法,有一个问题,如果是用户自定义的Factory,那要就获取不到了。所以在SaveStateHandle的注释中有提到:
如果想拿到这货,就需要使用SaveStateViewModelFactory了,不能用自己的。如果用全局变量,感觉就没有构造函数的限制了。
大致依赖关系
1、在SaveStateHandle中,这里key是直接写死的:
获取的时候
2、比如ViewModelStore:使用的都是hashmap,而不是ArrayMap。
思考:是否在某些场景下,HashMap的性能会优于ArrayMap?因为如果HashMap一无是处,为什么没有被打上过期的标识?
3、外部访问权限控制
这段代码源自NewInstanceFactory中,可以发现,专门创建了一个instance用来对外返回sInstance这个对象。而sInstance被声明称内部私有。pubclic修饰不可修改,很早之前google的一些VeiwModel中也遇到过。感觉意义不是很大。
最后
ViewModel组件设计思路差不多就到这了。本次技术分享主要是浅谈一下ViewModel组件的作者可能是如何思考并设计这个组件的。ViewModel的源码细节实际更多一些,可以发现该组件的设计也运用了大量的设计模式和设计思想,当我们自己设计组件的时候,有相似场景的时候。可以参考借鉴一下~。