Android AOP 编译时注解开发

作者: 子非鱼的那个鱼 | 来源:发表于2017-12-09 00:09 被阅读1051次

    前言

    公司项目要开发自己的路由模块,在研究了已有的开源库和进行模拟之后,选择使用编译时注解来开发,好处主要是对已有的代码入侵程度最小也更灵活.之前一直都用别人现有的注解类的库直接开发,虽然知道一些原理,但是毕竟没有实践经验,理解并不深.结合这次的机会,对开发流程做一个记录.
    像我这么懒的人,写文章其实也有一点无奈.之前看过一些趣文,也看过斯坦福的 Android 开发公开课,米国中情局的内部黑客培训教程,从最开始的环境搭建开始讲,中间会细致到使用软件中的某个步骤的按钮在哪里都会标出来,清清楚楚,斯坦福的教授讲解时用的 eclipse 开发,跳转代码的快捷键是什么都会跟同学说,还会说忘记快捷键应该怎么操作.然后呢,就不得不吐槽一下网络上搜到的技术文的文风,经常是一个开发流程可能分为 ABC 三个步骤,博文直接从 B 开始讲了,B 怎么来的,原理是什么,也没有说明,整个文章看下来中间也有代码,也有截图,但是就是感觉无从下手,没办法得到博主的结果,感觉很无厘头.看了还不如不看,本来是带着一个 怎么弄 的问题去搜文章,看完了脑子里全变成了 为什么.

    理论

    主要是参考了 codeKK -- 公共技术点之 Java 注解 Annotation 这篇文章,对不太了解的关键词进行了搜索查阅,同时结合 butterknifeEventBus 两个开源库的源码帮助自己更快的理解上手.
    关于理论这方面每个人的理解能力不一样,我通常是在脑海里给自己做类比或者建模,自己的讲解可能会让人更困扰,就不在这里多说了,推荐的文章说的很详细,我也未必讲解的比别人更清楚.
    需要说明的是不管是 butterknife 使用的编译时注解,还是 EventBus 使用的运行时注解都会用到反射技术,所以注解的应用还是看开发项目时的取舍.

    分析

    编译时注解,主要的开发工作是对 自定义的注解 进行 编译时 处理,这么说明可能有点绕口,下面会用一个完整的示例来解释这句话,现在需要知道三点:

    1. 定义 注解
    2. 处理 注解
    3. 操作 处理结果

    工具

    AndroidStudio 最新稳定版本(3.0.1)即可

    开发

    1. 新建一个 Android 项目,按照提示新建一个空 activity,然后一路点击 next 即可
      image.png
    1. 在工具栏,选择 file -> new -> new Module... 在弹出框中选择 Java Library ,新建一个用来编写注解的java库工程 -- router-annotations

      2.png
    2. 新建一个注解,其中 @Target 表示该注解可以用在哪些元素上,这里标记的是 Type 也就是 (稍后会看到使用), @Retention 注解保留策略,编译时注解使用 RetentionPolicy.CLASS ,给这个注解定义了一个默认值为 heheda 的参数成员 name()

      3.png
    3. 同步骤2,新建一个进行注解处理的java库工程 -- router-compiler

      4.png
    4. 这时候可能就会开始有为什么了

      1. 为什么要建两个库工程
      2. 这两个工程有什么区别

      问题1:我们先来看看 butterknife 的使用说明, 依赖声明中添加了两个声明

      compile 'com.jakewharton:butterknife:8.8.1' // api库
      annotationProcessor 'com.jakewharton:butterknife-compiler:8.8.1' //字面意思是注解处理器
      

      如果我们不引用这个注解处理器的库, 只引用 api 这个库,构建之后,在项目的依赖目录下和引用注解处理器库是没有任何区别的,并没有增加任何信息, 编码的时候也可以各种正常使用,比如调用api库里面的类和注解进行编码, 但是项目运行之后,使用了 butterknife 注解的变量就会报空指针异常,这说明了两个问题:首先注解处理器库不管里面有什么不会打到包里面去,因为看不到任何类;其次,它是用来处理注解的,什么时候处理,绕来绕去其实就是说 注解处理器在项目编译时处理了注解 ,然后它就不再参与打包流程.废话说的有点多有点绕,可以自行搜索 annotationProcessor 加强个人的理解.
      那么问题2:步骤2和步骤4建的项目有什么区别,首先既然都是 java library 那最后能提供的都是jar,问题缩小到jar之间有什么区别,我就不贴图了,直接按照我的理解说,jar包除了提供编译好的类(在包目录下)之外,还有一个 META-INFO 的目录,有一个 MANIFEST.MF 文件(其实是可有可无的), 但是 能声明为 annotationProcessor 的jar在该目录下会多出一个 services 的目录,里面有一个固定为 javax.annotation.processing.Processor 的文件,文件中会一行行列出声明为注解处理器的类的路径,方便编译器加载类.(在这说一下,有些文章说 "继承自AbstractProcessor(下面会说到)的类会自动参与编译时注解处理" 我觉得有点问题,如果所有继承自AbstractProcessor的类会自动参与注解处理,难道编译器会遍历一个个的子类加到编译器中?) 我没有继续做深入的理论研究,现在我们只需要知道这么多,上面的话引出另一个问题,如何才能生成上面说了那么多废话的目录,文件,以及文件内容.很显然如果是固定套路就应该是一个自动化的流程,google为我们提供了相关库,把下面的依赖信息添加到 router-compiler 的依赖声明中

      compile project(':router-annotations')
      compile 'com.google.auto.service:auto-service:1.0-rc2' //提供 @AutoService() 注解
      

      第一个依赖是让 router-compiler 可以调用到刚刚在 router-annotations 中生成的注解,第二个依赖就是用来解决上面所说的问题的, google开发的这个库可以在编译jar包的时候把上面所说的信息自动生成(也是用的注解机制)

    5. 然后就是对刚刚定义好的 @Test 注解进行处理的编码工作,定义一个 AbstractProcessor 的子类, 加上 @AutoService(Processor.class) 该注解来自上面说到的google提供的包

    @AutoService(Processor.class)
    public class TestProcessor extends AbstractProcessor {
    }
    
    1. 重写相关的方法,首先是java版本,其次是要告诉编译器 -- 本处理器能处理哪些注解,当前只有一个注解,所以我们返回刚才声明的 @Test
        /**
         *
         * @return 声明java版本
         */
        @Override
        public SourceVersion getSupportedSourceVersion() {
            return SourceVersion.RELEASE_7;
        }
    
        /**
         *
         * @return 支持处理的注解集合
         */
        @Override
        public Set<String> getSupportedAnnotationTypes() {
            Set<String> annotations = new LinkedHashSet<>();
            annotations.add(Test.class.getCanonicalName());
            return annotations;
        }
    
    1. 重写 process 函数,开始我们的处理逻辑.返回值 true 表示该注解已经被处理完了,不需要继续传递给其他注解处理器( AbstractProcessor 的子类), false 相反,会继续传递
    @Override
        public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
            return false;
        }
    
    1. @Test 的作用域声明为 Type 所以我们处理 CLASS 种类的元素
        @Override
        public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
            if (set.isEmpty()) {
                return false;
            }
            Set<? extends Element> elements = roundEnvironment.getElementsAnnotatedWith(Test.class);
            try {
                processTestAnnotationClass(elements);
            } catch (IOException e) {
                e.printStackTrace();
            }
            return true;
        }
    
        private void processTestAnnotationClass(Set<? extends Element> elements) throws IOException {
            for (Element classElement : elements) {
                switch (classElement.getKind()) {
                case CLASS:
                    //todo 处理逻辑
                    generateClassAnnotation(classElement);
                    break;
                }
            }
        }
    
    1. 这里我的处理的逻辑是在 指定的包 下面生成一个 指定名称 的java类,并且重写 toString() 函数,返回一些信息. 关于动态编写代码,这里使用的是 com.squareup:javapoet:1.9.0 库 (相关用法可以查阅对应的文档, 刚接触不是很了解, 使用该库的相关api,可以免去自己编写 import 的语句,还是很方便的),同时我希望返回的信息是有结构的,所以这里也引用了 Android 里面使用的 apache 的 json 库,
    compile 'com.squareup:javapoet:1.9.0'
    compile 'org.json:json:20160810'
    

    添加到 router-compiler 的依赖声明中.这里的返回信息是被注解的类的路径和声明 @Test 注解时添加的 name() 成员信息 (其实查阅相关api可以直接用其他方式直接返回 xxxActivity.class,这里只是为了演示获取 被注解的类的相关信息 的用法)

        private void generateClassAnnotation(Element classElement) throws IOException {
            /*   获取到注解对象,可以读取定义的信息  */
            Test test = classElement.getAnnotation(Test.class);
    
            /* 填充要记录的信息 */
            Map<String, Object> params = Maps.newHashMap();
            //获取被注解的 class 的完整路径
            params.put("p", ClassName.get((TypeElement) classElement).reflectionName());
            //获取标记注解时输入的信息
            params.put("n", test.name());
    
            //以下类信息来自 com.squareup:javapoet:1.9.0 主要用来动态生成代码,推荐使用
            //方法生成器
            MethodSpec.Builder toStringBuilder = MethodSpec.methodBuilder("toString")
                    .addAnnotation(Override.class)
                    .addModifiers(javax.lang.model.element.Modifier.PUBLIC)
                    .returns(String.class)
                    .addStatement("return $S", new JSONObject(params).toString());
    
            //类生成器
            TypeSpec typeSpec = TypeSpec.classBuilder(classElement.getSimpleName().toString())
                    .addJavadoc("DO NOT EDIT THIS FILE!")
                    .addModifiers(javax.lang.model.element.Modifier.PUBLIC)
                    .addMethod(toStringBuilder.build())
                    .build();
        }
    
    1. 要生成的类和重写函数都有了,这个时候就要开始生成java文件了,重写 AbstractProcessorinit 函数,从编译环境参数中获取 Filer 对象,该对象可以在编译时写文件,将类和函数组装在一起,使用 JavaFile 就可以写文件了, 第一个参数是 10 里面说到的 指定的包 路径声明,第二个参数是java类的信息(包含类定义和重写的 toString() 函数),最后通过 Filer 输出文件
        /**
         * 编译时可以做写操作的封装类
         */
        private Filer mFiler;       // File util, write class file into disk.
    
        @Override
        public synchronized void init(ProcessingEnvironment processingEnvironment) {
            super.init(processingEnvironment);
            mFiler = processingEnvironment.getFiler();
        }
    
        private void generateClassAnnotation(Element classElement) throws IOException {
            /*   获取到注解对象,可以读取定义的信息  */
            Test test = classElement.getAnnotation(Test.class);
    
            /* 填充要记录的信息 */
            Map<String, Object> params = Maps.newHashMap();
            //获取被注解的 class 的完整路径
            params.put("p", ClassName.get((TypeElement) classElement).reflectionName());
            //获取标记注解时输出的信息
            params.put("n", test.name());
    
            //以下类信息来自 com.squareup:javapoet:1.9.0 主要用来动态生成代码,推荐使用
            //方法生成器
            MethodSpec.Builder toStringBuilder = MethodSpec.methodBuilder("toString")
                    .addAnnotation(Override.class)
                    .addModifiers(javax.lang.model.element.Modifier.PUBLIC)
                    .returns(String.class)
                    .addStatement("return $S", new JSONObject(params).toString());
    
            //类生成器
            TypeSpec typeSpec = TypeSpec.classBuilder(classElement.getSimpleName().toString())
                    .addJavadoc("DO NOT EDIT THIS FILE!")
                    .addModifiers(javax.lang.model.element.Modifier.PUBLIC)
                    .addMethod(toStringBuilder.build())
                    .build();
            //往 com.ll.support.router.table 包下面通过 mFiler 写 java 文件
            JavaFile.builder("com.ll.support.router.table", typeSpec).build().writeTo(mFiler);
        }
    
    1. 验证结果.添加相关的依赖到 Android app 工程,注解处理器需要对应的声明 annotationProcessor project(':router-compiler') ,但是这里的工程用到了kotlin(包括纯kotlin和java混合kotlin),所以这里是使用的是 kapt .注解处理器里面 jdk 声明的是 7, 所以标记开发环境是 java7, 然后进行 gradle 同步,依赖构建完成之后,在 app 工程中的 MainActivity 类声明我们的注解 @Test ,最后进行 build -> reBuild 或者直接 run
        apply plugin: 'com.android.application'
        apply plugin: 'kotlin-android'
        apply plugin: 'kotlin-android-extensions'
    
        android {
            compileSdkVersion 26
            defaultConfig {
                applicationId "com.ll.router"
                minSdkVersion 14
                targetSdkVersion 26
                versionCode 1
                versionName "1.0"
                testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
            }
            buildTypes {
                release {
                    minifyEnabled false
                    proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
                }
            }
            compileOptions {
                sourceCompatibility JavaVersion.VERSION_1_7
                targetCompatibility JavaVersion.VERSION_1_7
            }
        }
    
        dependencies {
            testImplementation 'junit:junit:4.12'
            androidTestImplementation 'com.android.support.test:runner:1.0.1'
            androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.1'
            implementation fileTree(dir: 'libs', include: ['*.jar'])
            implementation"org.jetbrains.kotlin:kotlin-stdlib-jre7:$kotlin_version"
            implementation 'com.android.support:appcompat-v7:26.1.0'
            implementation 'com.android.support.constraint:constraint-layout:1.0.2'
    
            kapt project(":router-compiler")
            compile project(":router-annotations")
        }
    
    12.png
    1. 成功之后打开 app\build\generated\source\kapt\debug\com\ll\support\router\table 目录(如果使用的 annotationProcessor 声明,那 kapt替换成apt目录),就可以看到我们动态生成的java类了,同时查看我们的 apk 文件,也可以在 10 里面的 指定的包 下面找到我们的类.
      image.png
    13-1.png

    这里面没有对分析的第三步进行讲解 -- 操作处理结果.这个地方每个人的需求不同,就不再展开了,类似于 butterknife 自己开发相关 api 处理库就可以了.

    开发流程就是上面所说的,掌握了这些操作之后,剩下的就是各种脑洞了.最后说一下在实践中会遇到的非理论性问题:经常是刚刚创建了 module 但是 Android studio 无法识别,我的解决办法是,在 settings.gradle 文件中删掉 module 声明,然后同步,再加上 module 声明再同步,或者干脆退出重新打开 studio,多试几次之后才会好,具体原因还不知道,世界上总有那么多未解之谜,只要中间不迷惑走到头就行了.另外这一招也可以用在重命名项目上.

    相关文章

      网友评论

      本文标题:Android AOP 编译时注解开发

      本文链接:https://www.haomeiwen.com/subject/bwiyextx.html