之前寫了注解基礎(chǔ)和運(yùn)行時注解這篇文章弧烤,里面使用運(yùn)行時注解來模仿ButterKnife綁定控件ID的功能昏翰,運(yùn)行時注解主要是運(yùn)行時使用反射來找到注解進(jìn)行一些操作棒卷;反射存在一定的性能問題衔肢,而且一般使用了注解的框架都是使用編譯時注解
仔細(xì)看了一下這篇文章久信,Android 如何編寫基于編譯時注解的項(xiàng)目,自己嘗試寫了代碼理解了一番蝴猪,感覺還是比較有意思的调衰;
實(shí)現(xiàn)原理
1.注解處理器(Annotation Processor):用來在編譯時掃描和處理注解,我們需要實(shí)現(xiàn)自己的注解處理器自阱,去處理我們自己的注解嚎莉,一般就是去生成我們需要的代碼文件;
2.我們實(shí)現(xiàn)的注解處理器會被打包成jar在編譯的過程中調(diào)用沛豌,為了讓java編譯器識別出這個自定義的注解處理器趋箩,我們需要注冊一下
- 需要使用到注解處理的插件,因?yàn)锳ndroid Studio原本是不支持注解處理器的
整個流程大概就是加派,我們先創(chuàng)建注解叫确,創(chuàng)建注解處理器,然后代碼中使用注解芍锦;在編譯的時候注解處理插件會使用我們的注解處理器去處理注解竹勉,生成相應(yīng)的代碼;
具體實(shí)現(xiàn)
創(chuàng)建注解
還是以實(shí)現(xiàn)一個ViewById注解為例娄琉,在項(xiàng)目中新創(chuàng)建一個Java Library次乓,模塊名為annotation
用來保存所有注解吓歇,然后創(chuàng)建一個編譯時注解
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.CLASS)
public @interface ViewById {
int value() default -1;
}
注解處理器的創(chuàng)建和注冊
注解處理器是必須放在一個Java Library中,所以創(chuàng)建一個annotator
模塊票腰,用來實(shí)現(xiàn)注解處理器城看,這里通過創(chuàng)建類IocProcessor
繼承AbstractProcessor
重寫其中方法來實(shí)現(xiàn)一個注解處理器
@AutoService(Processor.class)
public class IocProcessor extends AbstractProcessor {
...
注冊有比較簡單的方法,只需要給IocProcessor
加一個AutoService
注解就可以實(shí)現(xiàn)注冊杏慰,這個注解需要依賴一個庫
implementation 'com.google.auto.service:auto-service:1.0-rc4'
implementation project(':annotation')
重寫init方法测柠,獲取一些有用的變量
/**
* 生成代碼用的
*/
private Filer mFileUtils;
/**
* 跟元素相關(guān)的輔助類,幫助我們?nèi)カ@取一些元素相關(guān)的信息
* - VariableElement 一般代表成員變量
* - ExecutableElement 一般代表類中的方法
* - TypeElement 一般代表代表類
* - PackageElement 一般代表Package
*/
private Elements mElementUtils;
@Override
public synchronized void init(ProcessingEnvironment processingEnv) {
super.init(processingEnv);
mFileUtils = processingEnv.getFiler();
mElementUtils = processingEnv.getElementUtils();
}
重寫getSupportedAnnotationTypes
方法逃默,支持自己的注解
/**
* 添加需要支持的注解
*
* @return
*/
@Override
public Set<String> getSupportedAnnotationTypes() {
Set<String> annotationTypes = new LinkedHashSet<String>();
//添加需要支持的注解
annotationTypes.add(ViewById.class.getCanonicalName());
return annotationTypes;
}
這個固定寫法是設(shè)置支持的版本
@Override
public SourceVersion getSupportedSourceVersion() {
return SourceVersion.latestSupported();
}
然后是最重要的process
方法鹃愤,這個方法就是開始處理注解,這里是先保存獲取到的被注解的元素完域,以外部類為單元软吐,用ProxyInfo對象去保存一個類里面的所有被注解的元素;用mProxyMap
去保存所有的ProxyInfo吟税;然后再一個個拿出來凹耙,使用了ProxyInfo對象去實(shí)現(xiàn)生成代碼
mProxyMap.clear();
//獲取被注解的元素
Set<? extends Element> elements = roundEnvironment.getElementsAnnotatedWith(ViewById.class);
for (Element element : elements) {
//檢查element類型
if (!checkAnnotationValid(element, ViewById.class)) {
return false;
}
//獲取到這個成員變量
VariableElement variableElement = (VariableElement) element;
//獲取到這個變量的外部類,所在的類
TypeElement typeElement = (TypeElement) variableElement.getEnclosingElement();
//獲取外部類的類名
String qualifiedName = typeElement.getQualifiedName().toString();
//一個類里面的注解都在一個ProxyInfo中處理
ProxyInfo proxyInfo = mProxyMap.get(qualifiedName);
if (proxyInfo == null) {
proxyInfo = new ProxyInfo(mElementUtils, typeElement);
mProxyMap.put(qualifiedName, proxyInfo);
}
//把這個注解保存到proxyInfo里面肠仪,用于實(shí)現(xiàn)功能
ViewById annotation = variableElement.getAnnotation(ViewById.class);
int id = annotation.value();
proxyInfo.injectVariables.put(id, variableElement);
}
//生成類
for (String key : mProxyMap.keySet()) {
ProxyInfo proxyInfo = mProxyMap.get(key);
try {
//創(chuàng)建一個新的源文件肖抱,并返回一個對象以允許寫入它
JavaFileObject jfo = mFileUtils.createSourceFile(
proxyInfo.getProxyClassFullName(),
proxyInfo.getTypeElement());
Writer writer = jfo.openWriter();
writer.write(proxyInfo.generateJavaCode());
writer.flush();
writer.close();
} catch (IOException e) {
error(proxyInfo.getTypeElement(),
"Unable to write injector for type %s: %s",
proxyInfo.getTypeElement(), e.getMessage());
}
}
return true;
到這里其實(shí)都很好理解,拿到了注解的這個元素(對象)异旧,就知道它的一切信息意述,就可以去生成相應(yīng)的代碼,主要在于生成代碼吮蛹,是在上面代碼中的這個地方實(shí)現(xiàn)的
//創(chuàng)建一個新的源文件荤崇,并返回一個對象以允許寫入它
JavaFileObject jfo = mFileUtils.createSourceFile(
proxyInfo.getProxyClassFullName(),
proxyInfo.getTypeElement());
Writer writer = jfo.openWriter();
writer.write(proxyInfo.generateJavaCode());
writer.flush();
writer.close();
這里去創(chuàng)建Java對象,寫入代碼潮针,其中創(chuàng)建新的源文件需要傳入兩個參數(shù)术荤,一個是保存的文件的全路徑,你想保存在哪里就哪里隨便寫每篷,我是保存在被注解的這個變量所在類的同一個包下瓣戚,另一個參數(shù)是傳入一個基本元素,但是說實(shí)話焦读,這個基本元素我在網(wǎng)上查了很久子库,不知道這個東西有什么用,刪了也沒問題矗晃,直接刪了好了仑嗅;
代碼是通過proxyInfo.generateJavaCode()
來獲取的相應(yīng)生成的代碼去生成對象的,是具體生成代碼的方法
/**
* 生成代碼
*
* @return
*/
public String generateJavaCode() {
StringBuilder builder = new StringBuilder();
builder.append("http:// Generated code. Do not modify!\n");
builder.append("package ").append(packageName).append(";\n\n");
builder.append("import com.dhht.annotation.*;\n");
builder.append("import com.dhht.annotation.R;\n");
builder.append("import com.dhht.annotationlibrary.*;\n");
builder.append('\n');
builder.append("public class ").append(proxyClassName).append(" implements " + ProxyInfo.PROXY + "<" + typeElement.getQualifiedName() + ">");
builder.append(" {\n");
generateMethods(builder);
builder.append('\n');
builder.append("}\n");
return builder.toString();
}
就是使用字符串組建一下代碼,字符串拼接倒是非常簡單无畔,這里也沒有展示完全,可以看看拼接出來的代碼吠冤,我稍微優(yōu)化了一下格式浑彰;這里有專門生成代碼的庫可以使用,比字符串拼接好用拯辙,叫Javapoet
package com.dhht.annotation.activity;
import com.dhht.annotation.*;
import com.dhht.annotation.R;
import com.dhht.annotationlibrary.*;
public class MainActivity$$ViewInject implements ViewInject<com.dhht.annotation.activity.MainActivity> {
@Override
public void inject(com.dhht.annotation.activity.MainActivity host, Object source) {
if (source instanceof android.app.Activity) {
host.txtView = (android.widget.TextView) (((android.app.Activity) source).findViewById(R.id.txtView));
} else {
host.txtView = (android.widget.TextView) (((android.view.View) source).findViewById(R.id.txtView));
}
}
}
可以調(diào)用生成的這個類的inject()
方法郭变,為傳入的host對象的.txtView
控件進(jìn)行初始化,這里只是在MainActivity里面用了涯保,如果在其他類里面則會生成很多個這樣的文件诉濒,而且當(dāng)這個類不是Activity的時候需要傳入這個控件的根布局進(jìn)去;
調(diào)用生成的代碼
里面還有一些檢查參數(shù)是不是公共的呀夕春,注解的對象屬性是否正確呀未荒,具體的代碼生成可以下載源碼看看,都是比較簡單的及志;現(xiàn)在注解器處理器算是寫完了片排,需要在我們的項(xiàng)目中使用,我們也新建一個Android Library速侈,annotationlibrary
專門用于提供API率寡,這樣注解的實(shí)現(xiàn)完全和我們的項(xiàng)目分開;
//這個依賴是用于對外暴露注解的
api project(':annotation')
需要實(shí)現(xiàn)的功能很簡單,就是調(diào)用生成的代碼倚搬,首先不同的類里面的注解生成的代碼類是不一樣的冶共,而且生成的代碼是編譯的時候才生成的,肯定只能使用反射來獲取這個生成的類每界,所以肯定需要傳入使用注解的這個類捅僵,然后根據(jù)我們的命名規(guī)則獲取到;
/**
* 根據(jù)使用注解的類和約定的命名規(guī)則盆犁,反射獲取注解生成的類
*
* @param object
* @return
*/
private static ViewInject findProxyActivity(Object object) {
try {
Class clazz = object.getClass();
Class injectorClazz = Class.forName(clazz.getName() + SUFFIX);
return (ViewInject) injectorClazz.newInstance();
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
throw new RuntimeException(String.format("can not find %s , something when compiler.", object.getClass().getSimpleName() + SUFFIX));
}
再說我們這個綁定控件命咐,從生成的代碼來看,需要一個Activity或者一個View來調(diào)用findViewById
方法谐岁,所以使用注解的不是Activity的類醋奠,在需要加一個Object參數(shù),傳入View進(jìn)來伊佃;
public interface ViewInject<T> {
/**
* 提供給生成的代碼去綁定id用的
*
* @param t
* @param source
*/
void inject(T t, Object source);
}
然后考慮到Activity就不用傳另一個參數(shù)了窜司,所以新建兩個方法完事兒
public static void injectView(Activity activity) {
ViewInject proxyActivity = findProxyActivity(activity);
proxyActivity.inject(activity, activity);
}
public static void injectView(Object object, View view) {
ViewInject proxyActivity = findProxyActivity(object);
proxyActivity.inject(object, view);
}
然后在項(xiàng)目中依賴一下,并且使用之前說的注解插件
implementation project(':annotationlibrary')
annotationProcessor project(':annotator')
然后就可以在項(xiàng)目中使用注解了
@ViewById
TextView txtView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
ViewInjector.injectView(this);
txtView.setOnClickListener(v -> Toast.makeText(MainActivity.this, "醉了", Toast.LENGTH_SHORT).show());
}
使用了lambda 表達(dá)式簡單了不少航揉,在模塊的build.gradle
的android節(jié)點(diǎn)下面添加支持
compileOptions {
sourceCompatibility = '1.8'
targetCompatibility = '1.8'
}
就完成了塞祈,整體還是比較好理解的,關(guān)鍵在于得下載代碼自己試試
項(xiàng)目地址:https://github.com/tyhjh/Annotation