Android butterknife源碼淺析及APT實(shí)際應(yīng)用

介紹

apt即Annotation Processing Tool,通過在編譯期間掃描相關(guān)注解最后生成.java文件的一種注解處理工具。

目的

通過自動生成.java文件來簡化大量模板代碼的書寫,提高工作效率。

源碼淺析

butterknife Bind Android views and callbacks to fields and methods.
下面就淺析下butterknife是如何實(shí)現(xiàn)綁定的
1.首先看下項(xiàng)目結(jié)構(gòu),我們主要關(guān)注3個(gè)模塊

image.png

  • annotation模塊用于定義基礎(chǔ)注解
  • apt模塊用于處理annotation定義的基礎(chǔ)注解,也是核心模塊
  • app模塊可以運(yùn)行的demo例子

2.相關(guān)模塊淺析

  • 注解如何定義
    以經(jīng)常使用的BindView為例看一下


    image.png

    注解可以理解成一個(gè)TAG,更多注解相關(guān)的基礎(chǔ)知識這篇文章不做細(xì)講


  • apt重點(diǎn)來了

    第一步添加依賴
    implementation project(':butterknife-annotations') 添加注解模塊的依賴
    api deps.javapoet 這個(gè)javapoet是反向生成代碼的利器,后面在細(xì)講
    compileOnly deps.auto.service auto-service是google幫助我們在編寫apt代碼時(shí)自動生成相關(guān)必要目錄和文件的輔助工具

    第二步看下核心處理類 ButterKnifeProcessor

@AutoService(Processor.class)//這個(gè)注解幫助我們做了一些工作,后面再說
/**
*  ButterKnifeProcessor 繼承AbstractProcessor ,我們看關(guān)鍵幾個(gè)重寫的方法
*/
public final class ButterKnifeProcessor extends AbstractProcessor {
    @Override
    public Set<String> getSupportedAnnotationTypes() {//設(shè)置哪些注解可以支持處理
        Set<String> types = new LinkedHashSet<>();
        for (Class<? extends Annotation> annotation : getSupportedAnnotations()) {
            types.add(annotation.getCanonicalName());
        }
        return types;
    }

    private Set<Class<? extends Annotation>> getSupportedAnnotations() {//具體的注解添加邏輯
        Set<Class<? extends Annotation>> annotations = new LinkedHashSet<>();
        annotations.add(BindAnim.class);
        annotations.add(BindArray.class);
        annotations.add(BindBitmap.class);
        annotations.add(BindBool.class);
        annotations.add(BindColor.class);
        annotations.add(BindDimen.class);
        annotations.add(BindDrawable.class);
        annotations.add(BindFloat.class);
        annotations.add(BindFont.class);
        annotations.add(BindInt.class);
        annotations.add(BindString.class);
        annotations.add(BindView.class);
        annotations.add(BindViews.class);
        annotations.addAll(LISTENERS);
        return annotations;
    }

    //設(shè)置生成的.java源碼支持的jdk版本
    @Override
    public SourceVersion getSupportedSourceVersion() {
        return SourceVersion.latestSupported();
    }
    //核心處理方法,在這個(gè)方法里面生成我們需要的.java文件
    @Override
    public boolean process(Set<? extends TypeElement> elements, RoundEnvironment env) {
        Map<TypeElement, BindingSet> bindingMap = findAndParseTargets(env);//以BindView注解為例主要功能是收集id,變量名,變量類型,以及一些check處理

        for (Map.Entry<TypeElement, BindingSet> entry : bindingMap.entrySet()) {
            TypeElement typeElement = entry.getKey();
            BindingSet binding = entry.getValue();//BindingSet是butterknife的核心類這里面處理各種各樣模板代碼的生成

            JavaFile javaFile = binding.brewJava(sdk, debuggable, useAndroidX);//組裝JavaFile文件
            try {
                javaFile.writeTo(filer);//寫出.java文件到build的apt目錄下
            } catch (IOException e) {
                error(typeElement, "Unable to write binding for type %s: %s", typeElement, e.getMessage());
            }
        }

        return false;
    }

}

上面的代碼是AbstractProcessor 的一些關(guān)鍵的回調(diào),下面我們看下里面用到的核心方法
JavaFile javaFile = binding.brewJava(sdk, debuggable, useAndroidX);//組裝JavaFile文件

    JavaFile brewJava(int sdk, boolean debuggable, boolean useAndroidX) {
       //TypeSpec是什么?通俗的講是類,接口的描述
        TypeSpec bindingConfiguration = createType(sdk, debuggable, useAndroidX);
        return JavaFile.builder(bindingClassName.packageName(), bindingConfiguration)
                .addFileComment("Generated code from Butter Knife. Do not modify!")
                .build();
    }
    /**
     * 最核心的方法,也是javapoet的使用  https://github.com/square/javapoet
     * 為了方便大家理解我把自動生成的代碼貼到對應(yīng)位置,還是以BindView注解為例
     * @param sdk
     * @param debuggable
     * @param useAndroidX
     * @return
     */
    private TypeSpec createType(int sdk, boolean debuggable, boolean useAndroidX) {
        TypeSpec.Builder result = TypeSpec.classBuilder(bindingClassName.simpleName())
                .addModifiers(PUBLIC);//public class SimpleActivity_ViewBinding, 其中addModifiers是增加修飾符
        if (isFinal) {
            result.addModifiers(FINAL);//類不可被繼承加final修飾符
        }

        if (parentBinding != null) {
            result.superclass(parentBinding.bindingClassName);//如果有父類添加繼承關(guān)系
        } else {
            result.addSuperinterface(UNBINDER);//沒有父類添加實(shí)現(xiàn)  public class SimpleActivity_ViewBinding implements Unbinder
        }

        if (hasTargetField()) {
            result.addField(targetTypeName, "target", PRIVATE);//有target屬性   private SimpleActivity target;
        }

        if (isView) {
            result.addMethod(createBindingConstructorForView(useAndroidX));
        } else if (isActivity) {//通過getEnclosingElement()方法拿到包裝類,如果是Activity那么執(zhí)行下面的方法
            result.addMethod(createBindingConstructorForActivity(useAndroidX));//添加構(gòu)造方法
            /***
             *     @UiThread
             *     public SimpleActivity_ViewBinding(SimpleActivity target) {
             *      this(target, target.getWindow().getDecorView());
             *  }
             */
        } else if (isDialog) {
            result.addMethod(createBindingConstructorForDialog(useAndroidX));
        }
        if (!constructorNeedsView()) {
            // Add a delegating constructor with a target type + view signature for reflective use.
            result.addMethod(createBindingViewDelegateConstructor(useAndroidX)); //deprecated 不看了
        }
        result.addMethod(createBindingConstructor(sdk, debuggable, useAndroidX));//這一句也是添加構(gòu)造方法,在這個(gè)構(gòu)造函數(shù)里面有具體的綁定生成
        /**
         *    @UiThread
        public SimpleActivity_ViewBinding(final SimpleActivity target, View source) {
        this.target = target;

        View view;
        target.title = Utils.findRequiredViewAsType(source, R.id.title, "field 'title'", TextView.class);
        target.subtitle = Utils.findRequiredViewAsType(source, R.id.subtitle, "field 'subtitle'", TextView.class);
        view = Utils.findRequiredView(source, R.id.hello, "field 'hello', method 'sayHello', and method 'sayGetOffMe'");
        target.hello = Utils.castView(view, R.id.hello, "field 'hello'", Button.class);
        view7f06000a = view;
        view.setOnClickListener(new DebouncingOnClickListener() {
        @Override
        public void doClick(View p0) {
        target.sayHello();
        }
        });
        view.setOnLongClickListener(new View.OnLongClickListener() {
        @Override
        public boolean onLongClick(View p0) {
        return target.sayGetOffMe();
        }
        });
        view = Utils.findRequiredView(source, R.id.list_of_things, "field 'listOfThings' and method 'onItemClick'");
        target.listOfThings = Utils.castView(view, R.id.list_of_things, "field 'listOfThings'", ListView.class);
        view7f060012 = view;
        ((AdapterView<?>) view).setOnItemClickListener(new AdapterView.OnItemClickListener() {
        @Override
        public void onItemClick(AdapterView<?> p0, View p1, int p2, long p3) {
        target.onItemClick(p2);
        }
        });
        target.footer = Utils.findRequiredViewAsType(source, R.id.footer, "field 'footer'", TextView.class);
        target.headerViews = Utils.listOf(
        Utils.findRequiredView(source, R.id.title, "field 'headerViews'"),
        Utils.findRequiredView(source, R.id.subtitle, "field 'headerViews'"),
        Utils.findRequiredView(source, R.id.hello, "field 'headerViews'"));

        Context context = source.getContext();
        Resources res = context.getResources();
        target.butterKnife = res.getString(R.string.app_name);
        target.fieldMethod = res.getString(R.string.field_method);
        target.byJakeWharton = res.getString(R.string.by_jake_wharton);
        target.sayHello = res.getString(R.string.say_hello);
        }
         */
        if (hasViewBindings() || parentBinding == null) {
            result.addMethod(createBindingUnbindMethod(result, useAndroidX));//添加unbind方法這個(gè)方法都不陌生,ondestory()方法里面經(jīng)常調(diào)用
        }

        return result.build();//最后build()方法在內(nèi)部 new TypeSpec,這樣就完成了TypeSpec的組裝。
    }

javapoet的詳細(xì)使用<-這個(gè)是apt的基礎(chǔ)也是關(guān)鍵

補(bǔ)充一下 @AutoService(Processor.class)的作用
在使用注解處理器需要先聲明以下步驟:
1、需要在 apt 庫的 main 目錄下新建 resources 資源文件夾;
2、在 resources文件夾下建立 META-INF/services 目錄文件夾;
3、在 META-INF/services 目錄文件夾下創(chuàng)建 javax.annotation.processing.Processor 文件;
4、在 javax.annotation.processing.Processor 文件寫入注解處理器的全稱,包括包路徑;)
每次這樣寫太麻煩。這就是用引入auto-service的原因。
通過@AutoService可以自動生成上面的步驟,AutoService注解處理器是Google開發(fā)的,用來生成 META-INF/services/javax.annotation.processing.Processor 文件的

  • 使用apt

    第一步添加依賴
    implementation project(':butterknife-annotations')
    annotationProcessor project(':butterknife-compiler') 有沒有經(jīng)??吹竭@個(gè)gradle api?最早的時(shí)候是這樣的apt project(':butterknife-compiler')現(xiàn)在被廢棄了

    第二步代碼調(diào)用,大家都懂不說了

public class SimpleActivity extends Activity {
 private Unbinder mUnbinder;
  @BindView(R.id.title) TextView title;
  @Override protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.simple_activity);
    mUnbinder= ButterKnife.bind(this);
    title.setText(butterKnife);
  }
  @Override
  protected void onDestroy() {
    super.onDestroy();
    if (mUnbinder != null) {
        mUnbinder.unbind();
      }
    }
}

apt整個(gè)流程:定義注解->注解處理器結(jié)合javapoet去反向生成.java文件->調(diào)用這些.java文件相關(guān)代碼綁定關(guān)系

實(shí)際應(yīng)用

好了最后咱們實(shí)際應(yīng)用下apt,看看項(xiàng)目中可以怎么使用,以自動生成Model層為例


示例項(xiàng)目結(jié)構(gòu)

1.首先新建一個(gè)javalib 定義需要的注解

/**
 * 自動生成ApiFactory工具類的注解
 */
@Retention(RetentionPolicy.SOURCE)
@Target(ElementType.TYPE)//作用在類上面
public @interface ApiFactory {
    String path();//獲取相關(guān)參數(shù)
}

2.再建立一個(gè)javalib用來處理第一步定義的注解,apt模塊

//添加依賴
dependencies {
    compile 'com.google.auto.service:auto-service:1.0-rc2'//自動生成META-INF/services/javax.annotation.processing.Processor 文件
    compile 'com.squareup:javapoet:1.4.0'//反向生成代碼的api庫
    implementation project(':library-jcannotation')//注解定義module
}
package com.jcgroup.jcapt;

import com.google.auto.service.AutoService;
import com.jcgroup.jcapt.processor.ApiFactoryProcessor;
import com.jcgroup.jcapt.processor.InstanceProcessor;

import java.util.Set;

import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.Filer;
import javax.annotation.processing.Messager;
import javax.annotation.processing.Processor;
import javax.annotation.processing.RoundEnvironment;
import javax.annotation.processing.SupportedAnnotationTypes;
import javax.annotation.processing.SupportedSourceVersion;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.TypeElement;
import javax.lang.model.util.Elements;

@AutoService(Processor.class)//自動生成相關(guān)目錄文件,簡化開發(fā)步驟
@SupportedSourceVersion(SourceVersion.RELEASE_8)//java版本支持
@SupportedAnnotationTypes({//標(biāo)注注解處理器支持的注解類型
        "com.jcgroup.jcannotation.apt.ApiFactory"
})
public class AnnotationProcessor extends AbstractProcessor {
    public Filer mFiler; //文件相關(guān)的輔助類
    public Elements mElements; //元素相關(guān)的輔助類
    public Messager mMessager; //日志相關(guān)的輔助類

    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        mFiler = processingEnv.getFiler();
        mElements = processingEnv.getElementUtils();
        mMessager = processingEnv.getMessager();
        new ApiFactoryProcessor().process(roundEnv, this);
        return true;
    }
}
package com.jcgroup.jcapt.processor;

import com.jcgroup.jcannotation.apt.ApiFactory;
import com.jcgroup.jcapt.AnnotationProcessor;
import com.jcgroup.jcapt.inter.IProcessor;
import com.jcgroup.jcapt.util.Utils;
import com.squareup.javapoet.ClassName;
import com.squareup.javapoet.CodeBlock;
import com.squareup.javapoet.JavaFile;
import com.squareup.javapoet.MethodSpec;
import com.squareup.javapoet.TypeName;
import com.squareup.javapoet.TypeSpec;

import java.io.IOException;

import javax.annotation.processing.FilerException;
import javax.annotation.processing.RoundEnvironment;
import javax.lang.model.element.Element;
import javax.lang.model.element.ExecutableElement;
import javax.lang.model.element.TypeElement;
import javax.lang.model.element.VariableElement;
import javax.lang.model.util.ElementFilter;
import javax.tools.Diagnostic;

import static com.squareup.javapoet.TypeSpec.classBuilder;
import static javax.lang.model.element.Modifier.FINAL;
import static javax.lang.model.element.Modifier.PUBLIC;
import static javax.lang.model.element.Modifier.STATIC;

/**
 * Created by luohai on 2018.6.5.
 */
public class ApiFactoryProcessor implements IProcessor {
    @Override
    public void process(RoundEnvironment roundEnv, AnnotationProcessor mAbstractProcessor) {
        String CLASS_NAME = "ApiFactory";
        String DATA_ARR_CLASS = "DataArr";
        String LIST_CLASS = "ArrayList";
        TypeSpec.Builder tb = classBuilder(CLASS_NAME).addModifiers(PUBLIC, FINAL).addJavadoc("@ API工廠 此類由apt自動生成");
        try {
            for (TypeElement element : ElementFilter.typesIn(roundEnv.getElementsAnnotatedWith(ApiFactory.class))) {
                mAbstractProcessor.mMessager.printMessage(Diagnostic.Kind.NOTE, "正在處理: " + element.toString());
                ApiFactory apiFactory=element.getAnnotation(ApiFactory.class);
                String path=apiFactory.path();
                for (Element e : element.getEnclosedElements()) {
                    ExecutableElement executableElement = (ExecutableElement) e;
                    MethodSpec.Builder methodBuilder =
                            MethodSpec.methodBuilder(e.getSimpleName().toString())
                                    .addJavadoc("@此方法由apt自動生成")
                                    .addModifiers(PUBLIC, STATIC);
                    if (TypeName.get(executableElement.getReturnType()).toString().contains(DATA_ARR_CLASS) || TypeName.get(executableElement.getReturnType()).toString().contains(LIST_CLASS)) {//返回列表數(shù)據(jù)
                        methodBuilder.returns(ClassName.get("io.reactivex", "Flowable"));
                    } else {
                        methodBuilder.returns(TypeName.get(executableElement.getReturnType()));
                    }
                    ClassName apiUtil = ClassName.get(path+".util", "ApiHelper");
                    CodeBlock.Builder blockBuilder = CodeBlock.builder();
                    String paramsString = "";
                    for (VariableElement ep : executableElement.getParameters()) {
                        methodBuilder.addParameter(TypeName.get(ep.asType()), ep.getSimpleName().toString());
                        if (ep.asType().toString().equals(ClassName.get(path+".base.entity", "RequestBean").toString())) {
                            blockBuilder.add("$L.getBaseInfo($L,isEncrypt)", apiUtil, ep.getSimpleName().toString());
                            paramsString += blockBuilder.build().toString() + ",";
                        } else {
                            paramsString += ep.getSimpleName().toString() + ",";
                        }
                    }
                    methodBuilder.addParameter(TypeName.BOOLEAN, "isEncrypt");
                    methodBuilder.addParameter(TypeName.BOOLEAN, "isDecode");
                    methodBuilder.addStatement(
                            "return  new $L().addResultCodeLogic($T.getInstance()" +
                                    ".service.$L($L)" +
                                    ".compose($T.io_main()),isDecode)", apiUtil
                            , ClassName.get(path+".api", "Api")
                            , e.getSimpleName().toString()
                            , paramsString.equals("") ? paramsString : paramsString.substring(0, paramsString.length() - 1)
                            , ClassName.get("com.jctv.tvhome.util.helper", "RxSchedulers"));
                    tb.addMethod(methodBuilder.build());
                }
            }
            JavaFile javaFile = JavaFile.builder(Utils.PackageName, tb.build()).build();// 生成源代碼
            javaFile.writeTo(mAbstractProcessor.mFiler);// 在 app module/build/generated/source/apt 生成一份源代碼
        } catch (FilerException e) {
        } catch (IOException e) {
            e.printStackTrace();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

3.使用注解處理器

    implementation project(':library-jcannotation')//注解定義模塊
    annotationProcessor project(':library-jcapt')//apt模塊

調(diào)用自動生成的Model層,這里沒有遵循MVP架構(gòu),演示直接在View層里面操作了Model層

public class SplashActivity extends AppCompatActivity {
    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        ApiFactory.method1(new RequestBean(),false,false).subscribe(testRequestResponseBean -> {},throwable -> {});
    }
}

看下這個(gè)自動生成的Model

/**
 * @ API工廠 此類由apt自動生成 */
public final class ApiFactory {
  /**
   * @此方法由apt自動生成 */
  public static Flowable<ResponseBean<TestRequest>> method1(RequestBean bean, boolean isEncrypt, boolean isDecode) {
    return  new com.jctv.tvhome.util.ApiHelper().addResultCodeLogic(Api.getInstance().service.method1(com.jctv.tvhome.util.ApiHelper.getBaseInfo(bean,isEncrypt)).compose(RxSchedulers.io_main()),isDecode);
  }
}

可以發(fā)現(xiàn)類似這部分的代碼省掉了
工程里面的Model層.png

最后做個(gè)小結(jié)吧,合理的利用apt可以省掉大量的模板代碼,大大的提高開發(fā)效率,而掌握apt編程的核心是熟悉javapoet庫的api。

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

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

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