美文网首页
一步步带你实现简版 ButterKnife

一步步带你实现简版 ButterKnife

作者: 028257ecd619 | 来源:发表于2018-12-09 19:50 被阅读11次

    一、项目工程介绍

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

    查看生成的源文件

    相关文章

      网友评论

          本文标题:一步步带你实现简版 ButterKnife

          本文链接:https://www.haomeiwen.com/subject/awmkhqtx.html