目前Chain的能力在业务拆解过程中发挥了比较重要的作用,实现了各业务代码的分治,同时低侵入式的特性也大大降低了改造的成本。Chain可以作为组件化能力的补充,希望可以给大家带来一些启发和收获。
在业务迭代的过程中,避免不了业务代码交叉混排的情况,同一个调用时机出现各种不同业务或功能的代码。我们需要将调用点作为能力抽象出来,从而成为扩展点,将原本串行交叉的代码进行抽离,回到真正属于它的归处。本文聚焦于实现一种Android技术框架,以尽可能小的开发成本,实现混排业务分治的能力。
基于接口实现对通用能力的抽离,进行耦合拆分。面向接口编程,通过一定的抽象,保证业务分治的同时,还可以保证扩展性。
开闭原则,基于扩展点的设计,方便地进行扩展,无需修改原类;
依赖倒置,面向接口编程,不依赖具体的实现类;
接口隔离,将功能抽象成多个隔离的接口,降低耦合;
有A、B、C三个模块,B和C存在相互依赖、相互调用的情况,且在A模块中被交叉调用。
由A抽离扩展点能力到 A_Interface,B和C在A中的调用,就变成了 A_Interface 的实现类在A中的调用了,确保A不依赖B和C的具体实现。
将B、C各分成两层,上层 implement 为业务实现层,代码封闭不对外暴露。下层 interface 为接口层,会对外部公开,可以被外部依赖,表示对外提供的接口能力。B和C之间通过相互提供的接口能力调用。
分层之后,implement 实现层之间的依赖耦合被去除,只会依赖接口层。
将接口和实现的映射关系保存到工具API类中,实现层通过工具API类实现相互调用。
实现类标注自己实现的接口和名字,生成一种索引关系,将索引保存到API单例类 ChainBlock 中。
ChainBlock 通过 interface.class 和 key 唯一索引到一个实现类上,调用实现类的接口方法。
将接口类和实例对象的索引关系,通过手动调用的方式注册到 ChainBlock 的单例中,会变得相当繁琐。如果编译时可以扫描到所有注解类,通过注解生成索引关系,这样在运行时就可以直接使用,大大降低了使用成本。
有A、B、C三个 module,拆成独立的库,分别编译成aar集成到主工程。
在每个 module 中,通过@Chain
标注需要通过接口公开的实现类上,被注解的类会在子库编译成aar时,通过 AnnotationProcessor 和 javapoet 在编译期被索引到 XChainImpl.class 中,每个子库都会生成对应的XChainImpl.class。
在主工程编译时,通过自定义的 plugin,通过 transform、asm、javassist,将子库所有的索引类XChainImpl.class 插桩到指定的 ChainJoint.class 中,实现对索引类的聚合。
在运行时,ChainBlock
工具类可以从 ChainJoint 中获取所有的索引,Amodule 就可以通过 ChainBlock 获取 Bmodule 中注解了@Chain
的实现类了。
添加注解 @ChainHost、@Chain、@Block,分别对接口、实现类、方法进行注解,加上注解才能成为对外公开的API能力。
编译时索引,通过接口类、实现类名字,索引到唯一的实现类上。索引在编译时建立,不占用运行时效率。
运行时调用,通过ChainBlock工具类,完成具体实现类的调用。
详细API还包括,获取所有的Chain列表,以及通过动态代理的方式调用所有实现类的方法,甚至可以通过一个自定义的协议url调起某个公开实现类的方法。
// 1. 调用指定Chain
ChainBlock.instance().obtainChain(IProvider.class, "A").log("hello");
ChainBlock.instance().obtainChain(IProvider.class, "BProvider").log("hi");
// 2. 获取所有Chain
List<IProvider.class> list = ChainBlock.instance().getChainList(IProvider.class);
// 3. 按优先级调用所有Chain,按方法上@Block注解的priority从高到低调用
ChainBlock.instance().priorityListProxy(IProvider.class).log("hello");
// 4. 通过chainblock自定义协议调用
ChainBlock.instance().runChainBlock(
"chainblock://iprovider/A/log?text=hello", context);
编译时文档输出,将所有子库中添加了注解的接口和实现类在编译时输出到文档,用于查阅现有的一些能力。
Generated by @Chain
# interface com.taobao.idlefish.IProvider
> 描述接口的作用
1. A : { annotatedClass:com.taobao.idlefish.AProvider, singleton:false }
1. BProvider : { annotatedClass:com.taobao.idlefish.BProvider, singleton:false }
1. CProvider : { annotatedClass:com.taobao.idlefish.CProvider, singleton:false }
基于 Graphviz工具 的可视化输出,更直观看到ChainBlock的整体链路。
索引关系覆盖问题
通过 @Chain 注解添加的 name,有可能会重复,导致可能会覆盖其他Chain的索引,问题一旦出现,只能在运行时发现。
解决办法:在编译时进行干预,在Plugin的transform里,扫描所有的jar包,通过URLClassLoader加载XXXChainImpl.class的索引类,将索引关系添加到 Map 里面,检查覆盖情况,发现覆盖直接终止编译报错,将问题在编译时提前暴露出来。
稳定性问题
@Chain 注解的实现类有可能会被删除,删除之后obtainChain(IProvider.class, "A").log("hello")方法调用会遇到空指针问题。
解决办法:通过 obtainChain 返回的不是真正的实现类,返回的是动态代理的 Proxy 对象,代理到真正的实现类上,即使实现类为 null 了,调用代理类也不会报错。
编译时遇到的 broken jar 问题
在 Gradle 编译时运行 Plugin 的时候,出现了broken jar 的错误
解决办法:broken jar 出现的原因是读取了一个未被正常释放的 jar 包,可以通过./gradlew --stop临时解决。经过排查之后,发现 URLClassLoader 和 javassist 的 ClassPool 在加载类对象的时候,需要将 jar 包路径 add 进去,未被正常释放导致的问题,正确释放之后解决。
接口下沉,基于接口的依赖解耦;
低侵入式,注解即可;
自动组件注册,无需手动管理;
自定义协议,支持跨进程调用;
自动报表文档输出,图形化展示;
混淆友好,不需要额外新增混淆规则;
不建议使用的场景:
如果代码可以直接调用到,没必要使用Chain。
如果代码属于业务无关的通用能力,可以直接下沉到底层库,没必要使用Chain。
建议使用的场景:
Chain更适合业务能力的抽象,相互隔离的业务子库之间,有相关代码需要复用,可以通过Chain实现。
向外提供扩展点,通过接口约束扩展点的方法,可以使用Chain拿到所有实现类来批量处理,并且新增实现类的时候对扩展点是无感知的。比如JSBridge的调用分发,实现类可以通过Chain进行聚合分发。
改造前,在 MainActivity 中各种业务调用相互穿插耦合,难以维护。
改造后,抽象一个 IWorkflow 接口,将 MainActivity 中的生命周期能力提供出来,各业务方实现这个接口并添加注解,MainActivity 就可以通过 ChainBlock 工具将生命周期分发到各个业务实现上。各个业务模块代码可以收敛到一处,进行分治互不影响。
目前Chain的能力在业务拆解过程中发挥了比较重要的作用,实现了各业务代码的分治,同时低侵入式的特性也大大降低了改造的成本。Chain可以作为组件化能力的补充,希望可以给大家带来一些启发和收获。
闲鱼技术联合大淘宝技术
新春拜年
“虎虎虎”
纸质红包大派送
扫码关注”淘系技术“回复"红包“即可获得领取方式
(2月28日18:00截止)