ButterKnife使用方式源码分析+手写一个IOC注解框架

概述

我在之前的一篇文章xUtils源码分析 + 手写一个IOC注解框架讲过IOC的概念,并自己实现过一个IOC注解框架。其实这也是根据xUtils框架的源码分析而写的,是基于注解+反射来实现事件注入的。我们知道xUtils就包含IOC事件注入这一块的功能。xUtils之后,ButterKnife火了,使用了非反射的方式实现注入。

这篇文章主要根据源码分析,讲述一下ButterKnife的原理。然后根据原理手动搭建一个注解框架。
ButterKnife官方源码:ButterKnife

一、基础使用及源码分析

基础使用
我们从使用方式作为切入点,来窥探一下源码。
依赖:

implementation 'com.jakewharton:butterknife:10.2.3'
annotationProcessor 'com.jakewharton:butterknife-compiler:10.2.3'

使用:

public class MainActivity extends AppCompatActivity {
    private ActivityMainBinding binding;
    private Unbinder unbinder;
    
    @BindView(R.id.text_home)
    TextView textViewOne;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        binding = ActivityMainBinding.inflate(getLayoutInflater());
        setContentView(binding.getRoot());
        
        unbinder = ButterKnife.bind(this);
        textViewOne.setText("碧云天");
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        unbinder.unbind();
    }
}

源码分析:
框架的使用就不讲解了,下面我们直切主题,分析一下源码。看一下 ButterKnife.bind(this); 点进去看一下这个bind() 方法做了什么:

 @NonNull @UiThread
  public static Unbinder bind(@NonNull Object target, @NonNull View source) {
    Class targetClass = target.getClass();
    if (debug) Log.d(TAG, "Looking up binding for " + targetClass.getName());
   // 注释 1
    Constructor constructor = findBindingConstructorForClass(targetClass);
..........
  }

上面代码我们看到了反射。这里是一个重载方法,注释1 的地方也使用了方法嵌套。所以为了更好地分析这个方法干了啥,我将方法的主要部分整理了一下:

 @NonNull @UiThread
  public static Unbinder bind(@NonNull Object target, @NonNull View source) {
     // 利用反射生成帮助类的对象,框架唯一用到反射的地方
        View sourceView = target.getWindow().getDecorView();
        Class targetClass = target.getClass();
        String clsName = targetClass.getName();
        Unbinder unbinder = Unbinder.EMPTY;
        try {
            // 注释 2, 生成类名
            Class bindingClass = targetClass.getClassLoader().loadClass(clsName + "_ViewBinding");
            // 获取构造器
            Constructor constructor = (Constructor) bindingClass.getConstructor(targetClass);
            // 生成对象
            unbinder = constructor.newInstance(target);
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (InstantiationException e) {
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            e.printStackTrace();
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }
        return unbinder;
  }

好了,我们看到上面整理之后的代码。上面也加了部分注释,整个方法的功能就是使用了反射的方式来创建一个对象。那么创建的是什么对象呢?其实也不难发现,我们在MainActivity里传进来的是 this。所以上面注释 2的地方加上后缀之后,要创建对象的类名应该是:MainActivity_ViewBinding。所以总结一下:ButterKnife.bind(this);这个操作就做了一件事,就是用反射创建一个MainActivity_ViewBinding 对象并返回。

那么 MainActivity_ViewBinding 这个类又在哪,从哪来的呢?其实如果我们经常有用这个框架,或者细心一点可能会发现,在每次编译成功之后,框架都会自动为我们生成这样一个类。目录在:build/generrated/ap_generrated_sources/debug/out 下面,我们找到看一下这个类:

public class MainActivity_ViewBinding implements Unbinder {
  private MainActivity target;
  @UiThread
  public MainActivity_ViewBinding(MainActivity target) {
    this(target, target.getWindow().getDecorView());
  }
  @UiThread
  public MainActivity_ViewBinding(MainActivity target, View source) {
    this.target = target;
    // 注释 3, 将 MainActivity 中注解标注的 TextView进行 findViewById
    target.textViewOne = Utils.findRequiredViewAsType(source, R.id.text_home, "field 'textViewOne'", TextView.class);
  }
  @Override
  @CallSuper
  public void unbind() {
    MainActivity target = this.target;
    if (target == null) throw new IllegalStateException("Bindings already cleared.");
    this.target = null;
    target.textViewOne = null;
  }
}

从上面代码 我们可以了解到 MainActivity_ViewBinding这个类的作用了。这个类并不是一个Activity,它在上面注释 3处对我们的在 MainActivity用注解标注的 TextView 进行 View的注入。findRequiredViewAsType 这个方法最终用的 View的findViewById方法来绑定控件的,可以点进去看一下,这里不多说。

那么,MainActivity_ViewBinding 这个类是怎么生成的呢?又是什么时候生成的呢?这是我们接下来要研究的问题。

APT注解处理器
我们要了解ButterKnife是怎么给我们生成 java类的,首先要去了解一个东西,APT。
APT(Annotation Processing Tool),翻译过来就是注解处理器。它是 java用来处理注解的一个工具,它可以帮助我们在编译时扫描和处理注解。现在我们应该大概知道,为什么 ButterKnife使用的是编译时注解了。

在代码编译的时候,ButterKnife通过 APT获得了注解的信息,然后通过代码来帮我们生成上面的 MainActivity_ViewBinding类。那么,怎么用代码生成 java类呢?这个应该属于Java mirror 板块的知识,一般比较方便的做法是使用 javapoet提供的工具来生成java类。好,话不多说,下面我们将分析怎么搭建起这个注解框架。

二、手写一个注解框架

首先我们来画一个框架简图,来分析一下:


ButterKnife.png

话不多说,看上图。我们将在项目里建三个Module,一个Android Library和两个Java Library,三个库的依赖关系上图已标注。

注解库

首先我们要创建的第一个Java Library就是注解库,它用于定义框架所使用到的全部注解。像 ButterKnife就有很多注解,例如 @BindView()、@BindImage()、@OnClick()等等。这里我们就仿照创建一个 @BindView()作为例子,其他的同理:

@Target(ElementType.FIELD)
// 一定要编译时注解
@Retention(RetentionPolicy.CLASS)
public @interface BindView {
    int value();
}

看到上面,一定要定义编译时注解。这块简单,不多说。

注解处理器Processor

第二个Module我们要创建一个注解处理器的 Java Library。我们要使用到 AbstractProcessor这个Java抽象类。注解处理的 Library必须是 Java Library,这样才能继承 AbstractProcessor。

Android在编译时给我们开了一个口子,那就是注解 @AutoService(Processor.class)的使用。我们在这个新建的 Java Library里创建一个类继承 AbstractProcessor并实现process以及初始化方法,然后加上 Google提供的 @AutoService(Processor.class)注解,在工程编译时,我们就可以在 process方法接收到注解的扫描结果。更具体一点地说,哪些Activity或者Fragment的哪些属性或方法标注了我们框架自定义的注解,这些注解和属性以及方法和类的信息我们都可以在注解处理器里拿到。拿到信息之后我们就可以用代码生成带 _ViewBinding的 Java类了。

首先我们添加以下这个 Library的依赖,作用见下面注释:

implementation 'com.squareup:javapoet:1.13.0'//JavaPoet 用于编写 Java文件
implementation 'com.google.auto.service:auto-service:1.0-rc7'
// 用于注册注册Processor,@AutoService(Processor.class),一定要用 annotationProcessor 
annotationProcessor 'com.google.auto.service:auto-service:1.0-rc7'
implementation project(path: ':annotations') // 自定义注解库

下面是注解处理器的类,也是这个 Library里唯一的一个类:

// AutoService 这个注解一定要加上
@AutoService(Processor.class)
public class ButterKnifeProcessor extends AbstractProcessor {
    private Filer mFiler;//文件类
    @Override
    public boolean process(Set set, RoundEnvironment roundEnvironment) {
        Set bindViewElements = roundEnvironment.getElementsAnnotatedWith(BindView.class);
        // 解析 Element
        Map> typeElementMap = new LinkedHashMap<>();
        for (Element bindViewElement : bindViewElements) {
            Element enclosingElement = bindViewElement.getEnclosingElement();
            // 变量名
            System.out.println("--------->bindViewElement = " + bindViewElement.toString());
            // 完整类名
            System.out.println("--------->enclosingElement = " + enclosingElement.toString());
            // 将 View 按所在的类进行分类保存
            List viewElements = typeElementMap.get(enclosingElement);
            if (viewElements == null) {
                viewElements = new ArrayList<>();
                typeElementMap.put(enclosingElement, viewElements);
            }
            viewElements.add(bindViewElement);
        }
        // 循环构建所有帮助类
        for (Map.Entry> typeMap : typeElementMap.entrySet()) {
            // 完整类名
            Element typElement = typeMap.getKey();
            // 拿View
            List views = typeMap.getValue();
            // 创建一个 java 帮助类
            ClassName unbinderClassName = ClassName.get("com.butterknife", "Unbinder");
            TypeSpec.Builder typeBuilder = TypeSpec.classBuilder(typElement.getSimpleName() + "_ViewBinding")
                    .addModifiers(Modifier.PUBLIC, Modifier.FINAL)
                    .addSuperinterface(unbinderClassName);
            // 写构造器
            String classNameStr = typElement.getSimpleName().toString();
            ClassName uiThreadClassName = ClassName.get("androidx.annotation", "UiThread");
            ClassName parameterClassName = ClassName.bestGuess(classNameStr);
            MethodSpec.Builder constructorBuilder = MethodSpec.constructorBuilder()
                    .addAnnotation(uiThreadClassName)
                    .addModifiers(Modifier.PUBLIC)
                    .addParameter(parameterClassName, "target");
            // 组装unbind 方法
            MethodSpec.Builder unbindMethodBuilder = MethodSpec.methodBuilder("unBind")
                    .addAnnotation(Override.class)
                    .addModifiers(Modifier.PUBLIC)
                    .returns(TypeName.VOID);
            // 写所有 View 的 findById
            for (Element bindViewElement : views) {
                String filedName = bindViewElement.getSimpleName().toString();
                System.out.println("-------------> getClass = " + bindViewElement.asType().toString());
                int resId = bindViewElement.getAnnotation(BindView.class).value();
                constructorBuilder.addStatement("target.$L = target.findViewById($L)",
                        filedName, resId);
            }
            // 往类里添加方法
            typeBuilder.addMethod(constructorBuilder.build());
            typeBuilder.addMethod(unbindMethodBuilder.build());
            // 获取包名
            String packageName = ((PackageElement) typElement.getEnclosingElement()).getQualifiedName().toString();
            System.out.println("-------------> packageName = " + packageName);
            // 构建 java 文件
            JavaFile javaFile = JavaFile.builder(packageName, typeBuilder.build())
                    .addFileComment("ButterKnife add class")
                    .build();
            try {
                // 写入生成 java 类
                javaFile.writeTo(mFiler);
            } catch (IOException e) {
                e.printStackTrace();
                System.out.println("翻车了");
            }
        }
        return false;
    }
    @Override
    public synchronized void init(ProcessingEnvironment processingEnv) {
        super.init(processingEnv);
        mFiler = processingEnv.getFiler();
    }
    /**
     * 指定注解类型
     * 
     * @return 
     */
    @Override
    public Set getSupportedAnnotationTypes() {
        Set types = new LinkedHashSet<>();
        for (Class annotation : getSupportedAnnotations()) {
            types.add(annotation.getCanonicalName());
        }
        return types;
    }
    /**
     * 收集注解类型
     *
     * @return
     */
    private Set> getSupportedAnnotations() {
        Set> annotations = new LinkedHashSet<>();
        annotations.add(BindView.class);
        return annotations;
    }
    /**
     * 指定版本
     * 
     * @return 
     */
    @Override
    public SourceVersion getSupportedSourceVersion() {
        return SourceVersion.latestSupported();
    }
}

上面没几行代码,相信不难看懂。主要的逻辑在 process() 方法里,这个方法在工程编译注解扫描之后回调,然后携带我们标注的自定义注解的所有信息。然后根据这些信息,我们再用JavaPoet工具生成相应的带有 _ViewBinding后缀的 Java文件。

ButterKnife.bind()事件绑定

这个bind()方法在上面源码分析的地方我们已经讲过了。它主要是根据类名使用了反射生成 xxx_ViewBinding 对象,来实现 View的绑定。这里 ButterKnife类我们放在新建的 Android Library里:

public final class MyButterKnife {
    private MyButterKnife() {
        throw new AssertionError("No instances.");
    }
    private static final String TAG = "ButterKnife";
    private static boolean DEBUG_MODE = true;
    @UiThread
    public static Unbinder bind(@NonNull Activity target) {
        // 利用反射生成帮助类的对象,框架唯一用到反射的地方
        View sourceView = target.getWindow().getDecorView();
        Class targetClass = target.getClass();
        String clsName = targetClass.getName();
        Unbinder unbinder = Unbinder.EMPTY;
        if (DEBUG_MODE) Log.d(TAG, "clsName = " + clsName);
        try {
            // 生成类名
            Class bindingClass = targetClass.getClassLoader().loadClass(clsName + "_ViewBinding");
            // 获取构造器
            Constructor constructor = (Constructor) bindingClass.getConstructor(targetClass);
            // 生成对象
            unbinder = constructor.newInstance(target);
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (InstantiationException e) {
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            e.printStackTrace();
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }
        return unbinder;
    }
    public static void setDebugMode(boolean debugMode) {
        DEBUG_MODE = debugMode;
    }
}
public interface Unbinder {
    @UiThread
    void unBind();

    Unbinder EMPTY = () -> { };
}

框架绑定事件的源码上面已经分析过,这不再讲解。当然,这里只是仿照了ButterKnife,搭建了基础框架。看过ButterKnife源码的话会知道,一个优秀的框架会有很多细节的处理,好的东西需要时间和精力的打磨。我们要手写一个并不是说要重复造轮子,而是在研究的同时,体会优秀框架的设计思想。
Demo地址:ButterKnife Demo

你可能感兴趣的:(ButterKnife使用方式源码分析+手写一个IOC注解框架)