Android編譯期注解實踐

什么是注解

注解(Annotation),也叫元數(shù)據(jù)(即描述數(shù)據(jù)的數(shù)據(jù)),一種代碼級別的說明。

它是JDK1.5及以后版本引入的一個特性,與類、接口、枚舉是在同一個層次。它可以聲明在包、類、字段、方法、局部變量、方法參數(shù)等的前面,用來對這些元素進行說明,注釋。

起初作用基本類似是加強型的注釋語言,為了編譯檢查,增強可閱讀性,后續(xù)也可以用來生成代碼,起到輔助項目的作用。

基本注解

內(nèi)置注解

java內(nèi)置注解,舉個??:

@Override:讓編譯器檢查被標記的方法,保證其重寫了父類的某一個方法。此注解只能標記方法。源碼如下:


@Target(ElementType.METHOD)

@Retention(RetentionPolicy.SOURCE)

public @interface Override {

}

元注解

用來給其他注解打標簽的注解,即用來解釋其他注解的注解,舉兩個??:

@Retention:用于指定被此元注解標注的注解的保留時長,即作用域

RetentionPolicy.SOURCE: :在源文件中有效(即源文件保留),例如@Override

RetentionPolicy.CLASS::注解信息保留在class文件中,但是虛擬機不會持有其信息,可用來編譯期注解

RetentionPolicy.RUNTIME::注解信息保留在class文件中,而且虛擬機也會持有此注解信息,所以可以通過反射的方式獲得注解信息,用來運行時注解

編譯期注解: 在class文件中可用,能夠存于編譯之后的字節(jié)碼之中,該注解的注冊信息會保留在.java源碼里和.class文件里,在執(zhí)行的時候,會被Java虛擬機丟棄,不會加載到虛擬機中。

運行期注解:Java虛擬機在運行期保留注解信息,可以通過反射機制讀取注解的信息(.java源碼,.class文件和執(zhí)行的時候都有注解的信息)

@Target:注解對象的作用范圍

CONSTRUCTOR:用于描述構(gòu)造器

FIELD:用于描述字段

LOCAL_VARIABLE:用于描述局部變量

METHOD:用于描述方法

PACKAGE:用于描述包

PARAMETER:用于描述參數(shù)

TYPE:用于描述類、接口(包括注解類型) 或enum聲明

自定義注解

編譯期注解總體結(jié)構(gòu)

編譯時注解 + APT (Annotation Processing Tool)+ JavaPoet(自定義Java源文件) + auto-service(處理器注冊)

APT(Annotation Processing Tool) 是一種處理注釋的工具, 它對源代碼文件進行檢測找出其中的 Annotation,使用 Annotation 進行額外的處理。處理器在處理 Annotation 時可以根據(jù)源文件中的 Annotation 生成額外的源文件和其它的文件 (文件具體內(nèi)容由 Annotation 處理器的編寫者決定),APT 還會編譯生成的源文件和原來的源文件,將它們一起生成 class 文件。

JavaPoet是一款可以自動生成Java文件的第三方庫,簡潔易懂的API,讓繁雜、重復的Java文件,自動化生成,簡化流程。

auto-service:編譯時注解處理器AbstractProcessor需要注冊到JVM中。使用@Autoservice注解就完成了注冊,替代以前的手動配置(??:在Java Library項目中,在resources資源文件夾下創(chuàng)建META-INF.services,然后在該路徑下創(chuàng)建名為javax.annotation.processing.Processor的文件,在該文件中配置(處理器的完整路徑,每行一個)需要啟用的注解處理器)。

編譯期注解處理流程

screenshot-20220904-203719.png

編譯期注解實踐

寫了ShadowAptDemo,實現(xiàn)類似butterknife @BindView方法,根據(jù)注解自動獲取view對象

c7101025-1714-43cc-bf29-2d232875ccbd.png

app:主項目

apt_knife_api:功能API,給主項目依賴調(diào)用,Android lib

apt_complier:注解處理器,AbstractProcessor是javax.annotation.processing包下的一個抽象類,Android平臺是基于OpenJDK的,而OpenJDK中不包含AbstractProcessor的相關(guān)代碼,所以是一個Java lib

apt_annotations:注解的定義,需要給到注解處理器以及app主項目使用,所以也是Java lib

JDK的版本要一致,不然編譯出錯

注解的定義

apt_annotations:


@Retention(RetentionPolicy.CLASS)

@Target(ElementType.FIELD)

public @interface ShadowBindView {

    int value();

}

定義一個編譯期作用域的注解 ShadowBindView,用于描述字段

里面包含一個方法用于接收ResID

注解處理器

apt_complier:

需要配置相關(guān)三方庫的依賴

dependencies{

    //處理器注冊

    implementation 'com.google.auto.service:auto-service:1.0-rc6'

    annotationProcessor 'com.google.auto.service:auto-service:1.0-rc6'

    implementation 'com.google.auto:auto-common:0.10'

    //java文件生成

    implementation 'com.squareup:javapoet:1.11.1'

    //注解依賴

    implementation project(':apt_annotations')

}

自定義注解處理器

進行相關(guān)變量的初始化


@AutoService(Processor.class) //使用auto進行注冊注解處理器,只需要在這里聲明即可

@SupportedSourceVersion(SourceVersion.RELEASE_7) //指定jdk版本

//聲明需要處理的注解

@SupportedAnnotationTypes("com.example.apt_annotations.ShadowBindView")

public class ShadowProcessor extends AbstractProcessor {

//Processor工具元素

private Elements processorElements;

//生成文件

private Filer filer;

//自定義注解類集合

private Map<String, AnnotationClass> mAnnotatedClassMap;

//注解處理器初始化的時候回調(diào)

@Override

public synchronized void init(ProcessingEnvironment processingEnv) {

    super.init(processingEnv);

    //返回包含用于操作Element的工具方法元素

    processorElements = processingEnv.getElementUtils();

    filer = processingEnv.getFiler();

    mAnnotatedClassMap = new HashMap<>();

}

....

AbstractProcessor是抽象類,有一個必須實現(xiàn)的抽象方法process方法,是注解處理器處理注解時候進行回調(diào),需要在這里進行掃描和處理注解。

返回值邏輯

true,則這些注解已聲明,且不要求后續(xù)Processor處理它們

false,則這些注解未聲明,且可能要求后續(xù)Processor處理它們

RoundEnvironment,是指這輪處理注解所需要的元素信息,常用方法有:

getElementsAnnotatedWith,返回包含指定注解類型的元素的集合

errorRaised,上一輪注解處理器是否產(chǎn)生錯誤

Element,基礎(chǔ)元素, 以下是繼承它的元素

VariableElement ,代表成員變量

ExecutableElement ,代表類中的方法

TypeElement ,代表類

PackageElement ,代表Package


ShadowProcessor

@Override

public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {

System.out.println("process: " + roundEnv.toString());

    mAnnotatedClassMap.clear();

    //創(chuàng)建自定義注解處理類

    try {

        processBindView(roundEnv);

    } catch (Exception e) {

        e.printStackTrace();

    }

    //將自定義注解處理類,寫入文件

    for (AnnotationClass annotationClass : mAnnotatedClassMap.values()) {

        try {

            annotationClass.generateFiler().writeTo(filer);

        } catch (IOException e) {

            e.printStackTrace();

        }

    }

    return true;

}

private void processBindView(RoundEnvironment roundEnv) {

    //遍歷包含ShadowBindView注解類型的元素的集合

    for (Element element : roundEnv.getElementsAnnotatedWith(ShadowBindView.class)) {

        //注解類,AnnotationClass后續(xù)會詳細講到

        AnnotationClass annotationClass = createAnnotationClass(element);

        //構(gòu)建注解變量類,后續(xù)會詳細講到

        BindViewField bindViewField = new BindViewField(element);

        //添加對應(yīng)的變量

        annotationClass.addField(bindViewField);

System.out.println("processBindView annotatedClass: " + annotationClass);

System.out.println("processBindView bindViewField: " + bindViewField);

    }

}

//組建注解類

private AnnotationClass createAnnotationClass(Element element) {

    //獲取包含ShadowBindView注解的類元素

    TypeElement typeElement = (TypeElement) element.getEnclosingElement();

    String fullName = typeElement.getQualifiedName().toString();

System.out.println("createAnnotationClass typeElement: " + typeElement);

    AnnotationClass annotationClass = mAnnotatedClassMap.get(fullName);

    //集合中不存在,才添加到集合中,去重

    if (annotationClass == null) {

        //創(chuàng)建注解類 將類元素typeElement,工具元素processorElements傳入

        annotationClass = new AnnotationClass(typeElement, processorElements);

        mAnnotatedClassMap.put(fullName, annotationClass);

    }

    return annotationClass;

}

創(chuàng)建注解變量類,用來存儲注解變量數(shù)據(jù)


public class BindViewField {

    //變量元素

    private VariableElement mVariableElement;

    private int mResId;

    public BindViewField(Element element) {

        //判斷類型,是否是變量元素

if (element.getKind() != ElementKind.FIELD) {

            return;

        }

        mVariableElement = (VariableElement) element;

        //獲取自定義注解類型的變量

        ShadowBindView bindView = mVariableElement.getAnnotation(ShadowBindView.class);

        //獲取view對象Rid

        mResId = bindView.value();

    }

    public int getResId() {

        return mResId;

    }

    public Name getFieldName() {

        return mVariableElement.getSimpleName();

    }

  //變量對應(yīng)的類型

    public TypeMirror getFieldType() {

        return mVariableElement.asType();

    }

}

在注解類之前,先創(chuàng)建一個接口IViewBinder,它是用來銜接api和生成類文件的通信

就定義了兩個需要實際使用的方法,后面構(gòu)建類的時候會實現(xiàn)這個接口

apt_knife_api

public interface IViewBinder<T> {

    void bindView(T host);

    void unbindView(T host);

}

創(chuàng)建注解類,用來存儲類信息和構(gòu)建類文件


public class AnnotationClass {

static final ClassNameINTERFACE = ClassName.get("com.example.apt_knife_api", "IViewBinder");

    //注解類的BindView變量集合

    private final ArrayList<BindViewField> mFields;

    //注解類的元素

    private final TypeElement mTypeElement;

    //Processor元素

    private final Elements mProcessorElements;

    public AnnotationClass(TypeElement typeElement, Elements elements) {

        mFields = new ArrayList<>();

        mTypeElement = typeElement;

        mProcessorElements = elements;

    }

/**

    * 添加BindView變量

    *

    * @param field

    */

    public void addField(BindViewField field) {

        mFields.add(field);

    }

/**

    * 利用javaPoet生成對應(yīng)的.java代碼

    *

    * @return

    */

    public JavaFile generateFiler() {

        //生成java方法bindView

MethodSpec.Builder bindViewBuidler = MethodSpec.methodBuilder("bindView")

.addModifiers(Modifier.PUBLIC)//public

                .addAnnotation(Override.class)//接口的復寫方法

.addParameter(TypeName.get(mTypeElement.asType()), "host");//參數(shù)

        //添加bindView方法的處理解析

        for (BindViewField field : mFields) {

            //方法參數(shù)

          bindViewBuidler.addStatement("host.$N = ($T)(host.findViewById($L))"

                    , field.getFieldName()

, ClassName.get(field.getFieldType())

                    , field.getResId());

        }

        //生成java方法unbindView:

MethodSpec.Builder unbindViewBuilder = MethodSpec.methodBuilder("unbindView")

.addModifiers(Modifier.PUBLIC)//public

                .addAnnotation(Override.class)//接口的復寫方法

.addParameter(TypeName.get(mTypeElement.asType()), "host");//添加參數(shù)

        //添加unbindView方法的處理解析

        for (BindViewField field : mFields) {

      unbindViewBuilder.addStatement("host.$N = null", field.getFieldName());

        }

        //生成java的類文件(.java的文件),

TypeSpec injectClass = TypeSpec.classBuilder(mTypeElement.getSimpleName() + "$ShadowViewBinder")

.addModifiers(Modifier.PUBLIC)

.addSuperinterface(ParameterizedTypeName.get(INTERFACE,

TypeName.get(mTypeElement.asType())))//類實現(xiàn)的接口名IViewBinder

                .addMethod(bindViewBuidler.build())//添加bindView方法

                .addMethod(unbindViewBuilder.build())//添加unbindView方法

                .build();

        //添加包名 使用傳入的工具類元素獲取包名

        String packageName = mProcessorElements.getPackageOf(mTypeElement).getQualifiedName().toString();

        //生成JavaFile 然后會用JavaFile.writeTo(filer)寫入文件

return JavaFile.builder(packageName, injectClass).build();

    }

}

主要有兩個方法,一個添加構(gòu)建好的變量類,用于生成類文件時候生成變量,最后一個是使用javaPoet生成類文件

使用javaPoet生成類文件的關(guān)鍵步驟注釋都有,有個特別值得關(guān)注的點是構(gòu)建方法代碼的時候

//添加方法代碼
bindViewBuidler.addStatement("host.$N = ($T)(host.findViewById($L))", field.getFieldName() ,
 ClassName.get(field.getFieldType()) , field.getResId());

可以看見在寫配置代碼的時候有多個不同的通配符,分別代表不同的含義,可以簡單看一下源碼

private void addArgument(String format, char c, Object arg) {

      switch (c) {

      //$N表示獲取參數(shù)的name

        case 'N':

          this.args.add(argToName(arg));

          break;

      //$L表示字面意義,原樣輸出

        case 'L':

          this.args.add(argToLiteral(arg));

          break;

      //$S表示轉(zhuǎn)成字符串

        case 'S':

          this.args.add(argToString(arg));

          break;

      //$T表示轉(zhuǎn)成類型,并自動import

        case 'T':

          this.args.add(argToType(arg));

          break;

        default:

          throw new IllegalArgumentException(

              String.format("invalid format string: '%s'", format));

      }

    }

private String argToName(Object o) {

      if (o instanceof CharSequence) return o.toString();

      if (o instanceof ParameterSpec) return ((ParameterSpec) o).name;

      if (o instanceof FieldSpec) return ((FieldSpec) o).name;

      if (o instanceof MethodSpec) return ((MethodSpec) o).name;

      if (o instanceof TypeSpec) return ((TypeSpec) o).name;

      throw new IllegalArgumentException("expected name but was " + o);

    }

調(diào)用API

apt_knife_api:

創(chuàng)建主項目可以調(diào)用的api類ShadowKnife

public class ShadowKnife {

  //緩存類,避免重復反射創(chuàng)建

private static final MapbinderMap = new LinkedHashMap<>();

/**

    * 注解綁定

    *

    * @param host 表示注解 View 變量所在的類,也就是注解類 進行綁定的目標對象

    */

    public static void bind(Object host) {

        String className = host.getClass().getName();

        try {

            //看下對應(yīng)的ViewBinder是否存在

IViewBinder binder =binderMap.get(className);

            if (binder == null) {

                //不存在則通過 反射 創(chuàng)建一個 然后存入緩存 這個類是通過javapoet生成的

Class aClass = Class.forName(className + "$ShadowViewBinder");

                binder = (IViewBinder) aClass.newInstance();

binderMap.put(className, binder);

            }

            //使用注解類的進行綁定

            if (binder != null) {

                binder.bindView(host);

            }

        } catch (Throwable e) {

            e.printStackTrace();

        }

    }

/**

    * 解除注解綁定

    *

    * @param host

    */

    public static void unBind(Object host) {

        String className = host.getClass().getName();

IViewBinder binder =binderMap.get(className);

        if (binder != null) {

            binder.unbindView(host);

        }

binderMap.remove(className);

    }

}

主項目使用

先進行模塊依賴

dependencies{

    implementation fileTree(include: ['*.jar'], dir: 'libs')

    implementation 'com.android.support:appcompat-v7:28.0.0'

    //注解處理器依賴

    annotationProcessor project(':apt_complier')

    implementation project(':apt_annotations')

    implementation project(':apt_knife_api')

}

測試api代碼

public class MainActivity extends AppCompatActivity {

@ShadowBindView(R.id.text1)

    TextView textView;

    @Override

    protected void onCreate(Bundle savedInstanceState) {

        super.onCreate(savedInstanceState);

        setContentView(R.layout.activity_main);

        ShadowKnife.bind(this);

        textView.setOnClickListener(new View.OnClickListener() {

            @Override

            public void onClick(View v) {

            Toast.makeText(MainActivity.this, "test", Toast.LENGTH_LONG).show();

            }

        });

    }

    @Override

    protected void onDestroy() {

        super.onDestroy();

        ShadowKnife.unBind(this);

    }

}

然后進行clean,編譯,看一下是否生成預期所想的代碼

編譯過程打印的日志

7bd686bd-0a44-4358-bab9-1cf8a6d14078.png

查看所生成的代碼

695aaaba-22db-47ef-9c33-c05ced0e732f.png

大功告成!

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容