Android 利用 APT 技術在編譯期生成代碼
APT( Annotation Processing Tool 的簡稱),可以在代碼編譯期解析注解,并且生成新的 Java 文件,減少手動的代碼輸入。現在有很多主流庫都用上了 APT,比如 Dagger2, ButterKnife, EventBus3 等,我們要緊跟潮流,與時俱進吶! (? ??_??)?
下面通過一個簡單的 View 注入項目 ViewFinder 來介紹 APT 相關內容,簡單實現了類似于 ButterKnife 中的兩種注解 @BindView 和 @OnClick 。
大概項目結構如下:
- viewFinder-annotation - 注解相關模塊
- viewFinder-compiler - 注解處理器模塊
- viewfinder - API 相關模塊
- sample - 示例 Demo 模塊
實現目標
在通常的 Android 項目中,會寫大量的界面,那么就會經常重復地寫一些代碼,比如:
TextView text = (TextView) findViewById(R.id.tv);
text.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
// on click
}
});
天天寫這么冗長又無腦的代碼,還能不能愉快地玩耍啦。所以,我打算通過 ViewFinder 這個項目替代這重復的工作,只需要簡單地標注上注解即可。通過控件 id 進行注解,并且 @OnClick 可以對多個控件注解同一個方法。就像下面這樣子咯:
@BindView(R.id.tv) TextView mTextView;
@OnClick({R.id.tv, R.id.btn})
public void onSomethingClick() {
// on click
}
定義注解
創建 module viewFinder-annotation ,類型為 Java Library,定義項目所需要的注解。
在 ViewFinder 中需要兩個注解 @BindView 和 @OnClick 。實現如下:
@Retention(RetentionPolicy.CLASS)
@Target(ElementType.FIELD)
public @interface BindView {
int value();
}
@Retention(RetentionPolicy.CLASS)
@Target(ElementType.METHOD)
public @interface OnClick {
int[] value();
}
@BindView 需要對成員變量進行注解,并且接收一個 int 類型的參數; @OnClick 需要對方法進行注解,接收一組 int 類型參數,相當于給一組 View 指定點擊響應事件。
編寫 API
創建 module viewfinder ,類型為 Android Library。在這個 module 中去定義 API,也就是去確定讓別人如何來使用我們這個項目。
首先需要一個 API 主入口,提供靜態方法直接調用,就比如這樣:
ViewFinder.inject(this);
同時,需要為不同的目標(比如 Activity、Fragment 和 View 等)提供重載的注入方法,最終都調用 inject() 方法。其中有三個參數:
- host 表示注解 View 變量所在的類,也就是注解類
- source 表示查找 View 的地方,Activity & View 自身就可以查找,Fragment 需要在自己的 itemView 中查找
- provider 是一個接口,定義了不同對象(比如 Activity、View 等)如何去查找目標 View,項目中分別為 Activity、View 實現了 Provider 接口(具體實現參考 項目代碼 吧 :smile:)。
public class ViewFinder {
private static final ActivityProvider PROVIDER_ACTIVITY = new ActivityProvider();
private static final ViewProvider PROVIDER_VIEW = new ViewProvider();
public static void inject(Activity activity) {
inject(activity, activity, PROVIDER_ACTIVITY);
}
public static void inject(View view) {
// for view
inject(view, view);
}
public static void inject(Object host, View view) {
// for fragment
inject(host, view, PROVIDER_VIEW);
}
public static void inject(Object host, Object source, Provider provider) {
// how to implement ?
}
}
那么 inject() 方法中都寫一些什么呢?
首先我們需要一個接口 Finder ,然后為每一個注解類都生成一個對應的內部類并且實現這個接口,然后實現具體的注入邏輯。在 inject() 方法中首先找到調用者對應的 Finder 實現類,然后調用其內部的具體邏輯來達到注入的目的。
接口 Finder 設計如下 :
public interface Finder<T> {
void inject(T host, Object source, Provider provider);
}
舉個:chestnut:,為 MainActivity 生成 MainActivity$$Finder ,為 MainActivity 所注解的 View 進行初始化和設置點擊事件,這就跟我們平常所寫的重復代碼基本相同。
public class MainActivity$$Finder implements Finder<MainActivity> {
@Override
public void inject(final MainActivity host, Object source, Provider provider) {
host.mTextView = (TextView) (provider.findView(source, 2131427414));
host.mButton = (Button) (provider.findView(source, 2131427413));
host.mEditText = (EditText) (provider.findView(source, 2131427412));
View.OnClickListener listener;
listener = new View.OnClickListener() {
@Override
public void onClick(View view) {
host.onButtonClick();
}
};
provider.findView(source, 2131427413).setOnClickListener(listener);
listener = new View.OnClickListener() {
@Override
public void onClick(View view) {
host.onTextClick();
}
};
provider.findView(source, 2131427414).setOnClickListener(listener);
}
}
好了,所有注解類都有了一個名為 xx$$Finder 的內部類。我們首先通過注解類的類名,得到其對應內部類的 Class 對象,然后實例化拿到具體對象,調用注入方法。
public class ViewFinder {
// same as above
private static final Map<String, Finder> FINDER_MAP = new LinkedHashMap<>();
public static void inject(Object host, Object source, Provider provider) {
String className = host.getClass().getName();
try {
Finder finder = FINDER_MAP.get(className);
if (finder == null) {
Class<?> finderClass = Class.forName(className + "$$Finder");
finder = (Finder) finderClass.newInstance();
FINDER_MAP.put(className, finder);
}
finder.inject(host, source, provider);
} catch (Exception e) {
throw new RuntimeException("Unable to inject for " + className, e);
}
}
}
另外代碼中使用到了一點反射,為了提高效率,避免每次注入的時候都去找 Finder 對象,用一個 Map 將第一次找到的對象緩存起來,后面用的時候直接從 Map 里面取。
到此,API 模塊的設計基本搞定了,接下來就是去通過注解處理器來每一個注解類生成 Finder 內部類。
創建注解處理器
創建 module viewFinder-compiler ,類型為 Java Library,實現一個注解處理器。
這個模塊需要添加一些依賴:
compile project(':viewfinder-annotation')
compile 'com.squareup:javapoet:1.7.0'
compile 'com.google.auto.service:auto-service:1.0-rc2'
- 因為要用到前面定義的注解,當然要依賴 viewFinder-annotation 。
- javapoet 是 方塊公司 出的又一個好用到爆炸的褲子,提供了各種 API 讓你用各種姿勢去生成 Java 代碼文件,避免了字符串+++到底的尷尬。
- auto-service 是 Google 家的褲子,主要用于注解 Processor ,對其生成 META-INF 配置信息。
下面就來創建我們的處理器 ViewFinderProcessor 。
@AutoService(Processor.class)
public class ViewFinderProcesser extends AbstractProcessor {
/**
* 使用 Google 的 auto-service 庫可以自動生成 META-INF/services/javax.annotation.processing.Processor 文件
*/
private Filer mFiler; //文件相關的輔助類
private Elements mElementUtils; //元素相關的輔助類
private Messager mMessager; //日志相關的輔助類
@Override
public synchronized void init(ProcessingEnvironment processingEnv) {
super.init(processingEnv);
mFiler = processingEnv.getFiler();
mElementUtils = processingEnv.getElementUtils();
mMessager = processingEnv.getMessager();
}
/**
* @return 指定哪些注解應該被注解處理器注冊
*/
@Override
public Set<String> getSupportedAnnotationTypes() {
Set<String> types = new LinkedHashSet<>();
types.add(BindView.class.getCanonicalName());
types.add(OnClick.class.getCanonicalName());
return types;
}
/**
* @return 指定使用的 Java 版本。通常返回 SourceVersion.latestSupported()。
*/
@Override
public SourceVersion getSupportedSourceVersion() {
return SourceVersion.latestSupported();
}
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
// to process annotations
return false;
}
}
用 @AutoService 來注解這個處理器,可以自動生成配置信息。
在 init() 可以初始化拿到一些實用的工具類。
在 getSupportedAnnotationTypes() 方法中返回所要處理的注解的集合。
在 getSupportedSourceVersion() 方法中返回 Java 版本。
這幾個方法寫法基本上都是固定的,重頭戲在 process() 方法。
這里插播一下 Element 元素相關概念,后面會用到不少。
Element 元素,源代碼中的每一部分都是一個特定的元素類型,分別代表了包、類、方法等等,具體看 Demo。
package com.example;
public class Foo { // TypeElement
private int a; // VariableElement
private Foo other; // VariableElement
public Foo() {} // ExecuteableElement
public void setA( // ExecuteableElement
int newA // TypeElement
) {
}
}
這些 Element 元素,相當于 XML 中的 DOM 樹,可以通過一個元素去訪問它的父元素或者子元素。
element.getEnclosingElement();// 獲取父元素
element.getEnclosedElements();// 獲取子元素
注解處理器的整個處理過程跟普通的 Java 程序沒什么區別,我們可以使用面向對象的思想和設計模式,將相關邏輯封裝到 model 中,使得流程更清晰簡潔。分別將注解的成員變量、點擊方法和整個注解類封裝成不同的 model。
public class BindViewField {
private VariableElement mFieldElement;
private int mResId;
public BindViewField(Element element) throws IllegalArgumentException {
if (element.getKind() != ElementKind.FIELD) {
throw new IllegalArgumentException(
String.format("Only fields can be annotated with @%s", BindView.class.getSimpleName()));
}
mFieldElement = (VariableElement) element;
BindView bindView = mFieldElement.getAnnotation(BindView.class);
mResId = bindView.value();
}
// some getter methods
}
主要就是在初始化時校驗了一下元素類型,然后獲取注解的值,在提供幾個 get 方法。 OnClickMethod 封裝類似。
public class AnnotatedClass {
public TypeElement mClassElement;
public List<BindViewField> mFields;
public List<OnClickMethod> mMethods;
public Elements mElementUtils;
// omit some easy methods
public JavaFile generateFinder() {
// method inject(final T host, Object source, Provider provider)
MethodSpec.Builder injectMethodBuilder = MethodSpec.methodBuilder("inject")
.addModifiers(Modifier.PUBLIC)
.addAnnotation(Override.class)
.addParameter(TypeName.get(mClassElement.asType()), "host", Modifier.FINAL)
.addParameter(TypeName.OBJECT, "source")
.addParameter(TypeUtil.PROVIDER, "provider");
for (BindViewField field : mFields) {
// find views
injectMethodBuilder.addStatement("host.$N = ($T)(provider.findView(source, $L))", field.getFieldName(),
ClassName.get(field.getFieldType()), field.getResId());
}
if (mMethods.size() > 0) {
injectMethodBuilder.addStatement("$T listener", TypeUtil.ANDROID_ON_CLICK_LISTENER);
}
for (OnClickMethod method : mMethods) {
// declare OnClickListener anonymous class
TypeSpec listener = TypeSpec.anonymousClassBuilder("")
.addSuperinterface(TypeUtil.ANDROID_ON_CLICK_LISTENER)
.addMethod(MethodSpec.methodBuilder("onClick")
.addAnnotation(Override.class)
.addModifiers(Modifier.PUBLIC)
.returns(TypeName.VOID)
.addParameter(TypeUtil.ANDROID_VIEW, "view")
.addStatement("host.$N()", method.getMethodName())
.build())
.build();
injectMethodBuilder.addStatement("listener = $L ", listener);
for (int id : method.ids) {
// set listeners
injectMethodBuilder.addStatement("provider.findView(source, $L).setOnClickListener(listener)", id);
}
}
// generate whole class
TypeSpec finderClass = TypeSpec.classBuilder(mClassElement.getSimpleName() + "$$Finder")
.addModifiers(Modifier.PUBLIC)
.addSuperinterface(ParameterizedTypeName.get(TypeUtil.FINDER, TypeName.get(mClassElement.asType())))
.addMethod(injectMethodBuilder.build())
.build();
String packageName = mElementUtils.getPackageOf(mClassElement).getQualifiedName().toString();
// generate file
return JavaFile.builder(packageName, finderClass).build();
}
}
AnnotatedClass 表示一個注解類,里面放了兩個列表,分別裝著注解的成員變量和方法。在 generateFinder() 方法中,按照前面設計的模板,利用 JavaPoet 的 API 生成代碼。這部分沒啥特別的,照著 JavaPoet 文檔 來就好了,文檔寫得很細致。
有很多地方需要用到對象的類型,普通類型可以用
ClassName get(String packageName, String simpleName, String... simpleNames)
傳入包名、類名、內部類名,就可以拿到想要的類型了(可以參考 項目中 TypeUtil 類)。
用到泛型的話,可以用
ParameterizedTypeName get(ClassName rawType, TypeName... typeArguments)
傳入具體類和泛型類型就好了。
這些 model 都確定好了之后, process() 方法就很清爽啦。使用 RoundEnvironment 參數來查詢被特定注解標注的元素,然后解析成具體的 model,最后生成代碼輸出到文件中。
@AutoService(Processor.class)
public class ViewFinderProcesser extends AbstractProcessor {
private Map<String, AnnotatedClass> mAnnotatedClassMap = new LinkedHashMap<>();
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
// process() will be called several times
mAnnotatedClassMap.clear();
try {
processBindView(roundEnv);
processOnClick(roundEnv);
} catch (IllegalArgumentException e) {
error(e.getMessage());
return true; // stop process
}
for (AnnotatedClass annotatedClass : mAnnotatedClassMap.values()) {
try {
info("Generating file for %s", annotatedClass.getFullClassName());
annotatedClass.generateFinder().writeTo(mFiler);
} catch (IOException e) {
error("Generate file failed, reason: %s", e.getMessage());
return true;
}
}
return true;
}
private void processBindView(RoundEnvironment roundEnv) throws IllegalArgumentException {
for (Element element : roundEnv.getElementsAnnotatedWith(BindView.class)) {
AnnotatedClass annotatedClass = getAnnotatedClass(element);
BindViewField field = new BindViewField(element);
annotatedClass.addField(field);
}
}
private void processOnClick(RoundEnvironment roundEnv) {
// same as processBindView()
}
private AnnotatedClass getAnnotatedClass(Element element) {
TypeElement classElement = (TypeElement) element.getEnclosingElement();
String fullClassName = classElement.getQualifiedName().toString();
AnnotatedClass annotatedClass = mAnnotatedClassMap.get(fullClassName);
if (annotatedClass == null) {
annotatedClass = new AnnotatedClass(classElement, mElementUtils);
mAnnotatedClassMap.put(fullClassName, annotatedClass);
}
return annotatedClass;
}
}
首先解析注解元素,并放到對應的注解類對象中,最后調用方法生成文件。model 的代碼中還會加入一些校驗代碼,來判斷注解元素是否合理,數據是否正常,然后拋出異常,處理器接收到之后可以打印出錯誤提示,然后直接返回 true 來結束處理。
至此,注解處理器也基本完成了,具體細節參考項目代碼。
實際項目使用
創建 module sample ,普通的 Android module,來演示 ViewFinder 的使用。
在整個項目下的 build.gradle 中添加
classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8'
然后在 sample module 下的 build.gradle 中添加
apply plugin: 'com.neenbedankt.android-apt'
同時添加依賴:
compile project(':viewfinder-annotation')
compile project(':viewfinder')
apt project(':viewfinder-compiler')
然后隨便創建個布局,隨便添加幾個控件,就能體驗注解啦。
public class MainActivity extends AppCompatActivity {
@BindView(R.id.tv) TextView mTextView;
@BindView(R.id.btn) Button mButton;
@BindView(R.id.et) EditText mEditText;
@OnClick(R.id.btn)
public void onButtonClick() {
Toast.makeText(this, "onButtonClick", Toast.LENGTH_SHORT).show();
}
@OnClick(R.id.tv)
public void onTextClick() {
Toast.makeText(this, "onTextClick", Toast.LENGTH_SHORT).show();
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
ViewFinder.inject(this);
}
}
這個時候 build 一下項目,就能看到生成的 MainActivity$$Finder 類了,再運行項目就跑起來了。每次改變注解之后,build 一下項目就好啦。
all done ~
這個項目也就是個玩具級的 APT 項目,借此來學習如何編寫 APT 項目。感覺 APT 項目更多地是考慮如何去設計架構,類之間如何調用,需要生成什么樣的代碼,提供怎樣的 API 去調用。最后才是利用注解處理器去解析注解,然后用 JavaPoet 去生成具體的代碼。
思路比實現更重要,設計比代碼更巧妙。
參考
來自:http://brucezz.itscoder.com/articles/2016/08/06/apt-in-android/