一 前言
前面兩篇文章殊鞭,注解處理器,理解注解尼桶,對(duì)注解有了一個(gè)初步認(rèn)識(shí)操灿,第二篇文章末尾也提到了,注解不是代碼的一部分泵督,當(dāng)開發(fā)者使用了Annotation注解以后牲尺,注解不會(huì)自己起作用,必須提供相應(yīng)的代碼來處理這些信息。
這篇文章谤碳,我們就寫一個(gè)簡單的注解處理器,作用是類似于ButterKnife查找id溢豆。
源碼傳送門
二 項(xiàng)目結(jié)構(gòu)
整個(gè)項(xiàng)目采用如下所示的結(jié)構(gòu):
- BindViewAnnotation蜒简,Java Library,存放我們定義的注解漩仙。
- bindviewapi搓茬,Android Library,聲明注解框架使用的api队他,本例子中卷仑,我們要實(shí)現(xiàn)的是查找view控件,并將控件和xml中的綁定麸折。
- BindViewCompiler锡凝,Java Library,注解處理器垢啼,根據(jù)注解生成處理打代碼窜锯。
新建這幾個(gè)Library的過程不再陳述,特別注意的是芭析,建BindViewCompiler Java Library時(shí)锚扎,在build.gradle下要加入以下代碼:
// 用于生成Java文件的庫
implementation 'com.squareup:javapoet:1.11.1'
implementation 'com.google.auto.service:auto-service:1.0-rc6'
annotationProcessor 'com.google.auto.service:auto-service:1.0-rc6'
這些代碼后面會(huì)提及。
三 正式開始吧
(一)在BindViewAnnotation新建注解
前面說過了馁启,我們要實(shí)現(xiàn)的功能是查找xml文件的id功能驾孔,注解歹意如下:
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.CLASS)
public @interface BindView {
int value();
}
使用注解,在Activity中使用注解惯疙,在xml中定義一個(gè)button翠勉,
<Button
android:id="@+id/btn_bind"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="BindButton"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
在Activity中使用我們定義的注解。
@BindView(R.id.btn_bind)
public Button mButton;
前面說了螟碎,注解不會(huì)自動(dòng)起作用眉菱,如果我們直接運(yùn)行代碼,會(huì)直接報(bào)錯(cuò)的掉分,提示Button沒有定義俭缓,所以我們要寫代碼來處理這個(gè)注解的信息。
(二) 聲明注解框架用到的api
① 定義一個(gè)綁定注解的接口
public interface IViewBind<T> {
void bind(T t);
}
② 向外提供的綁定方法酥郭,這里使用靜態(tài)方法來管理华坦。
public class ViewBinder {
public static void bind(Activity activity){
try {
// 1
Class clazz=Class.forName(activity.getClass().getCanonicalName()+"$$ViewBinder");
// 2
IViewBind<Activity> iViewBinder= (IViewBind<Activity>) clazz.newInstance();
// 3
iViewBinder.bind(activity);
} catch (ClassNotFoundException e) {
Log.d("hbj--exp",e.getException()+"");
Log.d("hbj--cause",e.getCause()+"");
Log.d("hbj--mess",e.getMessage()+"");
Log.d("hbj--class",e.getClass()+"");
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
}
}
}
// 1 處獲取生成的類的名字,生成類的規(guī)則代碼在后面寫不从,生成的規(guī)則是 類名$$ViewBinder惜姐,例如在MainActivity中需要使用,生成的文件名字就是MainActivity$$ViewBinder.java。
// 2 獲取生成的類的實(shí)例歹袁。
// 3 完成綁定坷衍。
可能對(duì)第二條和第三條不是很好理解,現(xiàn)在貼出生成的java文件的源碼条舔,結(jié)合生成的文件枫耳,應(yīng)該就好理解了吧。
public class MainActivity$$ViewBinder< T extends MainActivity> implements IViewBind<T> {
@Override
public void bind(T activity) {
activity.mButton=activity.findViewById(2131165250);
}
}
(三) 根據(jù)注解生成代碼
現(xiàn)在只剩根據(jù)注解生成代碼孟抗。
創(chuàng)建一個(gè)自定義的Annotation Processor迁杨,繼承自AbstractProcessor。
// 1
@AutoService(Processor.class)
public class MyProcessor extends AbstractProcessor {
// 2
@Override
public synchronized void init(ProcessingEnvironment env){
}
// 3
@Override
public boolean process(Set<? extends TypeElement> annoations, RoundEnvironment roundEnv) { }
// 4
@Override
public Set<String> getSupportedAnnotationTypes() {
}
// 5
@Override
public SourceVersion getSupportedSourceVersion() {
}
}
// 1處 @AutoService(Processor.class)凄硼,向javac注冊(cè)我們自定義的注解處理器铅协, 這樣,在javac編譯時(shí)摊沉,才會(huì)調(diào)用到我們這個(gè)自定義的注解處理器方法狐史。
AutoService主要是生成 META-INF/services/javax.annotation.processing.Processor文件的。如果不加上這個(gè)注解的話坯钦,需要通過以下方法進(jìn)行手動(dòng)配置進(jìn)行手動(dòng)注冊(cè):
1.在main下創(chuàng)建resources目錄预皇,然后創(chuàng)建META-INF/services 目錄,最后在此目錄下創(chuàng)建文件:javax.annotation.processing.Processor婉刀,目錄如下吟温,
文件里的內(nèi)容是一些列的自定義注解處理器完整有效的類名集合,以換行符切割突颊,這里就自定義了一個(gè)注解處理器鲁豪,
注:但是有可能會(huì)出現(xiàn)使用@AutoService()無法動(dòng)態(tài)生成入口文件的,這個(gè)問題可以如下解決:
這個(gè)要從google auto service 和META_INF律秃,谷歌的 auto service 也是一種 Annotation Processor爬橡,它能自動(dòng)生成 META-INF 目錄以及相關(guān)文件,避免手工創(chuàng)建該文件棒动,手工創(chuàng)建的方法糙申,上文有,手工創(chuàng)建有可能失誤船惨。使用 auto service 中的 @AutoService(Processor.class) 注解修飾 Annotation Processor 類就可以在編譯過程中自動(dòng)生成文件柜裸。
如果要進(jìn)入的話,還要注意要引入兩個(gè)配置:
implementation 'com.google.auto.service:auto-service:1.0-rc6'
annotationProcessor 'com.google.auto.service:auto-service:1.0-rc6'
這兩個(gè)重復(fù)寫的原因是:annotationProcessor這個(gè)是新版 gradle 提供的 java plugin 內(nèi)置的配置項(xiàng)粱锐,在gradle 5.+ 中將 Annotation Processor 從編譯期 classpath 中去除了疙挺,javac 也就無法發(fā)現(xiàn) Annotation Processor。此處如果按照 gradle 4.+ 的寫法怜浅,只寫一個(gè) implementation 是無法使用 auto service 的 Annotation Processor 的铐然。必須要使用 annotationProcessor 來配置 Annotation Processor 使其生效。
// 2處 init(ProcessingEnvironment env): 每一個(gè)注解處理器類都必須有一個(gè)空的構(gòu)造函數(shù)。然而搀暑,這里有一個(gè)特殊的init()方法沥阳,它會(huì)被注解處理工具調(diào)用,并輸入ProcessingEnviroment參數(shù)险掀。ProcessingEnviroment提供很多有用的工具類Elements,Types和Filer沪袭。
// 3處 public boolean process(Set<? extends TypeElement> annoations, RoundEnvironment env)這相當(dāng)于每個(gè)處理器的主函數(shù)main()。 在這里寫掃描樟氢、評(píng)估和處理注解的代碼,以及生成Java文件侠鳄。輸入?yún)?shù)RoundEnviroment埠啃,可以讓查詢出包含特定注解的被注解元素。
// 4處 getSupportedAnnotationTypes()伟恶,這里必須指定碴开,這個(gè)注解處理器是注冊(cè)給哪個(gè)注解的。注意博秫,它的返回值是一個(gè)字符串的集合潦牛,包含本處理器想要處理的注解類型的合法全稱。換句話說挡育,在這里定義你的注解處理器注冊(cè)到哪些注解上巴碗。
// 5處 getSupportedSourceVersion();用來指定你使用的Java版本。
下面給出BindViewProcessor的完整代碼:
@AutoService(Processor.class)
public class BindViewProcessor extends AbstractProcessor {
// 文件相關(guān)輔助類
private Filer mFiler;
// 日志相關(guān)輔助類
private Messager mMessager;
@Override
public synchronized void init(ProcessingEnvironment processingEnv) {
super.init(processingEnv);
mFiler = processingEnv.getFiler();
mMessager = processingEnv.getMessager();
}
@Override
public SourceVersion getSupportedSourceVersion() {
return SourceVersion.latestSupported();
}
@Override
public Set<String> getSupportedAnnotationTypes() {
Set<String> annotations=new LinkedHashSet<>();
annotations.add(BindView.class.getCanonicalName());
return annotations;
}
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
Set<? extends Element> elements = roundEnv.getElementsAnnotatedWith(BindView.class);
Map<TypeElement,ArrayList<BindViewInfo>> bindViewMap=new HashMap<>();
for (Element element : elements) {
// 因?yàn)锽indView只作用于Filed即寒,判斷注解是否是屬性橡淆,不是的話直接結(jié)束
if (element.getKind() != ElementKind.FIELD) {
mMessager.printMessage(Diagnostic.Kind.ERROR, element.getSimpleName().toString() + " is not filed,can not use @BindView");
return false;
}
// 獲取注解元數(shù)據(jù)
int id = element.getAnnotation(BindView.class).value();
// 獲取屬性的類
TypeElement typeElement= (TypeElement) element.getEnclosingElement();
if (!bindViewMap.containsKey(typeElement)){
bindViewMap.put(typeElement,new ArrayList<BindViewInfo>());
}
ArrayList<BindViewInfo> bindViewInfos=bindViewMap.get(typeElement);
// 添加list
bindViewInfos.add(new BindViewInfo(id,element.getSimpleName().toString()));
}
produceClass(bindViewMap);
return true;
}
private void produceClass(Map<TypeElement,ArrayList<BindViewInfo>> hasMap){
if (hasMap == null || hasMap.isEmpty()){
return;
}
Set<TypeElement> typeElements=hasMap.keySet();
for (TypeElement typeElement:typeElements){
produceJavaClass(typeElement,hasMap.get(typeElement));
}
}
/**
* 產(chǎn)生Java文件
* @param typeElement
* @param bindViewInfos
*/
private void produceJavaClass(TypeElement typeElement, List<BindViewInfo> bindViewInfos){
try {
StringBuffer stringBuffer=new StringBuffer();
stringBuffer.append("package ");
stringBuffer.append(getPackageName(typeElement.getQualifiedName().toString())+";\n");
stringBuffer.append("import com.jackson.bindviewapi.IViewBind;\n");
stringBuffer.append("public class "+typeElement.getSimpleName()+"$$ViewBinder< T extends "+typeElement.getSimpleName()+"> implements IViewBind<T> {\n");
stringBuffer.append("@Override\n");
stringBuffer.append("public void bind(T activity) {\n");
for (BindViewInfo bindViewInfo:bindViewInfos){
stringBuffer.append("activity."+bindViewInfo.name+"=activity.findViewById("+bindViewInfo.id+");\n");
}
stringBuffer.append("}\n}");
JavaFileObject javaFileObject=mFiler.createSourceFile(typeElement.getQualifiedName().toString()+"$$ViewBinder");
Writer writer=javaFileObject.openWriter();
writer.write(stringBuffer.toString());
writer.close();
} catch (IOException e) {
e.printStackTrace();
}
}
private String getPackageName(String className){
if (className==null || className.equals("")){
return "";
}
return className.substring(0,className.lastIndexOf("."));
}
}
重新編譯項(xiàng)目,在app/build/source/apt/debug/com.jackson.annotationdemo下就會(huì)找到生成的文件母赵,如下圖
生成文件的代碼逸爵,前面已經(jīng)給出,可以對(duì)照著生成java文件的代碼凹嘲,來看BindViewProcessor生成java文件的代碼規(guī)則师倔。
(四) 使用
在MainActivity中使用,代碼如下:
@BindView(R.id.btn_bind)
public Button mButton;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
ViewBinder.bind(this);
mButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
Toast.makeText(MainActivity.this,"注解測(cè)試",Toast.LENGTH_SHORT).show();
}
});
}
可以看到周蹭,在MainActivity中趋艘,并沒有mButton的findViewById()來初始化,而是通過注解完成谷醉,代碼運(yùn)行正常致稀。
源碼傳送門