本博客就是为了让我自己更好的理解butterknife的原理,或者是更好的让大家学习一下运行时注解,所以本博客的大前提是在参考 张鸿洋 的 Android如何编写基于编译时注解的项目 编写的,但是 张鸿洋 大神有很多地方没有解释到,本篇文章可以让初学者,学习到怎样使用运行时注解.可以为大家更好的除去疑惑
butterknife-demo: 示例Demo模块,Android工程类型模块
依赖关系:
butterknife-compiler 依赖 butterknife-annotation
butterknife-api 依赖 butterknife-annotation
butterknife-demo 依赖 butterknife-api 和 butterknife-compiler
因为注解处理模块器butterknife-compiler只在我们编译过程中需要使用到,在APP运行阶段就不需要使用该模块了。所以在发布APP时,我们就不必把注解处理器模块打包进来,以免造成程序臃肿,所以把butterknife-compiler模块单独拿出来。同时注解处理模块和api模块都需要使用到自定义注解模块,所以就需要把自定义注解模块单独拿出来。这样为何需要分成三个模块的原因也就一目了然了,其实butterfnife框架也是这样分的。
我们只编写一个bindview注解,其他的注解一样的
//编译型注解
@Retention(RetentionPolicy.CLASS)
//注解在成员变量上
@Target(ElementType.FIELD)
public @interface BindView {
int value();
}
如果还有不懂 Retention 和 Target 的同学,请看我上篇文章 注解的详细介绍
要像jvm调用你写的处理器,你必须先注册,让他知道。怎么让它知道呢,其实很简单,google 为我们提供了一个库,简单的一个注解就可以。
在此build.gradle中添加依赖
compile ('com.google.auto.service:auto-service:1.0-rc2')
auto-service库可以帮我们去生成META-INF等信息。可以自动生成META-INF/services/javax.annotation.processing.Processor文件(该文件是所有注解处理器都必须定义的),免去了我们手动配置的麻烦。
所有的注解处理器都必须继承AbstractProcessor,所以我们也编写一个ButterknifeAbstractProcessor类,一般重写四个方法
@AutoService(Processor.class)
public class ButterknifeAbstractProcessor extends AbstractProcessor {
//存储信息
private Map mProxyMap = new HashMap();
private Filer mFileUtils;
private Elements mElementUtils;
/**
* Element 的子类
- VariableElement //一般代表成员变量
- ExecutableElement //一般代表类中的方法
- TypeElement //一般代表代表类
- PackageElement //一般代表Package
*/
private Messager mMessager;
/**
* //初始化父类的方法
* @param processingEnv
*/
@Override
public synchronized void init(ProcessingEnvironment processingEnv){
super.init(processingEnv);
//跟文件相关的辅助类,生成JavaSourceCode.
mFileUtils = processingEnv.getFiler();
//跟元素相关的辅助类,帮助我们去获取一些元素相关的信息。
mElementUtils = processingEnv.getElementUtils();
//跟日志相关的辅助类
mMessager = processingEnv.getMessager();
}
/**
* 返回支持的注解类型 一般都是固定写法一般都是固定写法
* @return
*/
@Override
public Set getSupportedAnnotationTypes(){
/** Class
* getName my.ExternalClassConfig
getCanonicalName my.ExternalClassConfig
getSimpleName ExternalClassConfig
getName my.ExternalClassConfig$InternalConfig
getCanonicalName my.ExternalClassConfig.InternalConfig
getName()返回的是虚拟机里面的class的表示,而getCanonicalName()返回的是更容易理解的表示。其实对于大部分class来说这两个方法没有什么不同的。但是对于array或内部类来说是有区别的。
另外,类加载(虚拟机加载)的时候需要类的名字是getName
*/
Set annotationTypes = new LinkedHashSet();
annotationTypes.add(BindView.class.getCanonicalName());
return annotationTypes;
}
/**
* 返回支持的源码版本 一般都写最近的 一般都是固定写法
* @return
*/
@Override
public SourceVersion getSupportedSourceVersion(){
return SourceVersion.latestSupported();
}
/**作用:
* 1.收集信息
* 就是根据你的注解声明,拿到对应的Element,然后获取到我们所需要的信息,这个信息肯定是为了后面生成JavaFileObject所准备的
* 2 .生成代理类(本文把编译时生成的类叫代理类)
* 我们会针对每一个类生成一个代理类,例如MainActivity我们会生成一个MainActivity$$ViewInjector
*
*
* @param set
* @param roundEnvironment
* @return
*/
@Override
public boolean process(Set extends TypeElement> set, RoundEnvironment roundEnvironment) {
//因为process可能会多次调用,避免生成重复的代理类,避免生成类的类名已存在异常
mProxyMap.clear();
// 通过roundEnvironment.getElementsAnnotatedWith拿到我们通过@BindView注解的元素,这里返回值,按照我们的预期应该是VariableElement集合,因为我们用于成员变量上。
Set extends Element> elements = roundEnvironment.getElementsAnnotatedWith(BindView.class);
//一、收集信息
for (Element element : elements){
//field type 拿到成员变量
VariableElement variableElement = (VariableElement) element;
//class type 类 拿到对应的类信息 从而生成生成ProxyInfo对象
TypeElement typeElement = (TypeElement) variableElement.getEnclosingElement();//TypeElement
String qualifiedName = typeElement.getQualifiedName().toString();
ProxyInfoClass proxyInfoClass = mProxyMap.get(qualifiedName);
if (proxyInfoClass == null){
proxyInfoClass = new ProxyInfoClass(mElementUtils, typeElement) ;
mProxyMap.put(qualifiedName, proxyInfoClass);
}
BindView annotation = variableElement.getAnnotation(BindView.class);
int id = annotation.value();
proxyInfoClass.injectVariables.put(id, variableElement);
}
// 生成代理类 用i/o流 写成一个代理类
for(String key : mProxyMap.keySet()){
ProxyInfoClass proxyInfoClass = mProxyMap.get(key);
JavaFileObject sourceFile = null;
try {
sourceFile = mFileUtils.createSourceFile(
proxyInfoClass.getProxyClassFullName(), proxyInfoClass.getTypeElement());
Writer writer = sourceFile.openWriter();
writer.write(proxyInfoClass.generateJavaCode());
writer.flush();
writer.close();
} catch (IOException e) {
e.printStackTrace();
}
}
return true;
}
- init(ProcessingEnvironment processingEnvironment): 每一个注解处理器类都必须有一个空的构造函数。然而,这里有一个特殊的init()方法,它会被注解处理工具调用,并输入ProcessingEnviroment参数。ProcessingEnviroment提供很多有用的工具类Elements,Types和Filer。
- process(Set
Element:代表程序中的元素,比如说 包,类,方法。每一个元素代表一个静态的,语言级别的结构.
比如:
public class Perison { // TypeElement
private int name; // VariableElement
public void setName(String name) // ExecuteableElement
}
Filer mFileUtils; 跟文件相关的辅助类,生成JavaSourceCode.
// 开始编写代理类
proxyInfoClass.generateJavaCode()
手写生成代理类,同样我们也可以使用 javapoet 其实就是把字符串简化了一下
public class ProxyInfoClass
{
private String packageName;
private String proxyClassName;
private TypeElement typeElement;
public Map injectVariables = new HashMap<>();
// 这个是 api 库中的方法名
public static final String PROXY = "ViewInject";
public ProxyInfoClass(Elements elementUtils, TypeElement classElement)
{
this.typeElement = classElement;
PackageElement packageElement = elementUtils.getPackageOf(classElement);
String packageName = packageElement.getQualifiedName().toString();
//classname
String className = ClassValidator.getClassName(classElement, packageName);
this.packageName = packageName;
this.proxyClassName = className + "_" + PROXY;
}
public String generateJavaCode()
{
StringBuilder builder = new StringBuilder();
builder.append("// Generated code. Do not modify!\n");
//
builder.append("package ").append(packageName).append(";\n\n");
//包名改一下
builder.append("import cn.nzy.butterknife_api.*;\n");
builder.append('\n');
builder.append("public class ").append(proxyClassName).append(" implements " + ProxyInfoClass.PROXY + "<" + typeElement.getQualifiedName() + ">");
builder.append(" {\n");
generateMethods(builder);
builder.append('\n');
builder.append("}\n");
return builder.toString();
}
private void generateMethods(StringBuilder builder)
{
builder.append("@Override\n ");
// 方法名 更改一下
builder.append("public void inject(" + typeElement.getQualifiedName() + " host, Object source ) {\n");
for (int id : injectVariables.keySet())
{
VariableElement element = injectVariables.get(id);
String name = element.getSimpleName().toString();
String type = element.asType().toString();
builder.append(" if(source instanceof android.app.Activity){\n");
builder.append("host." + name).append(" = ");
builder.append("(" + type + ")(((android.app.Activity)source).findViewById( " + id + "));\n");
builder.append("\n}else{\n");
builder.append("host." + name).append(" = ");
builder.append("(" + type + ")(((android.view.View)source).findViewById( " + id + "));\n");
builder.append("\n};");
}
builder.append(" }\n");
}
public String getProxyClassFullName()
{
return packageName + "." + proxyClassName;
}
public TypeElement getTypeElement()
{
return typeElement;
}
}
点击Build->rebuild project 就会生成 对应activity的代理类
路径在 : ButterKnife\butterknife_demo\build\generated\source\apt\debug\cn\nzy\butterknife_demo\MainActivity_ViewInject.java
如果build的时候遇到错误,下面总结一波错误:
编码GBK的不可映射字符的错误
因为 如果AbstractProcessor中或者ProxyInfoClass有中文, 会影响i/o流写入文件,所以在每一个build.gralde中加入
tasks.withType(JavaCompile) {
options.encoding = "UTF-8"
}
javac错误
Error:Execution failed for task ':butterknife-demo:javaPreCompileDebug'.
> Annotation processors must be explicitly declared now. The following dependencies on the compile classpath are found to contain annotation processor. Please add them to the annotationProcessor configuration.
- butterknife-compiler.jar (project :butterknife-compiler)
Alternatively
解决方法是:在demo的build.gradle中
defaultConfig {
//... 你的东西
javaCompileOptions { annotationProcessorOptions { includeCompileClasspath = true } }
}
如果MainActivity_ViewInject.java这个代理类中有错误
请核对好 ProxyInfoClass.java 中的方法 和 注释
在demo的Activity中写入demo,检验即可
Android 如何编写基于编译时注解的项目 (也可以说转载人家的)
利用APT实现Android编译时注解
深入理解ButterKnife源码并掌握原理(一)