徒手擼一個註解框架

運行時註解主要是通過反射來實現的,而編譯時註解則是在編譯期間幫助我們生成代碼,所以編譯時註解效率高,但是實現起來複雜一點,運行時註解效率較低,但是實現起來簡單。
首先來看下運行時註解怎麼實現的吧。

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.總結

註解框架看起來很高大上,其實弄懂之後也不難,都是一個套路。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章