背景
随着整个快看业务的复杂度越来越高,代码量的提升,导致代码复杂度越来越高,一个Activity、Fragment的代码动辄几千行,虽然有模块划分和接口隔离,但也很难完全避免代码量的增加,导致了整个页面的代码仍然很臃肿。正是在这样的背景下,推动了我们开始探索符合快看业务的页面开发框架的历程。
演变过程
MVP
相信大家对于MVP耳熟能详了,android官方也是推荐我们使用MVP作为我们的开发架构。官方提供了的sample地址为:github.com/android/arc… M(Model): 主要负责数据相关的操作 V(View):主要负责处理UI, 主要以Fragment、Activity的形式出现 P(Presenter):作为View和Model的中间件,负责逻辑处理,交互桥梁
优点:
- 通过Presenter将View层(Activity、Fragment)中的逻辑都抽离出来,在View层只处理UI相关的操作,将业务逻辑放在Presenter中,达到UI与逻辑解耦的效果,管理方便,利于代码复用
缺点:
- 通过接口交互,代码量变大,一旦业务逻辑足够复杂,会出现接口爆炸以及Presenter代码量过大。
MVVM
优点:
- 当Model发生改变时,View-Model会自动更新,View也会自动变化
缺点:
- 因为数据的双向绑定,导致每一个View都绑定了一个Model。不同的模块model基本都不一样,不利于View的复用
不管是MVP还是MVVM,都或多或少有些不足,没有办法完全满足我们复杂业务的现状,我们期望能够有一个通用的框架,能够具备以下的能力:
- 模块解耦,分层明确
- 开发使用便捷
- 性能好
- 扩展性、可复用性强
- 组件自动感知生命周期
- 便捷的组件间通信能力
但是目前市面上没有适合的架构,因此基于现有的复杂业务,进行梳理,最终沉淀出一套满足快看复杂业务的通用框架Arch。
Arch框架
整体的页面分层结构如图:
整体架构图如图所示:
核心设计
BaseArchView
是一个View层的抽象,表示一个具有生命周期的视图,比如我们的View、Activity和Fragment,作为我们一个业务模块的入口。
BaseDataProvider
对于整个页面的业务开发中,总是存在一些共有的数据传递到各个业务模块中,比如快看的漫画详情页中,各个业务模块比如付费、广告、社区等,Adapter中,ViewHolder中,埋点上报处理类中都需要对应的漫画ID,随处可见的传递公有参数的地方。 为了解决这一个问题,在BaseArchView的生命周期内,BaseDataProvider会作为这个BaseArchView的数据的提供者,主要目的是为了提供在BaseArchView的生命周期内的数据给任意子模块访问。所有的子模块都会默认直接持有DataProvider的引用,可以直接访问DataProvodier获取数据,以及设置数据到DataProvider中。
BaseMainController
在一个BaseArchView的生命周期内,BaseMainController作为BaseModule的管理者和BaseModule相互访问的枢纽。 所有Module的绑定都通过注解来实现的,也就是说当一个Module复杂到一定的程度,或者这个Module内部需要再聚合其他业务,那么这个Module就可以升级,成为一个SubMainController,内部再去聚合其他Module。
BaseModule
在一个BaseArchView的生命周期内, Module作为一个MainController管理的子模块,对于每一个子业务,我们都可以将其统一为一个Module,比如说在快看首页每日的fragment中,我们可以将其分为广告Module、动态推荐Module、运营位Module等等
BaseMvpPresent
在一个BaseArchView的生命周期内,如果一个Module使用MVP模式,那么可以在BaseModule中绑定BaseMvpPresent,来处理相关这个Module下的划分出来的一部分业务逻辑。
BaseMvpView
在一个BaseArchView的生命周期内,如果一个Module使用MVP模式,那么可以在Present中绑定BaseMvpView,来处理相关这个Present下的相关UI逻辑
BaseDataRepository
主要提供数据加载逻辑,网络请求、数据库请求、文件操作等
动态扩展
Module作为一个业务模块的最小单元,有比较强的可操作性。
- 在业务简单的时候,我们可以在Module内直接执行业务操作
- 在业务复杂时,我们可以在内部通过BindPresent注解来使用MVP模式,当然,也可以使用MVVM模式
- 在业务达到了十分复杂的程度,这个Module就可以升级为SubMainController,把内部复杂的业务拆分为Module
这样,在这个页面框架Arch下,不论业务有多复杂,业务层级有多深,整个框架都能像一棵树一样,通过Controller作为纽带,将业务模块一层一层的进行拆解和绑定。
场景应用
那么在快看的页面开发框架中,我们怎么去开发一个页面呢?以快看App的热门中的每日页签为例子。
可以先看看在使用新架构进行开发后的目录结构:
示例代码:
在Fragment中绑定Controller和DataProvider
class RecommendByDayFragment : BaseArchFragment() {
@BindController
lateinit var recommendByDayController: RecommendByDayController
@BindDataProvider
lateinit var recommendByDayDataProvider: RecommendByDayDataProvider
}
在Controller中定义业务模块
class RecommendByDayController: BaseMainController<Unit>() {
@BindModule(moduleClass = CacheRecommendModule::class)
lateinit var cacheRecommendModule: ICacheRecommendComic
@BindModule(moduleClass = AdModule::class)
lateinit var adModule: IHomeRecommendAd
@BindModule(moduleClass = CommonModule::class)
lateinit var commonModule: IRecommendCommon
@BindModule(moduleClass = MainModule::class)
lateinit var mainModule: IRecommendModuleMain
}
注意 上面BindModule注解,参数必须是实际的Class类型,被注解标的成员变量类型必须是接口。
大家可以提前思考下面两个问题:
- APT注解处理器正常是没办法获取到注解参数为class类型的参数的,那上边的MainModule::class等类型是如何获取出来的呢?
- 为什么被注解标的成员变量类型需要强制定义成接口呢?
这两个问题会在讲解APT和数据通信的时候给出答案。
ok,接着再来看看其中一个子模块的实现:
class CacheRecommendModule : BaseModule<RecommendByDayController, RecommendByDayDataProvider>(), ICacheRecommendComic {
@BindRepository(repository = CacheTodayRecommendRepository::class)
lateinit var cacheRepo: IRecommendCacheRepo
override fun onInit(view: View) {
super.onInit(view)
}
override fun onStartCall() {
super.onStartCall()
loadCacheData()
}
}
当Fragment创建后,会调用到CacheRecommendModule的onStartCall方法内,模块的逻辑就可以开始执行了。
所有的参数都是基于注解来实现的,通过注解,我们可以将实现屏蔽到注解处理器中,使用会非常便捷,仅需要一个注解,就能绑定一个模块,自动注入所有必须的参数。
对于注解,java中的注解类型根据作用域区分来说,有三种类型:
- 元注解: 注解的信息仅保留在源码中,经过编译,Class文件中的注解信息将会被丢失。
- 编译期注解: 注解在Class文件中保留,但是运行时不会加载到虚拟机中,在Android中,可以通过编译期注解来做一些自动化代码的生成。
- 运行时注解:注解在虚拟机中也保留,可以通过反射获取对应的注解信息。
要实现整套框架的自动绑定功能方式有很多种。整个框架的自动绑定也经过多版的改动,从最开始的全量使用反射构建到APT+反射,再从APT+反射到APT+ASM插桩,到最后,APT+反射和APT+ASM共存,下面介绍下通过不同方式实现自动绑定的优缺点。
运行时注解
所有的注解使用运行时注解, 在Activity/Fragment的onCreate生命周期中进行parse操作
open class BaseArchFragment : BaseFragment(), BaseArchView {
@CallSuper
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
ParseBindAnnotation.parse(this, BaseArchFragment::class.java)
delegate.onCreated()
}
}
class ParseBindAnnotation {
companion object {
fun parse(baseArchView: BaseArchView, endType: Class<*>) {
val time = System.currentTimeMillis()
try {
createEventBusInstance(baseArchView)
ReflectUtil.traversalClass(baseArchView, endType.superclass) {
parseDataProvider(baseArchView, it)
parseMainController(baseArchView, it)
}
ReflectRelationHelper.clearReflectData(baseArchView)
} catch (e: Exception) {
ErrorReporter.get().reportAndThrowIfDebug(e)
}
Logger.loggerReflectTime(baseArchView.javaClass.name, System.currentTimeMillis() - time)
}
}
整个运行时注解的解析分为下面几个流程
- 获取当前类所有Field注解,如果识别到我们提供的BaseDataProvider、BaseController、BaseModule等注解,就通过反射创建出实例,注入相关必要参数。
- 反射创建出的实例,需要递归调用内部成员,获取是否存在BaseModule等注解,再次通过反射创建出实例,注入参数。 比如当前反射创建的MainController实例
class BindMainControllerProcessor {
companion object {
fun create(field: Field, owner: Any, archView: BaseArchView) {
val fieldInstance = field.type.newInstance()
ReflectUtil.innerSetField(field, owner, fieldInstance)
val controller: BaseMainController<*> = fieldInstance as BaseMainController<*>
ReflectRelationHelper.registerController(archView, controller)
val dataItem = ReflectRelationHelper.findFromController(controller)
innerBind(controller, dataItem, owner)
}
private fun innerBind(controller: BaseMainController<*>, dataItem: ArchReflectDataItem?, owner: Any) {
dataItem?:return
controller.javaClass.declaredFields.forEach {
when {
it.isAnnotationPresent(BindModule::class.java) -> {
BindModuleProcessor.create(it, controller, dataItem)
}
it.isAnnotationPresent(BindController::class.java) -> {
create(it, controller, dataItem.archViewRef)
}
}
}
}
}
- 因为涉及到继承关系问题,所以需要递归调用反射创建出实例,再次执行第二步,解析运行时注解。
全量使用反射的这种方式的存在着比较大的性能问题,基本上一个存在30+注解的页面解析和绑定所有成员就需要30ms以上。这个性能损耗也基本也就意味着这种方式无法上线。
APT+反射
由于全量使用反射的方式进行绑定对性能影响比较大,需要进行优化,参考ButterKnife、EventBus的实现,将运行时注解修改为编译期注解,将需要执行绑定的代码自动生成。最后在parse方法内进行反射调用。 可以看看上边的RecommendByDayFragment代码的自动生成:
class RecommendByDayFragment_arch_binding(
recommendbydayfragment: RecommendByDayFragment
) {
init {
ReflectRelationHelper.registerEventBus(recommendbydayfragment,
recommendbydayfragment.eventProcessor)
val recommendByDayController = RecommendByDayController()
ReflectRelationHelper.registerController(recommendbydayfragment, recommendByDayController)
recommendbydayfragment.recommendByDayController=recommendByDayController
ReflectRelationHelper.registerEventBus(recommendbydayfragment,
recommendbydayfragment.eventProcessor)
val recommendByDayDataProvider = RecommendByDayDataProvider()
recommendByDayDataProvider.eventProcessor = recommendbydayfragment.eventProcessor
ReflectRelationHelper.registerDataProvider(recommendbydayfragment, recommendByDayDataProvider)
recommendByDayDataProvider.ownerView = recommendbydayfragment
recommendbydayfragment.registerArchLifeCycle(recommendByDayDataProvider)
recommendbydayfragment.recommendByDayDataProvider = recommendByDayDataProvider
recommendByDayDataProvider.parse()
recommendByDayController.parse()
}
}
class RecommendByDayController_arch_binding(
recommendbydaycontroller: RecommendByDayController
) {
init {
val archItem = ReflectRelationHelper.findFromController(recommendbydaycontroller)!!
val cacheRecommendModule = CacheRecommendModule()
recommendbydaycontroller.cacheRecommendModule = cacheRecommendModule
cacheRecommendModule.ownerController = recommendbydaycontroller
cacheRecommendModule.updateDataProvider(archItem.dataProviderRef)
cacheRecommendModule.ownerView = archItem.archViewRef
cacheRecommendModule.eventProcessor = archItem.eventBusRef
archItem.archViewRef.registerArchLifeCycle(cacheRecommendModule)
cacheRecommendModule.parse()
....
}
与Butterknife基本一致,在调用Butterknife.bind方法中,通过反射创建当前类名+view_binding后缀的实例进行绑定。在Arch成员绑定时,RecommendByDayFragment只需要通过反射创建RecommendByDayFragment_arch_binding的实例就可以,这样就能够执行到对应的逻辑绑定内。
object InitArchUtils {
fun init(className: String, clazzs: Class<*>, objectClass: Any) {
try {
ReflectUtil.traversalClassByType(clazzs, IArchBind::class.java) {
val clazz = Class.forName(it?.canonicalName + "_arch_binding")
clazz.getDeclaredConstructor(clazzs).newInstance(objectClass)
}
} catch (e: Exception) {
//ignore
}
}
}
因为需要递归所有的父类,在继承的层级比较深的情况下,一个页面首次构建时长大概需要3-5ms。
大家还记得我们上面提到一个问题吗,APT是不允许获取注解非基本类型的参数的,所以我们需要用非常规的手段去获取。
val realType: TypeMirror? = AnnotationValueTypeGetUtil.getAnnotationValueType {
it.classElement.getAnnotation(BindModule::class.java).moduleClass
}
大家看上面直接获取注解的moduleClass属性是会出现MirroredTypeException
异常的,那我们怎么办呢?实际上我们就是通过这个异常来获取到TypeMirror
而不是直接获取到KClass
类型,请看具体实现
fun getAnnotationValueType(accessAnnotationAction: () -> Unit): TypeMirror? {
var realType: TypeMirror? = null
try {
accessAnnotationAction.invoke()
} catch (e: MirroredTypeException) {
realType = e.typeMirror
}
return realType
}
拿到了TypeMirror
我们就可以做生成代码的操作了如下
methodBuilder.addStatement("val $realName = %T()", realType.asTypeName())
对应的生成代码就是上面的val cacheRecommendModule = CacheRecommendModule()
这一行
那如何做到参数的定义必须是接口而不能是普通类呢? 只需要的APT的过程中,进行参数校验即可。
APT+ASM
对于APT+反射的3-5ms, 性能上还是需要再进行优化,虽然可以添加一层缓存,将第二次反射查找的耗时缩减掉,但是首次调用的耗时还是比较长,需要再从技术手段,将这个耗时减下来。在多次探索之后,确定了最终的方案,使用APT+AOP技术。即通过编译期自动代码,然后通过ASM在编译期间的transfrom阶段插入方法调用,将反射调用的耗时,替换为方法调用的耗时。
每一个Arch相关的子类,都会继承接口IArchBind
interface IArchBind {
fun parse()
}
在编译期间的tranform阶段,会进行parse插入,插入规则如下:
- 扫描所有APT生成的代码辅助类, 所有以arch_binding结尾的class,记录在内存中
- 遍历所有class,查看是否符合插入规则,也就是class名称是否和arch_binging前缀匹配,如果匹配,执行parse方法插入,插入一个方法调用,对应的ASM代码为
@Override
public void visitEnd() {
String archBindingClassName = className + CollectArchBindingDataContainer.ARCH_END_POSIX;
MethodVisitor mv = super.visitMethod(Opcodes.ACC_PUBLIC, "parse", "()V", null, null);
mv.visitCode();
mv.visitVarInsn(ALOAD, 0);
mv.visitMethodInsn(INVOKESPECIAL, superClassName, "parse", "()V");
mv.visitTypeInsn(NEW, archBindingClassName);
mv.visitInsn(DUP);
mv.visitVarInsn(ALOAD, 0);
mv.visitMethodInsn(INVOKESPECIAL, archBindingClassName, "<init>", "(" + "L" + className + ";" + ")V");
mv.visitVarInsn(ASTORE, 1);
mv.visitInsn(Opcodes.RETURN);
mv.visitMaxs(2, 2);
mv.visitEnd();
}
在APK打包完成后,通过jadx进行apk的反编译,可以查看插桩后的代码为:
public final class RecommendByDayFragment {
public void parse() {
super.parse();
RecommendByDayFragment_arch_binding recommendByDayFragment_arch_binding = new RecommendByDayFragment_arch_binding(this);
}
}
上边三种页面自动绑定方式的耗时统计如下图表所示:
在经过自动绑定方案的多次迭代和优化,最终将整个页面创建时自动绑定的耗时优化到了0.2ms,达到最优效果。
生命周期
通常来说,Android内具有生命周期的有Activity和Fragment,我们会在页面初始化的时候做一些子模块初始化的操作,在页面即将销毁的时候执行子模块的销毁操作。框架内模块对于生命周期的感知非常重要,一旦能够感知到生命周期,框架内的模块就能做到自初始化和自销毁。 在Arch框架中,所有的成员DataProvider、BaseModule等都能够感知对应的ArchView的生命周期。
生命周期详情
interface IArchLifecycle {
fun onHandleCreate()
fun onInit(view: View)
fun onStartCall()
fun onStart()
fun onResumed()
fun onPaused()
fun onStop()
fun onViewDestroy()
fun onHandleDestroy()
}
Activity的生命周期对应到ArchView的生命周期
Activity BaseArchView
onCreate() ===> onHandleCreate()
setContentView() ===> onInit()
onStartCall()
onStart() ===> onStart()
onNewIntnt() ===> onNewIntnt()
onResume() ===> onResumed()
onPaused() ===> onPaused()
onStop() ===> onStop()
onDestroy() ===> onViewDestroy()
onHandleDestroy()
Fragment的生命周期对应到ArchView的生命周期
Fragment BaseArchView
onCreate() ===> onHandleCreate()
onCreateView() ===> onInit()
onStartCall()
onStart() ===> onStart()
onResume() ===> onResumed()
onPaused() ===> onPaused()
onStop() ===> onStop()
onViewDestroy() ===> onViewDestroy()
onDestroy() ===> onHandleDestroy()
上面的生命周期相对于常见的Activity和Fragment多了两个生命周期onInit()和onStartCall()。
这两个生命周期的含义是什么呢?为什么需要多这两个生命周期呢?
- 含义
onInit:表示子模块的初始化方法
onStartCall: 表示所有子模块的onInit方法已经执行完成,可以开始进行逻辑调用。
- 为什么需要多这两个生命周期
最主要的原因是想解决各个子模块的初始化时机和调用依赖问题。
在业务复杂的情况下,模块太多,如果仅有Create方法无法保证谁先初始化完成,谁后初始化完成,一旦出现一个模块调用另外一个未初始化完成模块,就可能出现一些未知的问题。 通常的做法可以给模块初始化添加优先级,但是这种方式不好维护和扩展。所以我们新增两个生命周期,在onInit方法中仅执行模块内初始化,不能够调用其他子模块。在所有onInit方法执行完成后,回调onStartCall,就可以在开始做其他模块的访问逻辑,此时能够保证需要初始化的已经被初始化了。
在BaseArchView中,我们也提供了相应的获取当前Lifecycle的状态,对应了BaseArchView的各个生命周期。
enum class LifecycleState {
Created,
Init,
StartCall,
Start,
Resume,
Pause,
Stop,
ViewDestroy,
Destroyed
}
fun getCurrentLifeCycleState(): LifecycleState
fun isState(lifecycleState: LifecycleState): Boolean
消息通信
在一个页面创建后,消息通信机制不可或缺,针对整个页面我们有下面几个通信方式。
事件总线
Android中常用的事件总线EventBus,有下面几个优点:
- 数据传递方式简单
- 可以动态设置接受线程
- 传递的数据可以自定义,
- 可以做到解耦
在整个页面建立时,会同时搭建一条EventBus的局部事件总线,所谓的局部事件总线,就是在当前页面内发送的事件,仅能在当前页面接收到。作用域仅在当前页面内。 对于EventBus发送的事件类型,我们做了下面两个类型的区分:
- IActionEvent 表示为通知型事件,不需要有状态的缓存,比如有ON_LOAD_MORE(拉下刷新)、RELOAD_DATA(从新加载数据)
- IChangeEvent:表示为数据修改型事件,需要有数据状态的持续性,比如弹幕开关等,漫画详情页数据修改 可以通过所有子模块绑定的eventProcessor来发送事件
为了规范和简化这个业务对于事件的发送和接收的复杂度,每一个页面内的每一个成员都能够非常方便快捷的进行事件发送和接收。
//事件接收
override fun handleActionEvent(type: IActionEvent, data: Any?) {}
override fun handleDataChangeEvent(type: IChangeEvent, data: Any?) {}
//事件发送
eventProcessor.postLocalActionEvent(RecommendActionEvent.CACHE_DATA_LOAD_SUCCEED, null)
显式调用
事件总线的消息通信方式,解决的是没有明确被调用的目标的通信解耦方式。如果两个模块明确有依赖,可以通过显式的方式进行调用。每一个Module默认都会有MainController的引用,可以通过MainController显式的调用其他模块提供的接口。比如:
recommendByDayController.mainModule.getRecommendMainFilterView().dismissFilterList()
上面有遗留一个问题,被注解标注的成员变量类型需要强制定义成接口呢? Arch框架不希望Module的所有功能因为显式调用而暴露给其他Module,所以通过在接口内定义允许暴露的功能,其他Module只允许访问该接口中定义的功能,既增加了模块安全性,又增加了可复用性。
列表(RecyclerView)
在很多业务中,Activity、Fragment的复杂,其实也是列表数据的复杂,在列表数据足够复杂时,可能存在一个recyclerView内部有几十个不同的ViewType,并且多个ViewType的holder之间可能有相互影响。Arch框架有针对列表操作进行了优化。
统一Adapter的数据类型
Adapter本身就是由数据驱动的,根据不同的数据集合来创建和绑定不同ViewHolder。在不同业务的不同Adapter中,数据定义必然不一致,所以第一步我们需要统一数据模型,隔离数据差异。
class ViewItemData<T>(var viewType: Int, var data: T?) {
companion object {
const val TYPE_UNKNOWN = -1
}
}
viewType:表示实际需要创建的ViewHolder的类型 data: 表示当前ViewHolder接收的数据对象 数据统一之后,所有业务的Adapter的通用逻辑就可以抽象出同一套逻辑。
统一Adapter相关的操作
对于不同的业务场景,每一个Adapter都会提供不同的能力,但是有很多方法是通用的,我们如何把通用的方法抽象出来,提供一套通用的API。
interface IAdapter {
fun refreshDataList(dataList: List<ViewItemData<out Any?>>?)
fun addDataList(dataList: List<ViewItemData<out Any?>>?)
fun addItemData(data: ViewItemData<out Any?>?)
fun removeData(position: Int)
fun insertData(data: ViewItemData<out Any?>?, dataIndex: Int): Boolean
fun insertListData(startIndex: Int, data: List<ViewItemData<out Any?>>?): Boolean
fun replaceData(data: ViewItemData<out Any?>?, dataIndex: Int): Boolean
fun clear()
fun isEmpty(): Boolean
fun getAllData(): MutableList<ViewItemData<out Any?>>
fun getDataFromIndex(position: Int): ViewItemData<out Any?>?
fun getAllListViewType(): List<Int>
fun getDataListByViewTypes(viewTypes: List<Int>): List<ViewItemData<out Any?>>
fun getRealPosition(position: Int): Int
fun registerCreateFactory(factory: CreateFactory): IAdapter
fun registerCreateFactory(factory: CreateFactory, viewTypes: ArrayList<Int>): IAdapter
fun unRegisterCreateFactory(factory: CreateFactory): IAdapter
fun registerBindFactory(factory: BindFactory): IAdapter
fun registerBindFactory(factory: BindFactory, viewTypes: ArrayList<Int>): IAdapter
fun unRegisterBindFactory(factory: BindFactory): IAdapter
fun registerScrollListener(callBack: IScrollListener)
fun unRegisterScrollListener(callBack: IScrollListener)
}
在我们页面开发框架中,如果模块内有列表元素,就可以使用这一套接口进行列表元素操作。
Adapter逻辑解耦
在老的业务中,Adapter的逻辑耦合非常重,需要负责所有ViewHolder的创建和绑定。比如下面的代码肯定你们的项目中也存在
@Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
switch (viewType) {
case 1:
return 1;
case 2:
return 2;
case 3:
return 3;
....
}
}
@Override
public void onBindViewHolder(RecyclerView.ViewHolder holder, final int position) {
final int noHeaderPosition = getNoHeaderPosition(position);
switch (getItemViewType(position)) {
case 1:
xxxxx
break;
case 2:
xxxxxx
break;
case 3:
xxxxxx
break;
....
}
}
我们期望这个业务可以打散在各个业务模块中,由各个Module负责数据插入、ViewHolder创建、ViewHolder绑定和数据删除。那如何实现这个ViewHolder的分发呢?
- 首先在Adapter中设置ViewHolder的创建工厂,支持在Module中添加注册
//ViewHolder创建工厂
interface CreateFactory {
fun createHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder?
}
- 在adapter的careteViewHolder和bindViewHolder进行分发。
final override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
val holder = createHolder(parent, viewType)
if (holder != null) {
return holder
}
val factoryHolder = holderFactoryContainer.dispatchCreateHolder(parent, viewType)
if (factoryHolder != null) {
return factoryHolder
}
if (LogUtils.sDebugBuild) {
throw IllegalArgumentException("数据异常~ viewType is $viewType ~")
} else {
return BaseEmptyViewHolder<Any>(parent, R.layout.empty_holder)
}
}
在Adapter的onCreateViewHolder中根据ViewType创建ViewHolder,分为下面三步:
-
首先会看子类是否会拦截这种类型,如果子类已经返回该类型的ViewHolder,那么直接返回
-
根据ViewType查看是否有注册进来的HolderFactory,如果有,调用HolderFactory的createHolder,返回
-
如果该ViewType没有地方进行处理,那么表示当前这个数据有异常,Debug环境抛出异常,线上环境默认使用宽高为0的占位holder。
-
在各个Module中进行HolderFactory注册和创建ViewHolder。
override fun onStartCall() {
super.onStartCall()
getAdapter().registerCreateFactory(this, arrayListOf(TYPE_DYNAMIC_RECOMMEND_TOPICS))
.registerScrollListener(this)
}
override fun createHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder? {
return RecommendHomeDayDynamicViewHolder(parent, R.layout.layout_home_day_dynamic_topics_vh)
}
在这个页面开发框架的ViewHolder创建逻辑如下图所示:
ViewHolder逻辑解耦
ViewHolder的复杂度通常是被我们忽略掉的,其实当一个ViewHolder的业务足够复杂,也可能达到上千行的代码。 针对这个场景,我们也给ViewHolder支持了Mvp模式,这样能够最小粒度的拆分和复用。每一个Viewholder的逻辑也能够拆分开。
BaseArchViewHolder
Arch框架中的ViewHolder基类,支持MVP自动绑定能力,主要用来做UI相关的操作。
BaseArchViewHolderPresent
Arch框架中的Presenter基类,用来处理业务逻辑。大家都可能或多或少的遇到因为ViewHolder复用,因为在RecyclerView中,为了性能,减少UI的重复创建,做了ViewHolder的复用。为了解决这个问题,Arch框架保证Presenter是不会复用的,在每一次ViewHolder执行onBind时,都会重新创建出Presenter,保证不会因为Presenter存在一些脏数据导致逻辑异常的问题。
在Arch框架中,一个ViewHolder的开发逻辑如下图所示:
同时,我们给Presenter提供了一套非常方便的功能
- 生命周期
- 数据自动注入
- 可见性回调
- 事件通信
ViewHolder生命周期
对于ViewHolder常用的使用的使用,通常我们会使用基于其itemView,添加AttachWindow的监听,做些注册与反注册的功能。
itemView.addOnAttachStateChangeListener(object : View.OnAttachStateChangeListener {
override fun onViewDetachedFromWindow(v: View?) {}
override fun onViewAttachedToWindow(v: View?) {}
在BaseArchViewHolderPresent中,我们重新定义了生命周期
fun onStartCall() {}
fun onShown(firstShow: Boolean) {}
fun onHide() {}
fun onRecycled() {
- onStartCall:ViewHolder UI初始化完成,数据绑定完成,可以开始操作逻辑
- onShown:ViewHolder可见
- onHide:ViewHolder不可见
- onRecycled:ViewHolder被回收
ViewHolder对应数据的自动注入
一个ViewHolder对应一个ViewType,也对应了一个Data,所以我们可以非常方便的在ViewHolder和对应的Present获取到对应的数据,只需要在BaseArchHolderPresent定义时声明数据类型的范型,就能在onBind方法中获取到实际的数据类型。
class RecommendComicPresent : BaseArchHolderPresent<Comic, IRecommendByDayAdapter, RecommendByDayDataProvider>() {
override fun onStartCall() {
data ?: return
recommendComicView?.refreshView(data!!)
adjustViewState()
}
}
ViewHolder可见性回调
在很多业务中,都会有依赖于曝光和消失的时机做的一些逻辑改动,所以我们在present中聚合了可见性回调的能力,内部机遇View曝光的逻辑计算,提供可见性回调。
interface IHolderStatus: IHolderLifecycle {
fun onShown(firstShow: Boolean)
fun onHide()
}
如果有依赖可见不可见时机做一些逻辑的,可以直接复写这两个方法。比如,想在可见时做曝光上报。
override fun onShown(firstShow: Boolean) {
data?.let {
trackHomepageComicExposure(it, realPosition)
}
}
数据通信
在一些复杂业务中,总有ViewHolder需要获取Activity或者Fragment的一些数据,也有一些Fragment、Activity的数据改变需要响应到ViewHolder的UI变化上,在没有框架的支持下,需要有无数层级的数据透传和callback透传。所以需要打通ViewHolder和Fragment、Activity的数据通信。
- Presenter默认支持从dataProvider获取页面的全局数据
- Presenter默认支持局部事件发送和事件接收
在使用了Arch框架之后,ViewHolder使用的简单示例如下,比如上面的日签页面的漫画大卡Holder:
class RecommendComicHolder(parent: ViewGroup, id: Int): BaseArchViewHolder<Comic>(parent, id), IRecommendComicView, IRecommendComicLikeView {
@BindPresent(present = RecommendComicPresent::class)
lateinit var comicPresent: IRecommendComicP
@BindPresent(present = RecommendComicCommentPresent::class)
lateinit var commentPresent: IRecommendComicCommentPresent
@BindPresent(present = RecommendComicLikePresent::class)
lateinit var likePresent: IRecommendComicLikePresent
}
将不同的业务逻辑拆分到了不同的Presenter中,ViewHolder只负责UI相关的操作。
结语
Arch框架是在快看复杂的业务开发中沉淀出来的,极大的改善了工程混乱的代码。目前已有的新业务模块都是基于该开发框架开发的,大大的提高了整体的研发效率。
Arch框架既保证了模块的解耦,提高了整个模块的复用性,又通过框架层的Controller层保证了可扩展性,并且通过APT自动生成代码保证使用的便捷性,再ASM插入方法调用将运行性能提升到最佳,同时提供了生命周期的自动感知和便捷的组件间通信能力。
当然,没有一个架构是完美的,适合自己业务才是最好的。