自定义Android注解解析器

前言

本文是IOC系列文章的第五篇,也是最后一篇,也是最重要的一篇。之所以说最重要,是因为掌握自定义注解解析器是所有Android架构师必备的技能,没有一个Android架构师说自己不会自定义注解解析器的,另外掌握注解解析器更加有助于我们理解那些优秀的开源框架,像Retrofit、EventBus和Dagger2等等。本文将详细给大家带来关于自定义注解解析器的知识。

APT的工作流程

在上一篇文章Butterknife源码全面解析的最后简单介绍了一下APT技术,这里我再给大家讲讲APT的工作流程。在代码编译阶段(javac),会扫描所有AbstractProcessor的已注册的子类,并且会调用其process()方法,在该方法中我们可以解析注解并生成java文件,然后在程序调用我们生成的代码即可,其大致流程如下图所示

apt大致流程.jpg

对我们开发者来说,最核心的目的就是实现AbstractProcessor并生成相关代码。

AbstractProcessor

实现自定义注解必须要掌握的就是AbstractProcessor,它是虚处理器,运行在单独的JVM中,使用AbstractProcessor对象必须要有javax环境。

AbstractProcessor有四个重要的方法:

  • init(ProcessingEnvironment processingEnvironment):会被注解工具所调用,并传入ProcessingEnvironment参数,通过ProcessingEnvironment参数我们可以拿到Filer和Messager等工具类,Filer看名字就知道是文件,生成java代码的时候使用,Messager是用来输出日志信息的。

  • process(Set<? extends TypeElement> annotations, RoundEnvironment env) :AbstractProcessor中最重要的方法,相当于main()函数,通过RoundEnvironment参数我们可以获得所有被注解标注的程序元素(包、类、成员变量、构造方法、成员方法...),在这里我们可以解析这些程序元素并生成java文件

  • getSupportedAnnotationTypes():需要解析的注解集合,这里返回的Set<String>,这里可以用@SupportedAnnotationTypes()来代替

  • getSupportedSourceVersion:获得支持的java版本,一般直接返回SourceVersion.latestSupported(),同样可以用@SupportedSourceVersion注解来代替

注册AbstractProcessor

写好了AbstractProcessor我们必须要注册才会生效,如何注册呢?我们必须把注解解析器打包到jar包,需要在META-INF/services路径下生成一个javax.annotation.processing.Processor文件,并在该文件中生成你所声明的Processor对象,是不是听起来很麻烦,其实做起来也很麻烦。谷歌爸爸为了方便广大开发者,特意开发了auto-service库,我们只需要在项目中引入auto-service库,并且在我们声明的
Processor类上面使用@AutoService(Processor.class)即可

@AutoService(Processor.class)
public class BindingProcessor extends AbstractProcessor {

    private Filer mFiler;//文件类
    private Messager mMessager;//打印错误信息
    private static final Map<TypeElement, List<ViewInfo>> bindViews = new HashMap<>();//绑定的view集合

注:这里需要注意的使用gradle版本大于5.0以上

#Sun Apr 26 15:32:47 PDT 2020
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.4-all.zip

在gradle5.0以上会自动忽略auto-service,所以在引入的时候我们需要

    implementation 'com.google.auto.service:auto-service:1.0-rc6'
    annotationProcessor 'com.google.auto.service:auto-service:1.0-rc6'(必须要加上这行代码,否则无法注册成功)

接着,我们可以编译一下项目,然后打开build下面的classes


注册Processor

如图所示,即代表注册成功了

JavaPoet

当我们解析好了注解的程序元素以后就需要生成java文件,一行行手打代码就会很麻烦,java很贴心地推出了JavaPoet开源库,我们只需要引入这个类库即可。它可以很帮助开发者很轻松地生成需要的代码,直接放上几行代码给大家看一下效果
引入类库

    implementation 'com.squareup:javapoet:1.12.1'
            FieldSpec fieldSpec = FieldSpec.builder(String.class,"name",Modifier.PRIVATE).build();//成员变量

            MethodSpec methodSpec = MethodSpec.constructorBuilder()//生成的方法对象
                    .addModifiers(Modifier.PUBLIC)//方法的修饰符
                    .addParameter(className, paramName)//方法中的参数,第一个是参数类型,第二个是参数名
                    .addCode(builder.build())//方法体重的代码
                    .build();

            TypeSpec typeSpec = TypeSpec.classBuilder(typeElement.getSimpleName().toString() + TestBindView.SUFFIX)//类对象,参数:类名
                    .addMethod(methodSpec)//添加方法
                    .addField(fieldSpec)//添加成员变量
                    .build();

            JavaFile javaFile = JavaFile.builder(packageName, typeSpec).build();//javaFile对象,最终用来写入的对象,参数1:包名;参数2:TypeSpec

            try {
                javaFile.writeTo(mFiler);
            } catch (IOException e) {
                e.printStackTrace();
            }

具体用法这里不多说了,JavaPoet使用起来很简单,最好就是先去GitHub上看一下官方文档,接着写一个Hello JavaPoet就能掌握了,送上github地址 JavaPoet

使用Android Studio搭建IOC项目

了解了AbstractProcessor和JavaPoet我们开始搭建IOC项目中,刚才介绍AbstractProcessor时提到了,其必须要有javax环境,而我们默认的Android Module中没有javax,所以必须要建立一个java library要来处理annotation-processor(注解解析器),另外还需要再创建一个java library来处理annotation(注解),这里之所以要把annotation-processor和annotation分开,是因为annotation-processor我们只有在编译期才用到,所以不必要把annotation-processor的相关代码打入到APK中,这里我们通过annotationProcessor方式依赖即可。关于Android module和java library的依赖关系是Android module依赖annotation-processor和annotation,其中annotation-processor又依赖annotation,因为要通过annotation-processor解析annotation中的注解,一图胜千文

ioc项目依赖关系.jpg

注:Demo的github地址在文章的最后

通过自定义注解实现Butterknife的BindView功能

在上一篇文章Butterknife源码全面解析中给大家介绍了Butterknife的源码和实现思路,如图

butterknife实现原理.png

这里我们仿照Butterknife的思路,首先在annotation中声明一个@TestBindView注解

@Retention(RetentionPolicy.CLASS)
@Target(ElementType.FIELD)
public @interface TestBindView {
    int value();
    String SUFFIX = "_TestBinding";
}

然后我们在自定义的BindingProcessor中的init()方法中初始化,Filer和Messager对象

@AutoService(Processor.class)
public class BindingProcessor extends AbstractProcessor {

    private Filer mFiler;//文件类
    private Messager mMessager;//打印错误信息
    private static final Map<TypeElement, List<ViewInfo>> bindViews = new HashMap<>();//绑定的view集合


    @Override
    public synchronized void init(ProcessingEnvironment processingEnvironment) {
        super.init(processingEnvironment);
        mFiler = processingEnvironment.getFiler();//初始化文件对象
        mMessager = processingEnvironment.getMessager();//初始化信息对象

    }

注意:这里Messager对象需要说一下,之所以出现错误要使用Messager对象打印出来,而不采取我们传统的try catch方法是因为往往注解解析器出错会引发一大堆的异常,这个时候如果用try catch会导致我们的异常信息特别多,定位问题很麻烦,而如果使用Messager就会让错误信息简单明了。

下面就来了解析注解并且生成java文件的核心代码了,这里我想讲一下我的思路:

  1. 通过process()方法里面的RoundEnvironment对象拿到所有被我们目标注解(TestBindView)注解的程序元素,然后遍历

2.对程序元素进行分类,用一个Map容器,其中key是TypeElement(类元素,这里就代表我们的Activity),value是一个ViewInfo集合,ViewInfo包含viewId和viewName

3.拿到第二步的Map容器,开始利用JavaPoet生成代码,我们这里的逻辑很简单就是声明一个后缀,通过Activity的名字拼接后缀作生成的文件名,然后在构造方法里面调用findViewById()方法

下面直接上代码

 @Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
        Set<? extends Element> elements = roundEnvironment.getElementsAnnotatedWith(TestBindView.class);//获取TestBindView注解的所有元素
        for (Element element : elements) {//遍历元素
            VariableElement variableElement = (VariableElement) element;//因为注解的作用域是成员变量,所以这里可以直接强转成 VariableElement
            Set<Modifier> modifiers = variableElement.getModifiers();//权限修饰符
            if (modifiers.contains(Modifier.PRIVATE) || modifiers.contains(Modifier.PROTECTED)) {//类型检查
                mMessager.printMessage(Diagnostic.Kind.ERROR, "成员变量的类型不能是PRIVATE或者PROTECTED");
                return false;
            }
            TypeElement typeElement = (TypeElement) variableElement.getEnclosingElement();//获得外部元素对象
            sortToAct(typeElement, variableElement);//以类元素进行分类
        }

        writeToFile();

        return false;
    }

第二步骤分类逻辑

 /**
     * 把view信息跟activity关联在一起
     */
    private void sortToAct(TypeElement typeElement, VariableElement variableElement) {

        List<ViewInfo> viewInfos;
        if (bindViews.get(typeElement) != null) {//判断之前是否存储过这个typeElement的ViewInfo集合
            viewInfos = bindViews.get(typeElement);
        } else {
            viewInfos = new ArrayList<>();
        }
        TestBindView annotation = variableElement.getAnnotation(TestBindView.class);//拿到注解
        int viewId = annotation.value();//获取viewId
        String viewName = variableElement.getSimpleName().toString();//获取viewName
        ViewInfo viewInfo = new ViewInfo(viewId, viewName);//生成viewinfo对象
        viewInfos.add(viewInfo);//放入集合
        bindViews.put(typeElement, viewInfos);//存入map中
    }

第三步骤生成代码

 /**
     * 生成文件
     */
    private void writeToFile() {
        Set<TypeElement> typeElements = bindViews.keySet();
        String paramName = "target";
        for (TypeElement typeElement : typeElements) {
            ClassName className = ClassName.get(typeElement);//获取参数类型
            PackageElement packageElement = (PackageElement) typeElement.getEnclosingElement();//获得外部对象
            String packageName = packageElement.getQualifiedName().toString();//获得包名
            List<ViewInfo> viewInfos = bindViews.get(typeElement);
            CodeBlock.Builder builder = CodeBlock.builder();//代码块对象
            for (ViewInfo viewInfo : viewInfos) {
                //生成代码
                builder.add(paramName + "." + viewInfo.getViewName() + " = " + paramName + ".findViewById(" + viewInfo.getViewId() + ");\n");

            }

            FieldSpec fieldSpec = FieldSpec.builder(String.class,"name",Modifier.PRIVATE).build();//成员变量

            MethodSpec methodSpec = MethodSpec.constructorBuilder()//生成的方法对象
                    .addModifiers(Modifier.PUBLIC)//方法的修饰符
                    .addParameter(className, paramName)//方法中的参数,第一个是参数类型,第二个是参数名
                    .addCode(builder.build())//方法体重的代码
                    .build();

            TypeSpec typeSpec = TypeSpec.classBuilder(typeElement.getSimpleName().toString() + TestBindView.SUFFIX)//类对象,参数:类名
                    .addMethod(methodSpec)//添加方法
                    .addField(fieldSpec)//添加成员变量
                    .build();

            JavaFile javaFile = JavaFile.builder(packageName, typeSpec).build();//javaFile对象,最终用来写入的对象,参数1:包名;参数2:TypeSpec

            try {
                javaFile.writeTo(mFiler);//写入文件
            } catch (IOException e) {
                e.printStackTrace();
            }

        }

注意:以上代码都写入了详细的注释,这里有有两点要说一下
第一:获得被标注的元素注解以后我们要判断一下它的修饰符类型,不能是private或者protected,因为我们要通过Activity对象访问它的控件对象,这一点跟Butterknife是一致的

第二:就是Element.getEnclosingElement()这个方法很重要,是获取元素的封闭元素。啥叫封闭元素呢?举个例子,就是如果你是一个VariableElement(成员变量元素),把你封闭起来的就是TypeElement(类元素);如果你是一个TypeElement的,把你封闭起来的就是PackageElement(包元素);如果你是一个PackageElement,那么返回的就是null了,因为没有东西把包封闭起来。我们注解的作用域是成员变量,所以我们直接拿到VariableElement,然后再通过VariableElement就可以拿到TypeElement和PackageElement,这对我们后面来生成代码非常重要。

BindingProcessor里面的代码写完以后,我们在App module下写一个工具类用来加载生成的java文件同时调用其构造方法

  public class ViewBindUtil {

    /**
     * 绑定Activity
     * */
    public static void bind(Activity activity) {
        if (activity == null) {
            return;
        }
        String activityName = activity.getClass().getName();//获取类的全限定名
        ClassLoader classLoader = activity.getClass().getClassLoader();//获得类加载器
        try {
            Class<?> loadClass = classLoader.loadClass(activityName + TestBindView.SUFFIX);//加载类
            Constructor<?> constructor = loadClass.getConstructor(activity.getClass());
            constructor.newInstance(activity);//调用其构造方法
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

下面我们用一下咱们写的注解解析器

public class MainActivity extends AppCompatActivity {

    @TestBindView(R.id.button)
    Button button;

    @TestBindView(R.id.textView)
    TextView textView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        ViewBindUtil.bind(this);
        button.setText("按钮");
        textView.setText("文本");
    }
}

编译一下项目,先看看生成的代码,有木有问题


生成的代码.jpg

最后运行一下,发现一切OK,这里就不贴图,这样一个简单的TestBindView注解解析器就完成了,本文的重点是不是这个例子,而且教大家如何完成一个注解解析器,以后再有模板类的代码咱们都可以考虑使用这种方法简化,同时对一些优秀的开源如何用注解实现的代码也会更加清楚。

如何调试Processor中的代码

考虑到大家刚开始使用注解解析器难免会存在一些问题,所以如何调试Processor中的代码还是很有必要讲一讲。我们运行时的代码都会调用,点一下绿色的🕷即可,而调试Processor的代码略微麻烦一点点。

调试步骤1.jpg

调试步骤2.jpg

调试步骤三.jpg

调试步骤四.jpg

完成前面四步,接下来
点击一下小蜘蛛.jpg

在Processor代码里打好断点
打好断点.jpg

最后一步,切换成app,点击运行
点击运行.jpg

这里还有一个很重要的点需要强调一下,除了第一次运行外,其余每次运行都必须要在app module下改点代码(哪怕是加个空格),否则,调试不会生效,切记!!!

总结

本文是系列文章IOC(依赖控制翻转)的最后一篇,终于打完收工了,如开篇所说掌握自定义注解解析器非常重要,未来大家要给公司搭建一些项目架构,难免会用到这项技术。本文首先介绍了AbstractProcessor和JavaPoet知识,然后交了如果使用AS搭建一个ioc项目,最后用了一个仿Butterknife的例子手撸一个自定义注解解析器的demo。

最后的最后,放上Demo的github地址:apt_demo,如果您觉得本文还不错,记得给个赞,谢谢~

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,126评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,254评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,445评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,185评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,178评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,970评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,276评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,927评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,400评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,883评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,997评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,646评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,213评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,204评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,423评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,423评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,722评论 2 345

推荐阅读更多精彩内容