Android Apt实战

来源:互联网 发布:知乎注册 不用手机号 编辑:程序博客网 时间:2024/06/07 02:41

前言

最近在做一个技术项目,解决的内容是:如何将模块动态注册到一个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编译时注解

原创粉丝点击