社會(huì)猶如一條船绸吸,每個(gè)人都要有掌舵的準(zhǔn)備。 — 易卜生
寫在前面
在上一篇文章《注解入門》最后提出了一個(gè)問題編譯時(shí)注解是什么甫菠?
注解的生命周期為RUNTIME稱為運(yùn)行時(shí)注解,注解的生命周期為CLASS就稱為編譯時(shí)注解。
APT(Annotation Processing Tool)技術(shù)就是通過編譯期解析注解,并且生成Java代碼的一種技術(shù),一般會(huì)結(jié)合Javapoet技術(shù)生成Java代碼哈误。
前期準(zhǔn)備
在正式講解APT技術(shù)之前菜谣,先認(rèn)識(shí)兩個(gè)知識(shí)點(diǎn)鸣皂,分別是Element和SPI機(jī)制荆陆,這是APT技術(shù)的基礎(chǔ)泡挺,一定要掌握。
1.Element
Element指的是一系列與之相關(guān)的接口集合缅帘,存在javax.lang.model.element包下失暂。Element代表的是程序的一個(gè)元素,可以是包扩借,類戒突,接口,屬性變量实牡,方法总滩,方法形參,泛型參數(shù)等元素。Element所代表的元素只在編譯器可見奸攻,用于保存元素在編譯期間的各種狀態(tài)新思。
2.SPI機(jī)制
SPI(Service Provider Interface)的作用是為接口尋找服務(wù)實(shí)現(xiàn),在項(xiàng)目中通過配置文件為接口尋找服務(wù)實(shí)現(xiàn)。
下面就介紹如何配置文件:
- 首先在main目錄下創(chuàng)建resources目錄洁灵。
- 然后在resources目錄下創(chuàng)建META-INF目錄牍汹。
- 接下來在META-INF目錄下創(chuàng)建services目錄有决。
- 最后創(chuàng)建一個(gè)文件名為javax.annotaion.processing.Processor的文件篱瞎,其中的內(nèi)容就是為接口尋找服務(wù)實(shí)現(xiàn)的全類名赠幕。
正式講解
現(xiàn)在通過一個(gè)例子介紹APT技術(shù),在Android應(yīng)用開發(fā)中产雹,ButterKnife是一個(gè)很好用的第三方框架,不知道的同學(xué)請(qǐng)自行百度赢笨,這里就通過寫一個(gè)簡單的ButterKnife實(shí)現(xiàn)自動(dòng)綁定View的框架。
1.創(chuàng)建注解
新建一個(gè)Java Library Module毕匀,名稱為apt_annotation躁垛,并創(chuàng)建一個(gè)注解BindView土铺。
@Documented
@Retention(RetentionPolicy.CLASS)
@Target(ElementType.FIELD)
public @interface BindView {
int viewId() default 0;
}
2.創(chuàng)建注解處理器
新建一個(gè)Java Library Module,名稱為apt_processor拷况,并創(chuàng)建一個(gè)注解處理器AptProcessor,用來生成java代碼套菜。
public class AptProcessor extends AbstractProcessor {
/**
* 初始化函數(shù)
*
* @param processingEnv
*/
@Override
public synchronized void init(ProcessingEnvironment processingEnv) {
super.init(processingEnv);
}
/**
* 指定Java版本號(hào)亲善,一般返回最新版本號(hào)
* 可以使用@SupportedSourceVersion注解代替
*
* @return
*/
@Override
public SourceVersion getSupportedSourceVersion() {
return SourceVersion.latestSupported();
}
/**
* 指定可解析的注解類型
* 可以使用@SupportedAnnotationTypes注解代替
*
* @return
*/
@Override
public Set<String> getSupportedAnnotationTypes() {
Set<String> annotationTypes = new LinkedHashSet<>();
annotationTypes.add(BindView.class.getCanonicalName());
return annotationTypes;
}
/**
* 核心函數(shù)
*
* @param annotations
* @param roundEnv
* @return
*/
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
Map<String, Map<String, Set<Element>>> elementMap = parseElements(roundEnv.getElementsAnnotatedWith(BindView.class));
generateJavaFile(elementMap);
return true;
}
/**
* 解析全部元素
*
* @param elements
* @return
*/
private Map<String, Map<String, Set<Element>>> parseElements(Set<? extends Element> elements) {
Map<String, Map<String, Set<Element>>> elementMap = new LinkedHashMap<>();
// 遍歷全部元素
for (Element element : elements) {
// 判斷當(dāng)前元素是否是屬性變量
if (!element.getKind().isField()) {
continue;
}
// 獲取屬性變量的上一級(jí)元素,即類元素
TypeElement typeElement = (TypeElement) element.getEnclosingElement();
// 獲取類名
String typeName = typeElement.getSimpleName().toString();
// 獲取類的上一級(jí)元素逗柴,即包元素
PackageElement packageElement = (PackageElement) typeElement.getEnclosingElement();
// 獲取包名
String packageName = packageElement.getQualifiedName().toString();
Map<String, Set<Element>> typeElementMap = elementMap.get(packageName);
if (typeElementMap == null) {
typeElementMap = new LinkedHashMap<>();
}
Set<Element> variableElements = typeElementMap.get(typeName);
if (variableElements == null) {
variableElements = new LinkedHashSet<>();
}
variableElements.add(element);
typeElementMap.put(typeName, variableElements);
elementMap.put(packageName, typeElementMap);
}
return elementMap;
}
/**
* 生成Java文件
*
* @param elementMap
*/
private void generateJavaFile(Map<String, Map<String, Set<Element>>> elementMap) {
Set<Map.Entry<String, Map<String, Set<Element>>>> packageElements = elementMap.entrySet();
for (Map.Entry<String, Map<String, Set<Element>>> packageEntry : packageElements) {
String packageName = packageEntry.getKey();
Map<String, Set<Element>> typeElementMap = packageEntry.getValue();
Set<Map.Entry<String, Set<Element>>> typeElements = typeElementMap.entrySet();
for (Map.Entry<String, Set<Element>> typeEntry : typeElements) {
String typeName = typeEntry.getKey();
Set<Element> variableElements = typeEntry.getValue();
ClassName className = ClassName.get(packageName, typeName);
FieldSpec.Builder fieldSpecBuilder = FieldSpec.builder(className, "target", Modifier.PRIVATE);
MethodSpec.Builder bindMethodBuilder = MethodSpec.methodBuilder("bind")
.addModifiers(Modifier.PUBLIC)
.addParameter(className, "activity")
.addStatement("target = activity");
MethodSpec.Builder unbindMethodBuilder = MethodSpec.methodBuilder("unbind")
.addAnnotation(Override.class)
.addModifiers(Modifier.PUBLIC);
for (Element element : variableElements) {
VariableElement variableElement = (VariableElement) element;
String variableName = variableElement.getSimpleName().toString();
BindView bindView = variableElement.getAnnotation(BindView.class);
bindMethodBuilder.addStatement("target." + variableName + " = activity.findViewById(" + bindView.viewId() + ")");
unbindMethodBuilder.addStatement("target." + variableName + " = null");
}
unbindMethodBuilder.addStatement("target = null");
TypeSpec.Builder typeBuilder = TypeSpec.classBuilder(typeName + "_ViewBinding")
.addModifiers(Modifier.PUBLIC)
.addSuperinterface(ClassName.get("com.chad.apt.binder", "UnBinder"))
.addField(fieldSpecBuilder.build())
.addMethod(bindMethodBuilder.build())
.addMethod(unbindMethodBuilder.build());
JavaFile javaFile = JavaFile.builder(packageName, typeBuilder.build()).build();
try {
javaFile.writeTo(processingEnv.getFiler());
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
由于BindView在apt_annotation中蛹头,并且通過Javapoet技術(shù)生成Java代碼,所以需要在apt_processor的build.gradle中引入apt_annotation和javapoet庫戏溺。如下圖:
3.創(chuàng)建SPI配置文件
接下來在apt_processor中創(chuàng)建SPI配置文件渣蜗,創(chuàng)建步驟在前面已經(jīng)講過,這里不再復(fù)述旷祸,只看javax.annotation.processing.Processor文件內(nèi)容如何定義耕拷,如下圖的文件內(nèi)容為com.chad.apt.processor.AptProcessor,也就是為Processor接口尋找AptProcessor服務(wù)實(shí)現(xiàn)類托享。
4.創(chuàng)建綁定者
新建一個(gè)Android Library Module斑胜,名稱為apt_binder,并且創(chuàng)建一個(gè)綁定者AptBinder和一個(gè)UnBinder接口嫌吠,AptBinder中的bind函數(shù)通過反射機(jī)制創(chuàng)建注解處理器生成的Java類并綁定View止潘,UnBinder接口用來解綁View。
public class AptBinder {
public static UnBinder bind(Activity activity) {
try {
Class<?> activityClass = activity.getClass();
Class<?> viewBindingClass = Class.forName(activityClass.getName() + "_ViewBinding");
Method bindMethod = viewBindingClass.getMethod("bind", activityClass);
UnBinder unBinder = (UnBinder) viewBindingClass.newInstance();
bindMethod.invoke(unBinder, activity);
return unBinder;
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
}
public interface UnBinder {
void unbind();
}
5.如何使用
新建一個(gè)Android Module辫诅,名稱為app凭戴,并且將apt_annotation,apt_processor和apt_binder三個(gè)Library引入到app build.gradle炕矮。如下圖:
- 創(chuàng)建activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<TextView
android:id="@+id/id_hello"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="50dp"
app:layout_constraintBottom_toBottomOf="@+id/id_world"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/id_world"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="50dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="@+id/id_hello" />
</android.support.constraint.ConstraintLayout>
- 創(chuàng)建MainActivity
public class MainActivity extends AppCompatActivity {
private static final String TAG = MainActivity.class.getSimpleName();
@BindView(viewId = R.id.id_hello)
TextView mTvHello;
@BindView(viewId = R.id.id_world)
TextView mTvWorld;
private UnBinder unBinder;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// 綁定
unBinder = AptBinder.bind(this);
mTvHello.setText("Hello!!!");
mTvWorld.setText("World!!!");
Log.d(TAG, "bind : mTvHello = " + mTvHello + " , mTvWorld = " + mTvWorld);
// 解綁
unBinder.unbind();
Log.d(TAG, "unbind : mTvHello = " + mTvHello + " , mTvWorld = " + mTvWorld);
}
}
在MainActivity中通過注解聲明TextView屬性變量么夫,在onCreate()函數(shù)中通過AptBinder.bind()實(shí)現(xiàn)自動(dòng)綁定View并返回一個(gè)UnBinder者冤,UnBinder調(diào)用unbind()函數(shù)用來解綁View。
然后rebuild就會(huì)自動(dòng)生成java代碼档痪,即build > generated > source > apt > debug/release > com.chad.annotation目錄下的MainActivity_ViewBinding.java文件涉枫。
public class MainActivity_ViewBinding implements UnBinder {
private MainActivity target;
public void bind(MainActivity activity) {
target = activity;
target.mTvHello = activity.findViewById(2131165252);
target.mTvWorld = activity.findViewById(2131165253);
}
@Override
public void unbind() {
target.mTvHello = null;
target.mTvWorld = null;
target = null;
}
}
6.工程目錄
下圖就是整個(gè)工程的目錄結(jié)構(gòu):
總結(jié)
APT技術(shù)可以在編譯期間生成Java代碼,對(duì)于注解而言腐螟,有效的避免了運(yùn)行時(shí)注解通過反射解析注解信息影響效率的問題愿汰。
SPI機(jī)制通過配置文件的方式使用起來很麻煩,即要?jiǎng)?chuàng)建目錄又要?jiǎng)?chuàng)建文件乐纸。Google的auto-service庫提供了一種簡單的配置方式衬廷,通過注解就可以解決繁瑣的創(chuàng)建過程,使用方式很簡單汽绢,這里就不詳細(xì)講解了吗跋。