zoukankan      html  css  js  c++  java
  • Android 常用开源框架源码解析 系列 (五)Butterknife 注解库

    一、前言
    作者
    JakeWharton 
     
    作用
    • 依赖动态注入框架
    • 减少findView/setListener 类初始化代码,减少工作量
     
    二、简单使用
        1、导入库 
    implementation 'com.jakewharton:butterknife:8.5.1'
    implementation 'com.jakewharton:butterknife-compiler:8.5.1'
        2、实例代码:
    @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);
     
    三、技术历史
    1、早期注入框架技术
         反射机制
    在Activity中使用反射机制完成注解库的注入早期
        缺陷
    在Activity runtime运行时大量加载反射注入框架完成依赖注入,会影响App的运行性能,造成UI卡顿,产生更多的临时对象增加内存损耗
        
        2、现今注入框架技术
        APT
    编译时解析技术,注解注入框架
        区别与旧技术
    编译时生成非运行时生成
     
    四、依赖注入框架基础
        A、注解
     
    分类
    •     普通注解
         1、@Override :当前的方法的定义一定要覆盖其父类的方法
         2、@Deprecated :使用该注解编译器会出现警告信息
         3、@SuppressWarnings :忽略编译器提示的警告信息
     
    •     元注解
    定义:
        用来注解其他注解的注解
        1、@Documented:这个注解应该被Java Document这个工具所记录
        2、@Target:表明注解的使用范围
        3、@Retention:描述注解的生命周期
        4、@Inherited:表明注解可以继承的,这个注解应该被用于class的子类
     
    实例:
    @Documented
    @Target(ElementType.TYPE)
    @Retention(RetentionPolicy.RUNTIME)
    @Inherited
     
    public @interface metaTest {
        public String getTest();
    }
     
    1、@Documented:表明这个注解应该被Java Document工具所记录的
     
    2、@Target:传入ElementType. xxx
     
    •     TYPE :表示用来描述类或是接口的 
    •     FIELD : 表示用来描述成员域
    •     METHOD :表示用来描述方法
    •     PARAMETER :表示用来描述参数
    •     CONSTRUCTOR :表示用来描述构造器
    •     LOCAL_VARIABLE :表示用来描述局部变量
    •     PACKAGE :表示用来描述包
     
    以下是非常用类型:
     ANNOTATION_TYPE,
    TYPE_PARAMETER,
    TYPE_USE
     
    3、@Retention 描述注解生命周期
     
    SOURCE :注解将被编译器所丢弃,原文件会被保留
    CLASS :注解在Class文件中使用,有可能会被JVM自己所抛弃,是编译时生成绑定代码的
    RUNTIME:注解只在运行时有效
        ps:运行时通过反射获取注解的内容
     
    4、@Inherited:表示该注解可以继承
    5、@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 流程
    如何生成 字节码文件?
     
    1、生命 注解的生命周期是 Class
    2、继承AbstractProcessor 类 (编译时编译器会扫描需要处理的注解)
    3、调用AbstractProcessor 的 Process 方法对注解进行处理,动态生成绑定事件和控件的java代码
     
    *.java  ———input file———> Parse and Enter  ———解析———> Annotation Processing (APT解析工具进行解析,不能加入、删除java方法)  
                                                                                                                                |
                                                                                                                                |
                                                                      编译成class文件    ——————  生成java代码
     
     
    C、反射机制
    目标
        通过开源的process库生成 java 代码
        
        反射
    反射机制允许在运行时发现和使用类的信息
     
    反射的作用
        1、判断任意一个对象所属的类
        2、构造任意一个类的对象
        3、判断任意一个类所具有的成员变量和方法 (通过反射甚至可以调用provate方法)
        4、调用任意一个对象的方法
     
    反射的缺陷
        1、JVM无法对反射部分的代码进行优化,造成性能的损失
        2、反射会造成大量的临时对象,进而造成大量的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 {
               //1、首先获取到类的Class 类型
                Class clazz = mian.getClass();
                //2、通过class类型获取到对应的field 对象
                Field field = clazz.getField("age”);
               //3、通过field、method的getAnnotation方法获取到注解的方法
                testInterface = field.getAnnotation(metaTest.class);
                //4、直接可以通过注解内定义的方法获取注解内的值
                System.out.println("==:" + testInterface.value());
            } catch (NoSuchFieldException e) {
                e.printStackTrace();
            }
        }
    }
     
    解析:通过反射获得field 这个变量,再通过内部的getAnnotation()方法获取到注解的方法
     
     
    五、ButterKnife的工作原理
     
    通过APT注解处理器生成Java代码之后,再编译成.class文件
     
        1、编译的时候扫描注解,并做相应的处理,生成java代码,生成java代码时是调用javapoet库生成的
            ps:同时通过其他类@Bind/@Click这类注解 在编译的时候动态生成需要的java文件;生成java文件后,编译器会对应的生成Class文件
        2、调用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) {
       //2、通过这个库的Builder 内部类方法构建需要的属性
      TypeSpec.Builder result = TypeSpec.classBuilder(bindingClassName.simpleName())
          .addModifiers(PUBLIC);
       //3、判断属性是否是final类型的,如果是就添加final的修饰符
      if (isFinal) {
        result.addModifiers(FINAL);
      }
        //4、将绑定的集合set 集中到一起,绑定的集合是否为null
    if (parentBinding != null) {
       //5、添加一些父类的信息,比如父类的类名
      result.superclass(parentBinding.bindingClassName);
    } else {
       //6、添加父类以上 UNBINDER这个接口作为属性添加进去
      result.addSuperinterface(UNBINDER);
    }
    //7、当前是否已经有了TargetField,Activity的成员变量有没有被绑定到
    if (hasTargetField()) {
        //8、有的话给targetTypeName添加一个private属性
      result.addField(targetTypeName, "target", PRIVATE);
    }
    //9、当前控件是否是View或是View的子类 ?或是 Activity ?or Dialog?
        //是的话就添加相应的构造方法
    if (isView) {
      result.addMethod(createBindingConstructorForView());
    } else if (isActivity) {
      result.addMethod(createBindingConstructorForActivity());
    } else if (isDialog) {
      result.addMethod(createBindingConstructorForDialog());
    }
    //10、如果自己的构造方法不需要View的参数的话,就必须要添加一个需要View参数的构造方法!!
    if (!constructorNeedsView()) {
      result.addMethod(createBindingViewDelegateConstructor());
    }
            …
    }
    //11、将注解换算成 java
    代码 比如findViewById,将注解生成所需要的Java代码
    createBindingConstructor(sdk)); 
     
    ps:
        判断是否有监听,如果有就会将View设置成final;
        遍历ViewBindings把绑定过的View都遍历一遍,然后调用addViewBinding()最终 生成findViewById()函数
    private MethodSpec createBindingConstructor(int sdk) {
         ...
       //12、是否有方法绑定
    if (hasMethodBindings()) {
        //13、有方法绑定的情况,添加targetTypeName类型的参数,并设置为final类型修饰
      constructor.addParameter(targetTypeName, "target", FINAL);
    } else {
       //14、无方法绑定的情况,依然添加一个targetTypeName类型的参数,区别与有方法绑定的就是不需要final修饰
      constructor.addParameter(targetTypeName, "target");
    }
        //15、有注解的View
    if (constructorNeedsView()) {
       //16、存在已经添加了注解的View的话,就需要给其添加一个View类型的source参数
      constructor.addParameter(VIEW, "source");
    } else {
        //17、不存在已经添加了注解的View的话,就需要给其添加一个View类型的context参数
      constructor.addParameter(CONTEXT, "context");
    }
        
        //18、如果调用了@OnTouch注解的话,需要添加一个SUPPRESS_LINT注解
    if (hasOnTouchMethodBindings()) {
      constructor.addAnnotation(AnnotationSpec.builder(SUPPRESS_LINT)
          .addMember("value", "$S", "ClickableViewAccessibility")
          .build());
    }
        
        //19、通过addStatement添加成员变量的方法
    if (hasTargetField()) {
      constructor.addStatement("this.target = target");
      constructor.addCode(" ");
    }
        
       //20、核心方法
    for (ViewBinding binding : viewBindings) {
      addViewBinding(constructor, binding);
    }
    addViewBinding():
     
    private void addViewBinding(MethodSpec.Builder result, ViewBinding binding) {
        
    //21、通过该方法优化场景,告诉用户这里有View需要绑定 target.l 不能获取private 修饰的成员变量!!
    FieldViewBinding fieldBinding = binding.getFieldBinding();
    CodeBlock.Builder builder = CodeBlock.builder()
        .add("target.$L = ", fieldBinding.getName());
        …
       //22、这里就是把findViewById添加到代码中
        if (!requiresCast && !fieldBinding.isRequired()) {
         builder.add("source.findViewById($L)", binding.getId().code);
        }
    }
  • 相关阅读:
    Ubuntu下Chromium for Android 源码的编译
    Ubuntu下编译Chromium for Android
    解决Inno Setup制作安装包无法创建桌面快捷方式的问题
    linux下验证码无法显示:Could not initialize class sun.awt.X1 解决方案
    在ubuntu 14.04 64位系统上安装32位库
    zxing实现二维码生成和解析
    H264码流打包分析
    YUV格式&像素
    谈谈“色彩空间表示方法”——RGB、YUY2、YUYV、YVYU、UYVY、AYUV
    RTP与RTCP协议介绍
  • 原文地址:https://www.cnblogs.com/cold-ice/p/9305361.html
Copyright © 2011-2022 走看看