一步步带你实现简版 ButterKnife

一、项目工程介绍

  • lib-annotation 是一个 Java Library 模块,主要用于自定义注解;
  • lib-compiler 是一个 Java Library 模块,需要依赖 lib-annotation 模块,主要用于解析自定义注解与生成源文件。lib-compiler 还需要依赖 3 个开源库来帮助开发;
    • auto-common/auto-service:为注解处理器自动生成 metadata 文件并将注解处理器 jar 文件加入构建路径,不再需要我们手动创建并更新 META-INF/services/javax.annotation.processing.Processor 文件;
    • javapoet:一款 Java 代码生成框架,可以令我们省去繁琐冗杂的拼接代码的重复工作。
  • lib-inject 是一个 Android Library 模块,需要依赖 lib-annotation 模块,主要用于提供 Api 给 app 模块调用;
  • app 为应用模块,依赖 lib-compiler 与 lib-inject;

二、lib-annotation-自定义注解模块

创建一个自定义注解类BindView

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.CLASS)
public @interface BindView {
    int value();
}
复制代码
  • @Target(ElementType.FIELD)表示该注解修饰的是成员变量;
  • @Retention(RetentionPolicy.CLASS)表示该注解只会在编译时使用;
  • int value()为注解的值,这里应该传入的是一个控件 id;

三、lib-compiler-注解处理器模块

首先在build.gradle里添加依赖

dependencies {
    api project(':lib-annotation')
    
    implementation 'com.google.auto:auto-common:0.8'
    implementation 'com.google.auto.service:auto-service:1.0-rc3'
    
    implementation 'com.squareup:javapoet:1.9.0'
}
复制代码

然后创建一个类 BindViewProcessor,通过继承 AbstractProcessor 来自定义注解处理器,继承 AbstractProcessor 要实现一个抽象方法process()

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

这里我们先不理会这个方法,先做一些准备工作

第一步,我们需要注册 BindViewProcessor,之前我们已经添加了 auto-service 这个库,那么注册就是一个注解的事,使用@AutoService(Processor.class)

@AutoService(Processor.class)
public class BindViewProcessor extends AbstractProcessor { }
复制代码

第二步,我们需要声明支持的 Java 版本,这里有两种方式,一种是重写getSupportedSourceVersion(),一种是使用注解@SupportedSourceVersion()

// 重写方法
@Override
public SourceVersion getSupportedSourceVersion() {
	return SourceVersion.latestSupported();
}

// 使用注解
@SupportedSourceVersion(SourceVersion.RELEASE_8)
public class BindViewProcessor extends AbstractProcessor { }
复制代码

SourceVersion 是一个枚举类,可以使用SourceVersion.RELEASE_0SourceVersion.RELEASE_8表示各个 Java 版本,也可以直接使用SourceVersion.latestSupported()表示最新的版本

第三步,我们需要声明自定义注解处理器要处理哪些注解,同样的,这里也有两种方式,一种是重写getSupportedAnnotationTypes(),一种是使用注解@SupportedAnnotationTypes()

// 重写方法
@Override
public Set<String> getSupportedAnnotationTypes() {
    Set<String> set = new LinkedHashSet<>();
    set.add(BindView.class.getCanonicalName());
    return set;
}

// 使用注解-传入注解的全类名
@SupportedAnnotationTypes({"com.fancyluo.lib_annotation.BindView"})
public class BindViewProcessor extends AbstractProcessor { }
复制代码

第四步,我们需要重写init()方法来获取一些辅助类

// 解析 Elementm 的工具类,主要用于获取包名
private Elements mElementUtils;
// 主要用于输出 Java 源文件
private Filer mFiler;

@Override
public synchronized void init(ProcessingEnvironment processingEnvironment) {
    super.init(processingEnvironment);
    mElementUtils = processingEnvironment.getElementUtils();
    mFiler = processingEnvironment.getFiler();
}
复制代码

第五步,这里要重新拿起之前忽略的process()方法,这个方法是重中之重,我们要在这里面解析自定义注解和生成 Java 源文件。

先来看看我们要生成什么样的代码

public class MainActivity$$ViewBinder<T extends MainActivity> 
	implements ViewBinder<MainActivity> {
  @Override
  public void bind(final MainActivity target) {
    target.btnAction=(Button)target.findViewById(2131165218);
  }
}
复制代码

当我们使用BindView修饰程序元素的时候,我们的自定义注解处理器就可以拿到相应的程序元素的节点,通过解析节点,拿到相应的数据,然后自动的为这个程序元素所在的类生成一个辅助类,在里面为程序元素赋值。

也可以这么理解,我们会为使用BindView修饰的控件所在的 Activity 自动的生成一个辅助类,在里面进行控件的findViewById

接下来的代码都是在process()方法里,只是我将其分拆出来讲解

@Override
public boolean process(Set<? extends TypeElement> set, 
                       RoundEnvironment roundEnvironment) {
    ...//代码下面讲解
	return false;
}
复制代码

首先,我们通过 roundEnvironment 拿到所有的被BindView修饰的节点

Set<? extends Element> elements = 
    roundEnvironment.getElementsAnnotatedWith(BindView.class);
复制代码

这里可以理解为一个控件,只是被封装转换成了 Element

@BindView(R.id.btnAction)
Button btnAction;

转换成 -> Element
复制代码

然后遍历 elements 集合,解析数据,将我们需要的数据封装成一个类,并按照 TypeElement 来进行分组。TypeElement 可以理解为类节点,而 Element 是成员节点,再具体来说,TypeElement 就是 MainActivity,而 Element 就是其中的 btnAction;那么,按照 TypeElement 分组也就是将控件按照其所在的 Activity 进行分组。

首先创建我们需要的数据封装类BindViewInfo

public class FieldBinding {

    // 可以理解为:Button 这个类型
    private TypeMirror typeMirror;
    // 可以理解为:成员变量名-btnAction
    private String name;
    // 可以理解为:Button 的 id-R.id.btnAction
    private int resId;
    
    ...
    
}
复制代码

开始遍历集合,并且将节点数据封装到 BindViewInfo,并将其分组保存到 Map 集合

// Key 为类型节点,可以理解为 MainActivity
// Value 可以理解为 MainActivity 里面所有被 BindView 注解的成员变量信息
Map<TypeElement, List<BindViewInfo>> cacheMap = new HashMap<>();

// 遍历所有被 BindView 注解的成员变量,按照 Activity 进行分组
for (Element element : elements) {
    // 得到类型节点,可以理解为得到MainActivity
    TypeElement enclosingElement = (TypeElement) element.getEnclosingElement();
    // 从缓存中获取数据,如果没有,则新建并添加到缓存
    List<BindViewInfo> fieldList = cacheMap.get(enclosingElement);
    if (fieldList == null) {
        fieldList = new ArrayList<>();
        cacheMap.put(enclosingElement, fieldList);
    }
    // 封装被 BindView 注解的成员变量的信息
    // 成员变量的类类型,例如 Button
    TypeMirror typeMirror = element.asType();
    // 成员变量名 例如 btnAction
    String fieldName = element.getSimpleName().toString();
    // 控件资源Id 例如 R.id.btn
    int resId = element.getAnnotation(BindView.class).value();
    BindViewInfo bindViewInfo = new BindViewInfo(typeMirror, fieldName, resId);
    fieldList.add(fieldBinding);
}
复制代码

将数据分好组缓存后,我们就可以来构建我们需要的 Java 源文件的代码了,前面说过,TypeElement 代表着一个Activity,而 List<BindViewInfo>就代表着里面使用 BindView 注解修饰的控件,我们要为 Activity 生成一个辅助类,在里面为这些控件生成 findViewById 代码

首先,我们遍历 cacheMap,并解析我们需要的数据

for (Map.Entry<TypeElement, List<FieldBinding>> entry : cacheMap.entrySet()) {
    List<FieldBinding> bindingList = entry.getValue();
    // 如果该Activity没有被BindView注解的成员变量,则执行下一个
    if (bindingList == null || bindingList.size() == 0) {
        continue;
    }
    // 获取类型节点 例如 MainActivity
    TypeElement typeElement = entry.getKey();
    // 获取包名 例如 com.fancyluo.k_butterknife
    String packageName = getPackageName(typeElement);
    // 获取类名 例如 MainActivity
    String classNameStr = getClassName(packageName, typeElement);
    ClassName classNamePackage = ClassName.bestGuess(classNameStr);
    // 获取ViewBinder
    ClassName viewBinder = ClassName.get("com.fancyluo.lib_inject", "ViewBinder");
    
    ...//代码下面讲解
}
复制代码

getPackageName(typeElement)

private String getPackageName(TypeElement enClosingElement) {
  	// 获取包节点
  	PackageElement packageElement = mElementUtils.getPackageOf(typeElement);
    //返回的是 com.fancyluo.k_butterknife
  	return packageElement.getQualifiedName().toString();
}
复制代码

getClassName(packageName, typeElement)

// 例如 com.fancyluo.k_butterknife.MainActivity
String qualifiedName = typeElement.getQualifiedName().toString();
// 例如 com.fancyluo.k_butterknife.
int length = packageName.length() + 1;
// 如果当前的TypeElement是内部类的话,裁剪掉包名和后面的点号,并将之后的点号替换为$
return qualifiedName.substring(length).replace(".", "$");
复制代码

ViewBinder是在lib_inject模块里定义的一个接口,我们生成的辅助类需要实现这个接口并且实现接口的bind()方法进行控件的findViewById

拿到我们需要的数据以后,就可以开始使用 javapoet 提供的 api 来构建 Java 源代码,下面,我们再来贴一下我们要生成的代码,然后我们会一步一步来构建这些代码。

public class MainActivity$$ViewBinder<T extends MainActivity> 
	implements ViewBinder<MainActivity> {
  @Override
  public void bind(final MainActivity target) {
    target.btnAction=(Button)target.findViewById(2131165218);
  }
}
复制代码

首先,我们要构建

TypeSpec.Builder typeBuilder = TypeSpec.classBuilder(classNameStr + "$$ViewBinder")
    .addModifiers(Modifier.PUBLIC)
    .addTypeVariable(TypeVariableName.get("T", classNamePackage))
    .addSuperinterface(ParameterizedTypeName.get(viewBinder, classNamePackage));
复制代码
  • classBuilder 里传入的是类名
  • addModifiers 是设置类的访问属性
  • addTypeVariable 是设置类的泛型参数,传入一个 TypeVariableName,TypeVariableName 第一个参数为泛型参数名,第二个参数为 ClassName,例如 T extends MainActivity
  • addSuperinterface 是设置当前类实现的接口,传入一个 ParameterizedTypeName,ParameterizedTypeName 第一个参数为父接口的 ClassName,第二个参数 ClassName,例如 ViewBinder<MainActivity>

这里就相当于构建了

public class MainActivity$$ViewBinder<T extends MainActivity> 
	implements ViewBinder<MainActivity> {
}
复制代码

第二,我们要构建方法

MethodSpec.Builder methodBuilder = MethodSpec.methodBuilder("bind")//方法名
    .addAnnotation(Override.class)//添加注解
    .addModifiers(Modifier.PUBLIC)//访问属性
    .returns(TypeName.VOID)// 返回值
    // 添加参数:1-ClassName 2-参数名  3-参数的访问权限
    .addParameter(classNamePackage, "target", Modifier.FINAL);
复制代码

构建完方法的基本元素后,现在的代码结构为

public class MainActivity$$ViewBinder<T extends MainActivity> 
	implements ViewBinder<MainActivity> {
	@Override
  	public void bind(final MainActivity target) {
  		...
  	}
}
复制代码

最后我们来构建方法里面的具体代码,也就是相应控件的 findViewById

for (BindViewInfo bindViewInfo : bindingList) {
    // 获取类型名称,例如 Button
    String packageNameStr = fieldBinding.getTypeMirror().toString();
    ClassName className = ClassName.bestGuess(packageNameStr);
    
    // $L/$T代表占位符,$L为基本类型  $T为类类型
    // 这里相当于生成了 target.btnAction=(Button)target.findViewById(2131165218);
    methodBuilder.addStatement("target.$L=($T)target.findViewById($L)",
                               fieldBinding.getName(),
                               className,
                               fieldBinding.getResId());
}
复制代码

方法完全构建完成后,我们将其添加到类里面

typeBuilder.addMethod(methodBuilder.build());
复制代码

最后,我们通过 Filer 类来生成 Java 源文件

try {
    //生成Java文件,最终写是通过filer类写出的
    JavaFile.builder(packageName,result.build())
            .addFileComment("auto create make")
            .build()
            .writeTo(filer);
} catch (IOException e) {
    e.printStackTrace();
}
复制代码

四、lib-inject-核心 Api 模块

定义一个ViewBinder接口,之前说过,这个接口是给注解处理器自动生成的类来实现的,然后在其bind()方法里面实现 findViewById 代码

public interface ViewBinder<T> {
    void bind(T target);
}
复制代码

接下来,定义一个核心类,其中的静态方法bind()会传入要绑定的 Activity,通过这个 Activity 的类名在运行时反射获取到注解处理器生成的对应的辅助类,然后调用辅助类的bind方法完成控件的 findViewById

public class KButterKnife {

    public static void bind(Activity activity) {
        String className = activity.getClass().getName();
        try {
            Class<?> clazz = Class.forName(className+"$$ViewBinder");
            ViewBinder viewBinder = (ViewBinder) clazz.newInstance();
            viewBinder.bind(activity);
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (InstantiationException e) {
            e.printStackTrace();
        }
    }
    
}
复制代码

五、App-应用层

最后来测试使用一下,首先, 要依赖 lib-compiler 模块与 lib-inject 模块

implementation project(':lib-inject')
// lib-compiler 为注解处理器
annotationProcessor project(':lib-compiler')
复制代码

然后在 Activity 里面使用

public class MainActivity extends AppCompatActivity {
    @BindView(R.id.btnAction)
    Button btnAction;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        
        KButterKnife.bind(this);
        
        btnAction.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Toast.makeText(MainActivity.this, 
                              "注入成功,哈哈哈", Toast.LENGTH_SHORT).show();
            }
        });
    }
}
复制代码

查看生成的源文件

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值