網(wǎng)易云微專業(yè)安卓-APT黃金搭檔JavaPoet,讓框架更穩(wěn)定更簡潔

文章純屬個人學(xué)習(xí)的代碼實現(xiàn)

網(wǎng)易云微專業(yè)公開課這節(jié)課 用JavaPoet簡單實現(xiàn)了ButterKnifeBindView功能

原理其實就是通過注解處理器處理注解,然后利用 JavaPoet面向?qū)ο蟮恼Z法動態(tài)生成了一個Java文件

這里加入我們希望完成MainActivityBindView功能,MainActivity的代碼如下

public class MainActivity extends AppCompatActivity {

    @BindView(R.id.tv)
    TextView tv;
    
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        ButterKnife.bind(this);
        tv.setText("Hello android");
    }

}

我們要看到兩個需要注意的地方,第一個就是

    @BindView(R.id.tv)
    TextView tv;

這里就是我們要綁定的視圖
第二個地方是這里

  ButterKnife.bind(this);

我們要用注解處理器和JavaPoet生成這樣的文件

public class MainActivity$$ViewBinder implements ViewBinder<MainActivity> {
  @Override
  public void bind(MainActivity target) {
    target.tv = target.findViewById(2131165359);
  }
}

這里可以看到我們生成了一個以MainActivity+$$ViewBinder的一個 java文件,他需要實現(xiàn)一個接口ViewBinder<MainActivity>
我們可以看看ViewBinder<MainActivity>這個接口

public interface ViewBinder<T> {
    void bind(T target);
}

這里聲明了一個泛型接口,表示他 接受任何泛型對象,不同泛型對象實現(xiàn)不同的bind(T target)
方法,比如我們剛剛看到的MainActivity$$ViewBinder類實現(xiàn)的

 @Override
  public void bind(MainActivity target) {
    target.tv = target.findViewById(2131165359);
  }

這里傳入的類型是MainActivity
然后我們可以看到這個文件其實就是幫我們做了一個操作,利用傳進(jìn)來的MainActivity完成了tv控件的綁定 ,記住了這里的代碼不用自己寫,是注解處理器和JavaPoet生成的。

但是看到這里有同學(xué)要問了,你生成了這個文件,但是我沒有調(diào)用呀?嗯,問的很好,這里我們還記得

ButterKnife.bind(this);

嗎,我們來看看他的代碼

public class ButterKnife {
    //target.tv=target.findViewById(R.id.tv)
    public static void bind(Object object) {
     String  className=object.getClass().getName()+"$$ViewBinder";

     //類加載
        try {
            Class<?> clazz = Class.forName(className);
            ViewBinder viewBinder = (ViewBinder) clazz.newInstance();
            viewBinder.bind(object);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

其實這里是通過類加載就是這里的Class.forName實現(xiàn)了java文件的查找,找到了之后我們就可以操作這個Class對象,我們這里直接調(diào)用Class對象的實例化方法newInstance()去新建一個對象,這個對象是什么呢?
很簡單,我們看到這里

 String  className=object.getClass().getName()+"$$ViewBinder";

這是我們上面看到的那個類呀MainActivity$$ViewBinder,那我們應(yīng)該記得它實現(xiàn)了ViewBinder接口,那接口的實現(xiàn)類就可以轉(zhuǎn)成對應(yīng)的接口對象,這個有點拗口,不對想想也應(yīng)該能理解,我們安卓里面很多類似的操作,比如把點擊事件的接口返回一個接口對象,比如下面這樣

 tv.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {

            }
        });

這個匿名內(nèi)部類不就完成了一個接口實現(xiàn)類,不同的點擊事件就是不同的接口實現(xiàn)類,結(jié)果都返回一個View.OnClickListener的接口對象

好回到我們剛剛那里,

viewBinder.bind(object);

最后我們調(diào)用了這個,啥意思?其實就相當(dāng)于

 MainActivity$$ViewBinder.bind(mainActivity)

這里是不是就調(diào)用了我們之前用注解處理器和JavaPoet生成的java文件,所以這里也很清楚的可以知道ButterKinfe為啥如果我們不調(diào)用這個方法,頁面視圖綁定會空指針。

上面基本把原理講完,最后我們看看注解處理器和JavaPoet怎么生成我們需要的這個文件

public class MainActivity$$ViewBinder implements ViewBinder<MainActivity> {
  @Override
  public void bind(MainActivity target) {
    target.tv = target.findViewById(2131165359);
  }
}

我先扔一堆代碼給大家,但是不要慌,后面我會一個個分析

@AutoService(Processor.class)
@SupportedAnnotationTypes({Constants.BINDERVIEW_ANNOTATION_TYPES})
@SupportedSourceVersion(SourceVersion.RELEASE_7)
public class BindViewProcessor extends AbstractProcessor {
    private Elements elementUitls;
    private Types typeUtils;
    private Messager messager;
    private Filer filer;

    private Map<TypeElement, List<Element>> tempBindViewMap = new HashMap<>();

    @Override
    public synchronized void init(ProcessingEnvironment processingEnv) {
        super.init(processingEnv);
        elementUitls = processingEnv.getElementUtils();
        typeUtils = processingEnv.getTypeUtils();
        messager = processingEnv.getMessager();
        filer = processingEnv.getFiler();
        messager.printMessage(Diagnostic.Kind.NOTE, "init finish,start process annotation ..........");
    }

    @Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnv) {
        if (set.isEmpty()) return false;
        Set<? extends Element> bindViewElements = roundEnv.getElementsAnnotatedWith(BindView.class);
        if (!bindViewElements.isEmpty()) {
            valueOfMap(bindViewElements);
            try {
                createJavaFile();
            } catch (IOException e) {
                e.printStackTrace();
            }
            return true;
        }
        return false;
    }

    private void createJavaFile() throws IOException {
        if (tempBindViewMap.isEmpty()) return;


        for (Map.Entry<TypeElement, List<Element>> entry : tempBindViewMap.entrySet()) {
            //獲取當(dāng)前類名
            ClassName className = ClassName.get(entry.getKey());

            //生成一個名為target參數(shù) 類型就是上面的typeName
            ParameterSpec parameterSpec = ParameterSpec.builder(ClassName.get(entry.getKey()), Constants.TARGET_PARAMETER_NAME)
                    .build();//target

            MethodSpec.Builder methodBuilder = MethodSpec.methodBuilder(Constants.BIND_METHOD_NAME)
                    .addAnnotation(Override.class)
                    .addModifiers(Modifier.PUBLIC)
                    .addParameter(parameterSpec);

            for (Element fieldElement : entry.getValue()) {
                String fieldName = fieldElement.getSimpleName().toString();
                //獲取BindView上面的id值
                int viewId = fieldElement.getAnnotation(BindView.class).value();
                String methodContent = "$N." + fieldName + " = $N.findViewById($L)";
                //拼接成target.tv=target.findViewById(R.id.tv)
                methodBuilder.addStatement(methodContent, Constants.TARGET_PARAMETER_NAME,
                        Constants.TARGET_PARAMETER_NAME, viewId);
            }

            //找得到所有實現(xiàn)了ViewBinder接口的類
            TypeElement viewBindType = elementUitls.getTypeElement(Constants.VIEWBINDER);
            // 這里需要理解一下 實現(xiàn)接口泛型 ViewBinder<MainActivity> 拼接成這個樣子
            ParameterizedTypeName typeName = ParameterizedTypeName.get(ClassName.get(viewBindType),
                    className);
            if(typeName!=null) {
                JavaFile.builder(className.packageName(),
                        TypeSpec.classBuilder(className.simpleName() + "$$ViewBinder")
                                .addSuperinterface(typeName)  //實現(xiàn)viewBinder接口
                                .addModifiers(Modifier.PUBLIC)
                                .addMethod(methodBuilder.build())
                                .build()

                ).build().writeTo(filer);
            }

        }

    }

    private void valueOfMap(Set<? extends Element> bindViewElements) {
        for (Element element : bindViewElements) {
            //注解在屬性之上,屬性節(jié)點父節(jié)點是類節(jié)點
            TypeElement classElement = (TypeElement) element.getEnclosingElement();
            if (tempBindViewMap.containsKey(classElement)) {
                tempBindViewMap.get(classElement).add(element);
            } else {
                List<Element> fields = new ArrayList<>();
                fields.add(element);
                tempBindViewMap.put(classElement, fields);
            }

        }
    }
}

首先我們看看這個類頭部的注解

@AutoService(Processor.class)
@SupportedAnnotationTypes({Constants.BINDERVIEW_ANNOTATION_TYPES})
@SupportedSourceVersion(SourceVersion.RELEASE_7)

這里@AutoService(Processor.class)表示這是一個注解處理器
@SupportedAnnotationTypes({Constants.BINDERVIEW_ANNOTATION_TYPES})表示這個注解處理器需要處理的注解類型,這里這個名字必須正確,比如我這里的是

public static final String BINDERVIEW_ANNOTATION_TYPES="com.example.annotation.BindView";

錯了就出問題了,如果發(fā)現(xiàn)注解處理器不生效首先檢查一下這里

@SupportedSourceVersion(SourceVersion.RELEASE_7)

然后這里就是支持JDK7
我們再看看下面這個幾個部分

private Elements elementUitls;
    private Messager messager;
    private Filer filer;

    private Map<TypeElement, List<Element>> tempBindViewMap = new HashMap<>();

    @Override
    public synchronized void init(ProcessingEnvironment processingEnv) {
        super.init(processingEnv);
        elementUitls = processingEnv.getElementUtils();
        messager = processingEnv.getMessager();
        filer = processingEnv.getFiler();
        messager.printMessage(Diagnostic.Kind.NOTE, "init finish,start process annotation ..........");
    }

首先是這個Elements elementUitls是用來操作類元素的,具體作用我們等一下分析下面的代碼可以看到
Messager messager用于控制臺打印消息的,Filer filer用于輸出Java文件的,Map<TypeElement, List<Element>> tempBindViewMap主要用來存儲類里面的元素信息,啥意思?就是用來存儲類似

 @BindView(R.id.tv)
    TextView tv;

這樣子的類變量信息,后面會分析到

init方法基本就是初始化這幾個變量

下面就是核心方法,注解處理了

 @Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnv) {
        if (set.isEmpty()) return false;
        Set<? extends Element> bindViewElements = roundEnv.getElementsAnnotatedWith(BindView.class);
        if (!bindViewElements.isEmpty()) {
            valueOfMap(bindViewElements);
            try {
                createJavaFile();
            } catch (IOException e) {
                e.printStackTrace();
            }
            return true;
        }
        return false;
    }

我們通過

   Set<? extends Element> bindViewElements = roundEnv.getElementsAnnotatedWith(BindView.class);

找到所有包含BindView注解的對象,然后我們看看valueOfMap做了啥

private void valueOfMap(Set<? extends Element> bindViewElements) {
        for (Element element : bindViewElements) {
            //注解在屬性之上,屬性節(jié)點父節(jié)點是類節(jié)點
            TypeElement classElement = (TypeElement) element.getEnclosingElement();
            if (tempBindViewMap.containsKey(classElement)) {
                tempBindViewMap.get(classElement).add(element);
            } else {
                List<Element> fields = new ArrayList<>();
                fields.add(element);
                tempBindViewMap.put(classElement, fields);
            }

        }
    }

這個方法其實就是通過遍歷所有的添加了BindView注解的對象,然后

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

找到對應(yīng)的屬于哪個類的屬性,這個 classElement就是對應(yīng)的類名,我們這里其實就是MainActivity類,然后判斷Map里面是否有該類,如果存在,就取出來然后給他的List<Element>對象添加一個element即使一個屬性對象,然后不存在,就新建一個List<Element>對象并且添加一個element然后把對應(yīng)的類和List<Element>放入Map存起來

說的很復(fù)雜哈,其實就是遍歷你類里面使用到BindView注解的屬性,然后完事

下面到了重頭戲createJavaFile()這個方法即使真正生成Java文件的地方

  private void createJavaFile() throws IOException {
        if (tempBindViewMap.isEmpty()) return;
        
        for (Map.Entry<TypeElement, List<Element>> entry : tempBindViewMap.entrySet()) {
            //獲取當(dāng)前類名
            ClassName className = ClassName.get(entry.getKey());

            //生成一個名為target參數(shù) 類型就是上面的typeName
            ParameterSpec parameterSpec = ParameterSpec.builder(ClassName.get(entry.getKey()), Constants.TARGET_PARAMETER_NAME)
                    .build();//target

            MethodSpec.Builder methodBuilder = MethodSpec.methodBuilder(Constants.BIND_METHOD_NAME)
                    .addAnnotation(Override.class)
                    .addModifiers(Modifier.PUBLIC)
                    .addParameter(parameterSpec);

            for (Element fieldElement : entry.getValue()) {
                String fieldName = fieldElement.getSimpleName().toString();
                //獲取BindView上面的id值
                int viewId = fieldElement.getAnnotation(BindView.class).value();
                String methodContent = "$N." + fieldName + " = $N.findViewById($L)";
                //拼接成target.tv=target.findViewById(R.id.tv)
                methodBuilder.addStatement(methodContent, Constants.TARGET_PARAMETER_NAME,
                        Constants.TARGET_PARAMETER_NAME, viewId);
            }

            //找得到所有實現(xiàn)了ViewBinder接口的類
            TypeElement viewBindType = elementUitls.getTypeElement(Constants.VIEWBINDER);
            // 這里需要理解一下 實現(xiàn)接口泛型 ViewBinder<MainActivity> 拼接成這個樣子
            ParameterizedTypeName typeName = ParameterizedTypeName.get(ClassName.get(viewBindType),
                    className);
            if(typeName!=null) {
                JavaFile.builder(className.packageName(),
                        TypeSpec.classBuilder(className.simpleName() + "$$ViewBinder")
                                .addSuperinterface(typeName)  //實現(xiàn)viewBinder接口
                                .addModifiers(Modifier.PUBLIC)
                                .addMethod(methodBuilder.build())
                                .build()

                ).build().writeTo(filer);
            }

        }

    }

這個方法很長,沒關(guān)系,我們從下往上看

//找得到所有實現(xiàn)了ViewBinder接口的類
            TypeElement viewBindType = elementUitls.getTypeElement(Constants.VIEWBINDER);
            // 這里需要理解一下 實現(xiàn)接口泛型 ViewBinder<MainActivity> 拼接成這個樣子
            ParameterizedTypeName typeName = ParameterizedTypeName.get(ClassName.get(viewBindType),
                    className);
            if(typeName!=null) {
                JavaFile.builder(className.packageName(),
                        TypeSpec.classBuilder(className.simpleName() + "$$ViewBinder")
                                .addSuperinterface(typeName)  //實現(xiàn)viewBinder接口
                                .addModifiers(Modifier.PUBLIC)
                                .addMethod(methodBuilder.build())
                                .build()

                ).build().writeTo(filer);
            }

這里啥意思,首先這兩句代碼

TypeElement viewBindType = elementUitls.getTypeElement(Constants.VIEWBINDER);
            // 這里需要理解一下 實現(xiàn)接口泛型 ViewBinder<MainActivity> 拼接成這個樣子
            ParameterizedTypeName typeName = ParameterizedTypeName.get(ClassName.get(viewBindType),
                    className);

我們通過開頭講到的Element elementUitls這個可以操作類的對象,找到一個接口類型Constants.VIEWBINDER=public static final CharSequence VIEWBINDER = "com.example.library.ViewBinder";這里也不能錯,錯了玩完
找到這個接口之后,通過ParameterizedTypeName typeName = ParameterizedTypeName.get(ClassName.get(viewBindType), className);構(gòu)建成一個類似這樣的接口ViewBinder<MainActivity>
這里的className在開頭的這個位置我們能看到

//獲取當(dāng)前類名
            ClassName className = ClassName.get(entry.getKey());

作用就是獲取當(dāng)前類名,這是一個對象哈,不單單是類名還有其他屬性
然后我們看這里

 JavaFile.builder(className.packageName(),
                        TypeSpec.classBuilder(className.simpleName() + "$$ViewBinder")

通過className獲取包名,然后傳入類型對應(yīng)要生成的類名這里其實就是MainActivity$$ViewBinder

然后

.addSuperinterface(typeName)  //實現(xiàn)viewBinder接口

實現(xiàn)ViewBinder<MainActivity>接口
然后

 .addModifiers(Modifier.PUBLIC)

聲明類的訪問權(quán)限為public
然后

  .addMethod(methodBuilder.build())

傳入要實現(xiàn)的方法,這個我們等一下往上看能看到,可以先預(yù)告一下,我們還記得這個類嗎

public class MainActivity$$ViewBinder implements ViewBinder<MainActivity> {
  @Override
  public void bind(MainActivity target) {
    target.tv = target.findViewById(2131165359);
  }
}

我們上面基本完成了類的外殼的搭建,結(jié)構(gòu)是這樣子的

package com.example.javapoetbutterknife;
public class MainActivity$$ViewBinder implements ViewBinder<MainActivity> {
}

有包名,然后的類的一些配置,那我們知道等一下要實現(xiàn)的就是bind方法
先把上面收尾

).build().writeTo(filer);

其實就是寫入Java文件,比如我這里的這個文件

image.png

不同版本的AS,位置不一樣,不過基本都在 這個build文件夾,具體可以百度一下

回到我們上面的,現(xiàn)在要實現(xiàn)一個方法,先分析一下,這個方法首先需要一個參數(shù),這個參數(shù)怎么獲取,參數(shù)類型是什么,還有參數(shù)有了之后,我們要實現(xiàn)里面的findViewById方法,好,我們先看代碼

 //生成一個名為target參數(shù) 類型就是上面的typeName
            ParameterSpec parameterSpec = ParameterSpec.builder(ClassName.get(entry.getKey()), Constants.TARGET_PARAMETER_NAME)
                    .build();//target

            MethodSpec.Builder methodBuilder = MethodSpec.methodBuilder(Constants.BIND_METHOD_NAME)
                    .addAnnotation(Override.class)
                    .addModifiers(Modifier.PUBLIC)
                    .addParameter(parameterSpec);

            for (Element fieldElement : entry.getValue()) {
                String fieldName = fieldElement.getSimpleName().toString();
                //獲取BindView上面的id值
                int viewId = fieldElement.getAnnotation(BindView.class).value();
                String methodContent = "$N." + fieldName + " = $N.findViewById($L)";
                //拼接成target.tv=target.findViewById(R.id.tv)
                methodBuilder.addStatement(methodContent, Constants.TARGET_PARAMETER_NAME,
                        Constants.TARGET_PARAMETER_NAME, viewId);
            }

又是一大坨,沒事,我們先看第一句話

  ParameterSpec parameterSpec = ParameterSpec.builder(ClassName.get(entry.getKey()), Constants.TARGET_PARAMETER_NAME)
                    .build();//target

我們先看看這個Constants.TARGET_PARAMETER_NAME=public static final String TARGET_PARAMETER_NAME = "target";
其實就是聲明一個參數(shù)類型 ClassName.get(entry.getKey())取出來的是MainActivity,然后這個對象ParameterSpec parameterSpec其實就是類似這個(MainActivity target),記得對比上面那個方法代碼

繼續(xù)走起

   MethodSpec.Builder methodBuilder = MethodSpec.methodBuilder(Constants.BIND_METHOD_NAME)
                    .addAnnotation(Override.class)
                    .addModifiers(Modifier.PUBLIC)
                    .addParameter(parameterSpec);

這里構(gòu)造了一個方法對象,首先老規(guī)矩看看這個 Constants.BIND_METHOD_NAME是啥

    public static final String BIND_METHOD_NAME = "bind";

其實就是方法名稱bind,沒錯第一個參數(shù)傳的就是方法 名稱,然后是Override.class注解,方法訪問權(quán)限為public,參數(shù)為(MainActivity target)上面生成的

最后最后啦 ,就是里面的方法 實現(xiàn)

 for (Element fieldElement : entry.getValue()) {
                String fieldName = fieldElement.getSimpleName().toString();
                //獲取BindView上面的id值
                int viewId = fieldElement.getAnnotation(BindView.class).value();
                String methodContent = "$N." + fieldName + " = $N.findViewById($L)";
                //拼接成target.tv=target.findViewById(R.id.tv)
                methodBuilder.addStatement(methodContent, Constants.TARGET_PARAMETER_NAME,
                        Constants.TARGET_PARAMETER_NAME, viewId);
            }

首先 解釋一下這個循環(huán),因為一個類里面一般都不止一個控件需要綁定把,我們這里是demo所以我只寫了一個TextView tv ,然后

String fieldName = fieldElement.getSimpleName().toString();

獲取控件名稱,比如這里fieldName ==tv
然后獲取注解的ID

 //獲取BindView上面的id值
                int viewId = fieldElement.getAnnotation(BindView.class).value();

我們demo就是R.id.tv的整型值

下面有個比較需要稍微了解JavaPoet的東西

  String methodContent = "$N." + fieldName + " = $N.findViewById($L)";

$N for Name就是名字 $L for Link就是引用 這里 $N就是target
$L 即使R.id.tv的整型值

最后

 //拼接成target.tv=target.findViewById(R.id.tv)
                methodBuilder.addStatement(methodContent, Constants.TARGET_PARAMETER_NAME,
                        Constants.TARGET_PARAMETER_NAME, viewId);

拼接成target.tv=target.findViewById(R.id.tv)

Ok 全劇終

大家想了解更多直接去我的github看代碼實現(xiàn)

?著作權(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)容