Android 经过 APT 解耦模块依赖

本文开源实验室原创,转载请以连接形式注明地址:https://kymjs.com/code/2018/08/12/01
Android APT 的新玩法,生成类的特殊加载方式。在 Android 多 module 工程中使用 APT,会出现类冲突问题,若是你也碰上这种问题,但愿本文对你有所帮助。javascript

对本文有任何问题,可加个人我的微信:kymjs123css

APT 是什么?Annotation Process Tool,注解处理工具。
这本是 Java 的一个工具,但 Android 也可使用,他能够用来处理编译过程时的某些操做,好比 Java 文件的生成,注解的获取等。java

在 Android 上,咱们使用 APT 一般是为了生成某些处理标注有指定注解的方法、类或变量,好比 EventBus3.0开始,就是使用 APT 去处理onEvent 注解的;dagger二、butterknife 等著名的开源库也都是使用 APT 去实现的。再举一个你们很是熟悉的实际使用场景:在 Android 模块化重构的过程当中,就会须要大量用到 APT 去生成做为跨模块转发层的中间类,在我以前讲《饿了么模块化平台设计》中的铁金库 IronBank 就大量使用了 APT 与 AOP 技术去实现跨模块的处理工做。安全

实现 APT

固然,本文要讲的是 APT 的新玩法,讲 APT demo 的文章有太多了,你们随便网上搜一下就一大把,若是会了的同窗,能够跳过本节。
要实现一个简单的 APT demo 是很容易的。首先在 idea 中建立一个 Java 工程(因为 Android Studio 不能直接建立 Java 工程,咱们选用 idea 更简单)微信

一、首先建立一个咱们须要处理的注解声明:dom

@Retention(RetentionPolicy.SOURCE) @Target({ElementType.METHOD}) public @interface Produce { Class<?> returnType() default Produce.class; Class<?>[] params() default {}; } 

关于注解类的建立以及上面各个给注解类加注解的含义,在我很早以前的一篇博客《Android注解式绑定控件,没你想象的那么难》中已经有很详细的介绍了,不知道的同窗能够再去看一看。ide

二、第二步,咱们为了以后处理方便,建立一个 JavaBean 用来封装须要的数据。模块化

class ItemData { Element element; String className = ""; String returnType = ""; String methodName = ""; String[] params = {}; } 

三、最后就是最重要的一个类了:注解是处理方式函数

public class MyAnnotationProcessor extends AbstractProcessor { } 

全部的注解处理类必须继承自系统的AbstractProcessor,若是想要让这个注解处理类生效,还要在咱们的工程中建立一个 meta 文件,meta 文件中写好要提供注解处理功能的那个类的包名+类名。好比个人是这样写的:
开源实验室工具

3.一、重写两个方法

public class MyAnnotationProcessor extends AbstractProcessor { @Override public Set<String> getSupportedAnnotationTypes() { Set<String> supportTypes = new HashSet<>(); supportTypes.add(Produce.class.getCanonicalName()); return supportTypes; } @Override public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) { boolean isProcess = false; try { isProcess = true; List<ItemData> creatorList = parseProduce(roundEnvironment); genJavaFile(creatorList); } catch (Exception e) { isProcess = false; } return isProcess; } } 

getSupportedAnnotationTypes是用来告诉 APT,我要关注的注解类型是哪些类型。这里只有一个注解@Produce因此咱们的 set 就只添加了一个类型。
process()就是真正用于处理注解的函数,这里我是经过parseProduce()返回了全部被@Produce修饰的方法的信息,就是咱们前面封装的 JavaBean,包含了方法所在类名、方法返回值、方法名、方法参数等信息。
而后再经过genJavaFile()去生成方法对应的跨模块的中间类。

生成类文件

在 APT 中,要生成一个类办法有不少,好比读取某个 Java 文件模板,将文件内的类模板转换成目标代码;可使用square公司开源的javapoet库,经过传参直接输出目标类文件;也能够最简单的直接经过输出流将一个 Java 代码字符串输出到文件中。

好比,写 demo 我就直接用输出 Java 字符串的办法了。(代码节选,删掉多余类声明、try...catch)

private void genJavaFile(List<Item> pageList) { JavaFileObject jfo = processingEnv.getFiler().createSourceFile(PACKAGE + POINT + className); PrintStream ps = new PrintStream(jfo.openOutputStream()); ps.println(String.format("public class %s implements com.kymjs.Interceptor {", className)); ps.println("\tpublic <T> T interception(Class<T> clazz, Object... params) {"); for (Item item : pageList) { ps.print(String.format("if (%s.class.equals(clazz)", item.returnType)); // 省略多参数判断逻辑 for (int count = 0; count < item.params.length; count++) { } ps.println(") {"); ps.print(String.format("\t\t\tobj = (T) %s.%s(", item.className, item.methodName)); // 参数类型判断逻辑 for (int count = 0; count < item.params.length; count++) { } ps.println(");} else "); } ps.println("{\n}return obj;}}"); ps.flush(); } 

最终,就会在工程目录下生成相似这样的一个文件:开源实验室

运行时加载类

本节介绍的内容,相关详细内容建议优先阅读:《优雅移除模块间耦合》这篇我在 droidcon 大会上分享的文字稿。
新类生成好了之后,天然须要让生成的类生效,一般咱们之间使用 ClassLoader 加载咱们生成好的类。而在生效以前的编译阶段,会碰上一个很大的问题:普通的单 module 的 Android 工程使用 APT 不会有任何问题,可是多 module 使用的时候就会发生每一个 module 都有一个包名类名彻底相同的生成类,这就会发生类冲突了。

最简单的解决类冲突的办法就是让每次生成的类,类名都不同。
好比你能够讲类的文件加一个 hashcode或者随机数后缀,这样就基本能避免类冲突问题了(只能说基本,毕竟hashcode、random也有重复的概率)。

可是若是类名不同的话,如何在运行时经过 ClassLoader 加载一个不知道类名的类呢?有两种办法,一种是经过接口遍历,给每一个 APT 生成的类一个空接口父类,在运行时遍历全部类的父接口,是不是这个接口的,若是是就用ClassLoader加载他;另外一种办法是经过类前缀,好比让全部类都有一个特殊的前缀,在运行时就能知道全部 APT 生成类了。
这种方法对应的代码我能够给你们看一下(节选,删掉某些不重要的代码):

private void getAllDI(Context context) { mInterceptors.writeLock().lock(); try { ApplicationInfo info = context.getPackageManager().getApplicationInfo(context.getPackageName(), 0); String path = info.sourceDir; DexFile dexfile = new DexFile(path); Enumeration entries = dexfile.entries(); byte isLock = NONE; while (entries.hasMoreElements()) { String name = (String) entries.nextElement(); if (name.startsWith(PACKAGE + "." + SUFFIX)) { threadIsRunned = true; if (isLock <= 0) { mInterceptors.writeLock().lock(); isLock = LOCK; } Class clazz = Class.forName(name); if (Interceptor.class.isAssignableFrom(clazz) && !Interceptor.class.equals(clazz)) { mInterceptors.add((Interceptor) clazz.newInstance()); } } else { if (isLock > 0) { mInterceptors.writeLock().unlock(); isLock = UNLOCK; } } } } catch (Exception e) { e.printStackTrace(); } finally { mInterceptors.writeLock().unlock(); } } 

因为遍历全部类是一个耗时操做,因此一般咱们将其放在线程中,所以还须要保证多个线程的线程安全问题,防止类尚未被 ClassLoader 加载,就已经去访问这个类的状况。

另外一种实现方式就是经过额外的 gradle 插件,在编译期讲全部 APT 生成类找到,记录到某个类中,这样就能够在加载的时候避免遍历全部类这步耗时操做。或者,若是实际需求中 APT 生成类中的内容是容许乱序的,好比本例中将全部类中加了@Produce 注解的方法记录下来这样的操做,也能够在编译期,将全部 APT 生成的类的内容集中到一个统一的类中,在运行时加载这个固定类(事实上咱们就是这么作的),这样就能大大提升初始化时的速度了。

相关文章
相关标签/搜索