APT(Annotation Processing Tool)即註解處理器,是一種用來處理註解的工具。JVM會在編譯期就運行APT去掃描處理代碼中的註解而後輸出java文件。java
簡單來講~~就是你只須要添加註解,APT就能夠幫你生成須要的代碼git
許多的Android開源庫都使用了APT技術,如ButterKnife、ARouter、EventBus等程序員
在使用Java開發Android時,頁面初始化的時候咱們一般要寫大量的view = findViewById(R.id.xx)代碼github
做爲一個優(lan)秀(duo)的程序員,咱們如今就要實現一個APT來完成這個繁瑣的工做,經過一個註解就能夠自動給View得到實例api
本demo地址bash
建立一個項目,名稱叫 apt_demo
建立一個Activity,名稱叫 MainActivity
在佈局中添加一個TextView, id爲test_textview
app
建立一個Java Library Module名稱叫 apt-annotation
框架
在這個module中建立自定義註解 @BindView
ide
@Retention(RetentionPolicy.CLASS)
@Target(ElementType.FIELD)
public @interface BindView {
int value();
}
複製代碼
@Retention(RetentionPolicy.CLASS)
:表示這個註解保留到編譯期@Target(ElementType.FIELD)
:表示註解範圍爲類成員(構造方法、方法、成員變量)建立一個Java Library Module名稱叫 apt-compiler
工具
在這個Module中添加依賴
dependencies {
implementation project(':apt-annotation')
}
複製代碼
在這個Module中建立BindViewProcessor
類
直接給出代碼~~
public class BindViewProcessor extends AbstractProcessor {
private Filer mFilerUtils; // 文件管理工具類
private Types mTypesUtils; // 類型處理工具類
private Elements mElementsUtils; // Element處理工具類
private Map<TypeElement, Set<ViewInfo>> mToBindMap = new HashMap<>(); //用於記錄須要綁定的View的名稱和對應的id
@Override
public synchronized void init(ProcessingEnvironment processingEnv) {
super.init(processingEnv);
mFilerUtils = processingEnv.getFiler();
mTypesUtils = processingEnv.getTypeUtils();
mElementsUtils = 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.latestSupported();// 表示支持最新的Java版本
}
@Override
public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnv) {
System.out.println("start process");
if (set != null && set.size() != 0) {
Set<? extends Element> elements = roundEnv.getElementsAnnotatedWith(BindView.class);//得到被BindView註解標記的element
categories(elements);//對不一樣的Activity進行分類
//對不一樣的Activity生成不一樣的幫助類
for (TypeElement typeElement : mToBindMap.keySet()) {
String code = generateCode(typeElement); //獲取要生成的幫助類中的全部代碼
String helperClassName = typeElement.getQualifiedName() + "$$Autobind"; //構建要生成的幫助類的類名
//輸出幫助類的java文件,在這個例子中就是MainActivity$$Autobind.java文件
//輸出的文件在build->source->apt->目錄下
try {
JavaFileObject jfo = mFilerUtils.createSourceFile(helperClassName, typeElement);
Writer writer = jfo.openWriter();
writer.write(code);
writer.flush();
writer.close();
} catch (IOException e) {
e.printStackTrace();
}
}
return true;
}
return false;
}
//將須要綁定的View按不一樣Activity進行分類
private void categories(Set<? extends Element> elements) {
for (Element element : elements) { //遍歷每個element
VariableElement variableElement = (VariableElement) element; //被@BindView標註的應當是變量,這裏簡單的強制類型轉換
TypeElement enclosingElement = (TypeElement) variableElement.getEnclosingElement(); //獲取表明Activity的TypeElement
Set<ViewInfo> views = mToBindMap.get(enclosingElement); //views儲存着一個Activity中將要綁定的view的信息
if (views == null) { //若是views不存在就new一個
views = new HashSet<>();
mToBindMap.put(enclosingElement, views);
}
BindView bindAnnotation = variableElement.getAnnotation(BindView.class); //獲取到一個變量的註解
int id = bindAnnotation.value(); //取出註解中的value值,這個值就是這個view要綁定的xml中的id
views.add(new ViewInfo(variableElement.getSimpleName().toString(), id)); //把要綁定的View的信息存進views中
}
}
//按不一樣的Activity生成不一樣的幫助類
private String generateCode(TypeElement typeElement) {
String rawClassName = typeElement.getSimpleName().toString(); //獲取要綁定的View所在類的名稱
String packageName = ((PackageElement) mElementsUtils.getPackageOf(typeElement)).getQualifiedName().toString(); //獲取要綁定的View所在類的包名
String helperClassName = rawClassName + "$$Autobind"; //要生成的幫助類的名稱
StringBuilder builder = new StringBuilder();
builder.append("package ").append(packageName).append(";\n"); //構建定義包的代碼
builder.append("import com.example.apt_api.template.IBindHelper;\n\n"); //構建import類的代碼
builder.append("public class ").append(helperClassName).append(" implements ").append("IBindHelper"); //構建定義幫助類的代碼
builder.append(" {\n"); //代碼格式,能夠忽略
builder.append("\t@Override\n"); //聲明這個方法爲重寫IBindHelper中的方法
builder.append("\tpublic void inject(" + "Object" + " target ) {\n"); //構建方法的代碼
for (ViewInfo viewInfo : mToBindMap.get(typeElement)) { //遍歷每個須要綁定的view
builder.append("\t\t"); //代碼格式,能夠忽略
builder.append(rawClassName + " substitute = " + "(" + rawClassName + ")" + "target;\n"); //強制類型轉換
builder.append("\t\t"); //代碼格式,能夠忽略
builder.append("substitute." + viewInfo.viewName).append(" = "); //構建賦值表達式
builder.append("substitute.findViewById(" + viewInfo.id + ");\n"); //構建賦值表達式
}
builder.append("\t}\n"); //代碼格式,能夠忽略
builder.append('\n'); //代碼格式,能夠忽略
builder.append("}\n"); //代碼格式,能夠忽略
return builder.toString();
}
//要綁定的View的信息載體
class ViewInfo {
String viewName; //view的變量名
int id; //xml中的id
public ViewInfo(String viewName, int id) {
this.viewName = viewName;
this.id = id;
}
}
}
複製代碼
咱們本身實現的APT類須要繼承AbstractProcessor
這個類,其中須要重寫如下方法:
init(ProcessingEnvironment processingEnv)
getSupportedAnnotationTypes()
getSupportedSourceVersion()
process(Set<? extends TypeElement> set, RoundEnvironment roundEnv)
這不是一個抽象方法,但progressingEnv參數給咱們提供了許多有用的工具,因此咱們須要重寫它
@Override
public synchronized void init(ProcessingEnvironment processingEnv) {
super.init(processingEnv);
mFilerUtils = processingEnv.getFiler();
mTypesUtils = processingEnv.getTypeUtils();
mElementsUtils = processingEnv.getElementUtils();
}
複製代碼
mFilterUtils
文件管理工具類,在後面生成java文件時會用到mTypesUtils
類型處理工具類,本例不會用到mElementsUtils
Element處理工具類,後面獲取包名時會用到限於篇幅就不展開對這幾個工具的解析,讀者能夠自行查看文檔~
由方法名咱們就能夠看出這個方法是提供咱們這個APT可以處理的註解
這也不是一個抽象方法,但仍須要重寫它,不然會拋出異常(滑稽),至於爲何有興趣能夠自行查看源碼~
這個方法有固定寫法~
@Override
public Set<String> getSupportedAnnotationTypes() {
HashSet<String> supportTypes = new LinkedHashSet<>();
supportTypes.add(BindView.class.getCanonicalName());
return supportTypes; //將要支持的註解放入其中
}
複製代碼
顧名思義,就是提供咱們這個APT支持的版本號
這個方法和上面的getSupportedAnnotationTypes()相似,也不是一個抽象方法,但也須要重寫,也有固定的寫法
@Override
public SourceVersion getSupportedSourceVersion() {
return SourceVersion.latestSupported();// 表示支持最新的Java版本
}
複製代碼
最主要的方法,用來處理註解,這也是惟一的抽象方法,有兩個參數
set
參數是要處理的註解的類型集合roundEnv
表示運行環境,能夠經過這個參數得到被註解標註的代碼塊@Override
public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnv) {
System.out.println("start process");
if (set != null && set.size() != 0) {
Set<? extends Element> elements = roundEnv.getElementsAnnotatedWith(BindView.class);//得到被BindView註解標記的element
categories(elements);//對不一樣的Activity進行分類
//對不一樣的Activity生成不一樣的幫助類
for (TypeElement typeElement : mToBindMap.keySet()) {
String code = generateCode(typeElement); //獲取要生成的幫助類中的全部代碼
String helperClassName = typeElement.getQualifiedName() + "$$Autobind"; //構建要生成的幫助類的類名
//輸出幫助類的java文件,在這個例子中就是MainActivity$$Autobind.java文件
//輸出的文件在build->source->apt->目錄下
try {
JavaFileObject jfo = mFilerUtils.createSourceFile(helperClassName, typeElement);
Writer writer = jfo.openWriter();
writer.write(code);
writer.flush();
writer.close();
} catch (IOException e) {
e.printStackTrace();
}
}
return true;
}
return false;
}
複製代碼
process方法的大概流程是:
@BindView
標記的Elementcategories
方法,把全部須要綁定的View變量按所在的Activity進行分類,把對應關係存在mToBindMap中generateCode
方法得到要生成的代碼,每一個Activity生成一個幫助類把全部須要綁定的View變量按所在的Activity進行分類,把對應關係存在mToBindMap中
private void categories(Set<? extends Element> elements) {
for (Element element : elements) { //遍歷每個element
VariableElement variableElement = (VariableElement) element; //被@BindView標註的應當是變量,這裏簡單的強制類型轉換
TypeElement enclosingElement = (TypeElement) variableElement.getEnclosingElement(); //獲取表明Activity的TypeElement
Set<ViewInfo> views = mToBindMap.get(enclosingElement); //views儲存着一個Activity中將要綁定的view的信息
if (views == null) { //若是views不存在就new一個
views = new HashSet<>();
mToBindMap.put(enclosingElement, views);
}
BindView bindAnnotation = variableElement.getAnnotation(BindView.class); //獲取到一個變量的註解
int id = bindAnnotation.value(); //取出註解中的value值,這個值就是這個view要綁定的xml中的id
views.add(new ViewInfo(variableElement.getSimpleName().toString(), id)); //把要綁定的View的信息存進views中
}
}
複製代碼
註解應該已經很詳細了~
實現方式僅供參考,讀者能夠有本身的實現
按照不一樣的TypeElement生成不一樣的幫助類(注:參數中的TypeElement對應一個Activity)
private String generateCode(TypeElement typeElement) {
String rawClassName = typeElement.getSimpleName().toString(); //獲取要綁定的View所在類的名稱
String packageName = ((PackageElement) mElementsUtils.getPackageOf(typeElement)).getQualifiedName().toString(); //獲取要綁定的View所在類的包名
String helperClassName = rawClassName + "$$Autobind"; //要生成的幫助類的名稱
StringBuilder builder = new StringBuilder();
builder.append("package ").append(packageName).append(";\n"); //構建定義包的代碼
builder.append("import com.example.apt_api.template.IBindHelper;\n\n"); //構建import類的代碼
builder.append("public class ").append(helperClassName).append(" implements ").append("IBindHelper"); //構建定義幫助類的代碼
builder.append(" {\n"); //代碼格式,能夠忽略
builder.append("\t@Override\n"); //聲明這個方法爲重寫IBindHelper中的方法
builder.append("\tpublic void inject(" + "Object" + " target ) {\n"); //構建方法的代碼
for (ViewInfo viewInfo : mToBindMap.get(typeElement)) { //遍歷每個須要綁定的view
builder.append("\t\t"); //代碼格式,能夠忽略
builder.append(rawClassName + " substitute = " + "(" + rawClassName + ")" + "target;\n"); //強制類型轉換
builder.append("\t\t"); //代碼格式,能夠忽略
builder.append("substitute." + viewInfo.viewName).append(" = "); //構建賦值表達式
builder.append("substitute.findViewById(" + viewInfo.id + ");\n"); //構建賦值表達式
}
builder.append("\t}\n"); //代碼格式,能夠忽略
builder.append('\n'); //代碼格式,能夠忽略
builder.append("}\n"); //代碼格式,能夠忽略
return builder.toString();
}
複製代碼
你們能夠對比生成的代碼
package com.example.apt_demo;
import com.example.apt_api.template.IBindHelper;
public class MainActivity$$Autobind implements IBindHelper {
@Override
public void inject(Object target ) {
MainActivity substitute = (MainActivity)target;
substitute.testTextView = substitute.findViewById(2131165315);
}
}
複製代碼
有沒有以爲字符串拼接很麻煩呢,不只麻煩還容易出錯,那麼有沒有更好的辦法呢(留個坑)
一樣的~ 這個方法的設計僅供參考啦啦啦~~~
這應該是最簡單的一步
這應該是最麻煩的一步
客官別急,往下看就知道了~
註冊一個APT須要如下步驟:
正如我前面所說的~
簡單是由於都是一些固定的步驟
麻煩也是由於都是一些固定的步驟
4.1 建立一個Android Library Module,名稱叫apt-api
,並添加依賴
dependencies {
...
api project(':apt-annotation')
}
複製代碼
4.2 分別建立launcher、template文件夾
4.3 在template文件夾中建立IBindHelper
接口
public interface IBindHelper {
void inject(Object target);
}
複製代碼
這個接口主要供APT生成的幫助類實現
4.4在launcher文件夾中建立AutoBind
類
public class AutoBind {
private static AutoBind instance = null;
public AutoBind() {
}
public static AutoBind getInstance() {
if(instance == null) {
synchronized (AutoBind.class) {
if (instance == null) {
instance = new AutoBind();
}
}
}
return instance;
}
public void inject(Object target) {
String className = target.getClass().getCanonicalName();
String helperName = className + "$$Autobind";
try {
IBindHelper helper = (IBindHelper) (Class.forName(helperName).getConstructor().newInstance());
helper.inject(target);
} catch (Exception e) {
e.printStackTrace();
}
}
}
複製代碼
這個類是咱們的API的入口類,使用了單例模式
inject方法是最主要的方法,但實現很簡單,就是經過反射去調用APT生成的幫助類的方法去實現View的自動綁定
在app module裏添加依賴
dependencies {
...
annotationProcessor project(':apt-compiler')
implementation project(':apt-api')
}
複製代碼
public class MainActivity extends AppCompatActivity {
@BindView(value = R.id.test_textview)
public TextView testTextView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
AutoBind.getInstance().inject(this);
testTextView.setText("APT 測試");
}
}
複製代碼
大功告成!咱們來運行項目試試看
TextView已經正確顯示了文字,咱們的小demo到這裏就完成啦~
咱們的APT還能夠變得更簡單!
下面咱們來逐個擊破~
JavaPoet是一個用來生成Java代碼的框架,對JavaPoet不瞭解的請移步官方文檔
JavaPoet生成代碼的步驟大概是這樣(摘自官方文檔):
MethodSpec main = MethodSpec.methodBuilder("main")
.addModifiers(Modifier.PUBLIC, Modifier.STATIC)
.returns(void.class)
.addParameter(String[].class, "args")
.addStatement("$T.out.println($S)", System.class, "Hello, JavaPoet!")
.build();
TypeSpec helloWorld = TypeSpec.classBuilder("HelloWorld")
.addModifiers(Modifier.PUBLIC, Modifier.FINAL)
.addMethod(main)
.build();
JavaFile javaFile = JavaFile.builder("com.example.helloworld", helloWorld)
.build();
javaFile.writeTo(System.out);
複製代碼
使用JavaPoet來生成代碼有不少的優勢,不容易出錯,能夠自動import等等,這裏不過多介紹,有興趣的同窗能夠自行了解
@SupportedSourceVersion(SourceVersion.RELEASE_7)
@SupportedAnnotationTypes("com.example.apt_annotation.BindView")
public class BindViewProcessor extends AbstractProcessor {
...
}
複製代碼
這是javax.annotation.processing中提供的註解,直接使用便可
這是谷歌官方出品的一個開源庫,能夠省去註冊APT的步驟,只須要一行註釋
先在apt-compiler模塊中添加依賴
dependencies {
...
implementation 'com.google.auto.service:auto-service:1.0-rc2'
}
複製代碼
而後添加註釋便可
@AutoService(Processor.class)
public class BindViewProcessor extends AbstractProcessor {
...
}
複製代碼
APT是一個很是強大並且頻繁出如今各類開源庫的工具,學習APT不只可讓咱們在閱讀開源庫源碼時遊刃有餘也能夠本身開發註解框架來幫本身寫代碼~ 本demo地址