前言
话说在android这座大山里,有一座庙(方块公司-square),庙里住着一个神-jake(我是这么叫的嘻嘻)。
不要小看这个小jake,这个神可是为android应用开发们提供了强有力的帮助。比如流行的开源库okhttp,leakcanary ,retrofit,butterknife 等等都是出于他之手。小弟佩服的不要不要的…,可以说是为android的应用开发效率和耦合性提高了一个台阶啊。
其它的大神我也是佩服的不要不要的…嘻嘻
声明
这一系列的文章是对ButterKnife的源码进行分析的,涉及的细节比较多,但也比较广。一次看不懂no 问题,慢慢来嘛,嘻嘻
如有不对的地方,还望指导。
最后会给出自己实现的一个demo,主要是原理和思想哦!!!
代码结构
我们这里对ButterKnife的最新版本8.4.0进行分析。
我们先down下来看下代码的结构,可以看到代码结构分的还是很好的。
butterknife ;android library model 提供android使用的API
butterknife-annotations; java-model,使用时的注解
butterknife-compiler;java-model,编译时用到的注解的处理器
butterknife-gradle-plugin;自定义的gradle插件,辅助生成有关代码
butterknife-integration-test;该项目的测试用例
butterknife-lint;该项目的lint检查
sample;demo
可以看到大神的代码很风骚的,很清晰啊。
这里重点分析butterknife-compiler及butterknife
原理图
你可以看完了再回来看这个图,会更明白。
使用方法
这里不再给出。不过会在原代码分析的时候给出一些注意的地方。
我们拿官方的demo-SimpleActivity
编译完后最后生成的文件为:SimpleActivity_ViewBinding
路径在:
内容:
// Generated code from Butter Knife. Do not modify! package com.example.butterknife.library; import android.support.annotation.CallSuper; import android.support.annotation.UiThread; import android.view.View; import android.widget.AdapterView; import android.widget.Button; import android.widget.ListView; import android.widget.TextView; import butterknife.Unbinder; import butterknife.internal.DebouncingOnClickListener; import butterknife.internal.Utils; import java.lang.IllegalStateException; import java.lang.Override; public class SimpleActivity_ViewBinding<T extends SimpleActivity> implements Unbinder { protected T target; private View view2130968578; private View view2130968579; @UiThread public SimpleActivity_ViewBinding(final T target, View source) { this.target = target; View view; target.title = Utils.findRequiredViewAsType(source, R.id.title, "field 'title'", TextView.class); target.subtitle = Utils.findRequiredViewAsType(source, R.id.subtitle, "field 'subtitle'", TextView.class); view = Utils.findRequiredView(source, R.id.hello, "field 'hello', method 'sayHello', and method 'sayGetOffMe'"); target.hello = Utils.castView(view, R.id.hello, "field 'hello'", Button.class); view2130968578 = view; view.setOnClickListener(new DebouncingOnClickListener() { @Override public void doClick(View p0) { target.sayHello(); } }); view.setOnLongClickListener(new View.OnLongClickListener() { @Override public boolean onLongClick(View p0) { return target.sayGetOffMe(); } }); view = Utils.findRequiredView(source, R.id.list_of_things, "field 'listOfThings' and method 'onItemClick'"); target.listOfThings = Utils.castView(view, R.id.list_of_things, "field 'listOfThings'", ListView.class); view2130968579 = view; ((AdapterView<?>) view).setOnItemClickListener(new AdapterView.OnItemClickListener() { @Override public void onItemClick(AdapterView<?> p0, View p1, int p2, long p3) { target.onItemClick(p2); } }); target.footer = Utils.findRequiredViewAsType(source, R.id.footer, "field 'footer'", TextView.class); target.headerViews = Utils.listOf( Utils.findRequiredView(source, R.id.title, "field 'headerViews'"), Utils.findRequiredView(source, R.id.subtitle, "field 'headerViews'"), Utils.findRequiredView(source, R.id.hello, "field 'headerViews'")); } @Override @CallSuper public void unbind() { T target = this.target; if (target == null) throw new IllegalStateException("Bindings already cleared."); target.title = null; target.subtitle = null; target.hello = null; target.listOfThings = null; target.footer = null; target.headerViews = null; view2130968578.setOnClickListener(null); view2130968578.setOnLongClickListener(null); view2130968578 = null; ((AdapterView<?>) view2130968579).setOnItemClickListener(null); view2130968579 = null; this.target = null; } }
我们看到了我们熟悉的代码,虽然比较乱(因为是生成的),
可以看出 在构造中findview 在unbind中进行置null处理,让告诉gc在合适的机会回收占用的内存 ;但是这是后面真正生成代码我们看不到的,没关系 嘻嘻。
整体的原理-(编译时期-注解处理器)
在java代码的编译时期,javac 会调用java注解处理器来进行处理。因此我们可以定义自己的注解处理器来干一些事情。一个特定注解的处理器以 java 源代码(或者已编译的字节码)作为输入,然后生成一些文件(通常是.java文件)作为输出。因此我们可以在用户已有的代码上添加一些方法,来帮我们做一些有用的事情。这些生成的 java 文件跟其他手动编写的 java 源代码一样,将会被 javac 编译。(个人参考及个人理解)
定义处理器 继承AbstractProcessor
在java中定义自己的处理器都是继承自AbstractProcessor
前3个方法都试固定写法,主要是process方法。
public class MyProcessor extends AbstractProcessor { //用来指定你使用的 java 版本。通常你应该返回 SourceVersion.latestSupported() @Override public SourceVersion getSupportedSourceVersion() { return SourceVersion.latestSupported(); } //会被处理器调用,可以在这里获取Filer,Elements,Messager等辅助类,后面会解释 @Override public synchronized void init(ProcessingEnvironment processingEnv) { super.init(processingEnv); } //这个方法返回stirng类型的set集合,集合里包含了你需要处理的注解 @Override public Set<String> getSupportedAnnotationTypes() { Set<String> annotataions = new LinkedHashSet<String>(); annotataions.add("com.example.MyAnnotation"); return annotataions; } //核心方法,这个一般的流程就是先扫描查找注解,再生成 java 文件 //这2个步骤设计的知识点细节很多。 @Override public boolean process(Set<? extends TypeElement> annoations, RoundEnvironment env) { return false; } }
注册你的处理器
要像jvm调用你写的处理器,你必须先注册,让他知道。怎么让它知道呢,其实很简单,google 为我们提供了一个库,简单的一个注解就可以。
首先是依赖
compile 'com.google.auto.service:auto-service:1.0-rc2'
@AutoService(Processor.class) public class BindViewProcessor extends AbstractProcessor { //...省略非关键代码 }
基本概念
Elements:一个用来处理Element的工具类
Types:一个用来处理TypeMirror的工具类
Filer:你可以使用这个类来创建.java文件
源码分析
分析之前呢先要有写基本的概念
可以看到AutoService注解
@AutoService(Processor.class) public final class ButterKnifeProcessor extends AbstractProcessor { //... }
(1)init 方法,这个主要是获取一些辅助类
private Filer mFiler; //文件相关的辅助类 private Elements mElementUtils; //元素相关的辅助类 private Messager mMessager; //日志相关的辅助类 @Override public synchronized void init(ProcessingEnvironment env) { super.init(env); elementUtils = env.getElementUtils(); typeUtils = env.getTypeUtils(); filer = env.getFiler(); try { trees = Trees.instance(processingEnv); } catch (IllegalArgumentException ignored) { } }
(2)getSupportedSourceVersion()方法就是默认的,获取你该处理器使用的java版本
@Override public SourceVersion getSupportedSourceVersion() { return SourceVersion.latestSupported(); }
(4)接下来就是process方法,这个方法是核心方法。
@Override public boolean process(Set<? extends TypeElement> elements, RoundEnvironment env) { //1.查找所有的注解信息,并形成BindingClass(是什么 后面会讲) 保存到 map中 Map<TypeElement, BindingClass> targetClassMap = findAndParseTargets(env); //2.遍历步骤1的map 的生成.java文件也就是上文的 类名_ViewBinding 的java文件 for (Map.Entry<TypeElement, BindingClass> entry : targetClassMap.entrySet()) { TypeElement typeElement = entry.getKey(); BindingClass bindingClass = entry.getValue(); JavaFile javaFile = bindingClass.brewJava(); try { javaFile.writeTo(filer); } catch (IOException e) { error(typeElement, "Unable to write binding for type %s: %s", typeElement, e.getMessage()); } } return true; }
咦,很明显,2步走啊。是的没错, 满满套路啊!!大家都回农村吧,嘻嘻
下面我们仔细走一下该方法流程。
每个注解的查找与解析-findAndParseTargets
private Map<TypeElement, BindingClass> findAndParseTargets(RoundEnvironment env) { Map<TypeElement, BindingClass> targetClassMap = new LinkedHashMap<>(); Set<TypeElement> erasedTargetNames = new LinkedHashSet<>(); scanForRClasses(env); //...下面是每个注解的解析 // Process each @BindArray element. for (Element element : env.getElementsAnnotatedWith(BindArray.class)) { if (!SuperficialValidation.validateElement(element)) continue; try { parseResourceArray(element, targetClassMap, erasedTargetNames); } catch (Exception e) { logParsingError(element, BindArray.class, e); } } // Process each @BindView element. for (Element element : env.getElementsAnnotatedWith(BindView.class)) { // we don't SuperficialValidation.validateElement(element) // so that an unresolved View type can be generated by later processing rounds try { parseBindView(element, targetClassMap, erasedTargetNames); } catch (Exception e) { logParsingError(element, BindView.class, e); } } //.... // Process each annotation that corresponds to a listener. for (Class<? extends Annotation> listener : LISTENERS) { findAndParseListener(env, listener, targetClassMap, erasedTargetNames); } // Try to find a parent binder for each. for (Map.Entry<TypeElement, BindingClass> entry : targetClassMap.entrySet()) { TypeElement parentType = findParentType(entry.getKey(), erasedTargetNames); if (parentType != null) { BindingClass bindingClass = entry.getValue(); BindingClass parentBindingClass = targetClassMap.get(parentType); bindingClass.setParent(parentBindingClass); } } return targetClassMap; }
首先我们先看一下参数 RoundEnvironment 这个是什么呢?个人理解是注解框架里的一个工具什么工具呢?一个可以在处理器处理该处理器 用来查询注解信息的工具,当然包含你在getSupportedAnnotationTypes注册的注解。
接下来呢?
创建了一个LinkedHashMap保证了先后的顺序就是先注解的先生成java文件,其实也没有什么先后无所谓。最后将其返回。
我们这里只分析一个具有代表性的BindView注解,其它的都是一样的,连代码都一毛一样。
在这之前先看一下Element这个类我们看一下官方注释
* Represents a program element such as a package, class, or method. * Each element represents a static, language-level construct * (and not, for example, a runtime construct of the virtual machine).
在注解处理器中,我们扫描 java 源文件,源代码中的每一部分都是Element的一个特定类型。换句话说:Element代表程序中的元素,比如说 包,类,方法。每一个元素代表一个静态的,语言级别的结构.
比如:
public class ClassA { // TypeElement private int var_0; // VariableElement public ClassA() {} // ExecuteableElement public void setA( // ExecuteableElement int newA // TypeElement ) { } }
可以看到类为TypeElement,变量为VariableElement,方法为ExecuteableElement
这些都是Element的子类,自己可以看下源码,的确如此的。
TypeElement aClass ; for (Element e : aClass.getEnclosedElements()){ //获取所有的子节点 Element parent = e.getEnclosingElement(); // 获取父节点 }
Elements代表源代码,TypeElement代表源代码中的元素类型,例如类。然后,TypeElement并不包含类的相关信息。你可以从TypeElement获取类的名称,但你不能获取类的信息,比如说父类。这些信息可以通过TypeMirror获取。你可以通过调用element.asType()来获取一个Element的TypeMirror。
这个是对于理解源码的基础。
继续,我们看到了for循环查找所有包含BindView的注解。
parseBindView(element, targetClassMap, erasedTargetNames);
把element和targetClassMap传入
private void parseBindView(Element element, Map<TypeElement, BindingClass> targetClassMap, Set<TypeElement> erasedTargetNames) { TypeElement enclosingElement = (TypeElement) element.getEnclosingElement(); //1.检查用户使用的合法性 // Start by verifying common generated code restrictions. boolean hasError = isInaccessibleViaGeneratedCode(BindView.class, "fields", element) || isBindingInWrongPackage(BindView.class, element); // Verify that the target type extends from View. TypeMirror elementType = element.asType(); if (elementType.getKind() == TypeKind.TYPEVAR) { TypeVariable typeVariable = (TypeVariable) elementType; elementType = typeVariable.getUpperBound(); } if (!isSubtypeOfType(elementType, VIEW_TYPE) && !isInterface(elementType)) { if (elementType.getKind() == TypeKind.ERROR) { note(element, "@%s field with unresolved type (%s) " + "must elsewhere be generated as a View or interface. (%s.%s)", BindView.class.getSimpleName(), elementType, enclosingElement.getQualifiedName(), element.getSimpleName()); } else { error(element, "@%s fields must extend from View or be an interface. (%s.%s)", BindView.class.getSimpleName(), enclosingElement.getQualifiedName(), element.getSimpleName()); hasError = true; } } // 不合法的直接返回 if (hasError) { return; } //2.获取id 值 // Assemble information on the field. int id = element.getAnnotation(BindView.class).value(); // 3.获取 BindingClass,有缓存机制, 没有则创建,下文会仔细分析 BindingClass bindingClass = targetClassMap.get(enclosingElement); if (bindingClass != null) { ViewBindings viewBindings = bindingClass.getViewBinding(getId(id)); if (viewBindings != null && viewBindings.getFieldBinding() != null) { FieldViewBinding existingBinding = viewBindings.getFieldBinding(); error(element, "Attempt to use @%s for an already bound ID %d on '%s'. (%s.%s)", BindView.class.getSimpleName(), id, existingBinding.getName(), enclosingElement.getQualifiedName(), element.getSimpleName()); return; } } else { bindingClass = getOrCreateTargetClass(targetClassMap, enclosingElement); } //4 生成FieldViewBinding 实体 String name = element.getSimpleName().toString(); TypeName type = TypeName.get(elementType); boolean required = isFieldRequired(element); FieldViewBinding binding = new FieldViewBinding(name, type, required); //5。加入到 bindingClass 成员变量的集合中 bindingClass.addField(getId(id), binding); // Add the type-erased version to the valid binding targets set. erasedTargetNames.add(enclosingElement); }
element.getEnclosingElement();是什么呢?是父节点。就是上面我们说的。
基本的步骤就是上面的5步
1.检查用户使用的合法性
// Start by verifying common generated code restrictions. boolean hasError = isInaccessibleViaGeneratedCode(BindView.class, "fields", element) || isBindingInWrongPackage(BindView.class, element);
private boolean isInaccessibleViaGeneratedCode(Class<? extends Annotation> annotationClass, String targetThing, Element element) { boolean hasError = false; // 得到父节点 TypeElement enclosingElement = (TypeElement) element.getEnclosingElement(); //判断修饰符,如果包含private or static 就会抛出异常。 // Verify method modifiers. Set<Modifier> modifiers = element.getModifiers(); if (modifiers.contains(PRIVATE) || modifiers.contains(STATIC)) { error(element, "@%s %s must not be private or static. (%s.%s)", annotationClass.getSimpleName(), targetThing, enclosingElement.getQualifiedName(), element.getSimpleName()); hasError = true; } //判断父节点是否是类类型的,不是的话就会抛出异常 //也就是说BindView 的使用必须在一个类里 // Verify containing type. if (enclosingElement.getKind() != CLASS) { error(enclosingElement, "@%s %s may only be contained in classes. (%s.%s)", annotationClass.getSimpleName(), targetThing, enclosingElement.getQualifiedName(), element.getSimpleName()); hasError = true; } //判断父节点如果是private 类,则抛出异常 // Verify containing class visibility is not private. if (enclosingElement.getModifiers().contains(PRIVATE)) { error(enclosingElement, "@%s %s may not be contained in private classes. (%s.%s)", annotationClass.getSimpleName(), targetThing, enclosingElement.getQualifiedName(), element.getSimpleName()); hasError = true; } return hasError; }
上面的代码里遇见注释了,这里说一下也就是我们在使用bindview注解的时候不能用使用
类不能是private修饰 ,可以是默认的或者public //in adapter private static final class ViewHolder { //.... } //成员变量不能是private修饰 ,可以是默认的或者public @BindView(R.id.word) private TextView word;
接下来还有一个方法isBindingInWrongPackage。
这个看名字也才出来个大概 就是不能在android ,java这种源码的sdk中使用。如果你的包名是以android或者java开头就会抛出异常。
private boolean isBindingInWrongPackage(Class<? extends Annotation> annotationClass, Element element) { //得到父节点 TypeElement enclosingElement = (TypeElement) element.getEnclosingElement(); String qualifiedName = enclosingElement.getQualifiedName().toString(); if (qualifiedName.startsWith("android.")) { error(element, "@%s-annotated class incorrectly in Android framework package. (%s)", annotationClass.getSimpleName(), qualifiedName); return true; } if (qualifiedName.startsWith("java.")) { error(element, "@%s-annotated class incorrectly in Java framework package. (%s)", annotationClass.getSimpleName(), qualifiedName); return true; } return false; }
到这里合法性检查就完了,如果你使用不当,就会抛出异常。
// 。。。 异常抛出了 // 不合法的直接返回 if (hasError) { return; }
这里说明一点,在处理器中抛出异常,你不能直接像平常写java代码一样new thow xxx 一样,这样抛出去的异常不太好看。所以java处理器帮我们提供了一个辅助类Messager,这个可以帮助我们
/** * Returns the messager used to report errors, warnings, and other * notices. * * @return the messager */ Messager getMessager();
比如检查使用合法性抛出的异常信息-error方法最后都会调用
private void error(Element element, String message, Object... args) { //Kind.ERROR 级别,就像你使用android的log一样 printMessage(Kind.ERROR, element, message, args); } private void note(Element element, String message, Object... args) { printMessage(Kind.NOTE, element, message, args); } private void printMessage(Kind kind, Element element, String message, Object[] args) { if (args.length > 0) { message = String.format(message, args); } processingEnv.getMessager().printMessage(kind, message, element); }
ok,到这里说了这么多才完成了检查。我们接着parseBindView的步骤2 获取值 ,这里就不多说了。好了,先休息下吧…
下一篇我们接着看。
作者:顾修忠