Android 架構師之路 APT (1)

Android-Developers.jpg

今天我們先給出 demo 的代碼,隨后會就 APT 技術進行詳細介紹

APT

目標通過自己視圖綁定功能來了解如何 APT 幫助我們在編譯期間自動生成代碼。在網上收集一些資料自己實現一下。

  • 目標使用 APT 實現類似 Butter Knife 庫提供的通過注解實現視圖綁定的功能
    最近使用 binding 實現視圖綁定功能。有點漸漸遠離了 ButterKnife ,不過這個庫還是給我留下美好的回憶,簡潔明了的 API 讓人印象深刻。

創(chuàng)建一個空 Android 項目,接下我們添加模塊到項目,每一個模塊在 APT 中實現不同功能。

創(chuàng)建模塊

  • 創(chuàng)建 zi-annotation 注意這是一個 (java 模塊),在這個模塊中,可以創(chuàng)建注解 ZiViewBind


    apt_create_java_module.JPG
  • 創(chuàng)建 zi-api (android 模塊)提供用戶可以調用 api ,這里實現 view 的綁定。


    apt_create_android_module.JPG
  • 創(chuàng)建 zi-compiler(java 模塊) 編寫注解處理 。
No. 模塊名 模塊類型 說明
1 zi-annotation java 模塊 在這個模塊中,可以創(chuàng)建注解
2 zi-api Android 模塊 提供用戶可以調用 api
3 zi-compiler java 模塊 編寫注解處理

Android Studio 在當我們創(chuàng)建好模塊自動在 setting 文件中寫入了這些模塊

rootProject.name='zi application'
include ':app'
include ':zi-annotation'
include ':zi-api'
include ':zi-compiler'

處理依賴關系

  • build.gradle(zi-compiler)
apply plugin: 'java-library'

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    implementation 'com.google.auto.service:auto-service:1.0-rc2'
    implementation 'com.squareup:javapoet:1.10.0'
    implementation project(':zi-annotation')
}

sourceCompatibility = "7"
targetCompatibility = "7"

  • 注意 zi-compiler 依賴于 zi-annotation 模塊
implementation 'com.google.auto.service:auto-service:1.0-rc2'
implementation 'com.squareup:javapoet:1.10.0'

在主項目中添加剛剛創(chuàng)建好的幾個模塊,值得注意 zi-compiler 為 annotationProcessor

dependencies {
    ...

    implementation project(':zi-annotation')
    implementation project(':zi-api')
    annotationProcessor project(':zi-compiler')
}

到此我們已經完成模塊創(chuàng)建、添加以及他們之間依賴關系,剩下的工作就是為這些模塊添加代碼。


project_architeure.JPG

zi-annotation 模塊

創(chuàng)建 ZiBindView 注解

@Retention(RetentionPolicy.CLASS)
@Target(ElementType.FIELD)
public @interface ZiBindView {
}

@Target說明了Annotation所修飾的對象范圍

說明
CONSTRUCTOR 用于描述構造器
FIELD 用于描述域
LOCAL_VARIABLE 用于描述局部變量
METHOD 用于描述方法
PACKAGE 用于描述包
PARAMETER 用于描述參數
TYPE 用于描述類、接口(包括注解類型) 或enum聲明

zi-compiler 模塊

創(chuàng)建 ZiBindViewProcessor 和 ClassCreatorProxy
在代碼中不涉及的注解這里就不解釋了

@Retention定義了該Annotation被保留的時間長短:某些Annotation僅出現在源代碼中,而被編譯器丟棄;而另一些卻被編譯在class文件中;編譯在class文件中的Annotation可能會被虛擬機忽略,而另一些在class被裝載時將被讀?。ㄕ堊⒁獠⒉挥绊慶lass的執(zhí)行,因為Annotation與class在使用上是被分離的)。使用這個meta-Annotation可以對 Annotation的“生命周期”限制。

注解處理器需要繼承于 AbstractProcessor ,其中部分代碼寫法基本是固定的。

public class ZiBindViewProcessor extends AbstractProcessor {
    @Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
        return false;
    }
}

getSupportedAnnotationTypes() 返回支持的注解類型
getSupportedSourceVersion() 返回支持的源碼版本

    @Override
    public Set<String> getSupportedAnnotationTypes() {
        HashSet<String> supportTypes = new LinkedHashSet<>();
        //獲取類名
        supportTypes.add(ZiBindView.class.getCanonicalName());
        return supportTypes;
    }

這里,getName()返回的是虛擬機里面的class的表示,而getCanonicalName()返回的是更容易理解的表示。其實對于大部分class來說這兩個方法沒有什么不同的。但是對于array或內部類來說是有區(qū)別的。
另外,類加載(虛擬機加載)的時候需要類的名字是getName。

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

收集信息

謂信息收集,就是根據我們聲明,得到對應 Element,然后注解進行收集所需的信息,這些信息用于后期生產對象

生產代理類(在編譯時,將文本生產為類的生產代理類 ClassCreatorProxy

針對每一個都會生產一個注解類

初始化

public interface ProcessingEnvironment {
    Elements getElementUtils();
    Types getTypeUtils();
    Filer getFiler();
    Locale getLocale();
    Messager getMessager();
    Map<String, String> getOptions();
    SourceVersion getSourceVersion();
    
}

ZiBindViewProcessor 完整代碼

@SupportedSourceVersion(SourceVersion.RELEASE_7)
@SupportedAnnotationTypes({"zidea.example.com.zi_compiler.ZiBindViewProcessor"})
@AutoService(Processor.class)
public class ZiBindViewProcessor extends AbstractProcessor {

    //跟日志相關的輔助類
    private Messager mMessager;
    //跟元素相關的輔助類,幫助我們去獲取一些元素相關的信息。
    private Elements mElementUtils;
    private Map<String,ClassCreatorProxy> mProxyMap = new HashMap<>();

    //利用 ProcessingEnvironment 對象獲取 Elements
    @Override
    public synchronized void init(ProcessingEnvironment processingEnvironment) {
        super.init(processingEnvironment);
        mMessager = processingEnvironment.getMessager();
        mElementUtils = processingEnvironment.getElementUtils();
    }

    @Override
    public Set<String> getSupportedAnnotationTypes() {
        HashSet<String> supportTypes = new LinkedHashSet<>();
        //獲取類名
        supportTypes.add(ZiBindView.class.getCanonicalName());
        return supportTypes;
    }

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

    // 收集信息
    //  所謂信息收集,就是根據我們聲明,得到對應 Element,然后注解進行收集所需的信息,這些信息用于后期生產對象
    // 生產代理類(在編譯時,將文本生產為類的生產代理類 ClassCreatorProxy,針對每一個都會生產一個注解類
    @Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
        //輸出信息
        mMessager.printMessage(Diagnostic.Kind.NOTE,"processing...");

        //根據注解獲取所需信息

        //用來獲取,注解所修飾的Element對象,getElementsAnnotatedWith 用于獲取注解 ZiBindView 的 Element 對象
        Set<? extends Element> elements =roundEnvironment.getElementsAnnotatedWith(ZiBindView.class);

        //遍歷說有在類中,添加了 ZiBindView 類
        for (Element element : elements){

            VariableElement variableElement = (VariableElement) element;
            TypeElement classElement = (TypeElement) variableElement.getEnclosingElement();
            String fullClassName = classElement.getQualifiedName().toString();

            ClassCreatorProxy proxy = mProxyMap.get(fullClassName);
            if(proxy == null){
                proxy = new ClassCreatorProxy(mElementUtils,classElement);
                mProxyMap.put(fullClassName,proxy);
            }
            ZiBindView bindAnnotation = variableElement.getAnnotation(ZiBindView.class);
            int id = bindAnnotation.value();
            proxy.putElement(id,variableElement);

        }

        for(String key:mProxyMap.keySet()){
            ClassCreatorProxy proxyInfo = mProxyMap.get(key);
            JavaFile javaFile = JavaFile.builder(proxyInfo.getPackageName(),proxyInfo.generateJavaCode2()).build();

            try {
                javaFile.writeTo(processingEnv.getFiler());
            }catch (IOException e){
                e.printStackTrace();
            }
        }
        mMessager.printMessage(Diagnostic.Kind.NOTE,"process finish ...");
        return true;
    }
}

        for(String key:mProxyMap.keySet()){
            ClassCreatorProxy proxyInfo = mProxyMap.get(key);
            JavaFile javaFile = JavaFile.builder(proxyInfo.getPackageName(),proxyInfo.generateJavaCode2()).build();

            try {
                javaFile.writeTo(processingEnv.getFiler());
            }catch (IOException e){
                e.printStackTrace();
            }
        }
  • 返回用來創(chuàng)建新的類的 filer

ClassCreatorProxy 完整代碼

public class ClassCreatorProxy {
    //綁定的類名稱
    private String mBindingClassName;
    //綁定的包名稱
    private String mPackageName;
    private TypeElement mTypeElement;
    private Map<Integer, VariableElement> mVariableElementMap = new HashMap<>();

    public ClassCreatorProxy(Elements elementUtils, TypeElement classElement){
        this.mTypeElement = classElement;

        PackageElement packageElement = elementUtils.getPackageOf(mTypeElement);
        String packageName = packageElement.getQualifiedName().toString();
        String className = mTypeElement.getSimpleName().toString();
        this.mPackageName = packageName;
        this.mBindingClassName = className + "_ViewBinding";
    }

    public void putElement(int id, VariableElement element){
        mVariableElementMap.put(id,element);
    }

    public String generateJavaCode(){
        StringBuilder builder = new StringBuilder();
        builder.append("package ").append(mPackageName).append(";\n\n");
        builder.append("import com.example.gavin.apt_library.*;\n");
        builder.append('\n');
        builder.append("public class ").append(mBindingClassName);
        builder.append(" {\n");

        generateMethods(builder);
        builder.append('\n');
        builder.append("}\n");
        return builder.toString();
    }


    private void generateMethods(StringBuilder builder) {
        builder.append("public void bind(" + mTypeElement.getQualifiedName() + " host ) {\n");
        for (int id : mVariableElementMap.keySet()) {
            VariableElement element = mVariableElementMap.get(id);
            String name = element.getSimpleName().toString();
            String type = element.asType().toString();
            builder.append("host." + name).append(" = ");
            builder.append("(" + type + ")(((android.app.Activity)host).findViewById( " + id + "));\n");
        }
        builder.append("  }\n");
    }

    public String getProxyClassFullName() {
        return mPackageName + "." + mBindingClassName;
    }

    public TypeElement getTypeElement() {
        return mTypeElement;
    }
//JavaPoet是提供于自動生成java文件的構建工具類框架,使用該框架可以方便根據我們所注解的內容在編譯時進行代碼構建。
    public TypeSpec generateJavaCode2() {
        TypeSpec bindingClass = TypeSpec.classBuilder(mBindingClassName)
                .addModifiers(Modifier.PUBLIC)
                .addMethod(generateMethods2())
                .build();
        return bindingClass;

    }

    private MethodSpec generateMethods2() {
        ClassName host = ClassName.bestGuess(mTypeElement.getQualifiedName().toString());
        MethodSpec.Builder methodBuilder = MethodSpec.methodBuilder("bind")
                .addModifiers(Modifier.PUBLIC)
                .returns(void.class)
                .addParameter(host, "host");

        for (int id : mVariableElementMap.keySet()) {
            VariableElement element = mVariableElementMap.get(id);
            String name = element.getSimpleName().toString();
            String type = element.asType().toString();
            methodBuilder.addCode("host." + name + " = " + "(" + type + ")(((android.app.Activity)host).findViewById( " + id + "));");
        }
        return methodBuilder.build();
    }

    public String getPackageName(){
        return mPackageName;
    }
}

zi-api 模塊

public class ZiBindViewTools {

    public static void bind(Activity activity) {

        Class clazz = activity.getClass();
        try {
            Class bindViewClass = Class.forName(clazz.getName() + "_ViewBinding");
            Method method = bindViewClass.getMethod("bind", activity.getClass());
            method.invoke(bindViewClass.newInstance(), activity);
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (InstantiationException e) {
            e.printStackTrace();
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            e.printStackTrace();
        }
    }
}

Class bindViewClass = Class.forName(clazz.getName() + "_ViewBinding");
Method method = bindViewClass.getMethod("bind", activity.getClass());
method.invoke(bindViewClass.newInstance(), activity);

我們通過

### 使用 Api 
我們已經完成 APT 部分代碼,那么如何使用我們創(chuàng)建好 ZiBindView 呢?具體使用方法和 butterKnife 基本一樣。
```java
public class MainActivity extends AppCompatActivity {

    @ZiBindView(value = R.id.main_activity_textView)
    TextView textView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        ZiBindViewTools.bind(this);
        textView.setText("Zidea");
    }
}
Caused by: java.lang.NullPointerException: Attempt to invoke virtual method 'void android.widget.TextView.setText(java.lang.CharSequence)' on a null object reference

但是當我們運行時候可能還無法生存代碼,這是因為使用 gradle 版本過高,需要我們自己手動創(chuàng)建,

  • 首先將目錄從 Android 切換到 Project 視圖
  • zi-compiler目錄下創(chuàng)建如圖結構resources/META-INF/services文件目錄
  • 然后在目錄下添加一個問題file 即可,在 file中添加如下內容
    001.JPG

    也就是我們剛剛創(chuàng)建 ZiBindViewProcessor文件包名加文件名
    002.JPG
?著作權歸作者所有,轉載或內容合作請聯系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

友情鏈接更多精彩內容