運行時註解主要是通過反射來實現的,而編譯時註解則是在編譯期間幫助我們生成代碼,所以編譯時註解效率高,但是實現起來複雜一點,運行時註解效率較低,但是實現起來簡單。
首先來看下運行時註解怎麼實現的吧。
1.運行時註解
1.1定義註解
首先定義兩個運行時註解,其中Retention標明此註解在運行時生效,Target標明此註解的程序元範圍,下面兩個示例RuntimeBindView用於描述成員變量和類,成員變量綁定view,類綁定layout;RuntimeBindClick用於描述方法,讓指定的view綁定click事件。
@Retention(RetentionPolicy.RUNTIME)//運行時生效
@Target({ElementType.FIELD,ElementType.TYPE})//描述變量和類
public @interface RuntimeBindView {
int value() default View.NO_ID;
}
@Retention(RetentionPolicy.RUNTIME)//運行時生效
@Target(ElementType.METHOD)//描述方法
public @interface RuntimeBindClick {
int[] value();
}
1.2反射實現
以下代碼是用反射實現的註解功能,其中ClassInfo是一個能解析處類的各種成員和方法的工具類,
源碼見https://github.com/huangbei1990/HDemo/blob/master/hutils/src/main/java/com/android/hutils/reflect/ClassInfo.java
其實邏輯很簡單,就是從Activity裏面取出指定的註解,然後再調用相應的方法,如取出RuntimeBindView描述類的註解,然後得到這個註解的返回值,接着調用activity的setContentView將layout的id設置進去就可以了。
public static void bindId(Activity obj){
ClassInfo clsInfo = new ClassInfo(obj.getClass());
//處理類
if(obj.getClass().isAnnotationPresent(RuntimeBindView.class)) {
RuntimeBindView bindView = (RuntimeBindView)clsInfo.getClassAnnotation(RuntimeBindView.class);
int id = bindView.value();
clsInfo.executeMethod(clsInfo.getMethod("setContentView",int.class),obj,id);
}
//處理類成員
for(Field field : clsInfo.getFields()){
if(field.isAnnotationPresent(RuntimeBindView.class)){
RuntimeBindView bindView = field.getAnnotation(RuntimeBindView.class);
int id = bindView.value();
Object view = clsInfo.executeMethod(clsInfo.getMethod("findViewById",int.class),obj,id);
clsInfo.setField(field,obj,view);
}
}
//處理點擊事件
for (Method method : clsInfo.getMethods()) {
if (method.isAnnotationPresent(RuntimeBindClick.class)) {
int[] values = method.getAnnotation(RuntimeBindClick.class).value();
for (int id : values) {
View view = (View) clsInfo.executeMethod(clsInfo.getMethod("findViewById", int.class), obj, id);
view.setOnClickListener(v -> {
try {
method.invoke(obj, v);
} catch (Exception e) {
e.printStackTrace();
}
});
}
}
}
}
1.3使用
如下所示,將我們定義好的註解寫到相應的位置,然後調用BindApi的bind函數,就可以了。很簡單吧
@RuntimeBindView(R.layout.first)//類
public class MainActivity extends AppCompatActivity {
@RuntimeBindView(R.id.jump)//成員
public Button jump;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
BindApi.bindId(this);//調用反射
}
@RuntimeBindClick({R.id.jump,R.id.jump2})//方法
public void onClick(View view){
Intent intent = new Intent(this,SecondActivity.class);
startActivity(intent);
}
}
2.編譯時註解
編譯時註解就是在編譯期間幫你自動生成代碼,其實原理也不難。
2.1定義註解
我們可以看到,編譯時註解定義的時候Retention的值和運行時註解不同。
@Retention(RetentionPolicy.CLASS)//編譯時生效
@Target({ElementType.FIELD,ElementType.TYPE})//描述變量和類
public @interface CompilerBindView {
int value() default -1;
}
@Retention(RetentionPolicy.CLASS)//編譯時生效
@Target(ElementType.METHOD)//描述方法
public @interface CompilerBindClick {
int[] value();
}
2.2根據註解生成代碼
1)準備工作
首先我們要新建一個java的lib庫,因爲接下需要繼承AbstractProcessor類,這個類Android裏面沒有。
然後我們需要引入兩個包,javapoet是幫助我們生成代碼的包,auto-service是幫助我們自動生成META-INF等信息,這樣我們編譯的時候就可以執行我們自定義的processor了。
apply plugin: 'java-library'
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
api 'com.squareup:javapoet:1.9.0'
api 'com.google.auto.service:auto-service:1.0-rc2'
}
sourceCompatibility = "1.8"
targetCompatibility = "1.8"
2)繼承AbstractProcessor
如下所示,我們需要自定義一個類繼承子AbstractProcessor並複寫他的方法,並加上AutoService的註解。
ClassElementsInfo是用來存儲類信息的類,這一步先暫時不用管,下一步會詳細說明。
其實從函數的名稱就可以看出是什麼意思,init初始化,getSupportedSourceVersion限定所支持的jdk版本,getSupportedAnnotationTypes需要處理的註解,process我們可以在這個函數裏面拿到擁有我們需要處理註解的類,並生成相應的代碼。
@AutoService(Processor.class)
public class CompilerBindProcessor extends AbstractProcessor{
private Filer mFileUtils;//文件相關的輔助類,負責生成java代碼
private Elements mElementUtils;//元素相關的輔助類,獲取元素相關的信息
private Map<String,ClassElementsInfo> classElementsInfoMap;
@Override
public synchronized void init(ProcessingEnvironment processingEnvironment) {
super.init(processingEnvironment);
mFileUtils = processingEnvironment.getFiler();
mElementUtils = processingEnvironment.getElementUtils();
classElementsInfoMap = new HashMap<>();
}
@Override
public SourceVersion getSupportedSourceVersion() {
return SourceVersion.latestSupported();
}
@Override
public Set<String> getSupportedAnnotationTypes() {
Set<String> set = new LinkedHashSet<>();
set.add(CompilerBindClick.class.getCanonicalName());
set.add(CompilerBindView.class.getCanonicalName());
return set;
}
@Override
public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
classElementsInfoMap.clear();
//1.蒐集所需要的信息
collection(roundEnvironment);
//2.生成具體的代碼
generateClass();
return true;
}
3)蒐集註解
首先我們看下ClassElementsInfo這個類,也就是我們需要蒐集的信息。
TypeElement爲類元素,VariableElement爲成員元素,ExecutableElement爲方法元素,從中我們可以獲取到各種註解信息。
classSuffix爲前綴,例如原始類爲MainActivity,註解生成的類名就爲MainActivity+classSuffix
public class ClassElementsInfo {
//類
public TypeElement mTypeElement;
public int value;
public String packageName;
//成員,key爲id
public Map<Integer,VariableElement> mVariableElements = new HashMap<>();
//方法,key爲id
public Map<Integer,ExecutableElement> mExecutableElements = new HashMap<>();
//後綴
public static final String classSuffix = "proxy";
public String getProxyClassFullName() {
return mTypeElement.getQualifiedName().toString() + classSuffix;
}
public String getClassName() {
return mTypeElement.getSimpleName().toString() + classSuffix;
}
......
}
然後我們就可以開始蒐集註解信息了,
如下所示,按照註解類型一個一個的蒐集,可以通過roundEnvironment.getElementsAnnotatedWith函數拿到註解元素,拿到之後再根據註解元素的類型分別填充到ClassElementsInfo當中。
其中ClassElementsInfo是存儲在Map當中,key是String是classPath。
private void collection(RoundEnvironment roundEnvironment){
//1.蒐集compileBindView註解
Set<? extends Element> set = roundEnvironment.getElementsAnnotatedWith(CompilerBindView.class);
for(Element element : set){
//1.1蒐集類的註解
if(element.getKind() == ElementKind.CLASS){
TypeElement typeElement = (TypeElement)element;
String classPath = typeElement.getQualifiedName().toString();
String className = typeElement.getSimpleName().toString();
String packageName = mElementUtils.getPackageOf(typeElement).getQualifiedName().toString();
CompilerBindView bindView = element.getAnnotation(CompilerBindView.class);
if(bindView != null){
ClassElementsInfo info = classElementsInfoMap.get(classPath);
if(info == null){
info = new ClassElementsInfo();
classElementsInfoMap.put(classPath,info);
}
info.packageName = packageName;
info.value = bindView.value();
info.mTypeElement = typeElement;
}
}
//1.2蒐集成員的註解
else if(element.getKind() == ElementKind.FIELD){
VariableElement variableElement = (VariableElement) element;
String classPath = ((TypeElement)element.getEnclosingElement()).getQualifiedName().toString();
CompilerBindView bindView = variableElement.getAnnotation(CompilerBindView.class);
if(bindView != null){
ClassElementsInfo info = classElementsInfoMap.get(classPath);
if(info == null){
info = new ClassElementsInfo();
classElementsInfoMap.put(classPath,info);
}
info.mVariableElements.put(bindView.value(),variableElement);
}
}
}
//2.蒐集compileBindClick註解
Set<? extends Element> set1 = roundEnvironment.getElementsAnnotatedWith(CompilerBindClick.class);
for(Element element : set1){
if(element.getKind() == ElementKind.METHOD){
ExecutableElement executableElement = (ExecutableElement) element;
String classPath = ((TypeElement)element.getEnclosingElement()).getQualifiedName().toString();
CompilerBindClick bindClick = executableElement.getAnnotation(CompilerBindClick.class);
if(bindClick != null){
ClassElementsInfo info = classElementsInfoMap.get(classPath);
if(info == null){
info = new ClassElementsInfo();
classElementsInfoMap.put(classPath,info);
}
int[] values = bindClick.value();
for(int value : values) {
info.mExecutableElements.put(value,executableElement);
}
}
}
}
}
4)生成代碼
如下所示使用javapoet生成代碼,使用起來並不複雜。
public class ClassElementsInfo {
......
public String generateJavaCode() {
ClassName viewClass = ClassName.get("android.view","View");
ClassName clickClass = ClassName.get("android.view","View.OnClickListener");
ClassName keepClass = ClassName.get("android.support.annotation","Keep");
ClassName typeClass = ClassName.get(mTypeElement.getQualifiedName().toString().replace("."+mTypeElement.getSimpleName().toString(),""),mTypeElement.getSimpleName().toString());
//構造方法
MethodSpec.Builder builder = MethodSpec.constructorBuilder()
.addModifiers(Modifier.PUBLIC)
.addParameter(typeClass,"host",Modifier.FINAL);
if(value > 0){
builder.addStatement("host.setContentView($L)",value);
}
//成員
Iterator<Map.Entry<Integer,VariableElement>> iterator = mVariableElements.entrySet().iterator();
while(iterator.hasNext()){
Map.Entry<Integer,VariableElement> entry = iterator.next();
Integer key = entry.getKey();
VariableElement value = entry.getValue();
String name = value.getSimpleName().toString();
String type = value.asType().toString();
builder.addStatement("host.$L=($L)host.findViewById($L)",name,type,key);
}
//方法
Iterator<Map.Entry<Integer,ExecutableElement>> iterator1 = mExecutableElements.entrySet().iterator();
while(iterator1.hasNext()){
Map.Entry<Integer,ExecutableElement> entry = iterator1.next();
Integer key = entry.getKey();
ExecutableElement value = entry.getValue();
String name = value.getSimpleName().toString();
MethodSpec onClick = MethodSpec.methodBuilder("onClick")
.addAnnotation(Override.class)
.addModifiers(Modifier.PUBLIC)
.addParameter(viewClass,"view")
.addStatement("host.$L(host.findViewById($L))",value.getSimpleName().toString(),key)
.returns(void.class)
.build();
//構造匿名內部類
TypeSpec clickListener = TypeSpec.anonymousClassBuilder("")
.addSuperinterface(clickClass)
.addMethod(onClick)
.build();
builder.addStatement("host.findViewById($L).setOnClickListener($L)",key,clickListener);
}
TypeSpec typeSpec = TypeSpec.classBuilder(getClassName())
.addModifiers(Modifier.PUBLIC)
.addAnnotation(keepClass)
.addMethod(builder.build())
.build();
JavaFile javaFile = JavaFile.builder(packageName,typeSpec).build();
return javaFile.toString();
}
}
最終使用了註解之後生成的代碼如下
package com.android.hdemo;
import android.support.annotation.Keep;
import android.view.View;
import android.view.View.OnClickListener;
import java.lang.Override;
@Keep
public class MainActivityproxy {
public MainActivityproxy(final MainActivity host) {
host.setContentView(2131296284);
host.jump=(android.widget.Button)host.findViewById(2131165257);
host.findViewById(2131165258).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
host.onClick(host.findViewById(2131165258));
}
});
host.findViewById(2131165257).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
host.onClick(host.findViewById(2131165257));
}
});
}
}
5)讓註解生效
我們生成了代碼之後,還需要讓原始的類去調用我們生成的代碼
public class BindHelper {
static final Map<Class<?>,Constructor<?>> Bindings = new HashMap<>();
public static void inject(Activity activity){
String classFullName = activity.getClass().getName() + ClassElementsInfo.classSuffix;
try{
Constructor constructor = Bindings.get(activity.getClass());
if(constructor == null){
Class proxy = Class.forName(classFullName);
constructor = proxy.getDeclaredConstructor(activity.getClass());
Bindings.put(activity.getClass(),constructor);
}
constructor.setAccessible(true);
constructor.newInstance(activity);
}catch (Exception e){
e.printStackTrace();
}
}
}
2.3調試
首先在gradle.properties裏面加入如下的代碼
android.enableSeparateAnnotationProcessing = true
org.gradle.daemon=true
org.gradle.jvmargs=-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=8888
然後點擊Edit Configurations
新建一個remote
然後填寫相關的參數,127.0.0.1表示本機,port與剛纔gradle.properties裏面填寫的保持一致,然後點擊ok
然後將Select Run/Debug Configuration選項調整到剛纔新建的Configuration上,然後點擊Build–Rebuild Project,就可以開始調試了。
2.4使用
如下所示爲原始的類
@CompilerBindView(R.layout.first)
public class MainActivity extends AppCompatActivity {
@CompilerBindView(R.id.jump)
public Button jump;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
BindHelper.inject(this);
}
@CompilerBindClick({R.id.jump,R.id.jump2})
public void onClick(View view){
Intent intent = new Intent(this,SecondActivity.class);
startActivity(intent);
}
}
以下爲生成的類
package com.android.hdemo;
import android.support.annotation.Keep;
import android.view.View;
import android.view.View.OnClickListener;
import java.lang.Override;
@Keep
public class MainActivityproxy {
public MainActivityproxy(final MainActivity host) {
host.setContentView(2131296284);
host.jump=(android.widget.Button)host.findViewById(2131165257);
host.findViewById(2131165258).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
host.onClick(host.findViewById(2131165258));
}
});
host.findViewById(2131165257).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
host.onClick(host.findViewById(2131165257));
}
});
}
}
3.總結
註解框架看起來很高大上,其實弄懂之後也不難,都是一個套路。