APT
APT (Annotation Processing Tool)
是一种处理注释的工具,它对源代码文件进行检测并找出其中的 Annotation,根据注解自动生成代码,如果想要自定义的注解处理器能够运行,必须要通过 APT 工具来处理。
简单说:根据规则,帮我们生成代码、生成类文件
编译时注解就是通过 APT 来通过注解信息生成代码来完成某些功能,典型代表有 ButterKnife、Dagger、ARouter 等 ButterKnife 原理分析
使用 ButterKnife :
① 添加依赖 :
dependencies {
implementation 'com.jakewharton:butterknife:10.2.3'
annotationProcessor 'com.jakewharton:butterknife-compiler:10.2.3'
}
② Activity 中使用 ButterKnife :
package kim.hsl.apt;
import androidx.appcompat.app.AppCompatActivity;
import android.os.Bundle;
import android.widget.TextView;
import butterknife.BindView;
import butterknife.ButterKnife;
public class MainActivity extends AppCompatActivity {
@BindView(R.id.hello)
TextView hello;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
ButterKnife.bind(this);
hello.setText("ButterKnife");
}
}
BindView 注解分析 : 在 TextView hello 成员变量处添加了 @BindView(R.id.hello) 注解 ;
@Target(FIELD) 元注解 : 表示其作用与类的成员字段 ;
@Retention(RUNTIME) 元注解 : 表示该注解保留到运行时阶段 ;
int value() 注解属性 : 只有一个注解属性 , 并且属性名是 value , 则使用注解时 “value =” 可省略 ;
@Retention(RUNTIME) @Target(FIELD)
public @interface BindView {
/** View ID to which the field will be bound. */
@IdRes int value();
}
TextView hello 需要使用 findViewById 进行赋值 , 在上述代码中没有写 findViewById 相关的代码 ; 肯定是在某个地方执行了 findViewById 的方法 ;
ButterKnife.bind(this) 代码就是执行了 findViewById 方法 ;
ButterKnife 用到了编译时技术会 , 在项目编译时 , 会生成 MainActivity_ViewBinding 类 , 在该类中 , 会查找添加了 @BindView 直接的成员变量 , 再获取 注解属性 value 的值 , 然后调用 findViewById 方法获取组件并为成员变量赋值 ;
// Generated code from Butter Knife. Do not modify!
package kim.hsl.apt;
import android.view.View;
import android.widget.TextView;
import androidx.annotation.CallSuper;
import androidx.annotation.UiThread;
import butterknife.Unbinder;
import butterknife.internal.Utils;
import java.lang.IllegalStateException;
import java.lang.Override;
public class MainActivity_ViewBinding implements Unbinder {
private MainActivity target;
@UiThread
public MainActivity_ViewBinding(MainActivity target) {
this(target, target.getWindow().getDecorView());
}
@UiThread
public MainActivity_ViewBinding(MainActivity target, View source) {
this.target = target;
target.hello = Utils.findRequiredViewAsType(source, R.id.hello, "field 'hello'", TextView.class);
}
@Override
@CallSuper
public void unbind() {
MainActivity target = this.target;
if (target == null) throw new IllegalStateException("Bindings already cleared.");
this.target = null;
target.hello = null;
}
}
ButterKnife 涉及到的源码 :
public final class ButterKnife {
/**
* BindView annotated fields and methods in the specified {@link Activity}. The current content
* view is used as the view root.
*
* @param target Target activity for view binding.
*/
@NonNull @UiThread
public static Unbinder bind(@NonNull Activity target) {
View sourceView = target.getWindow().getDecorView();
return bind(target, sourceView);
}
/**
* BindView annotated fields and methods in the specified {@code target} using the {@code source}
* {@link View} as the view root.
*
* @param target Target class for view binding.
* @param source View root on which IDs will be looked up.
*/
@NonNull @UiThread
public static Unbinder bind(@NonNull Object target, @NonNull View source) {
Class<?> targetClass = target.getClass();
if (debug) Log.d(TAG, "Looking up binding for " + targetClass.getName());
Constructor<? extends Unbinder> constructor = findBindingConstructorForClass(targetClass);
if (constructor == null) {
return Unbinder.EMPTY;
}
//noinspection TryWithIdenticalCatches Resolves to API 19+ only type.
try {
return constructor.newInstance(target, source);
} catch (IllegalAccessException e) {
throw new RuntimeException("Unable to invoke " + constructor, e);
} catch (InstantiationException e) {
throw new RuntimeException("Unable to invoke " + constructor, e);
} catch (InvocationTargetException e) {
Throwable cause = e.getCause();
if (cause instanceof RuntimeException) {
throw (RuntimeException) cause;
}
if (cause instanceof Error) {
throw (Error) cause;
}
throw new RuntimeException("Unable to create binding instance.", cause);
}
}
@Nullable @CheckResult @UiThread
private static Constructor<? extends Unbinder> findBindingConstructorForClass(Class<?> cls) {
Constructor<? extends Unbinder> bindingCtor = BINDINGS.get(cls);
if (bindingCtor != null || BINDINGS.containsKey(cls)) {
if (debug) Log.d(TAG, "HIT: Cached in binding map.");
return bindingCtor;
}
String clsName = cls.getName();
if (clsName.startsWith("android.") || clsName.startsWith("java.")
|| clsName.startsWith("androidx.")) {
if (debug) Log.d(TAG, "MISS: Reached framework class. Abandoning search.");
return null;
}
try {
Class<?> bindingClass = cls.getClassLoader().loadClass(clsName + "_ViewBinding");
//noinspection unchecked
bindingCtor = (Constructor<? extends Unbinder>) bindingClass.getConstructor(cls, View.class);
if (debug) Log.d(TAG, "HIT: Loaded binding class and constructor.");
} catch (ClassNotFoundException e) {
if (debug) Log.d(TAG, "Not found. Trying superclass " + cls.getSuperclass().getName());
bindingCtor = findBindingConstructorForClass(cls.getSuperclass());
} catch (NoSuchMethodException e) {
throw new RuntimeException("Unable to find binding constructor for " + clsName, e);
}
BINDINGS.put(cls, bindingCtor);
return bindingCtor;
}
}
实战APT实现Butterknife
在开始之前,我们先做个假设,要是我们可以把以下代码摘出来到另一个文件,通过某种方式自动生成这种代码会发生什么事。
package com.calm.baseknowledge02;
import com.calm.annotations.IBind;
import android.view.View;
public class MainActivity_ViewBinding implements IBind<com.calm.baseknowledge02.MainActivity> {
@Override
public void bind(com.calm.baseknowledge02.MainActivity target) {
target.tvInfo = (android.widget.TextView) target.findViewById(2131231077);
target.btnChange = (android.widget.Button) target.findViewById(2131230807);
target.findViewById(2131230807).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
target.onClick(view);
}
});
target.findViewById(2131230807).setOnLongClickListener(new View.OnLongClickListener() {
@Override
public boolean onLongClick(View view) {
target.onLongClick(view);
return true;
}
});
}
}
我们将findViewById、setOnClickListener、setOnLongClickListener这种代码摘到另一个文件,然后实例化该类与原始的Activity进行绑定。由于这些代码非常类似,我们可以通过模板化处理,就不需要我们在一行行去写了,只需要一个注解就搞定。那么关键来了,这份代码如何生成,这时候我们的APT就该上场了。 先将我们的注解写出来,由于这次的注解只需要在源码期存在即可,因此我们的作用域有一定变化,且较上篇会简单些。
/**
* 绑定控件
*/
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.SOURCE)
public @interface BindView {
int value();
}
/**
* 单击事件
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
public @interface OnClick {
int[] value() default -1;
}
/**
- 长按事件 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
public @interface OnLongClick {
int[] value() default -1;
}
为了做绑定和注入,我们统一一个公共的接口。
public interface IBind<T> {
void bind(T target);
}
注解处理器需要用到的相关插件,已经有现成的了就不需要我们从头去弄了。
dependencies {
implementation project(path: ':annotations')
annotationProcessor 'com.google.auto.service:auto-service:1.0-rc4'
compileOnly 'com.google.auto.service:auto-service:1.0-rc4'
}
关键的类就是注解处理器的类,请看代码
/**
* 注解处理器
*/
@AutoService(Processor.class)
public class AnnotationsCompiler extends AbstractProcessor {
private Filer filer;
private Messager messager;
private static String END_WITH = "_ViewBinding";
private static String BIND_VIEW = "BindView";
private static String ON_CLICK = "OnClick";
private static String ON_LONG_CLICK = "OnLongClick";
/**
* 初始化
* @param processingEnv
*/
@Override
public synchronized void init(ProcessingEnvironment processingEnv) {
super.init(processingEnv);
filer = processingEnv.getFiler();
messager = processingEnv.getMessager();
}
/**
* 支持的版本
* @return
*/
@Override
public SourceVersion getSupportedSourceVersion() {
return SourceVersion.latestSupported();
}
@Override
public Set<String> getSupportedAnnotationTypes() {
Set<String> types = new HashSet<>();
types.add(BindView.class.getCanonicalName());
types.add(OnClick.class.getCanonicalName());
types.add(OnLongClick.class.getCanonicalName());
return types;
}
/**
* 真正做事的都在这个方法里
* @param set
* @param roundEnvironment
* @return
*/
@Override
public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
if(set.size() == 0){
return false;
}
//getElementsAnnotatedWith 搜索对应注解的元素
//ExecutableElement 注解在方法节点
//VariableElement 注解在成员变量节点
//TypeElement 注解在类节点
//PackageElement 注解在包节点
Set<? extends Element> bindViewElement = roundEnvironment.getElementsAnnotatedWith(BindView.class);
Set<? extends Element> onClickElement = roundEnvironment.getElementsAnnotatedWith(OnClick.class);
Set<? extends Element> onLongClickElement = roundEnvironment.getElementsAnnotatedWith(OnLongClick.class);
//存储节点的容器 结构为 <com.calm.baseknowledge02.MainActivity,<BindView,List<>>>
Map<String,Map<String, List<Element>>> map = new HashMap<>();
//将BindView注解的成员变量节点进行分类存储
for (Element element : bindViewElement) {
//得到成员变量节点
VariableElement variableElement = (VariableElement) element;
String activityName = variableElement.getEnclosingElement().getSimpleName().toString();
TypeElement typeElement = (TypeElement) variableElement.getEnclosingElement();
String packageName = processingEnv.getElementUtils().getPackageOf(typeElement).toString();
String canonicalName = packageName+"."+activityName;
Map<String, List<Element>> annotationMap = map.get(canonicalName);
if(annotationMap == null){
annotationMap = new HashMap<>();
map.put(canonicalName,annotationMap);
List<Element> bindViewAnnotations = new ArrayList<>();
annotationMap.put(BIND_VIEW,bindViewAnnotations);
bindViewAnnotations.add(element);
}else {
List<Element> bindViewAnnotations = annotationMap.get(BIND_VIEW);
if(bindViewAnnotations == null){
bindViewAnnotations = new ArrayList<>();
annotationMap.put(BIND_VIEW,bindViewAnnotations);
}
bindViewAnnotations.add(element);
}
}
//将OnClick注解节点分类存储
for (Element element : onClickElement) {
//得到方法节点
ExecutableElement executableElement = (ExecutableElement) element;
String activityName = executableElement.getEnclosingElement().getSimpleName().toString();
TypeElement typeElement = (TypeElement) executableElement.getEnclosingElement();
String packageName = processingEnv.getElementUtils().getPackageOf(typeElement).toString();
String canonicalName = packageName+"."+activityName;
Map<String, List<Element>> annotationMap = map.get(canonicalName);
if(annotationMap == null){
annotationMap = new HashMap<>();
map.put(canonicalName,annotationMap);
List<Element> onClickAnnotations = new ArrayList<>();
annotationMap.put(ON_CLICK,onClickAnnotations);
onClickAnnotations.add(element);
}else {
List<Element> onClickAnnotations = annotationMap.get(ON_CLICK);
if(onClickAnnotations == null){
onClickAnnotations = new ArrayList<>();
annotationMap.put(ON_CLICK,onClickAnnotations);
}
onClickAnnotations.add(element);
}
}
//将OnLongClick注解节点分类存储
for (Element element : onLongClickElement) {
//得到方法节点
ExecutableElement executableElement = (ExecutableElement) element;
String activityName = executableElement.getEnclosingElement().getSimpleName().toString();
TypeElement typeElement = (TypeElement) executableElement.getEnclosingElement();
String packageName = processingEnv.getElementUtils().getPackageOf(typeElement).toString();
String canonicalName = packageName+"."+activityName;
Map<String, List<Element>> annotationMap = map.get(canonicalName);
if(annotationMap == null){
annotationMap = new HashMap<>();
map.put(canonicalName,annotationMap);
List<Element> onLongClickAnnotations = new ArrayList<>();
annotationMap.put(ON_LONG_CLICK,onLongClickAnnotations);
onLongClickAnnotations.add(element);
}else {
List<Element> onLongClickAnnotations = annotationMap.get(ON_LONG_CLICK);
if(onLongClickAnnotations == null){
onLongClickAnnotations = new ArrayList<>();
annotationMap.put(ON_LONG_CLICK,onLongClickAnnotations);
}
onLongClickAnnotations.add(element);
}
}
//分类完成,开始写模板代码了
if(map.size() == 0){
return false;
}
Writer writer = null;
Iterator<String> canonicalNames = map.keySet().iterator();
while (canonicalNames.hasNext()){
//全类名
String canonicalName = canonicalNames.next();
print(canonicalName);
//包名
String packageName = canonicalName.substring(0,canonicalName.lastIndexOf("."));
print(packageName);
String activityName = canonicalName.substring(canonicalName.lastIndexOf(".")+1);
print(activityName);
try {
//package com.calm.baseknowledge02;
//import com.calm.annotations.IBind;
//public class MainActivity_ViewBinding implements IBind<MainActivity> {
// @Override
// public void bind(MainActivity target) {
// target.tvInfo = target.findViewById(R.id.tvInfo);
// target.btnChange = target.findViewById(R.id.btnChange);
// }
//}
//创建.java文件,名称类似com.calm.baseknowledge02.MainActivity_ViewBinding
JavaFileObject sourceFile = filer.createSourceFile(canonicalName+END_WITH);
writer = sourceFile.openWriter();
writer.write("package "+packageName+";\n");
writer.write("import com.calm.annotations.IBind;\n");
writer.write("import android.view.View;\n");
writer.write("public class "+activityName+END_WITH+" implements IBind<"+canonicalName+"> {\n");
writer.write("@Override\n");
writer.write("public void bind("+canonicalName+" target){\n");
//注解map
Map<String,List<Element>> annotationMap = map.get(canonicalName);
if(annotationMap != null){
//处理BindView注解
List<Element> bindViews = annotationMap.get(BIND_VIEW);
if(bindViews != null && bindViews.size() > 0){
for (Element bindView : bindViews) {
int id = bindView.getAnnotation(BindView.class).value();
writer.write("target."+bindView.getSimpleName()+" = ("+bindView.asType()+")target.findViewById("+id+");\n");
}
}
//处理OnClick注解
List<Element> onClicks = annotationMap.get(ON_CLICK);
if(onClicks != null && onClicks.size() > 0){
for (Element onClick : onClicks) {
int[] ids = onClick.getAnnotation(OnClick.class).value();
for (int id : ids) {
// findViewById(R.id.btnChange).setOnClickListener(new View.OnClickListener() {
// @Override
// public void onClick(View view) {
//
// }
// });
writer.write("target.findViewById("+id+").setOnClickListener(new View.OnClickListener() {\n");
writer.write("@Override\n");
writer.write("public void onClick(View view) {\n");
writer.write("target."+onClick.getSimpleName().toString()+"(view);\n");
writer.write("}\n");
writer.write("});\n");
}
}
}
//处理OnLongClick注解
List<Element> onLongClicks = annotationMap.get(ON_LONG_CLICK);
if(onLongClicks != null && onLongClicks.size() > 0){
for (Element onLongClick : onLongClicks) {
int[] ids = onLongClick.getAnnotation(OnLongClick.class).value();
for (int id : ids) {
// findViewById(R.id.btnChange).setOnLongClickListener(new View.OnLongClickListener() {
// @Override
// public boolean onLongClick(View view) {
//
// return false;
// }
// });
writer.write("target.findViewById("+id+").setOnLongClickListener(new View.OnLongClickListener() {\n");
writer.write("@Override\n");
writer.write("public boolean onLongClick(View view) {\n");
writer.write("target."+onLongClick.getSimpleName().toString()+"(view);\n");
writer.write("return true;\n");
writer.write("}\n");
writer.write("});\n");
}
}
}
}
writer.write("}\n");
writer.write("}\n");
} catch (IOException e) {
e.printStackTrace();
}finally {
if(writer != null){
try {
writer.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
return false;
}
private void print(String msg){
messager.printMessage(Diagnostic.Kind.NOTE,msg);
}
}
关键的地方都有注释,就不一一解释了。这个类都不需要我们主动调用,在编译的时候由javac帮我们进行调用。然后编译下代码,我们就会生成一个我们最开始的那样一个类。 为了能使用这些类,我们还要写一个注入的类
public class CButterknife{
public static void bind(Object o) {
String name = o.getClass().getName()+"_ViewBinding";
try {
Class<?> clzz = Class.forName(name);
IBind iBind = (IBind) clzz.newInstance();
iBind.bind(o);
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
}
}
}
这个类也很简单,由于我们生成的类是有命名套路的。比如类为MainActivity,那么生成的类则为MainActivity_ViewBinding,那么我们要进行注入就很简单了,通过反射实例化该类,调用bind方法即可,就这么简单。然后在Activity中使用就更简单,只需要一句代码即可。
public class MainActivity extends AppCompatActivity {
@BindView(R.id.tvInfo)
TextView tvInfo;
@BindView(R.id.btnChange)
Button btnChange;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
CButterknife.bind(this);
}
@OnClick(R.id.btnChange)
public void onClick(View view){
if(view.getId() == R.id.btnChange){
tvInfo.setText("我是点击按钮后变化的数据");
}
}
@OnLongClick(R.id.btnChange)
public void onLongClick(View view){
if(view.getId() == R.id.btnChange){
tvInfo.setText("我是长按按钮后变化的数据");
}
}
}
这里需要注意的是CButterknife.bind(this)这句代码一定要在setContentView之后调用。
以上是Android开发技术中APT实现ButterKnife的一些原理解析和实战演练,更多Android技术分享尽在《Android核心技术手册》里面几十个技术板块划分,上千个小技术点带你进阶进入高工领域。
文末
编译时技术 最重要的作用就是在编译时可以 生成模板代码 ;由于生成代码操作是在编译时进行的 , 不会对运行时的性能产生影响 。
程序的周期 :
- 源码期 : 开发时 , 刚编写完 " .java " 代码 , 还未编译之前 , 就处于源码期 ;
- 编译期 : 程序由 java 源码编译成 class 字节码文件 ;
- 运行期 : 将字节码文件加载到 Java 虚拟机中运行 ;
编译时技术 APT 作用于 编译期 , 在这个过程中使用该技术 , 生成代码 ;编译时技术 2 2 2 大核心要素 : 在编译时 , 执行生成代码的逻辑 , 涉及到两个重要概念 。
① 编译时注解
② 注解处理器