Android之APT(Annotation Processing Tools)編譯時創(chuàng)建View對象

前言:

上篇文章中講解了通過IOS(依賴注入)的方式來為View創(chuàng)建對象并設(shè)置事件監(jiān)聽 從而簡化我們的代碼 方便維護 但是其缺點就是:在運行時通過大量的注解反射去執(zhí)行,在性能上有所欠缺。
本篇將帶大家去了解通過APT(Annotation Processing Tools)的方式在.java文件編譯為.class文件時動態(tài)的創(chuàng)建對象 其優(yōu)點就是:對象的創(chuàng)建在編譯時就已經(jīng)確立,對程序運行性能上沒有影響。目前網(wǎng)上使用APT技術(shù)比較火的庫有:
butterknife
dagger
Retrofit

APT

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

Annotation處理器(注解處理器):是一個在javac中的,用來編譯時掃描和處理的注解的工具。你可以為特定的注解,注冊你自己的注解處理器

核心原理:
編譯時Annotation解析的基本原理是,在某些代碼元素上(如類型、函數(shù)、字段等)添加注解,在編譯時javac編譯器會檢查AbstractProcessor的子類,并且調(diào)用該類型的process函數(shù),然后將添加了注解的所有元素都傳遞到process函數(shù)中,使得開發(fā)人員可以在編譯器進行相應(yīng)的處理,例如,根據(jù)注解生成新的Java類,這也就是butterknife dagger等開源庫的基本原理

案例

上面介紹了那么一大堆文縐縐的東西(我看著也煩) ,還是來點代碼實際:

public class MainActivity extends AppCompatActivity {
    @BindView(R.id.textView)
    TextView textview;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        InjectView.bind(this);
        textview.setText("我是通過APT編譯時生成的對象文件");
    }

寫法跟IOC的方式相同,但是實現(xiàn)原理卻不一樣,使用APT編譯后其實是在MainActivity的類里面創(chuàng)建了一個內(nèi)部類 其中內(nèi)部類是在編譯時通過APT生成的 最終也編譯成.class文件:

public class MainActivity extends AppCompatActivity {
    TextView textview;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        InjectView binView=new InjectView();
        binView.bind(this);
        textview.setText("我是通過APT編譯時生成的對象文件");
    }

    public class InjectView{
        public void bind(MainActivity mainActivity){
            mainActivity.textview= (TextView) mainActivity.findViewById(R.id.textView);
        }
    }
}

使用

正式開車,在項目中使用APT 需要做如下配置操作

  • 在項目的gradle中配置:
buildscript {
    repositories {
        jcenter()
        mavenCentral()//--->添加mavenCentral倉庫
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:2.3.0'
        classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8'//--->導入APT
    }
}
  • 除了你的工程Module 還需要創(chuàng)建一個 Android Library 和兩個java Library
  • 創(chuàng)建好后進行引用

首先APP的gradle

apply plugin: 'com.neenbedankt.android-apt'//導入apt插件
dependencies {
    compile project(':injectlibrary')//導入Android Library庫
    apt project(':inject-complier')//將java Library(inject-complier) 作為apt
    }

Android Library(injectlibrary)庫的gradle

dependencies {
compile project(':inject-annotion')//引入java Library(inject-annotion)java工程
}

inject-complier 的gradle

dependencies {
    compile project(':inject-annotion')////引入java Library(inject-annotion)java工程
    compile 'com.google.auto:auto-common:0.8'
    compile 'com.google.auto.service:auto-service:1.0-rc3'
    compile 'com.squareup:javapoet:1.8.0'//生成java代碼的輔助工具類
}
  • 他們之間的引用關(guān)系及作用


APP: 這個不用講是我們的主工程
Android Library: 庫里定義了InjectView等始化相關(guān)的類
Inject-annotion: 該Java工程里面只負責聲明我們需要的自定義注解 比如@Onclick等
Inject-complier: 該Java工程會被APP作為APT插件使用 里面聲明了AbstractProcessor的子類(注解處理器) 也是本章的重點

  • 上面步驟做完后就開始寫代碼 先從java工程Inject-annotion開始

Inject-annotion里面只創(chuàng)建我們需要的自定義注解:

@Target(ElementType.FIELD)//聲明在成員變量上面
@Retention(RetentionPolicy.CLASS)//編譯時運行
public @interface BindView {
    int value();
}
  • Android Library:里面定義一些初始化的相關(guān)類:
public interface ViewBinder <T>{
    void  bind(T tartget);
}
public class InjectView {
    public static void bind(Activity activity) {
        //類名
        String clsName = activity.getClass().getName();
        try {
            //加載內(nèi)部類
            Class<?> viewBidClass = Class.forName(clsName + "$$ViewBinder");
            //創(chuàng)建內(nèi)部類對象
            ViewBinder viewBinder = (ViewBinder) viewBidClass.newInstance();
            //執(zhí)行內(nèi)部類方法
            viewBinder.bind(activity);
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (InstantiationException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }
    }
}

ViewBinder是一個接口 在APT編譯時凡有用到自定義注解的類都會在該類創(chuàng)建一個內(nèi)部類 并且實現(xiàn)我們定義的ViewBinder接口 并在接口的回調(diào)方法里做一些對象創(chuàng)建 初始化的操作。
這時我們在Activity中就可以使用了,但是運行肯定會報錯,因為通過@BindViewz注解的成員變量還沒賦值

public class MainActivity extends AppCompatActivity {
    @BindView(R.id.textView)
    TextView textview;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        InjectView.bind(this);
        textview.setText("我是通過APT編譯時生成的對象文件");
    }
}
  • Inject-complier 本篇的重點注解處理器 同時需要注意的是在編寫AbstractProcessor(注解處理器)的時候一定要細心 因為該類會在編譯時執(zhí)行出錯的話是沒辦法調(diào)試的 (PS:心里的苦說不出)

首先創(chuàng)建一個類繼承 AbstractProcessor(注解處理器) 并做相關(guān)初始化操作 注釋都做的非常詳細 其中@AutoService(Processor.class)這個注解一定不要忘記添加 就是通它來標示該類可以處理我們自定義注解的能力 在JAVAC編譯時源碼中遇到我們自定義的注解時都會交由這個類來編譯處理 這也是為什么我們能在編譯時向源碼中添加代碼的原因

@AutoService(Processor.class)//該標記表明可以處理注解的能力
public class BindViewProcessor extends AbstractProcessor {
    private Elements elementUtil;//處理節(jié)點的工具類
    private Types typesUtil;//類型工具類
    private Filer filer;//生成java文件的輔助類

    @Override
    public synchronized void init(ProcessingEnvironment processingEnvironment) {
        super.init(processingEnvironment);
        elementUtil = processingEnvironment.getElementUtils();
        typesUtil = processingEnvironment.getTypeUtils();
        filer = processingEnvironment.getFiler();
    }

    /**
     * 包含本處理器想要處理的注解類型的合法全稱。換句話說,你在這里定義你的注解處理器注冊到哪些注解上。
     *比如:@BindView @OnClick
     * @return
     */
    @Override
    public Set<String> getSupportedAnnotationTypes() {
        //創(chuàng)建Set集合添加需要支持的自定義注解
        Set<String> types = new LinkedHashSet<>();
        types.add(BindView.class.getCanonicalName());
        types.add(OnClick.class.getCanonicalName());
        return types;
    }

    /**
     * 支持JDK最新版本  
     * @return
     */
    @Override
    public SourceVersion getSupportedSourceVersion() {
        return SourceVersion.latestSupported();
    }

上面的只是一些初始化的操作 真正核心的方法是AbstractProcessor的process方法 整個APP中我們定義的注解都會傳遞到這里 供我們編程處理 我將分為兩段來講

@Override
public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
    //存放著整個App的所有注解類型 以類為Key 類下的注解成員變量為value
    Map<TypeElement, List<FieldViewBinding>> targetMap = new HashMap<>();
    //遍歷整個app的BindView注解成員變量 并將該類 和成員變量保存到Map中
    setElemtBindView(roundEnvironment, targetMap, BindView.class);
    //向源碼中添加代碼
    addClassAndMethod(targetMap);
    return false;
}

首先setElemtBindView(roundEnvironment, targetMap, BindView.class); 這個方法主要是得到整個app含有@BindView注解的類
并以類名為key 對聲明了@BindView注解的成員變量 獲取其 id,成員變量名,成員變量類型 保存到一個對象中再添加到List集合 并作為Map的Value

private void setElemtBindView(RoundEnvironment roundEnvironment, Map<TypeElement, List<FieldViewBinding>> targetMap, Class<BindView> annotation) {
        //得到整個app含有@BindView注解的類 Element代表類結(jié)構(gòu)
        for (Element element : roundEnvironment.getElementsAnnotatedWith(annotation)) {
            //獲取類名
            TypeElement enclosingElement = (TypeElement) element.getEnclosingElement();
            //根據(jù)類名獲取所有的注解
            List<FieldViewBinding> list = targetMap.get(enclosingElement);
            if (null == list) {
                list = new ArrayList<>();
                targetMap.put(enclosingElement, list);
            }
            //獲取控件id 自定義注解@BindView的value返回值
            int id = element.getAnnotation(annotation).value();
            //獲取成員變量的名--->titleText
            String fieldName = element.getSimpleName().toString();
            //獲取成員變量類型信息---->TextView
            TypeMirror typeMirror = element.asType();
            FieldViewBinding fieldViewBinding = new FieldViewBinding(fieldName, typeMirror, id);
            list.add(fieldViewBinding);
        }
    }

創(chuàng)建一個類來保存獲取到的(成員變量名稱,成員變量類型,布局中id值)等相關(guān)信息

public class FieldViewBinding {
    private String name; //成員變量名稱
    private TypeMirror typeMirror;//成員變量類型
    private int resId;//布局中id值
    
    public FieldViewBinding(String name, TypeMirror typeMirror, int resId) {
        this.name = name;
        this.typeMirror = typeMirror;
        this.resId = resId;
    }
    public String getName() { return name; }

    public TypeMirror getTypeMirror() { return typeMirror; }

    public int getResId() { return resId;}
}

其中涉及到的知識點Element : Element代表一個類的結(jié)構(gòu) 其對應(yīng)關(guān)系

package com.example;      ---> PackageElement

public class 類名 {        ---> TypeElement

    private int 成員變量;   ---> VariableElement
    private Foo 成員變量;   ---> VariableElement
    
    public 構(gòu)造函數(shù) () {}   ---> ExecuteableElement
    
    public void 普通方法 (  ---> ExecuteableElement
                方法形參    ---> TypeElement
                     ) {}
}

在來看addClassAndMethod(targetMap);方法 該方法就是向源碼中添加代碼的主要邏輯

private void addClassAndMethod(Map<TypeElement, List<FieldViewBinding>> targetMap) {
        //遍歷Map
        for (Map.Entry<TypeElement, List<FieldViewBinding>> item : targetMap.entrySet()) {
            List<FieldViewBinding> list = item.getValue();
            if (null == list || list.size() == 0) {
                continue;
            }
            //類類型 com.example....MainActivity
            TypeElement typeElement = item.getKey();
            //獲取包名 com.example...
            String packageName = getPackageName(typeElement);
            //根據(jù)包名獲取類名 MainActivity
            String className = getClassName(typeElement, packageName);
            //類型 <T extends MainActivity>
            ClassName name = ClassName.bestGuess(className);
            //獲取我們定義的接口包名 和類名
            ClassName viewBinder = ClassName.get("com.example.injectlibrary", "ViewBinder");
            //生成java類 MainActivity$$ViewBinder
            TypeSpec.Builder result = TypeSpec.classBuilder(className + "$$ViewBinder")
                    .addModifiers(Modifier.PUBLIC)//將該類聲明為public
                    .addTypeVariable(TypeVariableName.get("T", name))//聲明該類的類型 <T extends MainActivity>
                    .addSuperinterface(ParameterizedTypeName.get(viewBinder, name));//該類的實現(xiàn)接口 以及接口類型
            //生成方法
            MethodSpec.Builder methodBuilder = MethodSpec.methodBuilder("bind")//方法名 "bind"與ViewBinder接口中的回調(diào)保持一致
                    .addModifiers(Modifier.PUBLIC)//方法聲明為public
                    .returns(TypeName.VOID)//方法返回值為void
                    .addAnnotation(Override.class)//方法注解 實現(xiàn)接口方法
                    .addParameter(name, "target", Modifier.FINAL);//參數(shù)類型(MainActivity) 參數(shù)名 參數(shù)修飾符
            //遍歷該類下聲明了@BindView注解的成員變量List集合
            for (int i = 0; i < list.size(); i++) {
                FieldViewBinding fieldViewBinding = list.get(i);
                    //成員變量類型信息 --android.text.TextView
                    String packageNameString = fieldViewBinding.getTypeMirror().toString();
                    //得到成員變量的類名---TextView
                    ClassName viewclassName = ClassName.bestGuess(packageNameString);
                    //方法里面添加執(zhí)行邏輯 $L $T 占位符 參數(shù)順序一定要對 以及“target”一定要與上面的行參保持一致 代表的就是mainActivity
                    //相當于:mainActivity.textview= (TextView) mainActivity.findViewById(R.id.textView);
                    methodBuilder.addStatement("target.$L=($T)target.findViewById($L)", fieldViewBinding.getName(), viewclassName, fieldViewBinding.getResId());
            }
            result.addMethod(methodBuilder.build());//往類里面添加方法
            try {
                //生成Java類信息 包名 類
                JavaFile.builder(packageName, result.build())
                        .addFileComment("auto create make")//類注釋
                        .build()
                        .writeTo(filer);//寫出
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
/**
 * 獲取類名(通過截取包名獲取 若是內(nèi)部類會將"."替換為"$")
 * @param typeElement
 * @param packageName
 */
private String getClassName(TypeElement typeElement, String packageName) {
        int packageNameLength = packageName.length() + 1;
        return typeElement.getQualifiedName().toString().substring(packageNameLength).replace(".", "$");
 }

/**
 * 獲取包名
 * @param enclosingElement
 * @return
 */
 private String getPackageName(TypeElement enclosingElement) {
        return elementUtil.getPackageOf(enclosingElement).getQualifiedName().toString();
}

到這里就完成了 試著運行一次

public class MainActivity extends AppCompatActivity {
    @BindView(R.id.textView)
    TextView textview;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        InjectView.bind(this);
        textview.setText("我是通過APT編譯時生成的對象文件");
    }
}

同時我們打開項目編譯的class文件也可以看到通過APT插件生成一個內(nèi)部類


總結(jié):

使用APT javaC在對.java文件進行編譯的時候會對源代碼文件進行檢測找出其中的我們自定義的注解 并交由我們定義的注解處理器進行額外的處理(往源碼中添加類 方法等) 最后在通過APT編譯成class文件 交由JVM虛擬機運行
(本案例針對控件的初始化操作 就是通過自定義注解處理器 在process方法中向源碼中添加控件的初始化話的相關(guān)代碼 從而達到在編譯時就確定了對象的初始化)
優(yōu)點:

  1. 編譯時創(chuàng)建對象 對程序運行性能無影響
  2. 代碼整潔 方便維護
    缺點:
  3. 代碼編寫難度增加
  4. 若某處出錯 不方便調(diào)試
    不過現(xiàn)在網(wǎng)上已經(jīng)有很成熟的開源庫來滿足我們的要求 實現(xiàn)快速開發(fā) 其核心思想是一樣的:
    butterknife
    dagger
    Retrofit
最后編輯于
?著作權(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)容