阅读 90

探索Android开源框架 - 6. ButterKnife使用及源码解析

相关知识

  • ButterKnife中最重要的就是先自定义注解,再通过APT在编译期解析注解,解析器中又会用到反射,然后通过javapoet库来生成模板代码,

要想学习其源码,首先要基本的了解注解和反射的知识;

注解

  • Java 注解(Annotation)又称 Java 标注,是 JDK5.0 引入的一种注释机制;

  • Java中的类、方法、变量、参数和包等都可以被标注,Java 标注可以通过反射获取标注内容;

在编译器生成类文件时,标注可以被嵌入到字节码中,Java 虚拟机可以保留标注内容,在运行时可以获取到标注内容;

  • 注解以@注解名的形式存在于代码中,也支持自定义 Java 注解;

  • 注解相当于一种标记,在程序中加了注解就等于为程序打上了某种标记。程序可以利用ava的反射机制来了解你的类及各种元素上有无何种标记,针对不同的标记,就去做相应的事件。

内置注解

普通注解
  • 作用在代码的注解,在java.lang中

@Override
  • 检查该方法是否是重写方法。如果发现其父类,或者是引用的接口中并没有该方法时,会报编译错误。

@Deprecated
  • 标记表示过时的,不推荐使用。可以用于修饰方法,属性,类。如果使用被此注解修饰的方法,属性或类,会报编译警告。

@SuppressWarnings
  • 指示编译器去忽略注解中声明的警告

新增的普通注解
@SafeVarargs
  • Java7开始支持,忽略任何使用参数为泛型变量的方法或构造函数调用产生的警告;

@FunctionalInterface
  • Java8开始支持,标识一个匿名函数或函数式接口;

@Repeatable
  • Java8开始支持,标识某注解可以在同一个声明上使用多次;

元注解
  • 作用在其他注解的注解,在java.lang.annotation中

@Retention
  • 标识这个注解怎么保存,是只在代码中,还是编入class文件中,或者是在运行时可以通过反射访问

  • 其可选策略如下:

  1. RetentionPolicy.SOURCE: 注解仅存在于源码中,在class字节码文件中不包含;

  2. RetentionPolicy.CLASS: 默认的保留策略,注解会在class字节码文件中存在,但运行时无法获得;

  3. RetentionPolicy.RUNTIME: 注解会在class字节码文件中存在,在运行时可以通过反射获取到;

  • 如果自定义注解,只存在于源码或字节码中无法发挥作用,在运行期间能获取到注解才能实现我们目的,所以自定义注解中是使用 @Retention(RetentionPolicy.RUNTIME);

@Target
  • 标记这个注解作用的范围

  • 其可选类型如下

- ElementType.TYPE: 接口、类、枚举、注解
- ElementType.FIELD:属性字段、枚举的常量
- ElementType.METHOD:方法
- ElementType.PARAMETER: 方法参数
- ElementType.CONSTRUCTOR: 构造函数
- ElementType.LOCAL_VARIABLE: 局部变量
- ElementType.ANNOTATION_TYPE: 注解(@Retention注解中就使用该属性)
- ElementType.PACKAGE: 包
- ElementType.TYPE_PARAMETER:类型泛型,即泛型方法、泛型类、泛型接口 (jdk1.8加入)
- ElementType.TYPE_USE:类型使用.可以用于标注任意类型除了 class (jdk1.8加入)复制代码
@Documented
  • 标记这些注解是否包含在用户文档中

@Inherited
  • 如果一个类用上了@Inherited修饰的注解,那么其子类也会继承这个注解

自定义注解

  • 创建自定义注解和创建一个接口相似,但是注解的interface关键字需要以@符号开头,可以为注解声明方法,示例如下

//创建:
@Documented // 此注解包含在用户文档中
@Target(ElementType.METHOD)//此注解只能用在方法上
@Inherited//子类可以继承父类中的此注解
@Retention(RetentionPolicy.RUNTIME)// 此注解保存在运行时,可以通过反射访问
public @interface MyAnnotation {
    //注解方法不能带有参数,可以有默认值,
    //返回值类型限定为:基本类型、String、Enums、Annotation或者是这些类型的数组
    String name() ;//没有默认值,使用时需要显示赋值
    int age() default  -1;//有默认值,使用时可以不赋值
}

//使用:
@MyAnnotation(name = "ljy",age = 18)
private fun initView() {

}复制代码
实战:使用自定义注解+AOP实现打印方法耗时
  • 上面说了自定义注解如何定义,那么我们来结合实际业务需求写一个实战的例子:打印方法耗时,当然也可以用来做埋点统计

  1. 创建自定义注解

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface GetTime {
    String tag() default "";
}复制代码
  1. 通过AOP解析注解,我这里用的aspectj框架,关于AOP并不是本篇相关的技术就不做展开说明了;

@Around("execution(@GetTime * *(..))")
public void getTime(ProceedingJoinPoint joinPoint) throws Throwable {
    MethodSignature joinPointObject = (MethodSignature) joinPoint.getSignature();
    Method method = joinPointObject.getMethod();

    boolean flag = method.isAnnotationPresent(GetTime.class);
    LjyLogUtil.d("flag:"+flag);
    String tag = null;
    if (flag) {
        GetTime getTime = method.getAnnotation(GetTime.class);
        tag = getTime.tag();
    }
    if (TextUtils.isEmpty(tag)) {
        Signature signature = joinPoint.getSignature();
        tag = signature.toShortString();
    }
    long time = System.currentTimeMillis();
    try {
        joinPoint.proceed();
    } catch (Throwable throwable) {
        throwable.printStackTrace();
    }
    LjyLogUtil.d( tag+" get time: " + (System.currentTimeMillis() - time));
}复制代码
  1. 使用注解:给需要统计的方法添加注解,例如Application.onCreate方法里面经常会做许多初始化操作,我们可能很关系它的耗时过长,影响应用的启动时长

@GetTime(tag = "MyApplication.onCreate() 耗时")
@Override
public void onCreate() {
    super.onCreate();
    ProcessLifecycleOwner.get().getLifecycle().addObserver(new ApplicationLifecycleObserver());
    MMKV.initialize(this);
    GreenDaoHelper.getInstance().init(getApplicationContext());
    ARouter.init(MyApplication.this);
    // Application的onCreate方法中开启卡顿监控
    BlockCanary.install(this, new AppBlockCanaryContext()).start();
    //Application的onCreate方法中初始化ANR-WatchDog
    new ANRWatchDog().start();
    NetworkListener.getInstance().init(this);
}复制代码

反射

  • 在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法和属性;

  • 反射就是把java类中的各种成分映射成一个个的Java对象;

  • 反射有两个作用:1.反编译:.class->.java; 2.通过反射机制访问java对象中的属性,方法,构造器等;

  • 实现反射,实际上是得到Class对象

常用类

 java.lang.Class //类的创建
 java.lang.reflect.Constructor //反射构造方法
 java.lang.reflect.Field //反射属性
 java.lang.reflect.Method //反射方法
 java.lang.reflect.Modifier //访问修饰符的信息复制代码

获取类(Class)对象

  • 三种获取Class对象的方法

//1.会让ClassLoader装载类,并进行类的初始化
val c1 = Class.forName("com.ljy.demo.Person")
//2.会让ClassLoader装载类,不进行类的初始化操作
val c2 = Person::class.java
//3.在实例中获取,返回类对象运行时真正所指的对象
val c3 = Person("bob").javaClass复制代码

反射构造方法

  • 无参数创建对象

val person = c1.newInstance() as Person
LjyLogUtil.d(person.toString())
//new Person()是直接创建一个实列,同时完成类的装载和连接
//newInstance是使用类加载机制,可以灵活的创建类的实例,更换类的时候无需修改之前的代码复制代码
  • 有参数的创建对象: 使用Constructor

val constructor = c1.getConstructor(String::class.java)
val person2 = constructor.newInstance("Emily") as Person
LjyLogUtil.d(person2.toString())复制代码
  • 遍历构造器

for (it in c1.declaredConstructors) {
    LjyLogUtil.d("declaredConstructors.it:")
    for (i in it.parameterTypes) {
        LjyLogUtil.d("it.parameterTypes.i:${i.name}")
    }
}复制代码

反射属性

  • 获取属性(Field)

val fieldAge = c1.getDeclaredField("age")
val fieldName = c1.getDeclaredField("name")复制代码
  • 遍历属性

for (it in c1.declaredFields) {
    LjyLogUtil.d("declaredFields.it:${it.name}")
}复制代码
  • 修改属性

//取消封装,特别是取消私有字段的访问限制,并不是将方法的权限设置为public,而是取消java的权限控制检查,
// 所以即使是public方法,其isAccessible默认也是false
fieldAge.isAccessible = true
fieldName.isAccessible = true
fieldAge.set(person2, 18)
LjyLogUtil.d("fieldAge.name=${fieldAge.name}")复制代码

反射方法

  • 获取方法

val method = c1.getDeclaredMethod("setAge", Int::class.java)//获取类中的方法
method.invoke(person2, 22)//通过反射调用方法
LjyLogUtil.d(person2.toString())复制代码
  • 遍历方法

for (it in c1.declaredMethods) {
    LjyLogUtil.d("declaredMethods.it:${it.name}")
}复制代码
反射静态方法
val clz = Class.forName("com.ljy.publicdemo.util.LjyLogUtil")
val m = clz.getDeclaredMethod("d", CharSequence::class.java)
m.invoke(LjyLogUtil::class.java, "log.d")复制代码
反射泛型参数方法
//class Test<T> {
//    public void test(T t){
//        LjyLogUtil.d("Test.test(),t:"+t);
//    }
//}
val clz2 = Class.forName("com.ljy.publicdemo.activity.Test")
//注意这里有个泛型的基础--泛型擦除,编译器会自动类型向上转型,T向上转型是Object,所以下面第二个参数是Object.class
val tm = clz2.getDeclaredMethod("test", Object::class.java)
tm.isAccessible = true
tm.invoke(Test<Int>(), 666)复制代码

访问修饰符的信息

val modifierField = Modifier.toString(fieldName.modifiers)
val modifierMethod = Modifier.toString(method.modifiers)
LjyLogUtil.d("modifierField=$modifierField, modifierMethod=$modifierMethod")复制代码

实践:通过反射获取运行时注解

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface GetTime {
    String tag() default "";
}

public class Test {
    @GetTime(tag = "做啥子")
    private static void doSomething(String info) {
        //todo...
    }

    public static void main(String[] args) {
        Test test = new Test();
        GetTime getTime ;
        Class clazz = test.getClass();
        try {
            Method method = clazz.getMethod("doSomething", String.class);
            getTime = method.getAnnotation(GetTime.class);
            System.out.print("needLogin:"+getTime.tag());

        } catch (NoSuchMethodException e) {
            System.out.print("NoSuchMethod");

        }
    }
}复制代码

实践:通过反射越过泛型检查

  • 泛型用在编译期,编译过后泛型擦除(消失掉)。所以是可以通过反射越过泛型检查的

public static void main(String[] args) throws Exception{
    ArrayList<String> strList = new ArrayList<>();
    strList.add("aaa");
    strList.add("bbb");

//	strList.add(100);
    //获取ArrayList的Class对象,反向的调用add()方法,添加数据
    Class listClass = strList.getClass(); //得到 strList 对象的字节码 对象
    //获取add()方法
    Method m = listClass.getMethod("add", Object.class);
    //调用add()方法
    m.invoke(strList, 100);

    //遍历集合
    for(Object obj : strList){
        System.out.println(obj);
    }
}复制代码

实践:通过反射运行配置文件内容

  • 我们利用反射和配置文件,可以使:应用程序更新时,对源码无需进行任何修改,我们只需要将新类发送给客户端,并修改配置文件即可

//student类:
public class Student {
	public void show(){
		System.out.println("is show()");
	}
}
// 配置文件以txt文件为例子(pro.txt):
className = cm.ljy.Student
methodName = show
//测试类
public class Demo {
	public static void main(String[] args) throws Exception {
		//通过反射获取Class对象
		Class stuClass = Class.forName(getValue("className"));//"cn.fanshe.Student"
		//2获取show()方法
		Method m = stuClass.getMethod(getValue("methodName"));//show
		//3.调用show()方法
		m.invoke(stuClass.getConstructor().newInstance());

	}

	//此方法接收一个key,在配置文件中获取相应的value
	public static String getValue(String key) throws IOException{
		Properties pro = new Properties();//获取配置文件的对象
		FileReader in = new FileReader("pro.txt");//获取输入流
		pro.load(in);//将流加载到配置文件对象中
		in.close();
		return pro.getProperty(key);//返回根据key获取的value值
	}
}复制代码

实践:动态代理

  • java的反射机制提供了动态代理模式实现,代理模式的作用是为其他对象提供一种代理,以控制对这个对象的访问

interface Subject {
    fun doSomething()
}

class Test : Subject {
    override fun doSomething() {
        LjyLogUtil.d("Test.doSomething")
    }
}

class DynamicProxy(private val target: Subject) : InvocationHandler {
    override fun invoke(proxy: Any?, method: Method?, args: Array<out Any>?): Any? {
        LjyLogUtil.d("Proxy:${proxy?.javaClass?.name}")
        LjyLogUtil.d("before target")
        //Kotlin中数组转为可变长参数,通过前面加*符号
        val invoke = method!!.invoke(target, *(args ?: emptyArray()))
        LjyLogUtil.d("after target")
        return invoke
    }
}

//使用:
val test = Test()
val myProxy = DynamicProxy(test)
val subject: Subject =
    Proxy.newProxyInstance(
        test.javaClass.classLoader,
        test.javaClass.interfaces,
        myProxy
    ) as Subject
subject.doSomething()
LjyLogUtil.d("subject.className:" + subject.javaClass.name)复制代码

实践:动态创建fragment

//管理fragment标题及路径的类
public class PageConfig {
    public static List<String> pageTitles = new ArrayList<String>();

    public static List<String> getPageTitles(Context context) {
        pageTitles.clear();
        pageTitles.add("Fragment1");
        pageTitles.add("Fragment2");
        pageTitles.add("Fragment3");
        return pageTitles;
    }

    private static final String PATH_FRAGMENT1 = "com.ljy.publicdemo.activity.fragment.Fragment1";
    private static final String PATH_FRAGMENT2 = "com.ljy.publicdemo.activity.fragment.Fragment2";
    private static final String PATH_FRAGMENT3 = "com.ljy.publicdemo.activity.fragment.Fragment3";


    public static String[] fragmentNames = {
            PATH_FRAGMENT1,
            PATH_FRAGMENT2,
            PATH_FRAGMENT3,
    };
}

//通过反射遍历添加
class FragmentActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_frag)
        try {
            //遍历Fragment地址
            for (address in PageConfig.fragmentNames) {
                //反射获得Class
                val clazz = Class.forName(address)
                //创建类
                val fragment = clazz.newInstance() as Fragment
                //添加到viewPagerAdapter的资源
                supportFragmentManager
                        .beginTransaction()
                        .add(R.id.frame_layout, fragment)
                        .commit()
            }
        } catch (e: ClassNotFoundException) {
        } catch (e: IllegalAccessException) {
        } catch (e: InstantiationException) {
        }
    }
}复制代码

反射简化: jOOR

  • 当预期工程非常多的使用到反射时,我们需要更加简化的工具来优化开发流程,一个很棒的反射框架jOOR,非常轻量,让代码更加优雅

  • 项目地址 github.com/jOOQ/jOOR

//1. 添加依赖
implementation 'org.jooq:joor:0.9.13'
//2. 使用
val helloStr: String? = Reflect.onClass("java.lang.String")//类似于Class.forName
        .create("Hello jOOR")//调用类中构造方法
        .call("substring", 8)//调用类中方法
        .call("toString")
        .get()//获取包装好的对象
LjyLogUtil.d("helloStr=$helloStr")
//3. 也支持动态代理
Reflect.onClass(RealSubject::class.java).create().`as`(Subject::class.java).doSomething()复制代码

注意

  • 由于反射会额外消耗一定的系统资源,因此如果不需要动态地创建一个对象,那么就不需要用反射;

  • jvm无法对反射部分的代码进行优化,反射生成大量的临时对象,造成大量GC,导致UI卡顿;

  • 另外,反射调用方法时可以忽略权限检查,因此可能会破坏封装性而导致安全问题;

ButterKnife使用

  • ButterKnife使用还是非常简单的,示例如下

class ButterKnifeActivity : AppCompatActivity() {

    @BindView(R.id.et_name)
    lateinit var etName: EditText

    @BindView(R.id.et_pwd)
    lateinit var etPwd: EditText

    @BindString(R.string.login_error)
    lateinit var loginErrorMessage: String

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_butter_knife)
        ButterKnife.bind(this)
    }

    @OnClick(R.id.btn_submit)
    fun submit() {
        LjyLogUtil.d("etName=${etName.text}")
        LjyLogUtil.d("etPwd=${etPwd.text}")
        if ("ljy".equals(etName.text) && "123".equals(etPwd.text)) {
            LjyLogUtil.d("登录成功")
        } else {
            LjyLogUtil.d(loginErrorMessage)
        }
    }
}复制代码

ButterKnife源码解析

  • 主要有下面两步:

  1. 编译的时候通过APT扫描注解,并进行相应处理,通过 javapoet库生成Java代码;

  2. 调研ButterKnife.bind(this)方法时通过反射将id与相应的上下文绑定;

ButterKnifeProcessor类:通过APT扫描解析注解并生成代码

  • ButterKnife的APT注解解析类是ButterKnifeProcessor,继承了AbstractProcessor,我们先来看看他是如何实现的

init方法

  • 重写init方法初始化工具类, 注意该方法添加了synchronized关键字

 @Override public synchronized void init(ProcessingEnvironment env) {
    super.init(env);

    String sdk = env.getOptions().get(OPTION_SDK_INT);
    。。。
    debuggable = !"false".equals(env.getOptions().get(OPTION_DEBUGGABLE));
    typeUtils = env.getTypeUtils();
    filer = env.getFiler();
    。。。
  }复制代码

getSupportedAnnotationTypes方法

  • 该方法返回支持的注解类型,用于指定定义的注解

@Override public Set<String> getSupportedAnnotationTypes() {
    Set<String> types = new LinkedHashSet<>();
    for (Class<? extends Annotation> annotation : getSupportedAnnotations()) {
      types.add(annotation.getCanonicalName());
    }
    return types;
  }

  //指定定义的注解,注册了一系列的Bindxxx注解类和监听列表LISTENERS
  private Set<Class<? extends Annotation>> getSupportedAnnotations() {
    Set<Class<? extends Annotation>> annotations = new LinkedHashSet<>();

    annotations.add(BindAnim.class);
    annotations.add(BindArray.class);
    annotations.add(BindBitmap.class);
    annotations.add(BindBool.class);
    annotations.add(BindColor.class);
    annotations.add(BindDimen.class);
    annotations.add(BindDrawable.class);
    annotations.add(BindFloat.class);
    annotations.add(BindFont.class);
    annotations.add(BindInt.class);
    annotations.add(BindString.class);
    annotations.add(BindView.class);
    annotations.add(BindViews.class);
    annotations.addAll(LISTENERS);

    return annotations;
  }

  private static final List<Class<? extends Annotation>> LISTENERS = Arrays.asList(
        OnCheckedChanged.class, //
        OnClick.class, //
        OnEditorAction.class, //
        OnFocusChange.class, //
        OnItemClick.class, //
        OnItemLongClick.class, //
        OnItemSelected.class, //
        OnLongClick.class, //
        OnPageChange.class, //
        OnTextChanged.class, //
        OnTouch.class //
    );复制代码

process方法

  • 最核心的方法, 注解的处理和生成代码都是在这个方法中完成

@Override public boolean process(Set<? extends TypeElement> elements, RoundEnvironment env) {
    //查询并解析注解,保存到bindingMap中
    Map<TypeElement, BindingSet> bindingMap = findAndParseTargets(env);

    for (Map.Entry<TypeElement, BindingSet> entry : bindingMap.entrySet()) {
      TypeElement typeElement = entry.getKey();
      BindingSet binding = entry.getValue();

      JavaFile javaFile = binding.brewJava(sdk, debuggable);
      try {
        //通过javaFile.writeTo(filer)生成java源文件
        javaFile.writeTo(filer);
      } catch (IOException e) {
        error(typeElement, "Unable to write binding for type %s: %s", typeElement, e.getMessage());
      }
    }

    return false;
}复制代码

findAndParseTargets方法

  • 上面的process第一行就调用了findAndParseTargets方法,他主要是用来扫描注解,并将注解信息保存到map

private Map<TypeElement, BindingSet> findAndParseTargets(RoundEnvironment env) {
    Map<TypeElement, BindingSet.Builder> builderMap = new LinkedHashMap<>();
    Set<TypeElement> erasedTargetNames = new LinkedHashSet<>();

    // 扫描@BindAnim注解并调用parseResourceAnimation方法解析处理,保存到上面两个集合中
    for (Element element : env.getElementsAnnotatedWith(BindAnim.class)) {
      if (!SuperficialValidation.validateElement(element)) continue;
      try {
        parseResourceAnimation(element, builderMap, erasedTargetNames);
      } catch (Exception e) {
        logParsingError(element, BindAnim.class, e);
      }
    }
    。。。//省略了其他的一系列注解扫描
    //同样的扫描并解析监听注解,保存到集合中
    for (Class<? extends Annotation> listener : LISTENERS) {
        findAndParseListener(env, listener, builderMap, erasedTargetNames);
    }

    Map<TypeElement, ClasspathBindingSet> classpathBindings =
        findAllSupertypeBindings(builderMap, erasedTargetNames);

    // 创建一个双向队列entries,放入builderMap.entrySet()
    Deque<Map.Entry<TypeElement, BindingSet.Builder>> entries =
        new ArrayDeque<>(builderMap.entrySet());
    //
    Map<TypeElement, BindingSet> bindingMap = new LinkedHashMap<>();
    //遍历entries中每个
    while (!entries.isEmpty()) {
      Map.Entry<TypeElement, BindingSet.Builder> entry = entries.removeFirst();

      TypeElement type = entry.getKey();
      BindingSet.Builder builder = entry.getValue();

      TypeElement parentType = findParentType(type, erasedTargetNames, classpathBindings.keySet());
      //判断是否有父类
      if (parentType == null) {
        bindingMap.put(type, builder.build());
      } else {
        BindingInformationProvider parentBinding = bindingMap.get(parentType);
        if (parentBinding == null) {
          parentBinding = classpathBindings.get(parentType);
        }
        if (parentBinding != null) {
          builder.setParent(parentBinding);
          bindingMap.put(type, builder.build());
        } else {
          // Has a superclass binding but we haven't built it yet. Re-enqueue for later.
          entries.addLast(entry);
        }
      }
    }
    return bindingMap;
}复制代码

BindingSet.brewJava方法

  • 再回到process方法,接下来的循环中,通过brewJava返回javapoet的JavaFile对象,用来生成java文件,brewJava代码如下

JavaFile brewJava(int sdk, boolean debuggable) {
    TypeSpec bindingConfiguration = createType(sdk, debuggable);
    //通过javapoet的builder构造器将上面得到的bindingConfiguration对象构建生成一个JavaFile对象
    return JavaFile.builder(bindingClassName.packageName(), bindingConfiguration)
        .addFileComment("Generated code from Butter Knife. Do not modify!")
        .build();
}复制代码

BindingSet.createType方法

  • createType中使用javapoet库生成TypeSpec对象,里面保存各种的绑定配置信息

private TypeSpec createType(int sdk, boolean debuggable) {
    TypeSpec.Builder result = TypeSpec.classBuilder(bindingClassName.simpleName())
        .addModifiers(PUBLIC)
        .addOriginatingElement(enclosingElement);
    if (isFinal) {
      result.addModifiers(FINAL);
    }

    if (parentBinding != null) {
      result.superclass(parentBinding.getBindingClassName());
    } else {
      result.addSuperinterface(UNBINDER);
    }

    if (hasTargetField()) {
      result.addField(targetTypeName, "target", PRIVATE);
    }

    if (isView) {
      result.addMethod(createBindingConstructorForView());
    } else if (isActivity) {
      result.addMethod(createBindingConstructorForActivity());
    } else if (isDialog) {
      result.addMethod(createBindingConstructorForDialog());
    }
    if (!constructorNeedsView()) {
      result.addMethod(createBindingViewDelegateConstructor());
    }
    result.addMethod(createBindingConstructor(sdk, debuggable));

    if (hasViewBindings() || parentBinding == null) {
      result.addMethod(createBindingUnbindMethod(result));
    }

    return result.build();
}复制代码

ButterKnife类:通过反射调用APT生成的java文件的方法,完成绑定

1. bind

  • 代码如下:

@NonNull @UiThread
public static Unbinder bind(@NonNull Activity target) {
    //sourceView:当前界面的顶级父View
    View sourceView = target.getWindow().getDecorView();
    return bind(target, sourceView);
}

@NonNull @UiThread
public static Unbinder bind(@NonNull View target) {
    return bind(target, target);
}

@NonNull @UiThread
public static Unbinder bind(@NonNull Dialog target) {
    View sourceView = target.getWindow().getDecorView();
    return bind(target, sourceView);
}复制代码

2. createBinding

  • 可以看到单参的bind有几个重载方法,但最终都是调用了两个参数的bind方法,其实这个方法早期版本的名字可能更容易理解:createBinding,代码如下:

public static Unbinder bind(@NonNull Object target, @NonNull View source) {
    Class<?> targetClass = target.getClass();
    //这里调用findBindingConstructorForClass获取APT生成文件的构造器
    Constructor<? extends Unbinder> constructor = findBindingConstructorForClass(targetClass);
    //判空
    if (constructor == null) {
      return Unbinder.EMPTY;
    }
    //通过反射创建其对象,也就是createBinding
    try {
      return constructor.newInstance(target, source);
    } catch 。。。//省略异常处理代码
}复制代码

3. findBindingConstructorForClass

  • 再看一下上面代码中调用的findBindingConstructorForClass方法,代码如下:

static final Map<Class<?>, Constructor<? extends Unbinder>> BINDINGS = new LinkedHashMap<>();

@Nullable @CheckResult @UiThread
private static Constructor<? extends Unbinder> findBindingConstructorForClass(Class<?> cls) {
    //1. 先判断binding map中有没有缓存过cls,有则直接取用
    Constructor<? extends Unbinder> bindingCtor = BINDINGS.get(cls);
    if (bindingCtor != null || BINDINGS.containsKey(cls)) {
      return bindingCtor;
    }
    //2. 判断类路径是否以android.或androidx.或java.开头,是则返回null
    String clsName = cls.getName();
    if (clsName.startsWith("android.") || clsName.startsWith("java.")
        || clsName.startsWith("androidx.")) {
      return null;
    }
    try {
      //使用classLoader加载cls对应的绑定类(APT生成的),获取其class对象
      Class<?> bindingClass = cls.getClassLoader().loadClass(clsName + "_ViewBinding");
      //通过反射调用其构造器创建绑定类的对象
      bindingCtor = (Constructor<? extends Unbinder>) bindingClass.getConstructor(cls, View.class);
    } catch (ClassNotFoundException e) {
     //没找到则递归调用找其父类的
      bindingCtor = findBindingConstructorForClass(cls.getSuperclass());
    } catch (NoSuchMethodException e) {
      //没有找到构造函数则抛出异常
      throw new RuntimeException("Unable to find binding constructor for " + clsName, e);
    }
    BINDINGS.put(cls, bindingCtor);
    return bindingCtor;
}复制代码

番外:使用APT自定义注解处理器

  • 即Annotation Processing Tool,编译时注解处理器, APT可以用来在编译时扫描和处理注解

  • APT技术被广泛的运用在Java框架中,ButterKnife,EventBus,Dagger2以及ARouter等都运用到APT;

  • 新版的ButterKnife是使用APT编译时注解处理器在编译时扫描注解,并做相应处理,生成java代码,生成Java代码是调用javapoet库生成的;

  • 那么让我们尝试使用APT实现一个最简版的ButterKnife吧

使用流程

1. 新建Java Module(我的命名是"annotation")用于存放自定义注解,gradle配置文件如下:
apply plugin: 'java-library'

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

sourceCompatibility = "1.8"
targetCompatibility = "1.8"复制代码
  • 如果创建Android Library模块会发现不能找到AbstractProcessor这个类,这是因为Android平台是基于OpenJDK的,

而OpenJDK中不包含APT的相关代码。因此,在使用APT时,必须在Java Library中进行,如果不知道如何创建,直接修改为上面的配置即可;

2. 新建Java Module(我的命名是"processor")用于存放自定义apt注解处理器,并依赖annotation module, gradle配置文件如下:
apply plugin: 'java-library'

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    //AutoService是Google开发的一个库,用来生成META-INF/services/javax.annotation.processing.Processor文件
    implementation 'com.google.auto.service:auto-service:1.0-rc6'
    annotationProcessor 'com.google.auto.service:auto-service:1.0-rc6'
    //square的自动生成代码库
    implementation 'com.squareup:javapoet:1.11.1'
    implementation project(path: ':annotation', configuration:'default')
}

sourceCompatibility = "1.8"
targetCompatibility = "1.8"复制代码
3. 将前两部创建的module添加到主工程app module下
//导入自定义注解
implementation project(path: ':annotation')
// 指定自定义注释处理器 ,如果使用kotlin, replace annotationProcessor with kapt
kapt project(path: ':processor')复制代码
4. 自定义注解
@Target(ElementType.FIELD) // 作用于成员属性上
@Retention(RetentionPolicy.CLASS)
public @interface BindView {
    int value();
}复制代码
5. Element元素
  • Java中Element是一个接口,表示一个程序元素,它可以指代包、类、方法或者一个变量

PackageElement: 表示一个包程序元素。提供对有关包及其成员的信息的访问。
ExecutableElement: 表示某个类或接口的方法、构造方法或初始化程序(静态或实例),包括注释类型元素。
TypeElement: 表示一个类或接口程序元素。提供对有关类型及其成员的信息的访问。注意,枚举类型是一种类,而注解类型是一种接口。
VariableElement: 表示一个字段、enum 常量、方法或构造方法参数、局部变量或异常参数。复制代码
  • 自定义一个ElementInfo类,用于存储解析Element元素的信息

internal class ElementInfo(
    /**
     * 包路径
     */
    var packageName: String,
    /**
     * 节点所在类名称
     */
    var className: String,
    /**
     * 节点类型名称
     */
    var typeName: String,
    /**
     * 节点名称
     */
    var nodeName: String,
    /**
     * 注解的value
     */
    var value: Int
)复制代码
6. 自定义注解处理器,需继承AbstractProcessor
/**
 * @Author: LiuJinYang
 * @CreateDate: 2021/11/9
 * AutoService注解作用是用来生成META-INF/services/javax.annotation.processing.Processor文件的,
 */
@AutoService(Processor.class)
public class MyProcessor extends AbstractProcessor {
    private Types mTypeUtils;
    private Messager mMessagerUtils;
    private Filer mFilerUtils;
    private Elements mElementUtils;
    /**
     * 节点信息缓存
     */
    private Map<String, List<ElementInfo>> mCache = new HashMap<>();

    /**
     * 用于初始化处理器,可以通过ProcessingEnvironment来获取一些帮助我们来处理注解的工具类
     */
    @Override
    public synchronized void init(ProcessingEnvironment processingEnv) {
        super.init(processingEnv);
        //类信息工具类
        mTypeUtils = processingEnv.getTypeUtils();
        // Messager可以用来打印错误信息;
        mMessagerUtils = processingEnv.getMessager();
        // Filer可以用来编写新文件;
        mFilerUtils = processingEnv.getFiler();
        // Elements是一个可以处理Element的工具类。
        mElementUtils = processingEnv.getElementUtils();
    }

    /**
     * 只有一个返回值,用来指定当前正在使用的Java版本,
     * 通常return SourceVersion.latestSupported()即可
     */
    @Override
    public SourceVersion getSupportedSourceVersion() {
        return SourceVersion.latestSupported();
//        return super.getSupportedSourceVersion();
    }

    /**
     * 指明有哪些注解需要被扫描到,返回注解的全路径(包名+类名)
     */
    @Override
    public Set<String> getSupportedAnnotationTypes() {
        Set<String> types = new LinkedHashSet<>();
        for (Class<? extends Annotation> annotation : getSupportedAnnotations()) {
            types.add(annotation.getCanonicalName());
        }
        return types;
//        return super.getSupportedAnnotationTypes();
    }

    private Set<Class<? extends Annotation>> getSupportedAnnotations() {
        Set<Class<? extends Annotation>> annotations = new LinkedHashSet<>();
        annotations.add(LjyBindView.class);
        annotations.add(ClickGap.class);
        annotations.add(GetTime.class);
        annotations.add(NeedLogin.class);
        return annotations;
    }


    /**
     * 核心方法,注解的处理和生成代码都是在这个方法中完成
     */
    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        if (annotations == null || annotations.isEmpty()) {
            return false;
        }
        //扫描所有被@GetTime注解的元素
        Set<? extends Element> elements = roundEnv.getElementsAnnotatedWith(LjyBindView.class);
        if (elements == null || elements.isEmpty()) {
            return false;
        }
        //遍历被@GetTime注解的元素
        for (Element element : elements) {
            if (element.getKind() != ElementKind.FIELD) {
                throw new RuntimeException("Only classes can be annotated with " + element.getSimpleName());
            }
            // 获取节点包信息
            String packageName = mElementUtils.getPackageOf(element).getQualifiedName().toString();
            // 获取节点类信息,由于 @NeedLogin 作用方法上,所以这里使用 getEnclosingElement() 获取父节点信息
            String className = element.getEnclosingElement().getSimpleName().toString();
            // 获取节点类型
            String typeName = element.asType().toString();
            // 获取节点标记的属性名称
            String nodeName = element.getSimpleName().toString();
            // 获取注解的值
            int value = element.getAnnotation(LjyBindView.class).value();

            // 打印
            mMessagerUtils.printMessage(Diagnostic.Kind.NOTE, "packageName:" + packageName);
            mMessagerUtils.printMessage(Diagnostic.Kind.NOTE, "className:" + className);
            mMessagerUtils.printMessage(Diagnostic.Kind.NOTE, "typeName:" + typeName);
            mMessagerUtils.printMessage(Diagnostic.Kind.NOTE, "nodeName:" + nodeName);
            mMessagerUtils.printMessage(Diagnostic.Kind.NOTE, "value:" + value);

            // 缓存KEY
            String key = packageName + "." + className;
            // 缓存节点信息
            List<ElementInfo> nodeInfos = mCache.get(key);
            if (nodeInfos == null) {
                nodeInfos = new ArrayList<>();
                nodeInfos.add(new ElementInfo(packageName, className, typeName, nodeName, value));
                // 缓存
                mCache.put(key, nodeInfos);
            } else {
                nodeInfos.add(new ElementInfo(packageName, className, typeName, nodeName, value));
            }

        }

        // 判断临时缓存是否不为空
        if (!mCache.isEmpty()) {
            // 遍历临时缓存文件
            for (Map.Entry<String, List<ElementInfo>> stringListEntry : mCache.entrySet()) {
                try {
                    // 创建文件
                    createFile(stringListEntry.getValue());
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }
        return false;
    }

    /**
     * 创建文件,自动生成代码
     * javapoet的具体使用教程请参考javapoet官方文档
     */
    private void createFile(List<ElementInfo> infos) throws IOException {
        ElementInfo info = infos.get(0);
        // 生成的文件名(类名)
        String className = info.getClassName() + "__ViewBinding";
        // 方法参数
        ParameterSpec parameterSpec = ParameterSpec.builder(
                ClassName.get(info.getPackageName(), info.getClassName()), "target")
                .build();
        // 方法
        MethodSpec.Builder methodSpecBuilder = MethodSpec.methodBuilder("bind")
                .addModifiers(Modifier.PUBLIC, Modifier.STATIC)
                .addParameter(parameterSpec)
                .returns(void.class);
        // 给方法添加代码块
        for (ElementInfo nodeInfo : infos) {
            // target.textView = (TextView) target.findViewByID(R.id.text_view);
            methodSpecBuilder.addStatement("target.$L = ($L)target.findViewById($L)",
                    nodeInfo.getNodeName(),
                    nodeInfo.getTypeName(),
                    nodeInfo.getValue());
        }
        // 类
        TypeSpec typeSpec = TypeSpec.classBuilder(className)
                .addModifiers(Modifier.PUBLIC)
                .addMethod(methodSpecBuilder.build())
                .build();
        // 生成文件
        JavaFile.builder(info.getPackageName(), typeSpec)
                .build()
                .writeTo(mFilerUtils);
    }
}复制代码
  • reBuild项目之后,就可以|在项目的app\build\generated\source\kapt\debug\包名 路径下找到自动生成的文件ButterKnifeActivity__ViewBinding,其内容如下:

public class ButterKnifeActivity__ViewBinding {
  public static void bind(ButterKnifeActivity target) {
    target.etName = (android.widget.EditText)target.findViewById(2131296530);
  }
}复制代码
7. 自定义LjyButterKnife类
  • 然后就可以仿照ButterKnife在App模块下新建一个LjyButterKnife类,利用反射来调用上面方法

public class LjyButterKnife {
    public static void bind(Activity target) {
        try {
            Class<?> clazz = target.getClass();
            // 反射获取apt生成的指定类
            Class<?> bindViewClass = Class.forName(clazz.getName() + "__ViewBinding");
            // 获取它的方法
            Method method = bindViewClass.getMethod("bind", clazz);
            // 执行方法
            method.invoke(bindViewClass.newInstance(), target);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}复制代码
8. 在Activity中使用
  • 可以看到我是和ButterKnife同时使用的,也不会有任何冲突问题

class ButterKnifeActivity : AppCompatActivity() {

    @LjyBindView(R.id.et_name)
    lateinit var etName: EditText

    @BindView(R.id.et_pwd)
    lateinit var etPwd: EditText

    @BindString(R.string.login_error)
    lateinit var loginErrorMessage: String

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_butter_knife)
        ButterKnife.bind(this)
        LjyButterKnife.bind(this)
    }

    @OnClick(R.id.btn_submit)
    fun submit() {
        LjyLogUtil.d("etName=${etName.text}")
        LjyLogUtil.d("etPwd=${etPwd.text}")
        if ("ljy".equals(etName.text) && "123".equals(etPwd.text)) {
            LjyLogUtil.d("登录成功")
        } else {
            LjyLogUtil.d(loginErrorMessage)
        }
    }
}复制代码

参考

  • JakeWharton/ButterKnife源码

  • ButterKnife源码全面解析

  • ButterKnife原理解析

  • ButterKnife源码分析

  • Java编译期注解处理器APT

我是今阳,如果想要进阶和了解更多的干货,欢迎关注微信公众号 “今阳说” 接收我的最新文章


作者:今阳
链接:https://juejin.cn/post/7029147571641663525


文章分类
ANDROID
版权声明:本站是系统测试站点,无实际运营。本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 XXXXXXo@163.com 举报,一经查实,本站将立刻删除。
相关推荐