乐趣区

关于android:组件通信注解框架实践

组件通信注解框架实际

目录介绍

  • 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 时取得更好的灵活性,提供给了一种降耦的伎俩。- 区别

我的项目地址:https://github.com/yangchong2…

退出移动版