之前在项目中使用到Dagger2,在查看源码的过程中产生了一些疑惑。为什么我使用@Component , @Module , @Inject注解就可以实现依赖注入呢?于是我带着这个疑惑开始学习注解相关的一些知识,希望能通过一个Demo来了解它的原理。
demo地址:https://github.com/chrissen0814/AnnotationDemo
本文的目的是探究编译时注解的实现原理,对于注解的定义以及语法相关的基础知识还要请读者们自行学习,本文并不会涉及到这些知识点。
在demo里我们实现了@BindView的注解功能(类似于ButterKnife的@BindView),用于实现findViewById()的功能。
先来看一下我们实现的效果
public class MainActivity extends AppCompatActivity {
@BindView(R.id.text_view)
TextView mTextView;
@BindView(R.id.button)
Button mButton;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Butter.bind(this);
mTextView.setText("Hello World and Thank You");
mButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
mTextView.setText("Text has changed.");
}
});
}
}
和ButterKnife的@BindView()注解很相似,只是在绑定的时候我们是用Butter.bind()来代替ButterKnife.bind()。
我们再来看一下项目结构:
这个包里面我们定义了一个BindView的注解。
这个包里面包含两个文件,一个是VariableInfo,里面有两个属性:mViewId view的id , VariableElement VariableElement标示一个字段、enum 常量、方法或构造方法参数、局部变量或异常参数 ; 另外一个是继承子AbstractProcessor的ViewInjectProcessor , 这个类是我们的核心类,在这个类里面我们要实现注解@BindView()的功能。具体的逻辑在下文会细说。
这个包里面只有一个类Butter , 也就是我们在Activity中调用的Butter.bind()所在的包,这个类里面只有一个方法:bind()。
通过上面的讲解你应该了解到,如果我们想实现一个注解,我们需要三步:第一步是定义注解,第二步是实现注解功能,第三步是提供API。
那接下来我们就分别来讲解一下这三步以及具体的逻辑。
首先我们需要新建一个module叫apt-annotation,这个module的作用是定义我们所需要的注解。需要注意的是这个module的等级是java-library,为什么?因为我们所定义的注解只需要java等级就可以了。
新建文件名为BindView的注解,选择类型的时候注意选择@interface。
//保留策略(SOURCE,CLASS,RUNTIME)
@Retention(RetentionPolicy.CLASS)
//作用域
@Target(ElementType.FIELD)
public @interface BindView {
//注解中的值为int类型
int value();
}
这个是我们的核心module,我们要实现的BindView的功能需要在这个module里实现。
新建module,类型是java-library。这个包里只有两个类:VariableInfo:变量信息,主要是把view的id和VariableElement进行绑定。VariableElement里面提供了很多和变量相关的信息,这个在实现BindView的功能时是需要用到的。另外一个是ViewInjectProcessor,看名字也应该知道这个类的作用是View注解处理器。
//不要写成Process.class
@AutoService(Processor.class)
//设置支持的注解类型(也可以通过重写方法实现)
@SupportedAnnotationTypes({"com.chrissen.apt_annotation.BindView"})
//设置支持(也可以通过重写方法实现)
@SupportedSourceVersion(SourceVersion.RELEASE_7)
public class ViewInjectProcessor extends AbstractProcessor {
private Map> classMap = new HashMap<>();
private Map classTypeElement = new HashMap<>();
//工具类,用于获取Element信息
private Elements mUtils;
//生成java文件的类(生成代理工具类)
private Filer filer;
@Override
public synchronized void init(ProcessingEnvironment processingEnvironment) {
super.init(processingEnvironment);
filer = processingEnvironment.getFiler();
mUtils = processingEnvironment.getElementUtils();
}
@Override
public boolean process(Set extends TypeElement> set, RoundEnvironment roundEnvironment) {
//收集所需信息
collectInfo(roundEnvironment);
//生成相应的代理类代码
writeToFile();
return true;
}
private void writeToFile() {
try {
for (String classFullName : classMap.keySet()) {
TypeElement typeElement = classTypeElement.get(classFullName);
MethodSpec.Builder constructor = MethodSpec.constructorBuilder()
.addModifiers(Modifier.PUBLIC)
.addParameter(ParameterSpec.builder(TypeName.get(typeElement.asType()), "activity").build());
List variableList = classMap.get(classFullName);
for (VariableInfo variableInfo : variableList) {
VariableElement variableElement = variableInfo.getVariableElement();
String variableName = variableElement.getSimpleName().toString();
String variableFullName = variableElement.asType().toString();
constructor.addStatement("activity.$L=($L)activity.findViewById($L)", variableName, variableFullName, variableInfo.getViewId());
}
TypeSpec typeSpec = TypeSpec.classBuilder(typeElement.getSimpleName() + "$$ViewInjector")
.addModifiers(Modifier.PUBLIC)
.addMethod(constructor.build())
.build();
//通过mUtils获取完整的包名
String packageFullName = mUtils.getPackageOf(typeElement).getQualifiedName().toString();
JavaFile javaFile = JavaFile.builder(packageFullName, typeSpec)
.build();
javaFile.writeTo(filer);
}
} catch (Exception ex) {
ex.printStackTrace();
}
}
private void collectInfo(RoundEnvironment roundEnvironment) {
classMap.clear();
classTypeElement.clear();
Set extends Element> elements = roundEnvironment.getElementsAnnotatedWith(BindView.class);
for(Element element : elements){
int viewId = element.getAnnotation(BindView.class).value();
VariableElement variableElement = (VariableElement) element;
TypeElement typeElement = (TypeElement) variableElement.getEnclosingElement();
String classFullName = typeElement.getQualifiedName().toString();
List variableInfoList = classMap.get(classFullName);
if(variableInfoList == null){
variableInfoList = new ArrayList<>();
classMap.put(classFullName,variableInfoList);
classTypeElement.put(classFullName,typeElement);
}
VariableInfo variableInfo = new VariableInfo();
variableInfo.setVariableElement(variableElement);
variableInfo.setViewId(viewId);
variableInfoList.add(variableInfo);
}
}
// 可以通过重写方法来指定支持的SourceVersion
// @Override
// public SourceVersion getSupportedSourceVersion() {
// return SourceVersion.RELEASE_7;
// }
// 通过重写方法来设置支持的注解类型
// @Override
// public Set getSupportedAnnotationTypes() {
// Set set = new HashSet();
// set.add("com.chrissen.apt_annotation.BindView");
// return set;
// }
}
需要继承自AbstractProcessor,这个类的作用就是用来处理注解的。
在这个类上需要设置几个注解,具体的作用注释里已经写的非常清楚了,其中@SupportedAnnotationTypes和@SupportedSourceVersion也可以通过重写方法来实现,代码中也已经写出来了。我自己在实现的过程中犯了个小错误:在定义@AutoService()时错误的写成了Process.class导致报错异常,正确的应该是Processor.class。
在处理注解的时候主要分为两步:第一步是收集信息,第二步是生成相对应的代码。
这个部分的代码很多都是模板,不同的地方在于生成代码的时候是根据你注解所需要实现的功能。
新建module,类型为android.library,原因是这个类里我们需要Activity这个类,所以不能再用java-library。
这个类里只有一个Butter类,里面提供一个bind()方法。
public class Butter {
public static void bind(Activity host){
String classFullName = host.getClass().getName() + "$$ViewInjector";
try {
Class proxy = Class.forName(classFullName);
Constructor constructor = proxy.getConstructor(host.getClass());
//newInstance()返回Object
constructor.newInstance(host);
} catch (Exception e) {
e.printStackTrace();
}
}
}
通过自己实现一个注解,你应该大概了解了注解的工作机制:在AbstractProcessor的process()方法里处理,主要分为两步:第一步是收集和注解相关的信息,第二步生成对应的代码。
当然我们只要写了一个事例,所以会有细节的地方没有考虑到,我们的重点主要是了解编译时注解的工作原理。
《从0到1:实现 Android 编译时注解》
如何打造一个 Android 编译时注解框架