组件通信注解框架实际
目录介绍
- 01. 为何须要组件间通信
- 02. 实现同级组件通信形式
- 03. 先看一个简略的案例
- 04. 我的项目组件通信流程
- 05. 逆向简化注册流程
- 06. 这个注解是做什么的
- 07. 注解是如何生成代码
- 08. 如何定义注解处理器
- 09. 我的项目库的设计和欠缺
- 10. 封装该库有哪些特点
- 11. 一些常见的报错问题
- 12. 局部原理剖析的阐明
01. 为何须要组件间通信
-
明确一个前提:各个业务组件之间不会是互相隔离而是必然存在一些交互的;
- 业务复用:在 Module A 须要援用 Module B 提供的某个性能,比方须要版本更新业务逻辑,而咱们个别都是应用强援用的 Class 显式的调用;
- 业务复用:在 Module A 须要调用 Module B 提供的某个办法,例如别的 Module 调用用户模块退出登录的办法;
- 业务获取参数:登陆环境下,在 Module A,C,D,E 多个业务组件须要拿到 Module B 登陆注册组件中用户信息 id,name,info 等参数拜访接口数据;
-
这几种调用模式大家很容易明确,失常开发中大家也是毫不犹豫的调用。然而在组件化开发的时候却有很大的问题:
- 因为业务组件之间没有相互依赖,组件 Module B 的 Activity Class 在本人的 Module 中,那 Module A 必然援用不到,这样无奈调用类的性能办法;由此:必然须要一种反对组件化需要的交互方式,提供平行级别的组件间调用函数通信交互的性能。
-
我的项目库开源地址
- https://github.com/yangchong2…
02. 实现同级组件通信形式
-
至于对于页面跳转
- 那必定是首选路由,比方阿里的 ARouter。然而波及到组件之间业务复用,业务逻辑的交互等等,就有点难搞了……那该怎么解决比拟不便呢?
-
组件业务逻辑交互通信
-
比方业务组件层划分
- 组件 A,组件 B,组件 C,组件 D,组件 E 等等,这些业务组件并不是相互依赖,它们之间是雷同的层级!
-
举一个业务案例
- 比方有个抉择用户学员的弹窗,代码写到了组件 A 中,这个时候组件 C 和组件 D 须要复用组件 A 中的弹窗,该业务逻辑如何解决?
- 比方组件 E 是我的用户相干的业务逻辑,App 登陆后,组件 B 和组件 C 须要用到用户的 id 去申请接口,这个时候如何获取组件 E 中用户 id 呢?
-
该层级下定义一个公共通信组件
- 接口通信组件【被各个业务组件依赖】,该雷同层级的其余业务组件都须要依赖这个通信组件。这个时候各个模块都能够拿到通信组件的类……
-
-
须要具备的那些特点
- 应用简略不便,防止同级组件相互依赖。代码入侵性要低,反对业务交互,自动化等个性。
03. 先看一个简略的案例
-
先说一下业务场景
- 版本更新业务组件(解决更新弹窗,apk 下载,apk 的 md5 校验,装置等逻辑,还波及到一些业务逻辑,比方更新模式一般或者强更,还有渠道,还有时间段等)
- 主模块首页,我的组件,设置核心组件等多个 module 组件中都会用到版本更新性能,除了主模块外,其余组件没有依赖版本更新组件,那么如何调用外面的更新弹窗业务逻辑呢?
-
创立一个接口通信组件
- 如上所示,各个同级的业务组件,A,B,C,D 等都依赖该接口通信组件。那么这样就会拿到通信组件的类,为了实现通信交互。能够在该接口通信组件中定义接口并裸露形象更新弹窗办法,那么在版本更新组件中写接口实现类。
- 创立一个 map 汇合,存储实现类的全门路,而后 put 到 map 汇合中;这样能够 get 拿到实现类的门路,就能够利用反射创立实例对象。
-
通信组件几个次要类
- BusinessTransfer,次要是 map 汇合中 get 获取和 put 增加接口类的对象,利用反射机制创立实例对象。该类放到通信组件中。
- IUpdateManager,该类是版本更新接口类,定义更新形象办法。该类放到通信组件中。
- UpdateManagerImpl,该类是 IUpdateManager 接口实现类,次要是具体业务逻辑的实现。该类放到具体实现库代码中,比方我的组件。
-
次要实现的代码如下所示
// 接口 public interface IUpdateManager extends Serializable {void checkUpdate(UpdateManagerCallBack updateManagerCallBack); interface UpdateManagerCallBack {void updateCallBack(boolean isNeedUpdate); } } // 接口实现类 public class UpdateManagerImpl implements IUpdateManager {@Override public void checkUpdate(UpdateManagerCallBack updateManagerCallBack) {try { IConfigService configService = DsxxjServiceTransfer.$().getConfigureService(); String data = configService.getConfig(KEY_APP_UPDATE); if (TextUtils.isEmpty(data)) {if (updateManagerCallBack != null) {updateManagerCallBack.updateCallBack(false); } return; } ForceUpdateEntity xPageUpdateEntity = JSON.parseObject(data, ForceUpdateEntity.class); ForceUpdateManager.getInstance().checkForUpdate(xPageUpdateEntity, updateManagerCallBack); } catch (Exception e) {e.printStackTrace(); } } } // 如何应用 // 在初始化时注入,倡议放在 application 中设置,调用 setImpl 其实就是把门路字符串 put 到 map 汇合中 BusinessTransfer businessTransfer = BusinessTransfer.$(); businessTransfer.setImpl(BusinessTransfer.BUSINESS_IMPL_UPDATE_MANAGER, PACKAGE_NAME + ".base.businessimpl.UpdateManagerImpl"); ```- 那么如何调用呢?能够在各个组件中调用,代码如下所示……
// 版本更新 BusinessTransfer.$().getUpdate().checkUpdate(new IUpdateManager.UpdateManagerCallBack() {@Override public void updateCallBack(boolean isNeedUpdate) {}
});`
– 反射创立接口的实现类对象String className = implsMap.get(key); try {return (T) Class.forName(className).newInstance();} catch (InstantiationException e) {e.printStackTrace(); } ```- 这种形式存在几个问题 - 1. 注入的时候要填写正确的包名,否则在运行期会出错,且不容易找到;- 2. 针对接口实现类,不能混同,否则会导致反射找不到具体的类,因为是依据类的全门路反射创建对象;所以每次写一个接口 + 实现类,都要在混同文件中增加一下,比拟麻烦…… - 3. 每次增加新的接口通信,都须要手动去注入到 map 汇合,略微有点麻烦,是否改为主动注册呢?- 4. 每次还要在 Transfer 的类中,增加获取该接口对象的办法,是否主动一点?
04. 我的项目组件通信流程
-
具体实现计划
- 比方说,主 app 中的首页有版本更新,业务组件用户核心的设置页面也有版本更新,而版本升级的逻辑是写在版本更新业务组件中。这个时候操作如下所示
05. 逆向简化注册流程
-
在 module 通信组件中定义接口,留神须要继承 IRouteApi 接口
public interface IUpdateManager extends IRouteApi {void checkUpdate(UpdateManagerCallBack updateManagerCallBack); interface UpdateManagerCallBack {void updateCallBack(boolean isNeedUpdate); } }
@RouteImpl(IUpdateManager.class) public class UpdateImpl implements IUpdateManager {@Override public void checkUpdate(UpdateManagerCallBack updateManagerCallBack) {// 省略} } ```- 如何获取服务的实例对象
// 无返回值的案例 // 设置监听 IUpdateManager iUpdateManager = TransferManager.getInstance().getApi(IUpdateManager.class); iUpdateManager.checkUpdate(new IUpdateManager.UpdateManagerCallBack() {@Override public void updateCallBack(boolean isNeedUpdate) {}
}); // 有返回值的案例
userApi = TransferManager.getInstance().getApi(IUserManager.class); String userInfo = userApi.getUserInfo();`
– 对于 get/put 次要是存属什么呢/** * key 示意的是自定义通信接口 * value 示意自定义通信接口的实现类 */ private Map<Class, Class> apiImplementMap = new HashMap<>(); ```- 代码混同
-keep class com.yc.api.{;} -keep public class implements com.yc.api. {*;}
`
– 不须要在额定增加通信接口实现类的混同代码- 因为用到了反射,而且是用 Class.forName(name)创立反射对象。所以必须保障 name 门路是正确的,否则找不到类。
- 该库,你定义的实现类曾经继承了我定义的接口,因为针对继承 com.yc.api.** 的子类,会疏忽混同。曾经解决……所以不须要你额定解决混同问题!
06. 这个注解是做什么的
-
这个注解有什么用呢
- 框架会在我的项目的编译器扫描所有增加 @RouteImpl 注解的 XxxImpl 接口实现类,而后传入接口类的 class 对象。这样就能够通过注解拿到接口和接口的实现类……
-
apt 编译后生成的代码
- build—>generated—>ap_generated_sources—>debug—->out—->com.yc.api.contract
-
这段代码什么意思:编译器生成代码,并且该类是继承本人自定义的接口,调用 IRegister 接口中的 register 办法,key 是接口 class,value 是接口实现类 class,间接在编译器把接口和实现类存储起来。用的时候间接取……
public class IUpdateManager$$Contract implements IRouteContract {@Override public void register(IRegister register) {register.register(IUpdateManager.class, UpdateImpl.class); } }
`
–
07. 注解是如何生成代码
-
如何拿到注解标注的类,看个案例
@RouteImpl(IUserInfoManager.class) public class Test implements IUserInfoManager {@Override public String getUserId() {return null;} } private void test(){// 这个中央先写个假的业务代码,理论 apt 中是通过 roundEnvironment 对象拿到注解标记的类 Class c = Test.class; //Set<? extends Element> annotated = roundEnvironment.getElementsAnnotatedWith(typeElement); // 找到润饰了注解 RouteImpl 的类 RouteImpl annotation = (RouteImpl) c.getAnnotation(RouteImpl.class); if (annotation != null) {try { // 获取 ContentView 的属性值 Class value = annotation.value(); String name = value.getName(); System.out.println("注解标记的类名"+name); } catch (RuntimeException e) {e.printStackTrace(); System.out.println("注解标记的类名"+e.getMessage()); } } } ```- 手动编程还是主动生成
08. 如何定义注解处理器
-
apt 工具理解一下
- APT 是 Annotation Processing Tool 的简称, 即注解解决工具。它是在编译期对代码中指定的注解进行解析,而后做一些其余解决(如通过 javapoet 生成新的 Java 文件)。
-
定义注解处理器
- 用来在编译期扫描退出 @RouteImpl 注解的类,而后做解决。这也是 apt 最外围的一步,新建 RouteImplProcessor 继承自 AbstractProcessor, 而后实现 process 办法。在我的项目编译期会执行 RouterProcessor 的 process()办法,咱们便能够在这个办法里解决 RouteImpl 注解了。
-
初始化自定义 Processor
@AutoService(Processor.class) public class RouteImplProcessor extends AbstractProcessor {}
/** * 初始化办法 * @param processingEnvironment 获取信息 */ @Override public synchronized void init(ProcessingEnvironment processingEnvironment) {super.init(processingEnvironment); // 文件生成器 类 / 资源 filer = processingEnv.getFiler(); // 节点工具类 (类、函数、属性都是节点) elements = processingEnv.getElementUtils();} ```- 在 process 办法中拿到注解标记的类信息
@Override public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {for (TypeElement typeElement : set) {Set<? extends Element> annotated = roundEnvironment.getElementsAnnotatedWith(typeElement); for (Element apiImplElement : annotated) {// 被 RouteImpl 注解的节点汇合 RouteImpl annotation = apiImplElement.getAnnotation(RouteImpl.class); if (annotation == null || !(apiImplElement instanceof TypeElement)) {continue;} ApiContract<ClassName> apiNameContract = ElementTool.getApiClassNameContract(elements, annotationValueVisitor,(TypeElement) apiImplElement); if (RouteConstants.LOG){System.out.println(“RouteImplProcessor——–process——-apiNameContract—“+apiNameContract); } } } return true; }
`
– 而后生成代码,次要是指定生成代码门路,而后创立 typeSpec 注解生成代码。-
这个 javapoet 工具,目前还紧紧是套用 ARouter,创立类名,增加接口,增加注解,增加办法,增加修饰符,增加函数体等等。也就是说将一个类代码拆分成 n 个局部,而后逆向拼接到一起。最初去 write 写入代码……
// 生成注解类相干代码 TypeSpec typeSpec = buildClass(apiNameContract); String s = typeSpec.toString(); if (RouteConstants.LOG){System.out.println(“RouteImplProcessor——–process——-typeSpec—“+s); } try {// 指定门路:com.yc.api.contract JavaFile.builder(RouteConstants.PACKAGE_NAME_CONTRACT, typeSpec) .build() .writeTo(filer); } catch (IOException e) {e.printStackTrace(); }
`
– 来看看怎么创立注解类-
大略思路就是,将咱们平时的类,拆分,而后拼接成实体。ParameterSpec 是创立参数的实现,MethodSpec 是函数的生成实现等等……
private TypeSpec buildClass(ApiContract<ClassName> apiNameContract) {String simpleName = apiNameContract.getApi().simpleName(); // 获取 com.yc.api.IRouteContract 信息,也就是 IRouteContract 接口的门路 TypeElement typeElement = elements.getTypeElement(RouteConstants.INTERFACE_NAME_CONTRACT); ClassName className = ClassName.get(typeElement); String name = simpleName + RouteConstants.SEPARATOR + RouteConstants.CONTRACT; // 这外面又有增加办法注解,增加修饰符,增加参数规格,增加函数题,增加返回值等等 MethodSpec methodSpec = buildMethod(apiNameContract); // 创立类名 return TypeSpec.classBuilder(name) // 增加 super 接口 .addSuperinterface(className) // 增加修饰符 .addModifiers(Modifier.PUBLIC) // 增加办法【而后这外面又有增加办法注解,增加修饰符,增加参数规格,增加函数题,增加返回值等等】.addMethod(methodSpec) // 创立 .build();}
`
-
09. 我的项目库的设计和欠缺
-
ModuleBus 次要由三局部组成,包含对外提供的 api 调用模块、注解模块以及编译时通过注解生产相干的类模块。
- api-compiler 编译期解析注解信息并生成相应类以便进行注入的模块
- api-manager 注解的申明和信息存储类的模块,以及开发调用的 api 性能和具体实现
-
编译生成代码产生在编译器
- 编译期是在我的项目编译的时候,这个时候还没有开始打包,也就是没有生成 apk 呢!框架在这个期间依据注解去扫描所有文件,而后生成路由映射文件。这些文件都会对立打包到 apk 里!
-
无需初始化操作
- 先看 ARouter,会有初始化,次要是收集路由映射关系文件,在程序启动的时候扫描这些生成的类文件,而后获取到映射关系信息,保存起来。这个封装库不须要初始化,简化步骤,在获取的时候如果没有则在 put 操作 map 汇合。具体看代码!
10. 封装该库有哪些特点
-
注解生成代码主动注册
- 应用 apt 注解在编译阶段生成服务接口与实现的映射注册帮忙类,其实这部分就相当于是代替了之前在 application 初始化注入的步骤,获取服务时主动应用帮忙类实现注册,不用手动调用注册办法。
-
防止空指针解体
- 无服务实现注册时,应用空对象模式 + 动静代理的设计提前裸露调用谬误,次要抛出异样,在测试时就发现问题,避免空指针异样。
-
代码入侵性低
- 无需改变之前的代码,只须要在之前的接口和接口实现类依照约定增加注解标准即可。其接口 + 接口实现类还是用之前的,齐全无影响……
-
依照你须要来加载
- 首次获取接口服务的时候,用反射生成映射注册帮忙类的实例,再返回实现的实例。
-
丰盛的代码案例
- 代码案例丰盛,提供丰盛的案例,而后多个业务场景,尽可能欠缺好 demo。
-
该库注解生成代码在编译器
- 在编译器生成代码,并且该类是继承本人自定义的接口,存储的是 map 汇合,key 是接口 class,value 是接口实现类 class,间接在编译器把接口和实现类存储起来。用的时候间接取……
11. 一些常见的报错问题
-
Didn’t find class “com.yc.api.contract.IUserManager$$Contract” on path
-
注解生成的代码失败导致呈现这个问题。为什么会呈现这种状况?批改 gradle 的构建版本……
public class IUpdateManager$$Contract implements IApiContract {@Override public void register(IRegister register) {register.register(IUpdateManager.class, UpdateImpl.class); } }
`
– 对于 apt 编译器不能生成代码的问题,可能会有这么一些关键点-
第一查看 module 的依赖,如果没有依赖请先增加依赖
implementation project(path: ‘:api-manager’) annotationProcessor project(path: ‘:api-compiler’)
`
– 第二查看写完 wirter 的流没有敞开,会造成生成文件,但文件内容为空,或者不全;- 第三可能是 Android Gradle 及构建版本问题,我的是 3.4.1 + 5.2.1,会呈现不兼容的状况,大神倡议 3.3.2 + 4.10.1 以下都能够。听了倡议升高版本果然构建编译,新的文件生成了。
-
12. 局部原理剖析的阐明
-
注解是如何生成代码的?也就是 javapoet 原理……
- 这个 javapoet 工具,目前还紧紧是套用 ARouter,创立类名,增加接口,增加注解,增加办法,增加修饰符,增加函数体等等。也就是说将一个类代码拆分成 n 个局部,而后逆向拼接到一起。最初去 write 写入代码……
- 然而,怎么拼接和并且创立.java 文件的原理,待欠缺。目前处于会用……
-
Class.forName(name)反射如何找到 name 门路的这个类,从 jvm 层面剖析?
- 待欠缺
-
new 和 Class.forName(“”).newInstance()创建对象有何区别?
A a = (A)Class.forName("com.yc.demo.impl.UpdateImpl").newInstance(); A a = new A();``` - 它们的区别在于创建对象的形式不一样,前者 (newInstance) 是应用类加载机制,后者 (new) 是创立一个新类。- 为什么会有两种创建对象形式?- 次要思考到软件的可伸缩、可扩大和可重用等软件设计思维。- 从 JVM 的角度上看:- 咱们应用关键字 new 创立一个类的时候,这个类能够没有被加载。然而应用 newInstance()办法的时候,就必须保障:1、这个类曾经加载;2、这个类曾经连贯了。- 而实现下面两个步骤的正是 Class 的静态方法 forName()所实现的,这个静态方法调用了启动类加载器,即加载 java API 的那个加载器。- 当初能够看出,newInstance()实际上是把 new 这个形式合成为两步,即首先调用 Class 加载办法加载某个类,而后实例化。这样分步的益处是不言而喻的。咱们能够在调用 class 的动态加载办法 forName 时取得更好的灵活性,提供给了一种降耦的伎俩。- 区别