2. Android 3分钟手写ButterKnife 彻底搞懂 注解处理器 APT 和IOC

今天开始架构师之路!分为6节课,以手写retofit ,Butterknife,Arount,Dagger2,hilit,ASM ,AOP为主

****APT :处理提取和处理 Annotation 的代码统称为(Annotation Processing Tool)

作用

使用APT的优点就是方便、简单,可以少些很多重复的代码。

用过ButterKnifeDaggerEventBus等注解框架的同学就能感受到,利用这些框架可以少些很多代码,只要写一些注解就可以了。

编写注解处理器

   和运行时注解的解析不一样,编译时注解的解析需要我们自己去实现一个注解处理器。

注解处理器(Annotation Processor)是javac的一个工具,它用来在编译时扫描和处理注解(Annotation)。一个注解的注解处理器,以Java代码(或者编译过的字节码)作为输入,生成文件(通常是.java文件)作为输出。而且这些生成的Java文件同咱们手动编写的Java源代码一样可以调用。(注意:不能修改已经存在的java文件代码)。

   注解处理器所做的工作,就是在代码编译的过程中,找到我们指定的注解。然后让我们更加自己特定的逻辑做出相应的处理(通常是生成JAVA文件)。

   注解处理器的写法有固定套路的,两步:

注册注解处理器(这个注解器就是我们第二步自定义的类)。

自定义注解处理器类继承AbstractProcessor。

如何看:java里面的jdk。会在resources的里面有META-INF。里面会有注解处理器的文件!javax.annotation.

所以apt用的是java的lib,不是Android !

注解处理器会在路径Build/classes/java/main/com/META-INF/Services/javax_anontation_process.

APT本质:生成java类

APT(Annotation Processing Tool)即注解处理器,是一种处理注解的工具,确切的说它是javac的一个工具,它用来在编译时扫描和处理注解。注解处理器以Java代码(或者编译过的字节码)作为输入,生成.java文件作为输出。

简单来说就是在编译期,通过注解生成.java文件。

详细解释: 它对源代码文件进行检测找出其中的Annotation,使用Annotation进行额外的处理。 Annotation处理器在处理Annotation时可以根据源文件中的Annotation生成额外的源文件和其它的文件(文件具体内容由Annotation处理器的编写者决定),

APT还会编译生成的源文件和原来的源文件,将它们一起生成class文件。

在这里我们将在每一个业务组件的 build.gradle 都引入ActivityRouter 的 Annotation处理器,我们将会在声明组件和Url的时候使用,annotationProcessor是Android官方提供的Annotation处理器插件,代码如下:

dependencies {

compile fileTree(dir: 'libs', include: ['*.jar'])

annotationProcessor "com.github.mzule.activityrouter:compiler:$rootProject.annotationProcessor"

}

[图片上传失败...(image-c4ef24-1640265031541)]

JavaPoet

更好的方案:通过javapoet可以更加简单得生成这样的Java代码。(后面会说到)

JavaPoet是square推出的开源java代码生成框架,提供Java Api生成.java源文件

需要添加JavaPoet的依赖

<pre style="margin: 8px 0px; background-color: rgb(43, 43, 43); color: rgb(169, 183, 198); font-family: Consolas; font-size: 0.817rem;">implementation'com.squareup:javapoet:1.9.0'</pre>

javapoet的详细内容:

1.方法名

2.返回值

3.打印

<pre style="margin: 8px 0px; background-color: rgb(43, 43, 43); color: rgb(169, 183, 198); font-family: Consolas; font-size: 0.817rem;">MethodSpec main = MethodSpec.methodBuilder("main")
.addModifiers(Modifier.PUBLIC, Modifier.STATIC)
.returns(void.class)
.addParameter(String[].class, "args")
.addStatement("T.out.println(S)", System.class, "Hello, JavaPoet!")
.build(); TypeSpec helloWorld = TypeSpec.classBuilder("HelloWorld")
.addModifiers(Modifier.PUBLIC, Modifier.FINAL)
.addMethod(main)
.build(); JavaFile javaFile = JavaFile.builder("com.example.helloworld", helloWorld)
.build(); javaFile.writeTo(System.out);</pre>

生成的代码

上面的功能一直在完成一件事情,那就是生成Java代码,那么生成的代码在哪?
app/build/generated/source/apt中可以找到生成的Java文件

比如:MainActivity$$ARounter

autoService:主要作用:注册,****相当于安卓的四大组件需要注册一样!

google auto 中的autoservice 可以帮助我们生成对应的配置:使用AutoService注解,可以自动生成meta信息

spi 是一种服务发现的标准,对于开发中我们通常需要编写 META-INF/services 文件夹中定义的类。

自定义注解处理器注册才能被Java虚拟机调用,在上面的博客第四小节中用的方法是手动注册,这比较违反程序员懒的特点,在里面也提到了自动注册的方法,就是AutoService

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

怎么让jvm在编译期间调用我们自己写的这个注解处理器呢?

有一个快捷办法就是使用谷歌的开源库auto,然后使用它提供的AutoService注解来实现,

另外一种办法就是自己手动去创建指定的文件夹,然后配置我们的注解处理器的路径。

注解处理器的debug调试

注解处理器的debug 跟普通的代码debug 有点不同:

在当前工程路径下输入命令

gradlew --no-daemon

-Dorg.gradle.debug=true :app:clean :app:compileDebugJavaWithJavac

并在Edit Configurations 中新添加一个远程配置(remote),名字随意,端口为

5005。然后点击debug 按钮,就可以连接上远程调试器进行Annotation 的调试了。

demo地址:

https://github.com/jaminchanks/AnnotationProcess-Demo

参考博客:

https://www.jianshu.com/p/7af58e8e3e18

https://www.jianshu.com/p/6955a56844d7

点击世界是否会用到动态代理???---另外一种方案!

https://blog.csdn.net/u010008118/article/details/100896148

ButterKnife:黄油刀,被废弃了。现在已经被viewBind和databing取代,依赖注入框架。但是原理很重要!

问题:他和Arounter结合的时候会又什么问题?

R2?为什么会产生这样的问题?

编****译时注解实战: 手写 ButterKnife

4个模块

1.APP :使用

2.annoations:用于定义接口

3.annotation_complier:注解处理器,用于自动生成文件,注解处理器要放在java library里面

annotation_complier继承:abstractProcessor

4. lib :通过反射调用

重写几个方法

然后我们还需要重写AbstractProcessor

getSupportedAnnotationTypes() 方法

getSupportedSourceVersion() 方法

getSupportedAnnotationTypes() 方法用来指定该注解处理器是用来处理哪个注解的

getSupportedSourceVersion()方法用来指定java版本,一般给值为SourceVersion.latestSupported()

注意:如果必要的方法没有写,会导致不执行主要的处理方法

里面重写的方法可以用注解代替, evenbus源码就是这么写的

那我们开始:创建项目,4步搞定

创建Android Module命名为app
创建Java library Module命名为 apt-annotation
创建Java library Module命名为 apt-processor 依赖 apt-annotation
创建Android library Module 命名为apt-library依赖 apt-annotation、auto-service

分析:

1.看下使用流程:1.有一个注解 2.有自动生成的类可以自动注入!

<pre style="margin: 8px 0px; background-color: rgb(43, 43, 43); color: rgb(169, 183, 198); font-family: "JetBrains Mono", monospace; font-size: 0.817rem;"> @BindView(R.id.tv_bangding)
TextView btn; @Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
ButterKnife.bind(this); //自动生成的类有一个bind方法,进行动态注入
}
}</pre>

手写步骤:

1.新建java module。把注解方进来。

为什么要把注解单独放一个module?

因为主module和编译module都需要用!

那为什么不把注解放在自动生成注解的module里面?

field注解+编译时+java module

<pre style="margin: 8px 0px; background-color: rgb(43, 43, 43); color: rgb(169, 183, 198); font-family: "JetBrains Mono", monospace; font-size: 0.817rem;">@Retention(RetentionPolicy.CLASS)
@Target(ElementType.FIELD)
public @interface BindView {
int value(); }
</pre>

apt-annotation(自定义注解)

创建注解类BindView

@Retention(RetentionPolicy.CLASS)
@Target(ElementType.FIELD)
public @interface BindView {
    int value();
}

@Retention(RetentionPolicy.CLASS):表示编译时注解
@Target(ElementType.FIELD):表示注解范围为类成员(构造方法、方法、成员变量)

@Retention: 定义被保留的时间长短
RetentionPoicy.SOURCE、RetentionPoicy.CLASS、RetentionPoicy.RUNTIME
@Target: 定义所修饰的对象范围
TYPE、FIELD、METHOD、PARAMETER、CONSTRUCTOR、LOCAL_VARIABLE等

这里定义了运行时注解BindView,其中value()用于获取对应Viewid

2.新建java module,把注解处理器需要的写进来,依赖javapoat和autoservice

<pre style="margin: 8px 0px; background-color: rgb(43, 43, 43); color: rgb(169, 183, 198); font-family: "JetBrains Mono", monospace; font-size: 0.817rem;">implementation 'com.google.auto.service:auto-service:1.0-rc2' implementation 'com.squareup:javapoet:1.10.0' implementation project(':apt-annotation')</pre>

需要先模拟好生成的类,在主程序的build/generated/source/apt

[图片上传失败...(image-fd1f62-1640265031545)]

在自动生成模块里面会生成注册器

[图片上传失败...(image-15d481-1640265031545)]

一共5个方法:

其他3个方法可以用注解的形式下。

然后init方法和process方法注解处理器解析:

1.得到所以有标记的注解元素或者节点

2.得到包名

3.得到类名

4.拼接类

5.生成java文件

主要的一些对象:

1.元素

2.文件创造器

3.日志信息打印输出messager

<pre style="margin: 8px 0px; background-color: rgb(43, 43, 43); color: rgb(169, 183, 198); font-family: "JetBrains Mono", monospace; font-size: 0.817rem;">@AutoService(Processor.class)
public class BindViewProcessor extends AbstractProcessor {

private Messager mMessager; // 打印

private Elements mElementUtils;
private Map<String, ClassCreatorProxy> mProxyMap = new HashMap<>(); /***

  • 初始化一些类:比如日志,读写流 * @param processingEnv
    */
    @Override
    public synchronized void init(ProcessingEnvironment processingEnv) {
    super.init(processingEnv);
    mMessager = processingEnv.getMessager();
    mElementUtils = processingEnv.getElementUtils();
    }

    /***

  • 需要处理注解的类型 * @return
    */
    @Override
    public Set<String> getSupportedAnnotationTypes() {
    HashSet<String> supportTypes = new LinkedHashSet<>();
    supportTypes.add(BindView.class.getCanonicalName());
    return supportTypes;
    }

    /***

  • java的版本 * @return
    */
    @Override
    public SourceVersion getSupportedSourceVersion() {
    return SourceVersion.latestSupported();
    }

    @Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
    mMessager.printMessage(Diagnostic.Kind.NOTE, "processing...");
    mProxyMap.clear();
    //得到所有的带指定的注解BindView
    Set<? extends Element> elements = roundEnvironment.getElementsAnnotatedWith(BindView.class);
    for (Element element : elements) {
    VariableElement variableElement = (VariableElement) element;
    TypeElement classElement = (TypeElement) variableElement.getEnclosingElement();
    String fullClassName = classElement.getQualifiedName().toString();
    //elements的信息保存到mProxyMap中,key:类全名
    ClassCreatorProxy proxy = mProxyMap.get(fullClassName);
    if (proxy == null) {
    proxy = new ClassCreatorProxy(mElementUtils, classElement);
    mProxyMap.put(fullClassName, proxy);
    }
    BindView bindAnnotation = variableElement.getAnnotation(BindView.class);//通过元素得到注解类
    int id = bindAnnotation.value();//得到注解类里面的值
    proxy.putElement(id, variableElement);
    }
    //拿到开始map存放的信息
    //通过javapoet生成java类 for (String key : mProxyMap.keySet()) {
    ClassCreatorProxy proxyInfo = mProxyMap.get(key);
    JavaFile javaFile = JavaFile.builder(proxyInfo.getPackageName(), proxyInfo.generateJavaCode2()).build();
    try {
    // 生成文件
    javaFile.writeTo(processingEnv.getFiler());
    } catch (IOException e) {
    e.printStackTrace();
    }
    }
    mMessager.printMessage(Diagnostic.Kind.NOTE, "process finish ...");
    return true; }

}</pre>

<pre style="margin: 8px 0px;">

<pre style="margin: 8px 0px; font-family: "JetBrains Mono", monospace; font-size: 0.817rem; background-color: rgb(43, 43, 43); color: rgb(169, 183, 198);">/**

  • 加入Method * javapoet */ private MethodSpec generateMethods2() {
    ClassName host = ClassName.bestGuess(mTypeElement.getQualifiedName().toString());
    MethodSpec.Builder methodBuilder = MethodSpec.methodBuilder("bind")
    .addModifiers(Modifier.PUBLIC)
    .returns(void.class)
    .addParameter(host, "host"); for (int id : mVariableElementMap.keySet()) {
    VariableElement element = mVariableElementMap.get(id); //VariableElement就是相应的元素
    String name = element.getSimpleName().toString(); //控件名字
    String type = element.asType().toString();//控件的类型,是btn还是text
    methodBuilder.addCode("host." + name + " = " + "(" + type + ")(((android.app.Activity)host).findViewById( " + id + "));");
    }
    return methodBuilder.build(); }</pre>

<pre style="margin: 8px 0px; font-family: "JetBrains Mono", monospace; font-size: 0.817rem; background-color: rgb(43, 43, 43); color: rgb(169, 183, 198);">最重要的是要得到VariableElement</pre>

<pre style="margin: 8px 0px; font-family: "JetBrains Mono", monospace; font-size: 0.817rem; background-color: rgb(43, 43, 43); color: rgb(169, 183, 198);">主要就是从ElementsTypeElement得到想要的一些信息,如package name、Activity名、变量类型、id等,</pre>

<pre style="margin: 8px 0px; font-family: "JetBrains Mono", monospace; font-size: 0.817rem; background-color: rgb(43, 43, 43); color: rgb(169, 183, 198);">

<pre style="margin: 8px 0px; font-family: "JetBrains Mono", monospace;">使用的时候:
public class MainActivity extends AppCompatActivity {

@BindView(R.id.tv)
TextView mTextView;

@BindView(R.id.btn)
Button mButton;</pre>

</pre>

<pre style="margin: 8px 0px; font-family: "JetBrains Mono", monospace; font-size: 0.817rem; background-color: rgb(43, 43, 43); color: rgb(169, 183, 198);">获取和解析控件:</pre>

<pre style="margin: 8px 0px;">

<pre style="margin: 8px 0px; font-family: "JetBrains Mono", monospace; font-size: 0.817rem; background-color: rgb(43, 43, 43); color: rgb(169, 183, 198);">VariableElement element = mVariableElementMap.get(id); //VariableElement就是相应的元素 String name = element.getSimpleName().toString(); //控件名字 String type = element.asType().toString();//控件的类型,是btn还是text</pre>

<pre style="margin: 8px 0px; font-family: "JetBrains Mono", monospace; font-size: 0.817rem; background-color: rgb(43, 43, 43);">常用的几个类(打印,元素,javapoat文件读写操作JavaFile)</pre>

<pre style="margin: 8px 0px; font-family: "JetBrains Mono", monospace; font-size: 0.817rem; background-color: rgb(43, 43, 43);"> <pre style="margin: 8px 0px; color: rgb(169, 183, 198); font-family: "JetBrains Mono", monospace;">public class BindViewProcessor extends AbstractProcessor {

private Messager mMessager; // 打印

private Elements mElementUtils; // 对应的元素
private Map<String, ClassCreatorProxy> mProxyMap = new HashMap<>(); //把带注解的元素封装在map中</pre>

3、封装一个调用module。 </pre>

<pre style="margin: 8px 0px;"> #### apt-library 工具类在BindViewProcessor中创建了对应的xxxActivity_ViewBinding.java,我们改怎么调用?当然是反射啦!!!apt-library 工具类

为什么用通过反射调用?

因为这些类都是动态生成的,根据不同的activity动态生成的!

</pre>

<pre style="margin: 8px 0px; color: rgb(169, 183, 198); font-family: "JetBrains Mono", monospace;">public class BindViewTools {

public static void bind(Activity activity) {

    Class clazz = activity.getClass();

try {
Class bindViewClass = Class.forName(clazz.getName() + "_ViewBinding");
Method method = bindViewClass.getMethod("bind", activity.getClass());
method.invoke(bindViewClass.newInstance(), activity);
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
}
}
</pre>

<pre style="margin: 8px 0px; color: rgb(169, 183, 198); font-family: "JetBrains Mono", monospace;">4.使用编译时注解</pre>

<pre style="margin: 8px 0px; color: rgb(169, 183, 198); font-family: "JetBrains Mono", monospace;">

<pre style="margin: 8px 0px; font-family: "JetBrains Mono", monospace;">public class MainActivity extends AppCompatActivity {

@BindView(R.id.tv)
TextView mTextView;

@BindView(R.id.btn)
Button mButton; @Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
BindViewTools.bind(this);
mTextView.setText("bind TextView success");
mButton.setText("bind Button success");
}
}</pre>

</pre>

</pre>

</pre>

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念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

推荐阅读更多精彩内容