Android butterknife源码浅析及APT实际应用

介绍

apt即Annotation Processing Tool,通过在编译期间扫描相关注解最后生成.java文件的一种注解处理工具。

目的

通过自动生成.java文件来简化大量模板代码的书写,提高工作效率。

源码浅析

butterknife Bind Android views and callbacks to fields and methods.
下面就浅析下butterknife是如何实现绑定的
1.首先看下项目结构,我们主要关注3个模块

image.png

  • annotation模块用于定义基础注解
  • apt模块用于处理annotation定义的基础注解,也是核心模块
  • app模块可以运行的demo例子

2.相关模块浅析

  • 注解如何定义
    以经常使用的BindView为例看一下


    image.png

    注解可以理解成一个TAG,更多注解相关的基础知识这篇文章不做细讲


  • apt重点来了

    第一步添加依赖
    implementation project(':butterknife-annotations') 添加注解模块的依赖
    api deps.javapoet 这个javapoet是反向生成代码的利器,后面在细讲
    compileOnly deps.auto.service auto-service是google帮助我们在编写apt代码时自动生成相关必要目录和文件的辅助工具

    第二步看下核心处理类 ButterKnifeProcessor

@AutoService(Processor.class)//这个注解帮助我们做了一些工作,后面再说
/**
*  ButterKnifeProcessor 继承AbstractProcessor ,我们看关键几个重写的方法
*/
public final class ButterKnifeProcessor extends AbstractProcessor {
    @Override
    public Set<String> getSupportedAnnotationTypes() {//设置哪些注解可以支持处理
        Set<String> types = new LinkedHashSet<>();
        for (Class<? extends Annotation> annotation : getSupportedAnnotations()) {
            types.add(annotation.getCanonicalName());
        }
        return types;
    }

    private Set<Class<? extends Annotation>> getSupportedAnnotations() {//具体的注解添加逻辑
        Set<Class<? extends Annotation>> annotations = new LinkedHashSet<>();
        annotations.add(BindAnim.class);
        annotations.add(BindArray.class);
        annotations.add(BindBitmap.class);
        annotations.add(BindBool.class);
        annotations.add(BindColor.class);
        annotations.add(BindDimen.class);
        annotations.add(BindDrawable.class);
        annotations.add(BindFloat.class);
        annotations.add(BindFont.class);
        annotations.add(BindInt.class);
        annotations.add(BindString.class);
        annotations.add(BindView.class);
        annotations.add(BindViews.class);
        annotations.addAll(LISTENERS);
        return annotations;
    }

    //设置生成的.java源码支持的jdk版本
    @Override
    public SourceVersion getSupportedSourceVersion() {
        return SourceVersion.latestSupported();
    }
    //核心处理方法,在这个方法里面生成我们需要的.java文件
    @Override
    public boolean process(Set<? extends TypeElement> elements, RoundEnvironment env) {
        Map<TypeElement, BindingSet> bindingMap = findAndParseTargets(env);//以BindView注解为例主要功能是收集id,变量名,变量类型,以及一些check处理

        for (Map.Entry<TypeElement, BindingSet> entry : bindingMap.entrySet()) {
            TypeElement typeElement = entry.getKey();
            BindingSet binding = entry.getValue();//BindingSet是butterknife的核心类这里面处理各种各样模板代码的生成

            JavaFile javaFile = binding.brewJava(sdk, debuggable, useAndroidX);//组装JavaFile文件
            try {
                javaFile.writeTo(filer);//写出.java文件到build的apt目录下
            } catch (IOException e) {
                error(typeElement, "Unable to write binding for type %s: %s", typeElement, e.getMessage());
            }
        }

        return false;
    }

}

上面的代码是AbstractProcessor 的一些关键的回调,下面我们看下里面用到的核心方法
JavaFile javaFile = binding.brewJava(sdk, debuggable, useAndroidX);//组装JavaFile文件

    JavaFile brewJava(int sdk, boolean debuggable, boolean useAndroidX) {
       //TypeSpec是什么?通俗的讲是类,接口的描述
        TypeSpec bindingConfiguration = createType(sdk, debuggable, useAndroidX);
        return JavaFile.builder(bindingClassName.packageName(), bindingConfiguration)
                .addFileComment("Generated code from Butter Knife. Do not modify!")
                .build();
    }
    /**
     * 最核心的方法,也是javapoet的使用  https://github.com/square/javapoet
     * 为了方便大家理解我把自动生成的代码贴到对应位置,还是以BindView注解为例
     * @param sdk
     * @param debuggable
     * @param useAndroidX
     * @return
     */
    private TypeSpec createType(int sdk, boolean debuggable, boolean useAndroidX) {
        TypeSpec.Builder result = TypeSpec.classBuilder(bindingClassName.simpleName())
                .addModifiers(PUBLIC);//public class SimpleActivity_ViewBinding, 其中addModifiers是增加修饰符
        if (isFinal) {
            result.addModifiers(FINAL);//类不可被继承加final修饰符
        }

        if (parentBinding != null) {
            result.superclass(parentBinding.bindingClassName);//如果有父类添加继承关系
        } else {
            result.addSuperinterface(UNBINDER);//没有父类添加实现  public class SimpleActivity_ViewBinding implements Unbinder
        }

        if (hasTargetField()) {
            result.addField(targetTypeName, "target", PRIVATE);//有target属性   private SimpleActivity target;
        }

        if (isView) {
            result.addMethod(createBindingConstructorForView(useAndroidX));
        } else if (isActivity) {//通过getEnclosingElement()方法拿到包装类,如果是Activity那么执行下面的方法
            result.addMethod(createBindingConstructorForActivity(useAndroidX));//添加构造方法
            /***
             *     @UiThread
             *     public SimpleActivity_ViewBinding(SimpleActivity target) {
             *      this(target, target.getWindow().getDecorView());
             *  }
             */
        } else if (isDialog) {
            result.addMethod(createBindingConstructorForDialog(useAndroidX));
        }
        if (!constructorNeedsView()) {
            // Add a delegating constructor with a target type + view signature for reflective use.
            result.addMethod(createBindingViewDelegateConstructor(useAndroidX)); //deprecated 不看了
        }
        result.addMethod(createBindingConstructor(sdk, debuggable, useAndroidX));//这一句也是添加构造方法,在这个构造函数里面有具体的绑定生成
        /**
         *    @UiThread
        public SimpleActivity_ViewBinding(final SimpleActivity target, View source) {
        this.target = target;

        View view;
        target.title = Utils.findRequiredViewAsType(source, R.id.title, "field 'title'", TextView.class);
        target.subtitle = Utils.findRequiredViewAsType(source, R.id.subtitle, "field 'subtitle'", TextView.class);
        view = Utils.findRequiredView(source, R.id.hello, "field 'hello', method 'sayHello', and method 'sayGetOffMe'");
        target.hello = Utils.castView(view, R.id.hello, "field 'hello'", Button.class);
        view7f06000a = view;
        view.setOnClickListener(new DebouncingOnClickListener() {
        @Override
        public void doClick(View p0) {
        target.sayHello();
        }
        });
        view.setOnLongClickListener(new View.OnLongClickListener() {
        @Override
        public boolean onLongClick(View p0) {
        return target.sayGetOffMe();
        }
        });
        view = Utils.findRequiredView(source, R.id.list_of_things, "field 'listOfThings' and method 'onItemClick'");
        target.listOfThings = Utils.castView(view, R.id.list_of_things, "field 'listOfThings'", ListView.class);
        view7f060012 = view;
        ((AdapterView<?>) view).setOnItemClickListener(new AdapterView.OnItemClickListener() {
        @Override
        public void onItemClick(AdapterView<?> p0, View p1, int p2, long p3) {
        target.onItemClick(p2);
        }
        });
        target.footer = Utils.findRequiredViewAsType(source, R.id.footer, "field 'footer'", TextView.class);
        target.headerViews = Utils.listOf(
        Utils.findRequiredView(source, R.id.title, "field 'headerViews'"),
        Utils.findRequiredView(source, R.id.subtitle, "field 'headerViews'"),
        Utils.findRequiredView(source, R.id.hello, "field 'headerViews'"));

        Context context = source.getContext();
        Resources res = context.getResources();
        target.butterKnife = res.getString(R.string.app_name);
        target.fieldMethod = res.getString(R.string.field_method);
        target.byJakeWharton = res.getString(R.string.by_jake_wharton);
        target.sayHello = res.getString(R.string.say_hello);
        }
         */
        if (hasViewBindings() || parentBinding == null) {
            result.addMethod(createBindingUnbindMethod(result, useAndroidX));//添加unbind方法这个方法都不陌生,ondestory()方法里面经常调用
        }

        return result.build();//最后build()方法在内部 new TypeSpec,这样就完成了TypeSpec的组装。
    }

javapoet的详细使用<-这个是apt的基础也是关键

补充一下 @AutoService(Processor.class)的作用
在使用注解处理器需要先声明以下步骤:
1、需要在 apt 库的 main 目录下新建 resources 资源文件夹;
2、在 resources文件夹下建立 META-INF/services 目录文件夹;
3、在 META-INF/services 目录文件夹下创建 javax.annotation.processing.Processor 文件;
4、在 javax.annotation.processing.Processor 文件写入注解处理器的全称,包括包路径;)
每次这样写太麻烦。这就是用引入auto-service的原因。
通过@AutoService可以自动生成上面的步骤,AutoService注解处理器是Google开发的,用来生成 META-INF/services/javax.annotation.processing.Processor 文件的

  • 使用apt

    第一步添加依赖
    implementation project(':butterknife-annotations')
    annotationProcessor project(':butterknife-compiler') 有没有经常看到这个gradle api?最早的时候是这样的apt project(':butterknife-compiler')现在被废弃了

    第二步代码调用,大家都懂不说了

public class SimpleActivity extends Activity {
 private Unbinder mUnbinder;
  @BindView(R.id.title) TextView title;
  @Override protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.simple_activity);
    mUnbinder= ButterKnife.bind(this);
    title.setText(butterKnife);
  }
  @Override
  protected void onDestroy() {
    super.onDestroy();
    if (mUnbinder != null) {
        mUnbinder.unbind();
      }
    }
}

apt整个流程:定义注解->注解处理器结合javapoet去反向生成.java文件->调用这些.java文件相关代码绑定关系

实际应用

好了最后咱们实际应用下apt,看看项目中可以怎么使用,以自动生成Model层为例


示例项目结构

1.首先新建一个javalib 定义需要的注解

/**
 * 自动生成ApiFactory工具类的注解
 */
@Retention(RetentionPolicy.SOURCE)
@Target(ElementType.TYPE)//作用在类上面
public @interface ApiFactory {
    String path();//获取相关参数
}

2.再建立一个javalib用来处理第一步定义的注解,apt模块

//添加依赖
dependencies {
    compile 'com.google.auto.service:auto-service:1.0-rc2'//自动生成META-INF/services/javax.annotation.processing.Processor 文件
    compile 'com.squareup:javapoet:1.4.0'//反向生成代码的api库
    implementation project(':library-jcannotation')//注解定义module
}
package com.jcgroup.jcapt;

import com.google.auto.service.AutoService;
import com.jcgroup.jcapt.processor.ApiFactoryProcessor;
import com.jcgroup.jcapt.processor.InstanceProcessor;

import java.util.Set;

import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.Filer;
import javax.annotation.processing.Messager;
import javax.annotation.processing.Processor;
import javax.annotation.processing.RoundEnvironment;
import javax.annotation.processing.SupportedAnnotationTypes;
import javax.annotation.processing.SupportedSourceVersion;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.TypeElement;
import javax.lang.model.util.Elements;

@AutoService(Processor.class)//自动生成相关目录文件,简化开发步骤
@SupportedSourceVersion(SourceVersion.RELEASE_8)//java版本支持
@SupportedAnnotationTypes({//标注注解处理器支持的注解类型
        "com.jcgroup.jcannotation.apt.ApiFactory"
})
public class AnnotationProcessor extends AbstractProcessor {
    public Filer mFiler; //文件相关的辅助类
    public Elements mElements; //元素相关的辅助类
    public Messager mMessager; //日志相关的辅助类

    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        mFiler = processingEnv.getFiler();
        mElements = processingEnv.getElementUtils();
        mMessager = processingEnv.getMessager();
        new ApiFactoryProcessor().process(roundEnv, this);
        return true;
    }
}
package com.jcgroup.jcapt.processor;

import com.jcgroup.jcannotation.apt.ApiFactory;
import com.jcgroup.jcapt.AnnotationProcessor;
import com.jcgroup.jcapt.inter.IProcessor;
import com.jcgroup.jcapt.util.Utils;
import com.squareup.javapoet.ClassName;
import com.squareup.javapoet.CodeBlock;
import com.squareup.javapoet.JavaFile;
import com.squareup.javapoet.MethodSpec;
import com.squareup.javapoet.TypeName;
import com.squareup.javapoet.TypeSpec;

import java.io.IOException;

import javax.annotation.processing.FilerException;
import javax.annotation.processing.RoundEnvironment;
import javax.lang.model.element.Element;
import javax.lang.model.element.ExecutableElement;
import javax.lang.model.element.TypeElement;
import javax.lang.model.element.VariableElement;
import javax.lang.model.util.ElementFilter;
import javax.tools.Diagnostic;

import static com.squareup.javapoet.TypeSpec.classBuilder;
import static javax.lang.model.element.Modifier.FINAL;
import static javax.lang.model.element.Modifier.PUBLIC;
import static javax.lang.model.element.Modifier.STATIC;

/**
 * Created by luohai on 2018.6.5.
 */
public class ApiFactoryProcessor implements IProcessor {
    @Override
    public void process(RoundEnvironment roundEnv, AnnotationProcessor mAbstractProcessor) {
        String CLASS_NAME = "ApiFactory";
        String DATA_ARR_CLASS = "DataArr";
        String LIST_CLASS = "ArrayList";
        TypeSpec.Builder tb = classBuilder(CLASS_NAME).addModifiers(PUBLIC, FINAL).addJavadoc("@ API工厂 此类由apt自动生成");
        try {
            for (TypeElement element : ElementFilter.typesIn(roundEnv.getElementsAnnotatedWith(ApiFactory.class))) {
                mAbstractProcessor.mMessager.printMessage(Diagnostic.Kind.NOTE, "正在处理: " + element.toString());
                ApiFactory apiFactory=element.getAnnotation(ApiFactory.class);
                String path=apiFactory.path();
                for (Element e : element.getEnclosedElements()) {
                    ExecutableElement executableElement = (ExecutableElement) e;
                    MethodSpec.Builder methodBuilder =
                            MethodSpec.methodBuilder(e.getSimpleName().toString())
                                    .addJavadoc("@此方法由apt自动生成")
                                    .addModifiers(PUBLIC, STATIC);
                    if (TypeName.get(executableElement.getReturnType()).toString().contains(DATA_ARR_CLASS) || TypeName.get(executableElement.getReturnType()).toString().contains(LIST_CLASS)) {//返回列表数据
                        methodBuilder.returns(ClassName.get("io.reactivex", "Flowable"));
                    } else {
                        methodBuilder.returns(TypeName.get(executableElement.getReturnType()));
                    }
                    ClassName apiUtil = ClassName.get(path+".util", "ApiHelper");
                    CodeBlock.Builder blockBuilder = CodeBlock.builder();
                    String paramsString = "";
                    for (VariableElement ep : executableElement.getParameters()) {
                        methodBuilder.addParameter(TypeName.get(ep.asType()), ep.getSimpleName().toString());
                        if (ep.asType().toString().equals(ClassName.get(path+".base.entity", "RequestBean").toString())) {
                            blockBuilder.add("$L.getBaseInfo($L,isEncrypt)", apiUtil, ep.getSimpleName().toString());
                            paramsString += blockBuilder.build().toString() + ",";
                        } else {
                            paramsString += ep.getSimpleName().toString() + ",";
                        }
                    }
                    methodBuilder.addParameter(TypeName.BOOLEAN, "isEncrypt");
                    methodBuilder.addParameter(TypeName.BOOLEAN, "isDecode");
                    methodBuilder.addStatement(
                            "return  new $L().addResultCodeLogic($T.getInstance()" +
                                    ".service.$L($L)" +
                                    ".compose($T.io_main()),isDecode)", apiUtil
                            , ClassName.get(path+".api", "Api")
                            , e.getSimpleName().toString()
                            , paramsString.equals("") ? paramsString : paramsString.substring(0, paramsString.length() - 1)
                            , ClassName.get("com.jctv.tvhome.util.helper", "RxSchedulers"));
                    tb.addMethod(methodBuilder.build());
                }
            }
            JavaFile javaFile = JavaFile.builder(Utils.PackageName, tb.build()).build();// 生成源代码
            javaFile.writeTo(mAbstractProcessor.mFiler);// 在 app module/build/generated/source/apt 生成一份源代码
        } catch (FilerException e) {
        } catch (IOException e) {
            e.printStackTrace();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

3.使用注解处理器

    implementation project(':library-jcannotation')//注解定义模块
    annotationProcessor project(':library-jcapt')//apt模块

调用自动生成的Model层,这里没有遵循MVP架构,演示直接在View层里面操作了Model层

public class SplashActivity extends AppCompatActivity {
    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        ApiFactory.method1(new RequestBean(),false,false).subscribe(testRequestResponseBean -> {},throwable -> {});
    }
}

看下这个自动生成的Model

/**
 * @ API工厂 此类由apt自动生成 */
public final class ApiFactory {
  /**
   * @此方法由apt自动生成 */
  public static Flowable<ResponseBean<TestRequest>> method1(RequestBean bean, boolean isEncrypt, boolean isDecode) {
    return  new com.jctv.tvhome.util.ApiHelper().addResultCodeLogic(Api.getInstance().service.method1(com.jctv.tvhome.util.ApiHelper.getBaseInfo(bean,isEncrypt)).compose(RxSchedulers.io_main()),isDecode);
  }
}

可以发现类似这部分的代码省掉了
工程里面的Model层.png

最后做个小结吧,合理的利用apt可以省掉大量的模板代码,大大的提高开发效率,而掌握apt编程的核心是熟悉javapoet库的api。

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

推荐阅读更多精彩内容