Android之APT(Annotation Processing Tools)编译时创建View对象

前言:

上篇文章中讲解了通过IOS(依赖注入)的方式来为View创建对象并设置事件监听 从而简化我们的代码 方便维护 但是其缺点就是:在运行时通过大量的注解反射去执行,在性能上有所欠缺。
本篇将带大家去了解通过APT(Annotation Processing Tools)的方式在.java文件编译为.class文件时动态的创建对象 其优点就是:对象的创建在编译时就已经确立,对程序运行性能上没有影响。目前网上使用APT技术比较火的库有:
butterknife
dagger
Retrofit

APT

介绍:
APT(Annotation Processing Tool):是一种处理注释的工具,它对源代码文件进行检测找出其中的Annotation,使用Annotation处理器进行额外的处理。 Annotation处理器在处理Annotation时可以根据源文件中的Annotation生成额外的源文件和其它的文件(文件具体内容由Annotation处理器的编写者决定),APT还会编译生成的源文件和原来的源文件,将它们一起生成class文件。

Annotation处理器(注解处理器):是一个在javac中的,用来编译时扫描和处理的注解的工具。你可以为特定的注解,注册你自己的注解处理器

核心原理:
编译时Annotation解析的基本原理是,在某些代码元素上(如类型、函数、字段等)添加注解,在编译时javac编译器会检查AbstractProcessor的子类,并且调用该类型的process函数,然后将添加了注解的所有元素都传递到process函数中,使得开发人员可以在编译器进行相应的处理,例如,根据注解生成新的Java类,这也就是butterknife dagger等开源库的基本原理

案例

上面介绍了那么一大堆文绉绉的东西(我看着也烦) ,还是来点代码实际:

public class MainActivity extends AppCompatActivity {
    @BindView(R.id.textView)
    TextView textview;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        InjectView.bind(this);
        textview.setText("我是通过APT编译时生成的对象文件");
    }

写法跟IOC的方式相同,但是实现原理却不一样,使用APT编译后其实是在MainActivity的类里面创建了一个内部类 其中内部类是在编译时通过APT生成的 最终也编译成.class文件:

public class MainActivity extends AppCompatActivity {
    TextView textview;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        InjectView binView=new InjectView();
        binView.bind(this);
        textview.setText("我是通过APT编译时生成的对象文件");
    }

    public class InjectView{
        public void bind(MainActivity mainActivity){
            mainActivity.textview= (TextView) mainActivity.findViewById(R.id.textView);
        }
    }
}

使用

正式开车,在项目中使用APT 需要做如下配置操作

  • 在项目的gradle中配置:
buildscript {
    repositories {
        jcenter()
        mavenCentral()//--->添加mavenCentral仓库
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:2.3.0'
        classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8'//--->导入APT
    }
}
  • 除了你的工程Module 还需要创建一个 Android Library 和两个java Library
  • 创建好后进行引用

首先APP的gradle

apply plugin: 'com.neenbedankt.android-apt'//导入apt插件
dependencies {
    compile project(':injectlibrary')//导入Android Library库
    apt project(':inject-complier')//将java Library(inject-complier) 作为apt
    }

Android Library(injectlibrary)库的gradle

dependencies {
compile project(':inject-annotion')//引入java Library(inject-annotion)java工程
}

inject-complier 的gradle

dependencies {
    compile project(':inject-annotion')////引入java Library(inject-annotion)java工程
    compile 'com.google.auto:auto-common:0.8'
    compile 'com.google.auto.service:auto-service:1.0-rc3'
    compile 'com.squareup:javapoet:1.8.0'//生成java代码的辅助工具类
}
  • 他们之间的引用关系及作用


APP: 这个不用讲是我们的主工程
Android Library: 库里定义了InjectView等始化相关的类
Inject-annotion: 该Java工程里面只负责声明我们需要的自定义注解 比如@Onclick等
Inject-complier: 该Java工程会被APP作为APT插件使用 里面声明了AbstractProcessor的子类(注解处理器) 也是本章的重点

  • 上面步骤做完后就开始写代码 先从java工程Inject-annotion开始

Inject-annotion里面只创建我们需要的自定义注解:

@Target(ElementType.FIELD)//声明在成员变量上面
@Retention(RetentionPolicy.CLASS)//编译时运行
public @interface BindView {
    int value();
}
  • Android Library:里面定义一些初始化的相关类:
public interface ViewBinder <T>{
    void  bind(T tartget);
}
public class InjectView {
    public static void bind(Activity activity) {
        //类名
        String clsName = activity.getClass().getName();
        try {
            //加载内部类
            Class<?> viewBidClass = Class.forName(clsName + "$$ViewBinder");
            //创建内部类对象
            ViewBinder viewBinder = (ViewBinder) viewBidClass.newInstance();
            //执行内部类方法
            viewBinder.bind(activity);
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (InstantiationException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }
    }
}

ViewBinder是一个接口 在APT编译时凡有用到自定义注解的类都会在该类创建一个内部类 并且实现我们定义的ViewBinder接口 并在接口的回调方法里做一些对象创建 初始化的操作。
这时我们在Activity中就可以使用了,但是运行肯定会报错,因为通过@BindViewz注解的成员变量还没赋值

public class MainActivity extends AppCompatActivity {
    @BindView(R.id.textView)
    TextView textview;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        InjectView.bind(this);
        textview.setText("我是通过APT编译时生成的对象文件");
    }
}
  • Inject-complier 本篇的重点注解处理器 同时需要注意的是在编写AbstractProcessor(注解处理器)的时候一定要细心 因为该类会在编译时执行出错的话是没办法调试的 (PS:心里的苦说不出)

首先创建一个类继承 AbstractProcessor(注解处理器) 并做相关初始化操作 注释都做的非常详细 其中@AutoService(Processor.class)这个注解一定不要忘记添加 就是通它来标示该类可以处理我们自定义注解的能力 在JAVAC编译时源码中遇到我们自定义的注解时都会交由这个类来编译处理 这也是为什么我们能在编译时向源码中添加代码的原因

@AutoService(Processor.class)//该标记表明可以处理注解的能力
public class BindViewProcessor extends AbstractProcessor {
    private Elements elementUtil;//处理节点的工具类
    private Types typesUtil;//类型工具类
    private Filer filer;//生成java文件的辅助类

    @Override
    public synchronized void init(ProcessingEnvironment processingEnvironment) {
        super.init(processingEnvironment);
        elementUtil = processingEnvironment.getElementUtils();
        typesUtil = processingEnvironment.getTypeUtils();
        filer = processingEnvironment.getFiler();
    }

    /**
     * 包含本处理器想要处理的注解类型的合法全称。换句话说,你在这里定义你的注解处理器注册到哪些注解上。
     *比如:@BindView @OnClick
     * @return
     */
    @Override
    public Set<String> getSupportedAnnotationTypes() {
        //创建Set集合添加需要支持的自定义注解
        Set<String> types = new LinkedHashSet<>();
        types.add(BindView.class.getCanonicalName());
        types.add(OnClick.class.getCanonicalName());
        return types;
    }

    /**
     * 支持JDK最新版本  
     * @return
     */
    @Override
    public SourceVersion getSupportedSourceVersion() {
        return SourceVersion.latestSupported();
    }

上面的只是一些初始化的操作 真正核心的方法是AbstractProcessor的process方法 整个APP中我们定义的注解都会传递到这里 供我们编程处理 我将分为两段来讲

@Override
public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
    //存放着整个App的所有注解类型 以类为Key 类下的注解成员变量为value
    Map<TypeElement, List<FieldViewBinding>> targetMap = new HashMap<>();
    //遍历整个app的BindView注解成员变量 并将该类 和成员变量保存到Map中
    setElemtBindView(roundEnvironment, targetMap, BindView.class);
    //向源码中添加代码
    addClassAndMethod(targetMap);
    return false;
}

首先setElemtBindView(roundEnvironment, targetMap, BindView.class); 这个方法主要是得到整个app含有@BindView注解的类
并以类名为key 对声明了@BindView注解的成员变量 获取其 id,成员变量名,成员变量类型 保存到一个对象中再添加到List集合 并作为Map的Value

private void setElemtBindView(RoundEnvironment roundEnvironment, Map<TypeElement, List<FieldViewBinding>> targetMap, Class<BindView> annotation) {
        //得到整个app含有@BindView注解的类 Element代表类结构
        for (Element element : roundEnvironment.getElementsAnnotatedWith(annotation)) {
            //获取类名
            TypeElement enclosingElement = (TypeElement) element.getEnclosingElement();
            //根据类名获取所有的注解
            List<FieldViewBinding> list = targetMap.get(enclosingElement);
            if (null == list) {
                list = new ArrayList<>();
                targetMap.put(enclosingElement, list);
            }
            //获取控件id 自定义注解@BindView的value返回值
            int id = element.getAnnotation(annotation).value();
            //获取成员变量的名--->titleText
            String fieldName = element.getSimpleName().toString();
            //获取成员变量类型信息---->TextView
            TypeMirror typeMirror = element.asType();
            FieldViewBinding fieldViewBinding = new FieldViewBinding(fieldName, typeMirror, id);
            list.add(fieldViewBinding);
        }
    }

创建一个类来保存获取到的(成员变量名称,成员变量类型,布局中id值)等相关信息

public class FieldViewBinding {
    private String name; //成员变量名称
    private TypeMirror typeMirror;//成员变量类型
    private int resId;//布局中id值
    
    public FieldViewBinding(String name, TypeMirror typeMirror, int resId) {
        this.name = name;
        this.typeMirror = typeMirror;
        this.resId = resId;
    }
    public String getName() { return name; }

    public TypeMirror getTypeMirror() { return typeMirror; }

    public int getResId() { return resId;}
}

其中涉及到的知识点Element : Element代表一个类的结构 其对应关系

package com.example;      ---> PackageElement

public class 类名 {        ---> TypeElement

    private int 成员变量;   ---> VariableElement
    private Foo 成员变量;   ---> VariableElement
    
    public 构造函数 () {}   ---> ExecuteableElement
    
    public void 普通方法 (  ---> ExecuteableElement
                方法形参    ---> TypeElement
                     ) {}
}

在来看addClassAndMethod(targetMap);方法 该方法就是向源码中添加代码的主要逻辑

private void addClassAndMethod(Map<TypeElement, List<FieldViewBinding>> targetMap) {
        //遍历Map
        for (Map.Entry<TypeElement, List<FieldViewBinding>> item : targetMap.entrySet()) {
            List<FieldViewBinding> list = item.getValue();
            if (null == list || list.size() == 0) {
                continue;
            }
            //类类型 com.example....MainActivity
            TypeElement typeElement = item.getKey();
            //获取包名 com.example...
            String packageName = getPackageName(typeElement);
            //根据包名获取类名 MainActivity
            String className = getClassName(typeElement, packageName);
            //类型 <T extends MainActivity>
            ClassName name = ClassName.bestGuess(className);
            //获取我们定义的接口包名 和类名
            ClassName viewBinder = ClassName.get("com.example.injectlibrary", "ViewBinder");
            //生成java类 MainActivity$$ViewBinder
            TypeSpec.Builder result = TypeSpec.classBuilder(className + "$$ViewBinder")
                    .addModifiers(Modifier.PUBLIC)//将该类声明为public
                    .addTypeVariable(TypeVariableName.get("T", name))//声明该类的类型 <T extends MainActivity>
                    .addSuperinterface(ParameterizedTypeName.get(viewBinder, name));//该类的实现接口 以及接口类型
            //生成方法
            MethodSpec.Builder methodBuilder = MethodSpec.methodBuilder("bind")//方法名 "bind"与ViewBinder接口中的回调保持一致
                    .addModifiers(Modifier.PUBLIC)//方法声明为public
                    .returns(TypeName.VOID)//方法返回值为void
                    .addAnnotation(Override.class)//方法注解 实现接口方法
                    .addParameter(name, "target", Modifier.FINAL);//参数类型(MainActivity) 参数名 参数修饰符
            //遍历该类下声明了@BindView注解的成员变量List集合
            for (int i = 0; i < list.size(); i++) {
                FieldViewBinding fieldViewBinding = list.get(i);
                    //成员变量类型信息 --android.text.TextView
                    String packageNameString = fieldViewBinding.getTypeMirror().toString();
                    //得到成员变量的类名---TextView
                    ClassName viewclassName = ClassName.bestGuess(packageNameString);
                    //方法里面添加执行逻辑 $L $T 占位符 参数顺序一定要对 以及“target”一定要与上面的行参保持一致 代表的就是mainActivity
                    //相当于:mainActivity.textview= (TextView) mainActivity.findViewById(R.id.textView);
                    methodBuilder.addStatement("target.$L=($T)target.findViewById($L)", fieldViewBinding.getName(), viewclassName, fieldViewBinding.getResId());
            }
            result.addMethod(methodBuilder.build());//往类里面添加方法
            try {
                //生成Java类信息 包名 类
                JavaFile.builder(packageName, result.build())
                        .addFileComment("auto create make")//类注释
                        .build()
                        .writeTo(filer);//写出
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
/**
 * 获取类名(通过截取包名获取 若是内部类会将"."替换为"$")
 * @param typeElement
 * @param packageName
 */
private String getClassName(TypeElement typeElement, String packageName) {
        int packageNameLength = packageName.length() + 1;
        return typeElement.getQualifiedName().toString().substring(packageNameLength).replace(".", "$");
 }

/**
 * 获取包名
 * @param enclosingElement
 * @return
 */
 private String getPackageName(TypeElement enclosingElement) {
        return elementUtil.getPackageOf(enclosingElement).getQualifiedName().toString();
}

到这里就完成了 试着运行一次

public class MainActivity extends AppCompatActivity {
    @BindView(R.id.textView)
    TextView textview;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        InjectView.bind(this);
        textview.setText("我是通过APT编译时生成的对象文件");
    }
}

同时我们打开项目编译的class文件也可以看到通过APT插件生成一个内部类


总结:

使用APT javaC在对.java文件进行编译的时候会对源代码文件进行检测找出其中的我们自定义的注解 并交由我们定义的注解处理器进行额外的处理(往源码中添加类 方法等) 最后在通过APT编译成class文件 交由JVM虚拟机运行
(本案例针对控件的初始化操作 就是通过自定义注解处理器 在process方法中向源码中添加控件的初始化话的相关代码 从而达到在编译时就确定了对象的初始化)
优点:

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

推荐阅读更多精彩内容