什么是apt
APT(Annotation Processing Tool)即注解處理器,是一種處理注解的工具,確切的說它是javac的一個工具,它用來在編譯時掃描和處理注解。注解處理器以Java代碼(或者編譯過的字節(jié)碼)作為輸入,生成.java文件作為輸出。
簡單來說就是在編譯期,通過注解生成.java文件。
如果沒接觸過注解開發(fā)的同學(xué)可以看我之前的文章
Android注解--初探
apt 的作用
使用APT的優(yōu)點就是方便、簡單,可以少些很多重復(fù)的代碼。
用過ButterKnife、Dagger、EventBus等注解框架的同學(xué)就能感受到,利用這些框架可以少些很多代碼,只要寫一些注解就可以了。
其實,他們不過是通過注解,生成了一些代碼。
本文需求
通過APT實現(xiàn)一個功能,通過對View變量的注解,實現(xiàn)View的綁定(類似于ButterKnife中的@BindView)
創(chuàng)建項目
- 創(chuàng)建Android Module命名為app 依賴 apt_library
- 創(chuàng)建Java library Module命名為 apt_annotation
- 創(chuàng)建Java library Module命名為 apt_processor 依賴 apt_annotation
- 創(chuàng)建Android library Module 命名為 apt_library 依賴 apt_processor
注解開發(fā)需要創(chuàng)建Java library因為有些方法、類 Android library 中并不支持
Module職責(zé)
- apt_annotation:自定義注解,存放@BindView
- apt_processor:注解處理器,根據(jù)apt-annotation中的注解,在編譯期生成xxxActivity_ViewBinding.java代碼
- apt_library:工具類,調(diào)用xxxActivity_ViewBinding.java中的方法,實現(xiàn)View的綁定。
實現(xiàn)
其實有兩種方式可以實現(xiàn)這個功能
- RetentionPolicy.CLASS 編譯時注解
- RetentionPoicy.RUNTIME 運行時注解
在很多情況下,運行時注解和編譯時注解可以實現(xiàn)相同的功能,比如依賴注入框架,我們既可以在運行時通過反射來初始化控件,也可以再編譯時就生成控件初始化代碼。那么,這兩者有什么區(qū)別呢?
答:編譯時注解性能比運行時注解好,運行時注解需要使用到反射技術(shù),對程序的性能有一定影響,而編譯時注解直接生成了源代碼,運行過程中直接執(zhí)行代碼,沒有反射這個過程。
實現(xiàn)一:RetentionPolicy.CLASS 編譯時注解
1、創(chuàng)建注解類BindView
/**
* @author liuboyu E-mail:545777678@qq.com
* @Date 2019-08-21
* @Description BindView 注解定義
*/
@Retention(RetentionPolicy.CLASS)
@Target(ElementType.FIELD)
public @interface BindView {
int value();
}
@Retention(RetentionPolicy.CLASS):表示編譯時注解
@Target(ElementType.FIELD):表示注解范圍為類成員(構(gòu)造方法、方法、成員變量)
2、apt_processor(注解處理器)
在Module中添加依賴
dependencies {
implementation fileTree(include: ['*.jar'], dir: 'libs')
api project(':apt_annotation')
}
創(chuàng)建BindViewProcessor
/**
* @author liuboyu E-mail:545777678@qq.com
* @Date 2019-08-21
* @Description 注解處理器
*/
public class BindViewProcessor extends AbstractProcessor {
private Messager mMessager;
private Elements mElementUtils;
private Map<String, ClassCreatorProxy> mProxyMap = new HashMap<>();
@Override
public synchronized void init(ProcessingEnvironment processingEnv) {
super.init(processingEnv);
mMessager = processingEnv.getMessager();
mElementUtils = processingEnv.getElementUtils();
}
@Override
public Set<String> getSupportedAnnotationTypes() {
HashSet<String> supportTypes = new LinkedHashSet<>();
supportTypes.add(BindView.class.getCanonicalName());
return supportTypes;
}
@Override
public SourceVersion getSupportedSourceVersion() {
return SourceVersion.RELEASE_8;
}
@Override
public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
mMessager.printMessage(Diagnostic.Kind.NOTE, "processing...");
mProxyMap.clear();
//獲得被BindView注解標(biāo)記的element
Set<? extends Element> elements = roundEnvironment.getElementsAnnotatedWith(BindView.class);
//對不同的Activity進行分類
for (Element element : elements) {
VariableElement variableElement = (VariableElement) element;
TypeElement classElement = (TypeElement) variableElement.getEnclosingElement();
String fullClassName = classElement.getQualifiedName().toString();
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);
}
//通過遍歷mProxyMap,創(chuàng)建java文件
for (String key : mProxyMap.keySet()) {
ClassCreatorProxy proxyInfo = mProxyMap.get(key);
try {
mMessager.printMessage(Diagnostic.Kind.NOTE, " --> create " + proxyInfo.getProxyClassFullName());
JavaFileObject jfo = processingEnv.getFiler().createSourceFile(proxyInfo.getProxyClassFullName(), proxyInfo.getTypeElement());
Writer writer = jfo.openWriter();
writer.write(proxyInfo.generateJavaCode());
writer.flush();
writer.close();
} catch (IOException e) {
mMessager.printMessage(Diagnostic.Kind.NOTE, " --> create " + proxyInfo.getProxyClassFullName() + "error");
}
}
mMessager.printMessage(Diagnostic.Kind.NOTE, "process finish ...");
return true;
}
}
- init:初始化。可以得到ProcessingEnviroment,ProcessingEnviroment提供很多有用的工具類Elements, Types 和 Filer
- getSupportedAnnotationTypes:指定這個注解處理器是注冊給哪個注解的,這里說明是注解BindView
- getSupportedSourceVersion:指定使用的Java版本,通常這里返回SourceVersion.latestSupported()
- process:可以在這里寫掃描、評估和處理注解的代碼,生成Java文件
ClassCreatorProxy是創(chuàng)建Java代碼的代理類,如下:
/**
* @author liuboyu E-mail:545777678@qq.com
* @Date 2019-08-21
* @Description 生成代碼工具類
*/
public class ClassCreatorProxy {
private String mBindingClassName;
private String mPackageName;
private TypeElement mTypeElement;
private Map<Integer, VariableElement> mVariableElementMap = new HashMap<>();
public ClassCreatorProxy(Elements elementUtils, TypeElement classElement) {
this.mTypeElement = classElement;
PackageElement packageElement = elementUtils.getPackageOf(mTypeElement);
String packageName = packageElement.getQualifiedName().toString();
String className = mTypeElement.getSimpleName().toString();
this.mPackageName = packageName;
this.mBindingClassName = className + "_ViewBinding";
}
public void putElement(int id, VariableElement element) {
mVariableElementMap.put(id, element);
}
/**
* 創(chuàng)建Java代碼
*
* @return
*/
public String generateJavaCode() {
StringBuilder builder = new StringBuilder();
builder.append("package ").append(mPackageName).append(";\n\n");
builder.append("import androidtest.project.com.apt_library.*;\n");
builder.append('\n');
builder.append("public class ").append(mBindingClassName);
builder.append(" {\n");
generateMethods(builder);
builder.append('\n');
builder.append("}\n");
return builder.toString();
}
/**
* 加入Method
*
* @param builder
*/
private void generateMethods(StringBuilder builder) {
builder.append("public void bind(" + mTypeElement.getQualifiedName() + " host ) {\n");
for (int id : mVariableElementMap.keySet()) {
VariableElement element = mVariableElementMap.get(id);
String name = element.getSimpleName().toString();
String type = element.asType().toString();
builder.append("host." + name).append(" = ");
builder.append("(" + type + ")host.findViewById( " + id + ");\n");
}
builder.append(" }\n");
}
public String getProxyClassFullName() {
return mPackageName + "." + mBindingClassName;
}
public TypeElement getTypeElement() {
return mTypeElement;
}
}
添加SPI配置文件,對于SPI不是很理解的同學(xué),可以看我的Android 動態(tài)服務(wù)SPI--模塊節(jié)藕
- 需要在 processors 庫的 main 目錄下新建 resources 資源文件夾;
- 在 resources文件夾下建立 META-INF/services 目錄文件夾;
- 在 META-INF/services 目錄文件夾下創(chuàng)建 javax.annotation.processing.Processor 文件;
- 在 javax.annotation.processing.Processor 文件寫入注解處理器的全稱,包括包路徑;)
文件內(nèi)容如下
androidtest.project.com.apt_processor.BindViewProcessor
3、apt_library 工具類
在BindViewProcessor中創(chuàng)建了對應(yīng)的xxxActivity_ViewBinding.java,我們改怎么調(diào)用?當(dāng)然是反射啦!??!
在Module的build.gradle中添加依賴
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
api rootProject.ext.dependencies.appcompatV7
api project(':apt_processor')
}
創(chuàng)建注解工具類BindViewTools
/**
* @author liuboyu E-mail:545777678@qq.com
* @Date 2019-08-21
* @Description 注解工具類
*/
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();
}
}
}
apt_library的部分就比較簡單了,通過反射找到對應(yīng)的ViewBinding類,然后調(diào)用其中的bind()方法完成View的綁定。
3、app主模塊
在 app Module 的 build.gradle中添加依賴
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
api rootProject.ext.dependencies.appcompatV7
api rootProject.ext.dependencies.design
implementation project(':apt_library')
}
使用
在MainActivity中,在View的前面加上BindView注解,把id傳入即可
public class MainActivity extends AppCompatActivity {
@BindView(value = R.id.tv_1)
TextView mTextView;
@BindView(value = R.id.btn_2)
Button mButton;
@BindView(value = R.id.iv_3)
ImageView mImageView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
BindViewTools.bind(this);
mTextView.setText("我是 TextView");
mButton.setText("我是 Button");
mImageView.setImageResource(R.color.colorPrimary);
}
}
Make project 是騾子是馬出來溜溜,看一下我們生成的代碼

public class MainActivity_ViewBinding {
public void bind(androidtest.project.com.annotationstudy.MainActivity host) {
host.mButton = (android.widget.Button) host.findViewById(2131230754);
host.mImageView = (android.widget.ImageView) host.findViewById(2131230802);
host.mTextView = (android.widget.TextView) host.findViewById(2131230899);
}
}
看一下運行結(jié)果:

ok!我們的目的已經(jīng)達成了,接下來我們反思一下
- 手動配置spi略嫌麻煩,有沒有什么便捷方式?
- java 代碼都是通過 StringBuilder 一點一點拼出來的,很容易出錯,有什么更好的辦法么?
當(dāng)然都可以解決
問題一:
Google 提供的便捷的工具,通過auto-service中的@AutoService即可以自動生成AutoService注解處理器,自動生成 META-INF/services/javax.annotation.processing.Processor
使用方法也很簡單
apt_processor gradle 引入依賴
dependencies {
implementation fileTree(include: ['*.jar'], dir: 'libs')
implementation 'com.google.auto.service:auto-service:1.0-rc2'
api project(':apt_annotation')
}
這里有個坑,特么查好久,死活生成不了
Android Gradle由4.x升級至5.0,需要引入下面這句話,否則無法自動生成 spi配置文件
annotationProcessor "com.google.auto.service:auto-service:1.0-rc2"
然后修改 BindViewProcessor 文件,添加@AutoService(Processor.class)即可
@AutoService(Processor.class)
public class BindViewProcessor extends AbstractProcessor
看一下文件的生成位置

問題二:
可以利用java提供的 javapoet 來生成java 代碼
本文也做了實踐
/**
* 創(chuàng)建Java代碼
* @return
*/
public TypeSpec generateJavaCode2() {
TypeSpec bindingClass = TypeSpec.classBuilder(mBindingClassName)
.addModifiers(Modifier.PUBLIC)
.addMethod(generateMethods2())
.build();
return bindingClass;
}
/**
* 加入Method
*/
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);
String name = element.getSimpleName().toString();
String type = element.asType().toString();
methodBuilder.addCode("host." + name + " = " + "(" + type + ")host.findViewById( " + id + ");\n");
}
return methodBuilder.build();
}
public String getPackageName() {
return mPackageName;
}
修改 BindViewProcessor 生成方式即可
@Override
public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
//省略部分代碼...
//通過javapoet生成
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;
}
如果感興趣,可以學(xué)一下 javapoet詳細用法
實現(xiàn)二:RetentionPoicy.RUNTIME 運行時注解
這個就很簡單了,從字面就可以理解,運行的時候,再去解析注解,下面看一下實現(xiàn)
/**
* 運行時解析注解 BindView
*
* @param activity 使用InjectView的目標(biāo)對象
*/
public static void inject(Activity activity) {
Field[] fields = activity.getClass().getDeclaredFields();
//通過該方法設(shè)置所有的字段都可訪問,否則即使是反射,也不能訪問private修飾的字段
AccessibleObject.setAccessible(fields, true);
for (Field field : fields) {
boolean needInject = field.isAnnotationPresent(BindView.class);
if (needInject) {
BindView anno = field.getAnnotation(BindView.class);
int id = anno.value();
if (id == -1) {
continue;
}
View view = activity.findViewById(id);
Class fieldType = field.getType();
try {
//把View轉(zhuǎn)換成field聲明的類型
field.set(activity, fieldType.cast(view));
} catch (Exception e) {
Log.e(BindView.class.getSimpleName(), e.getMessage());
}
}
}
}
再把 BindView 注解改為運行時注解即可
/**
* @author liuboyu E-mail:545777678@qq.com
* @Date 2019-08-21
* @Description BindView 注解定義
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface BindView {
int value();
}
我們之前講過,編譯時注解性能比運行時注解好,運行時注解需要使用到反射技術(shù),對程序的性能有一定影響,而編譯時注解直接生成了源代碼,運行過程中直接執(zhí)行代碼,沒有反射這個過程。
最后呈上本文 github Demo 鏈接
參考博客