Android使用APT編譯時註解生成代碼

1.前言

最近在使用Butterknife的時候感覺它使用的註解挺有意思的,就瞭解一下,順便自己花點時間實現一個類似的框架。加深對這塊的理解,下面上乾貨。

2.註解

註解和class、interface一樣屬於一種類型。是在javaSE5.0後引入的概念。

註解通過關鍵字 @interface 進行定義:

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.CLASS)
public @interface BindView {
    int value();
}
元註解是可以註解到註解上的註解,是一種基本註解。元註解有 @Retention、@Documented、@Target、@Inherited、@Repeatable 5 種。

@Retention

Retention 的英文意爲保留期的意思。當 @Retention 應用到一個註解上的時候,它解釋說明了這個註解的的存活時間。

它的取值如下: 
- RetentionPolicy.SOURCE 註解只在源碼階段保留,在編譯器進行編譯時它將被丟棄忽視。 
- RetentionPolicy.CLASS 註解只被保留到編譯進行的時候,它並不會被加載到 JVM 中。 
- RetentionPolicy.RUNTIME 註解可以保留到程序運行的時候,它會被加載進入到 JVM 中,所以在程序運行時可以獲取到它們。

@Documented

這個元註解肯定是和文檔有關。它的作用是能夠將註解中的元素包含到 Javadoc 中去。

@Target

Target 是目標的意思,@Target 指定了註解運用的地方。 

你可以這樣理解,當一個註解被 @Target 註解時,這個註解就被限定了運用的場景。

類比到標籤,原本標籤是你想張貼到哪個地方就到哪個地方,但是因爲 @Target 的存在,它張貼的地方就非常具體了,比如只能張貼到方法上、類上、方法參數上等等。@Target 有下面的取值 

  • ElementType.ANNOTATION_TYPE 可以給一個註解進行註解
  • ElementType.CONSTRUCTOR 可以給構造方法進行註解
  • ElementType.FIELD 可以給屬性進行註解
  • ElementType.LOCAL_VARIABLE 可以給局部變量進行註解
  • ElementType.METHOD 可以給方法進行註解
  • ElementType.PACKAGE 可以給一個包進行註解
  • ElementType.PARAMETER 可以給一個方法內的參數進行註解
  • ElementType.TYPE 可以給一個類型進行註解,比如類、接口、枚舉

@Inherited

Inherited 是繼承的意思,但是它並不是說註解本身可以繼承,而是說如果一個超類被 @Inherited 註解過的註解進行註解的話,那麼如果它的子類沒有被任何註解應用的話,那麼這個子類就繼承了超類的註解。

@Repeatable

Repeatable 自然是可重複的意思。@Repeatable 是 Java 1.8 才加進來的,所以算是一個新的特性。 

什麼樣的註解會多次應用呢?通常是註解的值可以同時取多個。

註解屬性

註解的屬性也叫做成員變量。註解只有成員變量,沒有方法。註解的成員變量在註解的定義中以“無形參的方法”形式來聲明,其方法名定義了該成員變量的名字,其返回值定義了該成員變量的類型。下面的註解定義了value屬性。在使用的時候應該給它賦值

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Profile {
    public int id() default -1;
    public int heigh() default 0;
    public String nativePlace() default "";
}
public class Person {
    @Profile(id = 23,heigh = 180,nativePlace = "中國")
    String profile;
}
註解屬性的提取一般通過反射來獲取

註解通過反射獲取。首先可以通過 Class 對象的 isAnnotationPresent() 方法判斷它是否應用了某個註解

public boolean isAnnotationPresent(Class<? extends Annotation> annotationClass) {}

然後通過 getAnnotation() 方法來獲取 Annotation 對象。

public <A extends Annotation> A getAnnotation(Class<A> annotationClass) {}

或者是 getAnnotations() 方法。

public Annotation[] getAnnotations() {}

拿到Annotation對象後就可以獲取到裏面的值

public class CustomUtils {
    public static String  getInfo(Class<?> clazz){
        String str = "";
        Field[] fields = clazz.getFields();
        for (Field field:
             fields) {
            if(field.isAnnotationPresent(Name.class)){
                Name arg0 = field.getAnnotation(Name.class);
                str  = name + arg0.value() + "==";
            }else if(field.isAnnotationPresent(Sex.class)){
                Sex arg0 = field.getAnnotation(Sex.class);
                str = str + sex + arg0.sex() + "==";
            }else if(field.isAnnotationPresent(Profile.class)){
                Profile arg0 = field.getAnnotation(Profile.class);
                str = str + arg0.id() + ";" + arg0.heigh() + ";" + arg0.nativePlace();
            }
        }
        return str;
    }

3.Android使用APT(Annotation Processing Tool)

瞭解了以上關於註解的基本知識後,下面來在Android中使用APT進行開發。仿照Butterknife的結構,使用的gradle版本是3.0+,所以在build.gradle文件中使用的是 annotationProcessor。

基本思路就是使用註解標記某個域的屬性進行賦值,然後在程序編譯的時候自動生成對應的臨時文件,最後通過反射把註解裏面的值賦給被註解標記的域。

  • 首先在Android studio中建立一個Java Library,這裏我們取名爲anno,裏面用來存放註解。


package paic.com.anno;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.CLASS)
public @interface BindView {
    int value();
}
  • 新建一個Java Library

注意必須是Java,因爲AbstractProcessor位於javax.annotation.processing這個包下面,Android工程沒有。

引入一個第三方庫和上面的java library

implementation 'com.google.auto.service:auto-service:1.0-rc2'
implementation project(':anno')

auto-service:Google 公司出品,用於自動爲 JAVA Processor 生成 META-INF 信息

接下來的類是用於編譯時生成java文件,新建一個類繼承AbstractProcessor

@AutoService(Processor.class)
public class BindProcessor extends AbstractProcessor {
    private Elements mElementUtils;
    private Messager messager;
    private Map<String,ProxyInfo> mProxyMap = new HashMap<>();
    @Override
    public synchronized void init(ProcessingEnvironment processingEnvironment) {
        super.init(processingEnvironment);
        mElementUtils = processingEnvironment.getElementUtils();
        messager = processingEnvironment.getMessager();
    }

    @Override
    public Set<String> getSupportedAnnotationTypes() {
        Set<String> annotationType = new LinkedHashSet<>();
        annotationType.add(BindView.class.getCanonicalName());
        return annotationType;
    }

    @Override
    public SourceVersion getSupportedSourceVersion() {
        return SourceVersion.RELEASE_7;
    }

    @Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
        messager.printMessage(Diagnostic.Kind.NOTE,"process...");
        mProxyMap.clear();
        //獲取所有標註了BindView的元素
        Set<? extends Element> elements = roundEnvironment.getElementsAnnotatedWith(BindView.class);
        //遍歷元素
        for(Element element:elements){
            //判斷是否是域
            if(!checkAnnotationValid(element)){
                return false;
            }
            //獲取變量比如(button,textview...)
            VariableElement variableElement = (VariableElement) element;
            //獲取變量所在的類(比如paic.com.annotation.ManinActivity)
            TypeElement typeElement = (TypeElement) variableElement.getEnclosingElement();
           //獲取類名全稱
            String fqClassName = typeElement.getQualifiedName().toString();
            ProxyInfo proxyInfo = mProxyMap.get(fqClassName);
            if(proxyInfo == null){
                proxyInfo = new ProxyInfo(mElementUtils,typeElement);
                mProxyMap.put(fqClassName,proxyInfo);
            }
            BindView bindAnnotation = variableElement.getAnnotation(BindView.class);
            int id = bindAnnotation.value();
            proxyInfo.mInjectElements.put(id,variableElement);
        }
        for(String key:mProxyMap.keySet()){
            ProxyInfo proxyInfo = mProxyMap.get(key);
            try {//用於編譯時創建java文件
                JavaFileObject jfo = processingEnv.getFiler().createSourceFile(proxyInfo.getFullClassName(),proxyInfo.getTypeElement());
                Writer writer = jfo.openWriter();
                writer.write(proxyInfo.generateJavaCode());
                writer.flush();
                writer.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        return true;
    }

    private boolean checkAnnotationValid(Element element){
        if(element.getKind() != ElementKind.FIELD){
            return false;
        }
        if(ClassValidator.isPrivate(element)){
            return false;
        }
        return true;
    }
}

其中最主要的是process方法,getSupportedAnnotationType()和getSupportedSourceVersion()方法可以用下面兩個註解替代。

@AutoService(Processor.class),生成 META-INF 信息;
@SupportedAnnotationTypes({"com.example.BindView"}),聲明 Processor 處理的註解,注意這是一個數組,表示可以處理多個註解;
@SupportedSourceVersion(SourceVersion.RELEASE_7),聲明支持的源碼版本

public class ProxyInfo {
    private String packageName;
    private String proxyClassName;
    private TypeElement typeElement;
    public Map<Integer,VariableElement> mInjectElements = new HashMap<>();
    public static final String PROXY = "ViewInject";
    public ProxyInfo(Elements elementUtils,TypeElement classElement){
        this.typeElement = classElement;
        PackageElement packageElement = elementUtils.getPackageOf(classElement);
        String packageName = packageElement.getQualifiedName().toString(); //調用註解的類所在的包
        String className = ClassValidator.getClassName(classElement,packageName); //調用註解的類名(MainActivity)
        this.packageName = packageName;
        this.proxyClassName = className + "$$" + PROXY; //生成的類名
    }
    public String generateJavaCode(){
        StringBuilder builder = new StringBuilder();
        builder.append("// Generated code,Do not modify!\n");
        builder.append("package ").append(packageName).append(";\n\n");
//        builder.append("import paic.com.lib.bind.*;\n");
        builder.append('\n');                                                                       //類名的全稱 paic.com.annotation.MainActivity
        builder.append("public class ").append(proxyClassName).append(" implements " + PROXY + "<" + typeElement.getQualifiedName() + ">");
        builder.append(" {\n");
        generateJavaMethod(builder);
        builder.append('\n');
        builder.append("}\n");
        return builder.toString();
    }

    public String generateJavaMethod(StringBuilder builder){
        builder.append("@Override\n");
        builder.append("public void inject(" + typeElement.getQualifiedName() +" host,Object source){\n");
        for(int id:mInjectElements.keySet()){
            VariableElement variableElement = mInjectElements.get(id);
            String  name = variableElement.getSimpleName().toString();//註解對應的參數(button)
            String type = variableElement.asType().toString(); //註解對應參數的類型(android.widget.Button)
            builder.append(" if(source instanceof android.app.Activity){\n");
            builder.append("host."+name).append(" = ");
            builder.append("("+type+")(((android.app.Activity)source).findViewById("+id+"));");
            builder.append("\n}\n").append("else").append("\n{\n");
            builder.append("host."+name).append(" = ");
            builder.append("("+type+")(((android.view.View)source).findViewById("+id+"));");
            builder.append("\n}\n");
        }
        builder.append("\n}\n");
        return builder.toString();
    }

    public String getFullClassName(){
        return packageName + "." + proxyClassName;
    }

    public TypeElement getTypeElement(){
        return typeElement;
    }
}

上面的代碼邏輯還是比較容易看懂的

ProxyInfo這個類主要就是用來封裝生成java文件的代碼,通過id來區分自動構建相應的代碼。

BindProcessor這個類則通過註解所在的類來生成構建對應的java文件。
  • 新建一個Android model

接下來新建一個Android model來使用註解。記得build.gradle裏面需要這樣配置:

 annotationProcessor project(':lib')
    compile project(':anno')//這樣才能使用註解

我這裏爲了方便把反射註解生成的代碼一起放在了這個工程

public interface ViewInject<T>
{
    void inject(T t, Object source);
}
public class ViewInjector
{
    private static final String SUFFIX = "$$ViewInject";

    public static void injectView(Activity activity)
    {
        //獲取生成的代理對象
        ViewInject proxyActivity = findProxyActivity(activity);
        //代理對象裏的inject方法裏面是實現的具體邏輯
        //比如本例就是 activity.控件 = activity.findViewBy(id);
        proxyActivity.inject(activity, activity);
    }

    public static void injectView(Object object, View view)
    {

        ViewInject proxyActivity = findProxyActivity(object);

        proxyActivity.inject(object, view);
    }

    private static ViewInject findProxyActivity(Object activity)
    {
        try
        {
            Class clazz = activity.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.", activity.getClass().getSimpleName() + SUFFIX));
    }
}

新建一個Activity,進行調用

public class MainActivity extends AppCompatActivity {
    @BindView(R.id.btn)
    Button button;
    @BindView(R.id.text)
    TextView textView;
    @BindString("fuck the world shit")
    String word;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        ViewInjector.injectView(this);
        StringInjector.inject(this);
        button.setText("this is a button");
        textView.setText(word);
    }
}

在AS中進行rebuild一下工程,會發現下面的文件夾多了個文件(因爲工程裏面多寫了一個註解類,所以生成了多個)


打開ViewInject

// Generated code,Do not modify!
package paic.com.annotation;


public class MainActivity$$ViewInject implements ViewInject<paic.com.annotation.MainActivity> {
@Override
public void inject(paic.com.annotation.MainActivity host,Object source){
 if(source instanceof android.app.Activity){
host.button = (android.widget.Button)(((android.app.Activity)source).findViewById(2131165218));
}
else
{
host.button = (android.widget.Button)(((android.view.View)source).findViewById(2131165218));
}
 if(source instanceof android.app.Activity){
host.textView = (android.widget.TextView)(((android.app.Activity)source).findViewById(2131165306));
}
else
{
host.textView = (android.widget.TextView)(((android.view.View)source).findViewById(2131165306));
}

}

}

結合上面的BindProcessor和ProxyInfo,打個斷點就能很清晰的知道這個文件是如何定義的。如何打斷點後面會講到。

然後run一下工程就能成功調用了!

4.調試APT代碼

這部分《從0到1:實現 Android 編譯時註解》 這個博客裏有詳細配置,我就不囉嗦了。


項目源碼:DEMO

最後感謝以下同學提供的參考:

秒懂,Java 註解 (Annotation)你可以這樣學

Android 如何編寫基於編譯時註解的項目

《從0到1:實現 Android 編譯時註解》

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