Nara: 大搜车的注解收集器

daiyibo  |  2018. 10. 29   |  阅读 2086 次

前言

本文主要是对编译时注解展开讨论。Android端对自定义注解的应用程度远没有达到Java Web端的那种程度,而且市面上也没有一个专注于抽象编译时注解底层技术的库。

在真实开发场景中,如果想要实现一个利用编译时注解的功能,往往需要花大量精力来处理APT这块的繁琐逻辑。例如,遍历所有注解相关类然后生成一定规律的代码。这些繁琐操作阻碍了大家使用注解的热情。设想一下,如果使用编译时注解只需要增加一两行代码,那你会不会更愿意使用注解呢?

所以,我们在想是否能够对编译时注解做进一步的抽象,帮助开发者将重点放在具体的功能实现上,而不是处理编译时注解的繁琐操作上?

Nara的诞生

在动手之前,需要先弄清楚大家利用注解都做了哪些事情。清楚注解的使用场景,能帮助我们抽象出一个更好用的注解框架。于是,我们调研了Android端上常用的注解框架,看看他们利用注解都做了什么事情?

  • ButterKnife:例如@BindViews,ButterKnife在编译期,利用@BindView将控件ID和类属性建立对应关系。然后在页面启动时,根据控件ID通过findViewById方法将控件赋值到类属性上。

  • EventBus:通过@Subscribe对普通方法进行标记。然后,EventBus在编译期通过@Subscribe将这些方法收集起来,作为EventBus的订阅方法。

  • Retrofit:通过反射方式处理注解,不再本文范畴。

编译时注解的主要用途在于,收集注解并通过注解信息生成一些有规律的代码。

而这些有规律的代码,一般都是可以通过其他方式来间接实现的。比如说,ButterKnife的属性赋值可以通过反射来实现,EventBus的方法收集可以直接换个写法。所以,如果要对编译时注解做进一步抽象,我们认为可以从注解收集方面入手。由此,Nara注解收集器诞生了。

API的设计

对于底层库而言,设计一套好用的API是很重要的。从库的使用者角度看,希望注解收集器能够帮助我们实现哪些功能呢?

  • 注解的作用范围:通过分析市面上编译时注解的使用场景,发现对注解的应用主要在于类,方法和属性上。
  • 自定义注解:作为注解抽象库,如果无法支持自定义注解,那对可扩展性简直就是致命的打击。所以需要提供自定义注解功能。
  • 注解收集:注解收集作为核心功能,必须要提供一套好用的收集操作。我们考虑用链式Builder来完成注解收集功能。

最终设计的API如下:

/**
 * 自定义注解API
 **/
@Retention(RetentionPolicy.SOURCE)
@Target(ElementType.TYPE, ElementType.METHOD, ElementType.FIELD)
@ShadowBinding
public @interface CustomAnnotation {  
    String value() default "";
}

/**
 * 注解的使用API
 **/
@CustomAnnotation // 类
public class AnnotationClass extends Activity{}

@CustomAnnotation // 函数
public <T> T AnnotationMethod(int p1, String p2, AnnotationClass p3, T p4){  
    Log.e("souche", "test2(" + "p1(" + p1 + "),p2(" + p2 + "),p3(" + p3 + "),p4(" + p4 + "));");
    return p4;
}

@CustomAnnotation // 成员属性
public List<AnnotationClass> annotationField;

/**
 * 收集注解的使用API,以Class的收集举例
 **/
List<ClassDesc> listClass = Nara  
                .findClass(CustomAnnotation.class) // 查找类,参数为其所在注解
                .withExtends(Activity.class) // 筛选条件:查找的类,需要继承Activity类
                .withAnnotations(Collect.class) // 筛选条件:查找的类,需要被@Collect注解
                .filter(new AnnotationFilter<ClassDesc>() { // 自定义筛选条件:return true 表示过滤掉当前类
                    @Override
                    public boolean doFilter(ClassDesc obj) {
                        return false;
                    }
                })
                .list(); // 返回符合条件的类集合

实现的痛点

一个优秀的底层库,应该将复杂实现隐藏起来,并且做到实现对于上层使用者透明。下面简单分享下实现Nara时,遇到的几个技术痛点。

对泛型的支持

在收集注解信息时,会遇到方法参数或者属性中带有泛型信息的情况。为了收集这些泛型信息,我们利用了Gson实现的TypeToken泛型Type转化工具。将TypeToken的相关代码拷贝到了Nara内部。Gson获取Type的方法如下:

Type type = new TypeToken<clazz>() {}.getType();  

被注解对象的包级作用域问题

Nara注解收集是支持包级作用域的信息收集的。这里就存在一个问题,包作用域的类,方法和属性是不允许跨包访问的。那么,如何在不同包下获取包级作用域下的信息呢?我们采取的方式是利用编译时注解生成被注解类的同包名下的类,然后将包级作用域的信息用public的形式暴露出来。如下所示:

// 例如包级作用域的属性
package com.example.souche.annotation;  
public class People {

    @CustomAnnotation
    String name;
}

// 为了将People.name的get和set方法的作用域暴露出来
// 我们在编译时,生成了public作用域的get和set方法
package com.example.souche.annotation;  
/** This class is generated by SouChe Annotation Collection, do not edit. */
public class CompilerCollection$AdaptableClass1540292058755_29  
                    extends com.souche.android.annotation.core.FieldDesc.BeanMethod {

            @Override
            public void set(Object... params){ 
                if (params.length != 1) throw new IllegalArgumentException("set argument format error!");
                com.example.souche.annotation.People.name = (java.lang.String)params[0];
            };

            @Override
            public Object get(Object... params){ 
                if (params.length != 0) throw new IllegalArgumentException("get argument format error!");
                return com.example.souche.annotation.People.name;
            };
}

组件化下的编译时注解问题

我们团队是采用的是模块化的开发形式,模块将代码将aar包上传到maven仓库,主工程再通过gradle来依赖模块aar包。这样就会存在一个问题,由于编译时注解的作用范围是源码级别的,所以无法对aar包中的注解进行收集。

我们的解决思路是,如果模块需要使用Nara注解收集器,在模块打包时,就将Nara相关代码打进aar包,然后在主工程打包时,通过gradle插件扫描整个app的字节码,找到Nara相关代码,将这些代码整合起来,生成一个Nara初始化主类。总体打包流程如下:

编译流程图

框架愿景

Nara是对编译时注解的上层抽象,我们希望通过Nara来降低编译时注解的开发成本。让使用者把精力放在逻辑实现上,而不是在注解信息收集上。

分享到

   
babel-polyfill vs babel-runtime