一 前言
android中現今流行的各大框架,比如ButterFly、eventBus、OrmLite、Retrofit等都使用註解,註解是什 麼呢?註解就是元數據,可以理解爲屬性、方法、類等的一個說明,具體詳解可百度,也可移步我的另一篇註解原理詳解。一下就以ButterFly爲例,解讀徒手打造一個FinderView的框架。
獲取註解的元數據的方式有以下兩種:
1、直接通過Class|Method|Field.getAnnotation(xxxAnnotation.class)獲取註解實例,在獲取元數據, 具體可查看註解原理詳解。
2、通過註解處理器APT來獲取。
這裏使用的是方式二。註解器處理是在build編譯時執行的
二 原理
1、定義Method、Field的註解類分別爲OnClick、BindView,分別應用於Method和Field
2、apt註解處理器解析註解類,獲取Method/Field,隨後通過javapoet框架創建一個類爲xxxFind實現爲每 個Field生成findViewById()的實現方法,爲每個Method生成OnClickListener監聽器並實現調用被註解 的Method
3、通過工具類Inject(activity)調用,實現反射xxxFind,調用上述的方法實現Method、Field初始化
三 詳解
注:
爲了減少麻煩 這裏需要使用如下依賴:
項目的build.gradle中
向倉庫中添加組件對apt的依賴 :classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8'
module中的build.gradle引用兩個庫:
apt編譯執行時的庫 : compile 'com.squareup:javapoet:1.7.0'
生成java代碼的庫: compile 'com.google.auto.service:auto-service:1.0-rc2'
1.明確我們的註解是在編譯時期進行的,而且只用在控件屬性和點擊方法,所以定義瞭如下註解類:
(1)方法註解類OnClick:
@Retention(RetentionPolicy.CLASS)
@Target(ElementType.METHOD)
public @interface OnClick {
int[] value();
}
(2)控件屬性註解類BindView:
@Retention(RetentionPolicy.CLASS)
@Target(ElementType.FIELD)
public @interface BindView {
int value();
}
因爲初始化Method和Field都需要控件id,所以每個註解都要帶控件id,所以需要value值,而且是必須的。
2.apt的解析和生成自動初始化類以及相關方法實現
如要生成如下格式:
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);
}
}
apt代碼走起:
apt需要繼承AbstractProcessor並實現如下方法:
/**
* 使用 Google 的 auto-service 庫可以自動生成 META-INF/services/javax.annotation.processing.Processor 文件
*/
@AutoService(Processor.class)//用 @AutoService 來註解這個處理器,可以自動生成配置信息
public class ViewFinderProcesser extends AbstractProcessor {
private Filer mFiler; //文件的類
private Elements mElementUtils; //元素相光類
private Messager mMessager;//日誌相關類,也可以是用java的system.out輸出日誌
/***初始化會調用,一個處理器只執行一次*/
@Override
public synchronized void init(ProcessingEnvironment processingEnv) {
super.init(processingEnv);
}
/**
* 支持的註解類型
* @return types 指定哪些註解應該被註解處理器註冊
*/
@Override
public Set<String> getSupportedAnnotationTypes() {
}
/**
* @return 指定使用的 Java 版本。通常返回 SourceVersion.latestSupported()。
*/
@Override
public SourceVersion getSupportedSourceVersion() {
return SourceVersion.latestSupported();
}
private Map<String, AnnotatedClass> mAnnotatedClassMap = new HashMap<>();
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
// return true;//返回是True,那麼後續的處理器就不會在處理
return false;//返回是false,那麼後續處理器會繼續處理它們。一個處理器可能總是返回同樣的邏輯值,或者是根據選項改變結果。
}
最主要的方法:
1.init():初始化方法,一個處理器執行一次
2.getSupportedAnnotationTypes():指定這個處理器只處理哪些註解類
3.getSupportedSourceVersion():指定處理器使用的jdk版本
4.process(annotations,RoundEnvironment ):處理器最主要的方法,用於處理註解
annotations:是此次處理註解類的集合
RoundEnvironment :當作處理器和元素之間的上下文,就是個通信橋樑
接下來主要看看主要分析:
1.引入自動處理器的庫之後在處理器實現類中使用註解的方式編譯器就會執行這個類
@AutoService(Processor.class)//用 @AutoService 來註解這個處理器,可以自動生成配置信息
public class ViewFinderProcesser extends AbstractProcessor {
2.init()初始化文件相關類、元素(包括類、屬性、方法等)相關類、日誌管理類
@Override
public synchronized void init(ProcessingEnvironment processingEnv) {
super.init(processingEnv);
System.out.println("=== init ");
mFiler = processingEnv.getFiler();
mElementUtils = processingEnv.getElementUtils();
mMessager = processingEnv.getMessager();
}
3.這裏標註處理器要處理的註解類:有BindView和OnClick
/**
* 支持的註解類型
* @return types 指定哪些註解應該被註解處理器註冊
*/
@Override
public Set<String> getSupportedAnnotationTypes() {
Set<String> types = new LinkedHashSet<>();
types.add(BindView.class.getCanonicalName());
types.add(OnClick.class.getCanonicalName());
return types;
}
4.通過上下文roundEnv處理註解器 @Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
mAnnotatedClassMap.clear();
try {
info("process== %s", annotations.toString());//
processBindView(roundEnv);
processOnClick(roundEnv);
} catch (IllegalArgumentException e) {
info("Generate file failed,1111 reason: %s", e.getMessage());
return false; // stop process
}
// System.out.println("=== annotatedClass "+mAnnotatedClassMap.size());
for (AnnotatedClass annotatedClass : mAnnotatedClassMap.values()) {
try {
// System.out.println("=== annotatedClass "+annotatedClass.getFullClassName());
info("Generating file for %s", annotatedClass.getFullClassName());
annotatedClass.generateFinder().writeTo(mFiler);
} catch (IOException e) {
info("Generate file failed,2222 reason: %s", e.getMessage());
return false;
}
}
// return true;//不再執行這個
return false;//
}
mAnnotatedClassMap:是一個map用於緩存每一個註解相關信息
processBindView(roundEnv);
processOnClick(roundEnv);
這兩個方法實現都差不多,如下:
private void processBindView(RoundEnvironment roundEnv) throws IllegalArgumentException {
//get BindView AnnotionType for all Current Class of Elements
for (Element element : roundEnv.getElementsAnnotatedWith(BindView.class)) {
// TODO: 16/8/4
AnnotatedClass annotatedClass = getAnnotatedClass(element);
info("element name %s", element.getSimpleName());
BindViewField field = new BindViewField(element);
annotatedClass.addField(field);
}
}
private void processOnClick(RoundEnvironment roundEnv) {
for (Element element : roundEnv.getElementsAnnotatedWith(OnClick.class)) {
AnnotatedClass annotatedClass = getAnnotatedClass(element);
info("element OnClick %s", element.getSimpleName());
OnClickMethod method = new OnClickMethod(element);
annotatedClass.addMethod(method);
}
}
主要看這個方法:
roundEnv.getElementsAnnotatedWith(OnClick.class)
roundEnv.getElementsAnnotatedWith(BindView.class)
這裏是獲取被註解類OnClick和BindView註解的所有元素(包括類、方法、屬性等)
隨後調用getAnnotatedClass(element);
private AnnotatedClass getAnnotatedClass(Element element) {
TypeElement classElement = (TypeElement) element.getEnclosingElement();
String fullClassName = classElement.getQualifiedName().toString();
info("element fullClassName %s", fullClassName+" e "+element.getSimpleName());
AnnotatedClass annotatedClass = mAnnotatedClassMap.get(fullClassName);
if (annotatedClass == null) {
annotatedClass = new AnnotatedClass(classElement, mElementUtils);
mAnnotatedClassMap.put(fullClassName, annotatedClass);
}
return annotatedClass;
}
主要是獲取當前類名,創建一個AnnotationClass實例,將類名作爲map的key,實例作爲value緩存
爲了幫助理解補充如下:
element.getEnclosingElement();// 獲取父元素
element.getEnclosedElements();// 獲取子元素
其中父元素、子元素是根據xml中dom樹來決定的不是java中繼承關係上的父子元素。
所以這裏使用TypeElement classElement = (TypeElement) element.getEnclosingElement();
獲取父元素並強轉成TypeElement。那怎麼知道是TypeElement而不是其VariableElement。這就要理解關係如下
public class Foo { // TypeElement
private int a; // VariableElement
private Foo other; // VariableElement
public Foo() {} // ExecuteableElement
public void setA( // ExecuteableElement
int newA // TypeElement
) {
}
}
這裏表示每個類型關係:映射到dom樹的層級關係中。如定義的是方法、屬性那麼element實際類型是ExecuteableElement和VariableElement,那麼element.getEnclosingElement();就是獲取父元素,父元素就是TypeElement.如果瞭解html查找摸個元素層級,應該會很好理解。
以上補充完畢,代碼相信讀者都很好理解了,言歸正傳回到之前的思路:
調用getAnnatationClasss()之後,將當前類的所有的註解類轉化成一個AnnotationClass緩存到Map中
也就是Map中緩存了一個以當前類名爲key,AnnotationClass爲Value的Map,當前類有使用BindView或OnClick註解。在這裏Map會緩存兩個AnnotationClass。如下:
Map的key:
com.sample.MainActivity : 在MainActivity下使用了註解,所以會生成一個對應的AnnotationClass
com.sample.SecondActivity:在SecondActivity下使用了註解,所以會生成一個對應的AnnotationClass
獲取到AnnotationClass實例之後,回到方法出:再次貼出方法:
private void processBindView(RoundEnvironment roundEnv) throws IllegalArgumentException {
//get BindView AnnotionType for all Current Class of Elements
for (Element element : roundEnv.getElementsAnnotatedWith(BindView.class)) {
// TODO: 16/8/4
AnnotatedClass annotatedClass = getAnnotatedClass(element);
info("element name %s", element.getSimpleName());
BindViewField field = new BindViewField(element);
annotatedClass.addField(field);
}
}
new BindViewField(element)的實現如下:
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();
if (mResId < 0) {
throw new IllegalArgumentException(
String.format("value() in %s for field %s is not valid !", BindView.class.getSimpleName(),
mFieldElement.getSimpleName()));
}
}
主要是解析註解的元數據,獲取控件屬性的id,並保存到實例當中,
new OnClickMethod(element)和BindViewField(element)一模一樣,不重複解析。
創建相應的類:方法實例和屬性實例 隨後通過addxxx()加入到annotation實例中,最後看看map調用處
for (AnnotatedClass annotatedClass : mAnnotatedClassMap.values()) {
try {
// System.out.println("=== annotatedClass "+annotatedClass.getFullClassName());
info("Generating file for %s", annotatedClass.getFullClassName());
annotatedClass.generateFinder().writeTo(mFiler);
} catch (IOException e) {
info("Generate file failed,2222 reason: %s", e.getMessage());
return false;
}
}
最主要的代碼就是annotatedClass.generateFinder().writeTo(mFiler);也就是生成初始化代碼類以及實現,然後寫入到文件中,也就是生成了一個java類,類名是當前類名$$Finder
AnnotationClass如下:
public class AnnotatedClass {
public TypeElement mClassElement;//父元素:這裏表示當前類
public List<BindViewField> mFields;//被註解的元素:這裏是屬性
public List<OnClickMethod> mMethods;//被註解的元素:這裏是方法
public Elements mElementUtils;//元素操作類
public AnnotatedClass(TypeElement classElement, Elements elementUtils) {
this.mClassElement = classElement;
this.mFields = new ArrayList<>();
this.mMethods = new ArrayList<>();
this.mElementUtils = elementUtils;
}
public String getFullClassName() {
return mClassElement.getQualifiedName().toString();
}
public void addField(BindViewField field) {
mFields.add(field);
}
public void addMethod(OnClickMethod method) {
mMethods.add(method);
}
/***
*生成實現類,實現開頭3.2貼出的實現格式
*/
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();
return JavaFile.builder(packageName, finderClass).build();
}
}
這裏主要用到了javapoet庫,所以要講解主要用到的方法是什麼玩意:
1.addModifiers:添加修飾符:如public private protected等
2.addAnnotation:添加註解
3.addParameter:添加參數 如Override
4.addStatement("$L listerner",ClassName):添加語句:
如 :
injectMethodBuilder.addStatement("provider.findView(source, $L).setOnClickListener(listener)", id);
$L是變量,id是這個變量的值
5.returns(TypeName.VOID):返回類型
6.addSuperinterface(TypeUtil.ANDROID_ON_CLICK_LISTENER)實現接口
7.TypeSpec.anonymousClassBuilder("")構建一個匿名內部類
8.MethodSpec.methodBuilder("inject")構建一個方法名爲inject
9.TypeSpec.classBuilder(mClassElement.getSimpleName() + "$$Finder")構建一個實體類,名爲當前類名 $$Finder
10. JavaFile.builder(packageName, finderClass).build();構造這模式創建JavaFile實例
11.ParameterizedTypeName.get(TypeUtil.FINDER, TypeName.get(mClassElement.asType()))
泛型參數。第一個參數是實現的接口,第二個參數是具體的泛型類型
最後調用了javaFile.writeTo(mFiler);將構建的內容寫入文件,即生成了一個java文件。編譯器也會對齊進行編譯。
這樣自動生成代碼就完成了。接下來看看如何實現調用的?
自動生成方法:inject(final T host, Object source, Provider provider)
大致思路:
爲了解耦Provider是個接口具體實現初始化的地方就是Provider的方法findView();
所以這裏有兩個實現:
ActivityProvider:在activity調用ViewInject.inject(activity),使用這個findView()
public class ActivityProvider implements Provider {
@Override
public Context getContext(Object source) {
return ((Activity) source);
}
@Override
public View findView(Object source, int id) {
return ((Activity) source).findViewById(id);
}
}
ViewProvide:
public class ViewProvider implements Provider {
@Override
public Context getContext(Object source) {
return ((View) source).getContext();
}
@Override
public View findView(Object source, int id) {
return ((View) source).findViewById(id);
}
}
一般使用:
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
ViewFinder.inject(this);
}
ViewFinder:
public class ViewFinder {
private static final ActivityProvider PROVIDER_ACTIVITY = new ActivityProvider();
private static final ViewProvider PROVIDER_VIEW = new ViewProvider();
private static final Map<String, Finder> FINDER_MAP = new HashMap<>();
public static void inject(Activity activity) {
inject(activity, activity, PROVIDER_ACTIVITY);
}
public static void inject(View view) {
inject(view, view);
}
public static void inject(Object host, View view) {
// for fragment
inject(host, view, PROVIDER_VIEW);
}
/****
*
* @param host 是傳入的宿主,屬性方法所在的對象 實現解耦的地方
* @param source 傳入的source.findViewById()的source:可能是View/Activity/Fragement
* @param provider 具體實現findViewByid()複製給屬性的具體實現 ViewProvider/ActivityProvide/FragementProvider
*/
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);
}
}
}
具體的參數host、source、provider的職責在參數上寫的很清楚了。
傳入的宿主,然後生成一個宿主類創建,通過反射實例化通過javapoet自動生成的具體實現,隨後調用它的具體實現方法進行初始化。
看看ViewFinder.inject()有如下3個重載方法,第四個是留作擴展
1.inject(activity):用於在activity初始化控件
2.inject(View):用於view初始化子view:比如手動載入一個xml
3.inject(Fragment):用於fragment方法
4.inject(Hold,source): Hold作爲泛型的具體實現,自己生成hold的自動代碼生成,仿造xxx$$Finder的實現。
這樣就完成了。
總結:
1.ViewFinde.inject(this);會調用相應的方法,
2.通過反射調用javapoet生成的代碼實現初始化
藉此來了解整個apt工作方式。之後還會講解AOP之Aspectj和javassist框架的實際應用。
此文章的解讀原作者博客:如有侵權請告知
https://brucezz.itscoder.com/use-apt-in-android
demo:https://download.csdn.net/download/zhongwn/10426802