上一篇,我们基本了解了整个编译时注解的实现流程,现在我们就要开始来真正写一个有意义的功能了
本文最终demo已经上传到github上,欢迎大家star,follow
再次声明本文参考了鸿洋的demo
项目结构
同之前一样你要创建一个Java Library,但是这里你需要将android.jar拷贝进libs里面,因为我们这里需要使用到android的相关类
Library的编写
首先你需要将android.jar拷贝到java Library里面来,因为我们需要一些android的类
1 注解类的实现 ViewInject.java
@Retention(RetentionPolicy.CLASS)
@Target({ElementType.FIELD, ElementType.TYPE})
public @interface ViewInject {
int value();
}
我们将通过注解的id进行相应的处理
2 具体实现的功能 Finder.java
public enum Finder {
Activity {
@Override
public void setContentView(Object activity, int id) {
((Activity) activity).setContentView(id);
}
};
public abstract void setContentView(Object activity, int id);
}
本次功能主要是针对Activity去实现setContentView的功能。在枚举类里面定义了抽象方法,这样枚举对象就一定要实现才行。这样写的好处是方便扩展,后续扩展其他功能以及支持Fragment、View的话比较方便
3 动态生成类功能接口 InjectInterface.java
public interface InjectInterface {
void inject(Finder finder, T target);
}
每一个动态生成的类需要实现这个接口,这样activity就可以直接通过这个接口使用Finder来实现真正的setContentView功能
4 编译时执行类的实现 ViewInjectProcessor.java
提前介绍ProxyInfo.java,这个类存储着Activity的信息,我们声明一个map对象去用键值对存储不同Activity的信息,做到不混乱
//存储需要循环生成的注解类信息
HashMap proxyInfoHashMap;
@Override
public synchronized void init(ProcessingEnvironment processingEnv) {
super.init(processingEnv);
proxyInfoHashMap=new HashMap<>();
}
在初始化方法里面初始化一下
下面开始重写process方法
首先定义三个对象用来存储完整类路径、类名、包名
String fqClassName, className, packageName;
fqClassName是用来当键用的,因为包名或者类名都有可能出现重复的情况
包名跟类名是后续ProxyInfo需要使用的
for (Element element : roundEnv.getElementsAnnotatedWith(ViewInject.class)) {
if (element.getKind()== ElementKind.CLASS) {
TypeElement classElement= (TypeElement) element;
PackageElement packageElement= (PackageElement) classElement.getEnclosingElement();
fqClassName=classElement.getQualifiedName().toString();
className=classElement.getSimpleName().toString();
packageName=packageElement.getQualifiedName().toString();
int layoutId=classElement.getAnnotation(ViewInject.class).value();
}
}
遍历获得ViewInject下的所有被注解class的信息,同时获取到注解内容的值,也就是view的id。这边通过之前的学习,应该没有什么难度看不懂了
if (proxyInfoHashMap.containsKey(fqClassName)) {
proxyInfoHashMap.get(fqClassName).setLayoutId(layoutId);
} else {
ProxyInfo proxyInfo=new ProxyInfo(packageName, className);
proxyInfo.setClassElement(classElement);
proxyInfo.setLayoutId(layoutId);
proxyInfoHashMap.put(fqClassName, proxyInfo);
}
把每次取到的classElement信息放到那个map里面去,等全部加完之后,我们要开始手动去创建class文件了,这些文件就是提供给activity里面去操作的方法
5 存储Activity信息的类 ProxyInfo.java
public class ProxyInfo {
//原始包名
String packageName;
//注解上的class名称
String className;
//生成注解类名称
String proxyClassName;
//类元素
TypeElement classElement;
//布局Id
int layoutId;
public static final String PROXY = "PROXY";
public ProxyInfo(String packageName, String className) {
this.packageName = packageName;
this.className = className;
//设置新生成的注解类名
this.proxyClassName=className+"$$"+PROXY;
}
public TypeElement getClassElement() {
return classElement;
}
public void setClassElement(TypeElement classElement) {
this.classElement = classElement;
}
public int getLayoutId() {
return layoutId;
}
public void setLayoutId(int layoutId) {
this.layoutId = layoutId;
}
public String getProxyClassName() {
return proxyClassName;
}
/**
* 创建模板代码
* @return
*/
public String generateJavaCode() {
StringBuilder builder = new StringBuilder();
builder.append("// Generated code from ViewInjectProcessor. Do not modify!\n");
builder.append("package ").append(packageName).append(";\n\n");
builder.append("import com.example.annotation.Finder;\n");
builder.append("import com.example.annotation.InjectInterface;\n");
builder.append('\n');
builder.append("public class ").append(proxyClassName);
builder.append("");
builder.append(" implements InjectInterface");
builder.append(" {\n\n");
generateInjectMethod(builder);
builder.append("\n}\n");
return builder.toString();
}
/**
* 具体方法实现代码
*/
private void generateInjectMethod(StringBuilder builder) {
builder.append(" @Override\n");
builder.append(" public void inject(Finder finder, T target) {");
if (layoutId > 0) {
builder.append(" finder.setContentView(target, "+layoutId+");\n");
}
builder.append("\n }\n");
}
}
其实虽然代码看起来好像不少,但是实际上很简单,就是在class包下面创建好相应的类class
生成的代码是这样的
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//
package com.rg.annotationdemo;
import com.example.annotation.Finder;
import com.example.annotation.InjectInterface;
import com.rg.annotationdemo.MainActivity;
public class MainActivity$$PROXY implements InjectInterface {
public MainActivity$$PROXY() {
}
public void inject(Finder finder, T target) {
finder.setContentView(target, 2130968601);
}
}
只要能调用到这个方法,那么setContentView的功能就能实现
6 提供给Activity传递参数的类 ButterKnife.java
public class ButterKnife {
static final Map, InjectInterface
这个类其实也是很简单就能理解的,通过反射将之前动态生成的类找到,然后传递参数Finder跟Activity,再调用inject方法,完成setContentView的真正实现
Activity的使用
通过上述步骤,我们就完成了编译时注解,同上篇文章一样,打个包丢进去
@ViewInject(R.layout.activity_main)
public class MainActivity extends AppCompatActivity {
TextView textview;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
ButterKnife butterKnife=new ButterKnife();
butterKnife.bind(this);
}
public void click() {
Toast.makeText(this, "HHH", Toast.LENGTH_SHORT).show();
}
}
实际效果不截图了,也没什么意思,无非就是一个页面而已,大家可以根据我的demo自行尝试
注意点
没事多gradle clean/gradle build,有些不能理解的异常,比如说动态生成的类不符合JUnit文件规范之列的,都可以通过clean build解决