Android 经常使用开源框架源码解析 系列 (五)Butterknife 注解库

1、前言
做者
JakeWharton 
 
做用
  • 依赖动态注入框架
  • 减小findView/setListener 类初始化代码,减小工做量
 
2、简单使用
    一、导入库 
implementation 'com.jakewharton:butterknife:8.5.1'
implementation 'com.jakewharton:butterknife-compiler:8.5.1'
    二、实例代码:
@BindView(R.id.TextView_1)
TextView TextView1;
@OnClick(R.id.button_1)
void OnClick(View view) {
    textView1.setText("");
}
 
setContentView(R.layout.butterknife_layout);
//必须在setContentView绘制好布局以后调用 不然找不到对应的id对象 产生空指针
ButterKnife.bind(this);
 
3、技术历史
一、早期注入框架技术
     反射机制
在Activity中使用反射机制完成注解库的注入早期
    缺陷
在Activity runtime运行时大量加载反射注入框架完成依赖注入,会影响App的运行性能,形成UI卡顿,产生更多的临时对象增长内存损耗
    
    二、现今注入框架技术
    APT
编译时解析技术,注解注入框架
    区别与旧技术
编译时生成非运行时生成
 
4、依赖注入框架基础
    A、注解
 
分类
  •     普通注解
     一、@Override :当前的方法的定义必定要覆盖其父类的方法
     二、@Deprecated :使用该注解编译器会出现警告信息
     三、@SuppressWarnings :忽略编译器提示的警告信息
 
  •     元注解
定义:
    用来注解其余注解的注解
    一、@ Documented:这个注解应该被Java Document这个工具所记录
    二、@ Target:代表注解的使用范围
    三、@ Retention:描述注解的生命周期
    四、@ Inherited:代表注解能够继承的,这个注解应该被用于class的子类
 
实例:
@Documented
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Inherited
 
public @interface metaTest {
    public String getTest();
}
 
一、@Documented:代表这个注解应该被Java Document工具所记录的
 
二、@Target:传入ElementType. xxx
 
  •     TYPE :表示用来描述类或是接口的 
  •     FIELD : 表示用来描述成员域
  •     METHOD :表示用来描述方法
  •     PARAMETER :表示用来描述参数
  •     CONSTRUCTOR :表示用来描述构造器
  •     LOCAL_VARIABLE :表示用来描述局部变量
  •     PACKAGE :表示用来描述包
 
如下是很是用类型:
 ANNOTATION_TYPE,
TYPE_PARAMETER,
TYPE_USE
 
三、@Retention 描述注解生命周期
 
SOURCE :注解将被编译器所丢弃,原文件会被保留
CLASS :注解在Class文件中使用,有可能会被JVM本身所抛弃,是编译时生成绑定代码的
RUNTIME:注解只在运行时有效
    ps:运行时经过反射获取注解的内容
 
四、@Inherited:表示该注解能够继承
五、@interface :代表这个metaTest 是一个自定义的注解,以及配合上面4个元注解对其进行解释
  •     自定义注解
@BindView
@Retention( CLASS   //使用该注解代表会在class文件中保留,在runtime时是不存在的
@Target( FIELD )     //该注解用来修饰 域变量
public @interface BindView {
  @IdRes // 
    ps:经过自定义注解完成对变量的注解、对注解的注解
     int value();
}
   
    B、APT工做原理-编译时
注意⚠️:
  • APT不是经过运行时经过反射机制处理注解的!!!!!!
  • 整个注解处理器是运行在本身的Java虚拟机当中的!
 
Annotation Processor 注解处理器
Javac 工具;编译时扫描、处理注解;须要注册注解处理器
 
每个处理器都是 继承于AbstractProcessor 抽象类 (使用的时候须要继承并实现其内部的方法)
 
abstract AbstractProcessor :内几个比较重要的函数
 
  init方法 :会被注解处理工具调用,传入ProcessingEnvironment 提供不少经常使用的工具类共使用
 
  interface ProcessingEnvironment{
        Elements 工具类 ,扫描的全部java原文件的,element 表明程序中的元素也就说java原代码
        Types :获取原代码中的类型元素信息, Typs 处理Type Element当中一些所想得到的信息
        Filter :建立文件所用
    }
 
abstract process() : 重要级别堪比main 函数,方法的入口,每一个处理器主函数入口!
使用:自定义注解器须要实现其方法
做用: 扫描、评估、处理注解工做,并生成须要的java代码
 
set<String> getSupportedAnnotationTypes () 
    返回所支持的注解的类型
 
SourceVersion getSupportedSourceVersion()
    用来指定所使用的java版本
 
APT 流程
如何生成 字节码文件?
 
一、生命 注解的生命周期是 Class
二、继承AbstractProcessor 类 (编译时编译器会扫描须要处理的注解)
三、调用AbstractProcessor  的 Process 方法对注解进行处理,动态生成绑定事件和控件的java代码
 
*.java  ———input file———> Parse and Enter  ———解析———> Annotation Processing (APT解析工具进行解析,不能加入、删除java方法)  
                                                                                                                            |
                                                                                                                            |
                                                                  编译成class文件    ——————  生成java代码
 
 
C、反射机制
目标
    经过开源的process库生成 java 代码
    
    反射
反射机制容许在运行时发现和使用类的信息
 
反射的做用
    一、判断任意一个对象所属的类
    二、构造任意一个类的对象
    三、判断任意一个类所具备的成员变量和方法 (经过反射甚至能够调用provate方法)
    四、调用任意一个对象的方法
 
反射的缺陷
    一、JVM没法对反射部分的代码进行优化,形成性能的损失
    二、反射会形成大量的 临时对象,进而形成大量的Gc,从而形成卡顿
 
简单示例:
//定义一个含有Runtime 运行时注解的 注解,经过反射和运行时获取它的注解
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface metaTest {
    int value() default 100;
}
//经过反射获取到运行时的metaTest的注解
public class ReflactMain {
    @metaTest(10)
    public int age;
 
    public static void main(String[] args) {
        ReflactMain mian = new ReflactMain();
        metaTest testInterface = null;
        try {
            //一、首先获取到类的Class 类型
            Class clazz = mian.getClass();
             //二、经过class类型获取到对应的field 对象
            Field field = clazz.getField("age”);
            //三、经过field、method的getAnnotation方法获取到注解的方法
            testInterface = field.getAnnotation(metaTest.class);
             //四、直接能够经过注解内定义的方法获取注解内的值
            System.out.println("==:" + testInterface.value());
        } catch (NoSuchFieldException e) {
            e.printStackTrace();
        }
    }
}
 
解析:经过反射得到field 这个变量,再经过内部的getAnnotation()方法获取到注解的方法
 
 
5、ButterKnife的工做原理
 
经过APT注解处理器生成Java代码以后,再编译成.class文件
 
    一、编译的时候扫描注解,并作相应的处理,生成java代码,生成java代码时是调用javapoet库生成的
        ps:同时经过其余类@Bind/@Click这类注解 在编译的时候动态生成须要的java文件;生成java文件后,编译器会对应的生成Class文件
    二、调用ButterKnife.bind( this )的时候,将Id与对应的上下文绑定在一块儿
        ps:完成findViewById、setOnClickListene 等过程 
 
ButterKnifeProcessor extends AbstractProcessor 
 
几个辅助的方法 init() 、getSupportedAnnotationTypes()、getSupportedAnnotations()
@Override 
public synchronized void init(ProcessingEnvironment env) {
  •    该方法会在初始化的时候调用一次,用来获取一些辅助的工具类
  •    经过synchronized关键字保证获取到的对象都是单例的
  •    ProcessingEnvironment 做为参数能够提供几个有用的工具类
 
    //BK process在运行的时候会扫描Java 原文件,每个java原文件的每个独立的部分就是一个element,而后经过elementUtils对每个element进行解析
elementUtils = env.getElementUtils();
    //用于处理TypeElement 
typeUtils = env.getTypeUtils();
    //建立生成的辅助文件所用
filer = env.getFiler();
}
//返回所支持的注解类型
@Override public Set<String> getSupportedAnnotationTypes() {
  Set<String> types = new LinkedHashSet<>();
  for (Class<? extends Annotation> annotation : getSupportedAnnotations()) {
    types.add(annotation.getCanonicalName());
  }
  return types;
}
 
//确认butterknife 定义了哪些注解可使用
private Set<Class<? extends Annotation>> getSupportedAnnotations() {
  Set<Class<? extends Annotation>> annotations = new LinkedHashSet<>();
  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(BindInt.class);
  annotations.add(BindString.class);
  annotations.add(BindView.class);
  annotations.add(BindViews.class);
  annotations.addAll(LISTENERS);
  return annotations;
}
 
重要方法:
拿到全部的注解信息存储到一个Map<TypeElement,BindingSet>集合当中,
而后遍历map集合作相应的处理,最后生成需求的代码
 
process():处理注解
@Override 
public boolean process(Set<? extends TypeElement> elements, RoundEnvironment env) {
  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);
    try {
      javaFile.writeTo(filer);
    } catch (IOException e) {
      error(typeElement, "Unable to write binding for type %s: %s", typeElement, e.getMessage());
    }
  }
  return false;
}
 
 
findAndParseTargets(): 针对每个自定义好的注解
private Map<TypeElement, BindingSet> findAndParseTargets(RoundEnvironment env) {
  Map<TypeElement, BindingSet.Builder> builderMap = new LinkedHashMap<>();
  Set<TypeElement> erasedTargetNames = new LinkedHashSet<>();
  scanForRClasses(env);
    …
// Process each @BindView element.以BindView 为例子:
for (Element element : env.getElementsAnnotatedWith(BindView.class)) {
  try {
    //进行转化
    parseBindView(element, builderMap, erasedTargetNames);
  } catch (Exception e) {
    logParsingError(element, BindView.class, e);
     }
    }
}
parseBindView():
private void parseBindView(Element element, Map<TypeElement, BindingSet.Builder> builderMap,
    Set<TypeElement> erasedTargetNames) {
如下内容:拿到注解信息并验证是不是xx的子类,集中处理注解所须要的内容并保存到一个map集合当中
 
//建立一个原文件所对应的对象的element元素所对应的类型
   TypeElementenclosingElement = (TypeElement) element.getEnclosingElement();
//判断是否在被注解的属性上,若是是private 或是static 修饰的注解就会返回一个hasError false值 ,
同时包名是以android 或是java开头的也会出错
boolean hasError =  isInaccessibleViaGeneratedCode(BindView.class, "fields", element)
    || isBindingInWrongPackage(BindView.class, element);
        ...
//一些父类的信息用TypeElement获取不到须要经过TypeMirror 来获取,并经过asType()验证是不是需求的子类
TypeMirror elementType = element.asType();
     ...
//判断里面的元素是不是view 以及其子类或是是不是接口 ,若是不是view的继承类没有意义再往下进行
if (!isSubtypeOfType(elementType, VIEW_TYPE) && !isInterface(elementType)) {
    ...
}
   
//获取要绑定的View的 Id ,经过getAnnotation(BindView.class).value
int id = element.getAnnotation(BindView.class).value();
//传入TypeElement的值,根据所在的元素查找Build 
BindingSet.Builder builder = builderMap.get(enclosingElement);
//若是相应的build已经存在了
if (builder != null) {
  String existingBindingName = builder.findExistingBindingName(getId(id));
  if (existingBindingName != null) {
   //若是name 不为空 ,则说明已经被绑定过了就会报错并返回
    error(element, "Attempt to use @%s for an already bound ID %d on '%s'. (%s.%s)",
        BindView.class.getSimpleName(), id, existingBindingName,
        enclosingElement.getQualifiedName(), element.getSimpleName());
    return;
  }
} else {
   //建立一个新的builder
  builder = getOrCreateBindingBuilder(builderMap, enclosingElement);
}
    
    //经过addField()方法添加到集合当中
builder.addField(getId(id), new FieldViewBinding(name, type, required));
 
}
 
//将Id 和 View 进行绑定,传入到一个map集合中
addField————>getOrCreateViewBindings()————>getOrCreateViewBindings():
private ViewBinding.Builder getOrCreateViewBindings(Id id) {
  ViewBinding.Builder viewId = viewIdMap.get(id);
  if (viewId == null) {
    viewId = new ViewBinding.Builder(id);
    viewIdMap.put(id, viewId);
  }
  return viewId;
}
 
//生成java代码 ,返回一个JavaFile对象用来转编译成java代码
JavaFile brewJava(int sdk) {
  return JavaFile.builder(bindingClassName.packageName(), createType(sdk))
      .addFileComment("Generated code from Butter Knife. Do not modify!")
      .build();
}
// com.squareup.javapoet 第三方库来生成java代码 生成所须要的类型
private TypeSpec createType(int sdk) {
   //二、经过这个库的Builder 内部类方法构建须要的属性
  TypeSpec.Builder result = TypeSpec.classBuilder(bindingClassName.simpleName())
      .addModifiers(PUBLIC);
   //三、判断属性是不是final类型的,若是是就添加final的修饰符
  if (isFinal) {
    result.addModifiers(FINAL);
  }
    //四、将绑定的集合set 集中到一块儿,绑定的集合是否为null
if (parentBinding != null) {
    //五、添加一些父类的信息,好比父类的类名
  result.superclass(parentBinding.bindingClassName);
} else {
    //六、添加父类以上 UNBINDER这个接口做为属性添加进去
  result.addSuperinterface(UNBINDER);
}
//七、当前是否已经有了TargetField,Activity的成员变量有没有被绑定到
if (hasTargetField()) {
    //八、有的话给targetTypeName添加一个private属性
  result.addField(targetTypeName, "target", PRIVATE);
}
//九、当前控件是不是View或是View的子类 ?或是 Activity ?or Dialog?
    //是的话就添加相应的构造方法
if (isView) {
  result.addMethod(createBindingConstructorForView());
} else if (isActivity) {
  result.addMethod(createBindingConstructorForActivity());
} else if (isDialog) {
  result.addMethod(createBindingConstructorForDialog());
}
//十、若是本身的构造方法不须要View的参数的话,就必需要添加一个须要View参数的构造方法!!
if (!constructorNeedsView()) {
  result.addMethod(createBindingViewDelegateConstructor());
}
        …
}
//十一、将注解换算成 java
代码 好比findViewById,将注解生成所须要的Java代码
createBindingConstructor(sdk)); 
 
ps:
    判断是否有监听,若是有就会将View设置成final;
    遍历ViewBindings把绑定过的View都遍历一遍,而后调用addViewBinding()最终 生成findViewById()函数
private MethodSpec createBindingConstructor(int sdk) {
     ...
   //十二、是否有方法绑定
if (hasMethodBindings()) {
    //1三、有方法绑定的状况,添加targetTypeName类型的参数,并设置为final类型修饰
  constructor.addParameter(targetTypeName, "target", FINAL);
} else {
   //1四、无方法绑定的状况,依然添加一个targetTypeName类型的参数,区别与有方法绑定的就是不须要final修饰
  constructor.addParameter(targetTypeName, "target");
}
    //1五、有注解的View
if (constructorNeedsView()) {
   //1六、存在已经添加了注解的View的话,就须要给其添加一个View类型的source参数
  constructor.addParameter(VIEW, "source");
} else {
    //1七、不存在已经添加了注解的View的话,就须要给其添加一个View类型的context参数
  constructor.addParameter(CONTEXT, "context");
}
    
    //1八、若是调用了@OnTouch注解的话,须要添加一个SUPPRESS_LINT注解
if (hasOnTouchMethodBindings()) {
  constructor.addAnnotation(AnnotationSpec.builder(SUPPRESS_LINT)
      .addMember("value", "$S", "ClickableViewAccessibility")
      .build());
}
    
    //1九、经过addStatement添加成员变量的方法
if (hasTargetField()) {
  constructor. addStatement("this.target = target");
  constructor. addCode("\n");
}
    
   //20、核心方法
for (ViewBinding binding : viewBindings) {
  addViewBinding(constructor, binding);
}
addViewBinding():
 
private void addViewBinding(MethodSpec.Builder result, ViewBinding binding) {
    
//2一、经过该方法优化场景,告诉用户这里有View须要绑定 target.l 不能获取private 修饰的成员变量!!
FieldViewBinding fieldBinding = binding.getFieldBinding();
CodeBlock.Builder builder = CodeBlock.builder()
    .add(" target.$L = ", fieldBinding.getName());
    …
   //2二、这里就是把findViewById添加到代码中
    if (!requiresCast && !fieldBinding.isRequired()) {
     builder.add("source.findViewById($L)", binding.getId().code);
    }
}
相关文章
相关标签/搜索