文档章节

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

o
 osc_odyg6b92
发布于 2018/07/13 15:46
字数 3141
阅读 10
收藏 0

行业解决方案、产品招募中!想赚钱就来传!>>>

一、前言
作者
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("\n");
}
    
   //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);
    }
}
o
粉丝 1
博文 500
码字总数 0
作品 0
私信 提问
加载中
请先登录后再评论。
Netty那点事(三)Channel与Pipeline

Channel是理解和使用Netty的核心。Channel的涉及内容较多,这里我使用由浅入深的介绍方法。在这篇文章中,我们主要介绍Channel部分中Pipeline实现机制。为了避免枯燥,借用一下《盗梦空间》的...

黄亿华
2013/11/24
2W
22
访问安全控制解决方案

本文是《轻量级 Java Web 框架架构设计》的系列博文。 今天想和大家简单的分享一下,在 Smart 中是如何做到访问安全控制的。也就是说,当没有登录或 Session 过期时所做的操作,会自动退回到...

黄勇
2013/11/03
3.4K
6
浅入浅出Android(003):使用TextView类构造文本控件

基础: TextView是无法供编辑的。 当我们新建一个项目MyTextView时候,默认的布局(/res/layout/activity_main.xml)中已经有了一个TextView: <TextView 运行效果如下: 修改其文本内容...

樂天
2014/03/22
593
1
beego API开发以及自动化文档

beego API开发以及自动化文档 beego1.3版本已经在上个星期发布了,但是还是有很多人不了解如何来进行开发,也是在一步一步的测试中开发,期间QQ群里面很多人都问我如何开发,我的业余时间实在...

astaxie
2014/06/25
2.7W
22
Nutch学习笔记4-Nutch 1.7 的 索引篇 ElasticSearch

上一篇讲解了爬取和分析的流程,很重要的收获就是: 解析过程中,会根据页面的ContentType获得一系列的注册解析器, 依次调用每个解析器,当其中一个解析成功后就返回,否则继续执行下一个解...

强子哥哥
2014/06/26
712
0

没有更多内容

加载失败,请刷新页面

加载更多

如何在SQL Server中将多行文本合并为单个文本字符串?

问题: Consider a database table holding names, with three rows: 考虑一个包含名称的数据库表,该表具有三行: PeterPaulMary Is there an easy way to turn this into a single str......

富含淀粉
32分钟前
9
0
在JavaScript中生成特定范围内的随机整数? - Generating random whole numbers in JavaScript in a specific range?

问题: 如何可以生成两个指定的变量之间的随机整数在JavaScript中,例如x = 4和y = 8将输出任何的4, 5, 6, 7, 8 ? 解决方案: 参考一: https://stackoom.com/question/6PRz/在JavaScript中...

fyin1314
今天
8
0
Vim清除最后一个搜索突出显示 - Vim clear last search highlighting

问题: Want to improve this post? 想要改善这篇文章吗? Provide detailed answers to this question, including citations and an explanation of why your answer is correct. 提供此问题......

技术盛宴
今天
23
0
马化腾每天刷 Leetcode?代码你打算写到几岁?

本文作者:o****0 前几天,一张未证真伪的截图流传,图中显示马化腾几乎每天都会在 Leetcode 上提交代码。 截图还贴出一个 Leetcode 账户地址。该地址的头像已从马化腾的照片换成腾讯 logo,...

百度开发者中心
前天
13
0
滴滴 3000+ Kylin Cube 背后的实践经验揭秘

本次分享主要有三个部分:Kylin 在滴滴的整体应用、架构的实践经验、滴滴全局字典最新版本的实现以及 Kylin 最新实时 OLAP 探索经验分享。 Kylin 在滴滴的应用&架构 Kylin 在滴滴的三类应用场...

浪尖聊大数据
昨天
9
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部