前言
最近在做一个技术项目,解决的内容是:如何将模块动态注册到一个HashMap中。具体解释一下,项目的页面全部是模块化的,一个页面有若干个模块,这些模块分散在不同的业务库中,一个页面需要通过配置才能获取它自己的模块,配置文件(config)一般与Activity在一个库内。因为Activity所在库无法依赖所有的库,模块注册在config中,以key+class全路径形式存储,读取时通过反射获取模块。key对应模块的标志,一个页面由若干个key组成,每个key对应一个模块class。
问题是:一个页面的所有模块都要注册到一个文件中,并且以反射的形式读取,很可能出现,反射类的路径写错,或者反射模块移动了位置,导致运行时找不到;存在一个模块在多个页面被使用,有多个key;页面越来越多,配置文件也越来越多,管理复杂性不断上升。所以,一个页面的模块集中在一个config中管理很费劲,问题越来越多。
基于以上问题,我们打算模块的注册由模块自己决定,模块的key写在模块内部,在编译时或运行时生成一个大而全的配置文件,换句话说,就是将“将模块动态注册到一个HashMap中”,注册信息由模块自己决定。
技术实现分析
1、模块如何标志自己的key
模块要标志自己的key,并且支持多个key,这个信息要在编译时或运行时可以容易获取。
首先想到是模块提供一个静态数组,里面有key信息,这种做法必须运行时才能获取,使用有局限,运行时要要扫描所有模块类,这也很难实现。
最终定的方案:通过注解的方式标志key,首先定义注解类,然后在模块类的头部填写注解信息,如下所示:
注解类——AgentKey:
@Retention(RetentionPolicy.CLASS)
@Target(ElementType.TYPE)
public @interface AgentKey {
public String[] Keys();
}
模块LoopAgent注册Key信息:
@AgentKey(Keys = {"loop1","loop2","loop3","loop4"})
public class LoopAgent {
}
2、编译时还是运行时生成Map?
设想一下完全在运行时生成模块注册Map可行不………………,思考5秒钟发现不行,这种做法太坑。编译时如何做?有两种方案:
(1)全局gradle插件
在编译时扫描所有代码库,找到基类和注解符合要求类,读取注解信息,获取key和类的全路径,将这些信息汇总,在gradle生成注册代码(String 文本),通过Javassist将Map信息插入到全局Config中。
这种方法其实是完美方案,所有操作都在编译时完成,虽然影响了一点编译速度,但可控性、稳定性、方便性都是最好的。但是由于某种非技术原因,这个插件无法放到壳的build.gradle中,所以只能遗憾另寻出路。
(2)apt——单个库分别注册
单个库分别注册,也可以用gradle插件实现,这里我主要讲一下apt实现。单个库注册如何理解,每个引入模块化页面的库,都可以引入插件,在编译这个库时,插件会扫描这个库内部所有的类,满足注解要求的类,会读出他们的注解key信息,将key和类的信息存储这个库的编译资源中。在运行时,有个工具类专门负责将这些信息搜集起来,形成一个注册的Map。
这种方案其实是编译时每个库生成信息,运行时汇总信息,编译时使用Apt处理注解信息最为方便,对编译流程侵入性最小,还是那句话,用gradle插件也可以实现。
Apt插件
apt开发流程大家可以参考一些博客,如利用APT实现Android编译时注解,里面写的比较全。
1 将信息存储在资源文件中
apt插件最关键的是写Processor,Processor继承自AbstractProcessor,在编译时会执行,通过简单调用就可以获取注解的类信息。
难点在于我们要将key和模块class信息存储在这个库的打包文件中(如 aar),这些信息可以在打包成apk时汇总到apk中,我们知道apk的结构:资源文件+dex。dex文件其实是重新压缩的jar文件,里面只有class文件。资源文件是可以自动汇总到apk里面的,我们只有将key和class全路径信息,在编译时作为资源,写入到aar中。
总结一句话:编译时将生成信息写入到资源文件中!
2 如何获取资源路径
确定了将编译时生成的信息写入资源文件中(最后确定写入assets文件夹下面),那如何获取写入路径呢?apt居然没有相关函数,这是一个大坑!也就是说我们无法获取现在的编译脚本执行路径,无法获取资源路径。
历尽千辛,终于找到方案了,首先通过脚本创建可以临时文件,临时空的类文件,然后从类文件中读取存储路径,再根据Android Studio的文件路规律,获取Assets文件夹路径,如下所示。
final FileObject fo = processingEnv.getFiler().createResource(StandardLocation.CLASS_OUTPUT, "", "test");
String temFilePath = fo.toUri().getPath();
String outputPath = temFilePath.substring(0, temFilePath.indexOf("build/intermediates/classes"));
outputPath = outputPath + "src/main/assets/agentkeyvalue/";
通过上面的方法,我们获取了assets文件路径,然后就可以愉快的写入注册文件了,完整的process函数代码代码如下:
@Override
public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
messager.printMessage(Diagnostic.Kind.NOTE, "process...");
Set<? extends Element> agentKey = roundEnvironment.getElementsAnnotatedWith(AgentKey.class);
if (agentKey == null || agentKey.size() == 0) {
messager.printMessage(Diagnostic.Kind.NOTE, "none...");
return true;
}
List<String> keyValueContent = new ArrayList<>();
for (Element element : agentKey) {
if (element.getKind() != ElementKind.CLASS) {
error(element, "invalid kind type", element);
continue;
}
TypeElement variableElement = (TypeElement) element;
//full class name
String fqClassName = variableElement.getQualifiedName().toString();
messager.printMessage(Diagnostic.Kind.NOTE, "fqClassName name " + fqClassName);
AgentKey agentKeyAnnotation = variableElement.getAnnotation(AgentKey.class);
String[] keys = agentKeyAnnotation.Keys();
for (int k = 0; k < keys.length; k++) {
keyValueContent.add(keys[k] + ";" + fqClassName);
messager.printMessage(Diagnostic.Kind.NOTE, "key: " + keys[k] + " value: " + element.getSimpleName());
}
}
try {
if (keyValueContent.size() == 0) {
return true;
}
messager.printMessage(Diagnostic.Kind.NOTE, processingEnv.toString());
final FileObject fo = processingEnv.getFiler().createResource(
StandardLocation.CLASS_OUTPUT, // -d option to javac
"",
"test");
String temFilePath = fo.toUri().getPath();
String outputPath = temFilePath.substring(0, temFilePath.indexOf("build/intermediates/classes"));
outputPath = outputPath + "src/main/assets/agentkeyvalue/";
File assertsFile = new File(outputPath);
if (assertsFile.exists()) {
if (assertsFile.delete()){
messager.printMessage(Diagnostic.Kind.NOTE, "delete assets success");
}else {
messager.printMessage(Diagnostic.Kind.NOTE, "delete assets failed");
}
}
if (!assertsFile.mkdir()) {
messager.printMessage(Diagnostic.Kind.NOTE, "mkdir assets failed");
return false;
}
File keyValueFile = new File(outputPath + keyValueContent.get(0));
if (keyValueFile.exists()) {
keyValueFile.delete();
}
keyValueFile.createNewFile();
String[] valueKeyArrays = new String[keyValueContent.size()];
writeLineFile(keyValueFile.getAbsolutePath(), keyValueContent.toArray(valueKeyArrays));
messager.printMessage(Diagnostic.Kind.NOTE, outputPath);
} catch (Exception ex) {
}
return true;
}
总结
这篇文章主要讲解如何通过apt插件解决实际问题,如何在编译时获取资源文件路径,对如何配置apt,apt使用具体方法没有仔细讲解。
目前apt仍然可以使用,有些问题可以通过gradle插件来解决,或者使用Google的编译时注解框架来做也是可以的。
大家可以clone下我的demo项目,运行起来,感受一下apt的魔力,地址:
https://github.com/d198965/AgentRegister
apt使用教程可参考利用APT实现Android编译时注解
参考文献:
利用APT实现Android编译时注解