一步步带你实现简版 ButterKnife

2018-12-09  本文已影响11人  028257ecd619

一、项目工程介绍

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

创建一个自定义注解类BindView

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.CLASS)
public @interface BindView {
    int value();
}

三、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));

这里就相当于构建了

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();
            }
        });
    }
}

查看生成的源文件

上一篇下一篇

猜你喜欢

热点阅读