Android--仿ButterKnife自动绑定View实例以及点击事件之二(主要利用APT、JavaPoet技术)

前言

本文是前一篇的延续,阅读需要提前了解JAVA的APT技术、第三方代码生成框架JavaPoet。

Android--仿ButterKnife自动绑定View实例以及点击事件之一(主要利用反射技术)

 

下面有几篇写的很不错参考文章,本文也是参考这些文章或官网:

https://www.jianshu.com/p/b5be6b896a1a?utm_campaign=haruki&utm_content=note&utm_medium=seo_notes&utm_source=recommendation

https://www.jianshu.com/p/89ac9a2513c4

https://www.jianshu.com/p/acbb293722bc

JavaPoet官网:

https://github.com/square/javapoet#readme

 

一、 项目结构:

本文的项目结构也是采用三个module:

Fake-butterknife-annotation 模块:主要是负责注解的定义。模块类型是Java Library

Fake-butterknife-compiler 模块:主要是负责在编译器找到并处理注解,通过使用JavaPoet框架技术生成代理类。模块类型是Java Library

Fake-butterknife-library 模块:主要是负责通过约定的代理类命名找到compiler模块生成的代理类,利用反射实现View的初始化和添加点击事件。模块类型是Android Library

你没看错,最后还是离不开反射的技术,只是这种方式会比第一篇用的相对少很多,对效率的影响可以说很低。因为主要的处理逻辑注解查找和解析以及代理类源文件的创建都是在编译期做的。

Fake-butterknife-compiler 模块的build.gradle:

apply plugin: 'java-library'

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

    api group: 'com.google.auto.service', name: 'auto-service', version: '1.0-rc7'
    // Android Plugin for Gradle >= 3.4 或者 Gradle Version >=5.0 都要在自己的annotation processor工程里面增加如下的语句
    // 不然不会自动创建/main/META-INF/services/javax.annotation.processing.Processor
    annotationProcessor 'com.google.auto.service:auto-service:1.0-rc7'

    // javapoet:用于生成java源文件
    implementation group: 'com.squareup', name: 'javapoet', version: '1.13.0'

    implementation project(path: ':Fake-butterknife-annotation')

}

sourceCompatibility = "1.8"
targetCompatibility = "1.8"

另外两个模块的build.gradle不需要额外的配置。

 

二、Fake-butterknife-annotation 模块

package com.log.annotation;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Retention(RetentionPolicy.SOURCE) // 注解只保留在源文件,当Java文件编译成class文件的时候,注解被遗弃
@Target(ElementType.FIELD)
public @interface BindView {

    /**
     * 传入View的id
     * @return
     */
    int value();

}
package com.log.annotation;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Retention(RetentionPolicy.SOURCE) // 注解只保留在源文件,当Java文件编译成class文件的时候,注解被遗弃
@Target(ElementType.METHOD)
public @interface OnClick {

    /**
     * 传入View的id
     * @return
     */
    int[] value() default -1;

}

【注意】这里定义的两个注解的生命周期跟第一篇的不一样是:@Retention(RetentionPolicy.SOURCE)

 

三、Fake-butterknife-compiler 模块

package com.log.fake.butterknife.compiler;

import com.squareup.javapoet.ClassName;
import com.squareup.javapoet.MethodSpec;
import com.squareup.javapoet.ParameterizedTypeName;
import com.squareup.javapoet.TypeSpec;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javax.lang.model.element.ExecutableElement;
import javax.lang.model.element.Modifier;
import javax.lang.model.element.TypeElement;
import javax.lang.model.element.VariableElement;

public class ProxyInfo {

    // 源文件第一行的类的包名。如:package com.xx.xx
    public String packageName;

    // 添加了相应注解的成员变量或方法所在的类元素
    public TypeElement typeElement;

    // 一个类里面可能使用多个@BindView来注解View,这里把它们放到同一个集合里
    public Map<Integer, VariableElement> viewVariableElementMap;

    // 一个类里面可能使用了多个@OnClick注解多个方法
    public List<OnClickElement> onClickElementList;

    public ProxyInfo(String packageName, TypeElement typeElement) {
        this.packageName = packageName;
        this.typeElement = typeElement;
        this.viewVariableElementMap = new HashMap<>();
        this.onClickElementList = new ArrayList<>();
    }

    // 构造类名
    private String makeClassName(String className) {
        return className + "_ResourceBinding";
    }

    /**
     * 生成代理类
     *
     * @return
     */
    public TypeSpec buildProxyClass() {
        // 需要的实现接口类元素,程序在运行时根据类名通过反射实例化出本方法创建出来的类,并调用统一的接口完成对View的初始化或事件添加
        ClassName resourceBindClassName = ClassName.get("com.log.library", "IResourceBind");
        // 添加了注解的类的类元素
        ClassName targetClassName = ClassName.get(typeElement);
        // 泛型类型元素。该接口使用了泛型,需要根据传入的Activity或者Fragement确定具体的类型
        // 如:public Xxx implements IResourceBind<MainActivity>
        ParameterizedTypeName parameterizedTypeName = ParameterizedTypeName.get(resourceBindClassName, targetClassName);

        // 实现接口的方法元素
        MethodSpec.Builder bindMethodBuilder = MethodSpec.methodBuilder("bind") // 方法名
                .addModifiers(Modifier.PUBLIC) // 方法修饰符
                .addAnnotation(Override.class)
                .returns(void.class) // 方法返回值
                // addParameter()方法的参数1是参数类型元素,参数2是参数名
                .addParameter(targetClassName, "target") // 方法参数
                .addParameter(Object.class, "viewContainer");

        // 接口方法实体
        // 处理@BindView
        buildBindView(bindMethodBuilder);

        // 处理@OnClick
        buildOnClick(bindMethodBuilder);

        // 定义需要创建的类元素
        String className = makeClassName(typeElement.getSimpleName().toString());
        TypeSpec resourceBindClass = TypeSpec.classBuilder(className) // 简单类名,不包含包名
                .addModifiers(Modifier.PUBLIC)
                .addSuperinterface(parameterizedTypeName) // 实现接口
                .addMethod(bindMethodBuilder.build())  // 添加接口实现方法
                .build();

        return resourceBindClass;
    }

    private void buildBindView(MethodSpec.Builder bindMethodBuilder) {
        for (Integer viewId : viewVariableElementMap.keySet()) {
            VariableElement variableElement = viewVariableElementMap.get(viewId);
            String fieldName = variableElement.getSimpleName().toString();
            // 【注意】被注解的成员不能是private的,因为赋值是通过:对象实例.成员变量名,
            // 这也是生成的类的包名跟目标类的需要一样的原因
            bindMethodBuilder.beginControlFlow("if(target instanceof android.app.Activity)")
                    // 如果是Activity
                    .addStatement("target.$L = ((android.app.Activity)viewContainer).findViewById($L)", fieldName, viewId)
                    .nextControlFlow("else")
                    // 如果是View
                    .addStatement("target.$L = ((android.view.View)viewContainer).findViewById($L)", fieldName, viewId)
                    .endControlFlow();
        }
    }

    private void buildOnClick(MethodSpec.Builder bindMethodBuilder) {
        for (OnClickElement onClickElement : onClickElementList) {
            // 被代理调用的实际处理onClick方法
            ExecutableElement executableElement = onClickElement.executableElement;

//            // 定义类型为java.lang.reflect.InvocationHandler的匿名内部类
//            TypeSpec.Builder anonymousInvocationHandlerBuilder = TypeSpec.anonymousClassBuilder("");
//            anonymousInvocationHandlerBuilder.addSuperinterface(ParameterizedTypeName.get(InvocationHandler.class))
//                    .addMethod(MethodSpec.methodBuilder("invoke")
//                            .addModifiers(Modifier.PUBLIC)
//                            .addAnnotation(Override.class)
//                            .returns(Object.class)
//                            .addParameter(Object.class, "proxy")
//                            .addParameter(Method.class, "method")
//                            .addParameter(Object[].class, "args")
//                            .addException(Throwable.class)
//                            .addStatement("return $L.invoke(target, args)", executableElement)
//                            .build()
//                    );
//            TypeSpec anonymousInvocationHandler = anonymousInvocationHandlerBuilder.build();
//            bindMethodBuilder.addStatement("android.view.View.OnClickListener listener = (android.view.View.OnClickListener)$T.newProxyInstance(target.getClass().getClassLoader(), " +
//                    "new Class[]{android.view.View.OnClickListener.class}, $L)", Proxy.class, anonymousInvocationHandler);

            TypeSpec.Builder anonymousOnClickListenerBuilder = TypeSpec.anonymousClassBuilder("");
            anonymousOnClickListenerBuilder.addSuperinterface(ClassName.get("android.view.View", "OnClickListener"));
            // 实现OnClickListener的onClick()方法
            MethodSpec onClickMethod = MethodSpec.methodBuilder("onClick")
                    .addAnnotation(Override.class)
                    .addModifiers(Modifier.PUBLIC)
                    .returns(void.class)
                    .addParameter(ClassName.get("android.view", "View"), "v")
                    .addStatement("target.$L(v)", executableElement.getSimpleName())
                    .build();
            anonymousOnClickListenerBuilder.addMethod(onClickMethod);

            TypeSpec anonymousOnClickListener = anonymousOnClickListenerBuilder.build();
            bindMethodBuilder.addStatement("android.view.View.OnClickListener listener = $L", anonymousOnClickListener);

            bindMethodBuilder.addStatement("int[] resourceIds = new int[$L]", onClickElement.resourceIds.length);
            for (int i = 0; i < onClickElement.resourceIds.length; i++) {
                bindMethodBuilder.addStatement("resourceIds[$L] = $L", i, onClickElement.resourceIds[i]);
            }

            bindMethodBuilder.beginControlFlow("for (int viewId : resourceIds)")
                    .addStatement("android.view.View view = null")
                    .beginControlFlow("if(target instanceof android.app.Activity)")
                    .addStatement("view = ((android.app.Activity) target).findViewById(viewId)")
                    .nextControlFlow("else")
                    .addStatement("view = ((android.view.View) viewContainer).findViewById(viewId)")
                    .endControlFlow()
                    .addStatement("view.setOnClickListener(listener)")
                    .endControlFlow();
        }
    }

    protected static class OnClickElement {
        ExecutableElement executableElement;
        int[] resourceIds;

        public OnClickElement(ExecutableElement executableElement, int[] resourceIds) {
            this.executableElement = executableElement;
            this.resourceIds = resourceIds;
        }
    }

}
package com.log.fake.butterknife.compiler;

import com.google.auto.service.AutoService;
import com.log.annotation.BindView;
import com.log.annotation.OnClick;
import com.squareup.javapoet.JavaFile;

import java.io.IOException;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Set;

import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.Filer;
import javax.annotation.processing.Messager;
import javax.annotation.processing.ProcessingEnvironment;
import javax.annotation.processing.Processor;
import javax.annotation.processing.RoundEnvironment;
import javax.lang.model.SourceVersion;
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.Elements;
import javax.tools.Diagnostic;

/**
 * ProcessingEnvironment:是一个注解处理工具的集合
 * Element:是一个接口,表示一个程序元素,它可以是包、类、方法或者一个变量,只在编译期存在。
 *
 * Element已知的子接口有:
 *
 * package com.kxtx.demo.factory;       //  PackageElement
 * public class Demo {                  //  TypeElement
 *  private int age;                    //  VariableElement
 *  public Demo() {}                    //  ExecuteableElement
 *
 *  public void draw(                   //  ExecuteableElement
 *      String s)                       //  VariableElement
 *  {
 *      System.out.println(s);
 *  }
 * }
 * <p>
 * PackageElement 表示一个包程序元素。提供对有关包及其成员的信息的访问。
 * ExecutableElement 表示某个类或接口的方法、构造方法或初始化程序(静态或实例),包括注释类型元素。
 * TypeElement 表示一个类或接口程序元素。提供对有关类型及其成员的信息的访问。注意,枚举类型是一种类,而注解类型是一种接口。
 * VariableElement 表示一个字段、enum 常量、方法或构造方法参数、局部变量或异常参数。
 */


// 自动在源码路径创建文件:/main/META-INF/services/javax.annotation.processing.Processor,
// 并把本类CompilerProcessor的全限定名写进到该文件中
@AutoService(Processor.class)
public class CompilerProcessor extends AbstractProcessor {

    private Messager messager;
    private Elements elementUtils;
    private Map<String, ProxyInfo> proxyInfos;
    private Filer filer; // 生成文件的工具类

    /**
     * 每一个注解处理器类都必须有一个空的构造函数。然而,这里有一个特殊的init()方法,
     * 它会被注解处理工具调用,并输入ProcessingEnviroment参数。
     * ProcessingEnviroment提供很多有用的工具类Elements, Types和Filer
     *
     * @param processingEnvironment
     */
    @Override
    public synchronized void init(ProcessingEnvironment processingEnvironment) {
        super.init(processingEnvironment);
        // 打印信息工具
        messager = processingEnvironment.getMessager();
        // 参数1传入:Diagnostic.Kind.ERROR会信息中断
        messager.printMessage(Diagnostic.Kind.NOTE, "研究ButterKnife原理====>");
        elementUtils = processingEnvironment.getElementUtils();
        filer = processingEnvironment.getFiler();

        proxyInfos = new LinkedHashMap<>();
    }

    /**
     * 用来指定你使用的Java版本。通常这里返回SourceVersion.latestSupported()。
     * 然而,如果你有足够的理由只支持Java 6的话,你也可以返回SourceVersion.RELEASE_6。我推荐你使用前者。
     * 在Java 7中,你也可以使用注解来代替getSupportedAnnotationTypes()和getSupportedSourceVersion(),像这样:
     *
     * @return
     * @SupportedAnnotationTypes(“com.starcor.annotation.RouterAnnotation”),其中括号内内容表示要处理的注解名称,要写全名。
     */
    @Override
    public SourceVersion getSupportedSourceVersion() {
        return SourceVersion.latestSupported();
    }

    /**
     * 这里你必须指定,这个注解处理器是注册给哪个注解的。注意,它的返回值是一个字符串的集合,
     * 包含本处理器想要处理的注解类型的合法全称。换句话说,你在这里定义你的注解处理器注册到哪些注解上。
     *
     * @return
     */
    @Override
    public Set<String> getSupportedAnnotationTypes() {
        Set<String> types = new LinkedHashSet<>();
        types.add(BindView.class.getCanonicalName());
        types.add(OnClick.class.getCanonicalName());
        return types;
    }

    /**
     * 这相当于每个处理器的主函数main()。扫描、评估和处理注解的代码,以及生成Java文件。
     * 输入参数RoundEnviroment,可以让你查询出包含特定注解的被注解元素。后面我们将看到详细的内容。
     *
     * @param annotations
     * @param roundEnvironment
     * @return
     */
    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnvironment) {
        // 下面的信息会显示在Android Studio:Gradle Console控制台或Build->Build Output中
        messager.printMessage(Diagnostic.Kind.NOTE, "处理中=======>>>>");

        // process()方法会被调用多次,这里只有第一次处理
        if (proxyInfos.size() > 0) {
            return true;
        }

        // 1、查找所有添加了@BindView注解的类元素并解析
        findAndParseBindView(roundEnvironment);

//        // 2、查找所有添加了@Onclick注解的类元素并解析
        findAndParseOnClick(roundEnvironment);

        // 3、生成注解处理代理类源文件
        for (ProxyInfo info : proxyInfos.values()) {
            JavaFile javaFile = JavaFile.builder(info.packageName, info.buildProxyClass()).build();
            // 写入到对应的.java文件
            try {
                javaFile.writeTo(filer);
            } catch (IOException e) {
                e.printStackTrace();
            }
        }

        return true;
    }

    private void findAndParseOnClick(RoundEnvironment env) {
        Set<? extends Element> elements = env.getElementsAnnotatedWith(OnClick.class);
        for (Element element : elements) {
            // @OnClick只能作用于方法
            ExecutableElement executableElement = (ExecutableElement) element;
            TypeElement typeElement = (TypeElement) executableElement.getEnclosingElement();
            // 获取全类名
            String className = typeElement.getQualifiedName().toString();
            // 获取被注解元素的包名,用于创建代理类时使用
            String packageName = elementUtils.getPackageOf(element).getQualifiedName().toString();
            int[] resourceIds = element.getAnnotation(OnClick.class).value();
            ProxyInfo proxyInfo = proxyInfos.get(className);
            if (proxyInfo == null) {
                proxyInfo = new ProxyInfo(packageName, typeElement);
                proxyInfos.put(className, proxyInfo);
            }
            // 一个被OnClick注解的方法可能对应多个id
            proxyInfo.onClickElementList.add(new ProxyInfo.OnClickElement(executableElement, resourceIds));
        }
    }

    private void findAndParseBindView(RoundEnvironment env) {
        Set<? extends Element> elements = env.getElementsAnnotatedWith(BindView.class);
        // 按类来划分注解元素,因为每个使用注解的类都会生成相应的代理类
        for (Element element : elements) {
            // 获取注解的成员变量
            // 强转是因为@BindView只能作用变量
            VariableElement variableElement = (VariableElement) element;
            // 获取该成员所在的对象的类元素
            TypeElement typeElement = (TypeElement) variableElement.getEnclosingElement();
            // 获取全类名
            String className = typeElement.getQualifiedName().toString();
            // 获取被注解元素的包名,用于创建代理类时使用
            String packageName = elementUtils.getPackageOf(element).getQualifiedName().toString();
            // 获取注解的值
            int resourceId = element.getAnnotation(BindView.class).value();

            // 这里自定义的ProxyInfo包含一个类里所有添加@BindView注解的成员变量的信息
            ProxyInfo proxyInfo = proxyInfos.get(className);
            if (proxyInfo == null) {
                proxyInfo = new ProxyInfo(packageName, typeElement);
                proxyInfos.put(className, proxyInfo);
            }
            // 以View Id为key,成员变量元素为value,保存起来
            proxyInfo.viewVariableElementMap.put(resourceId, variableElement);
        }
    }


}

 

四、Fake-butterknife-library 模块

package com.log.library;

public interface IResourceBind<T> {

    void bind(T target, Object viewContainer);

}
package com.log.library;

public class FakeButterKnife {

    public static void bind(Object target) {
        if (target == null) {
            throw new IllegalArgumentException("Binding target is null!");
        }

        // 根据命名约定找到在编译期生成的IResourceBind接口实现类
        String targetClassName = target.getClass().getName() + "_ResourceBinding";
        try {
            Class<?> proxyClass = Class.forName(targetClassName);
            IResourceBind resourceBind = (IResourceBind) proxyClass.newInstance();
            resourceBind.bind(target, target);
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (InstantiationException e) {
            e.printStackTrace();
        }

    }

}

 

五、测试Activity

package com.log.basedemo.annoationProcessor;

import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;
import android.widget.Toast;

import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;

import com.log.annotation.BindView;
import com.log.annotation.OnClick;
import com.log.basedemo.R;
import com.log.library.FakeButterKnife;

/**
 * 分别测试:对View进行自动绑定初始化(findViewById),以及添加OnclickListener事件处理的两种不同时期实现:
 * 1、运行期
 * 2、编译期(ButterKnife原理)
 */
public class TestActivity1 extends AppCompatActivity {

    @BindView(R.id.textView)
    TextView textView;
    @BindView(R.id.button)
    Button button;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.butterknife_demo);

        FakeButterKnife.bind(this);
    }

    @OnClick(R.id.button)
    public void handleClick(View view) {
        Toast.makeText(this, "Button:" + button.toString(), Toast.LENGTH_SHORT).show();
        textView.setText("我是由运行时注解初始化,并被动态代理设置的点击事件设置的!接收点击事件的View Id:" + view.getId());
    }

}

 

测试工程的build.gradle:

android {
    ......

    compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }

}

dependencies {
    implementation project(path: ':Fake-butterknife-annotation')
    implementation project(path: ':Fake-butterknife-library')
    // 自定义注解处理器
    annotationProcessor project(path: ':Fake-butterknife-compiler')
}

 

六、通过JavaPoet生成的类

具体代码为:

package com.log.basedemo.annoationProcessor;

import android.view.View;
import android.view.View.OnClickListener;
import com.log.library.IResourceBind;
import java.lang.Object;
import java.lang.Override;

public class TestActivity1_ResourceBinding implements IResourceBind<TestActivity1> {
  @Override
  public void bind(TestActivity1 target, Object viewContainer) {
    if(target instanceof android.app.Activity) {
      target.button = ((android.app.Activity)viewContainer).findViewById(2131230819);
    } else {
      target.button = ((android.view.View)viewContainer).findViewById(2131230819);
    }
    if(target instanceof android.app.Activity) {
      target.textView = ((android.app.Activity)viewContainer).findViewById(2131231044);
    } else {
      target.textView = ((android.view.View)viewContainer).findViewById(2131231044);
    }
    android.view.View.OnClickListener listener = new OnClickListener() {
      @Override
      public void onClick(View v) {
        target.handleClick(v);
      }
    };
    int[] resourceIds = new int[1];
    resourceIds[0] = 2131230819;
    for (int viewId : resourceIds) {
      android.view.View view = null;
      if(target instanceof android.app.Activity) {
        view = ((android.app.Activity) target).findViewById(viewId);
      } else {
        view = ((android.view.View) viewContainer).findViewById(viewId);
      }
      view.setOnClickListener(listener);
    }
  }
}

七、效果图:

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值