cover_image

Butter Knife 源码解析

姚家艺 Android程序员 2016年03月16日 00:29

Butter Knife 相信很多朋友都用过,然而它究竟是如何工作的,大多数人估计了解得并不清楚,今天的投稿嘉宾姚家艺同学非常详细地为大家讲解这背后的原理。


其实之前拒绝过一些源码解析类的文章投稿,主要还是担心在公众号上大段代码阅读效果糟糕,然而也有一些朋友反馈希望看到这类文章,因此还是尝试推一下这篇,大家也可以选择在 PC 上阅读,效果更佳。因为微信公众号正文限制了外链,所以链接都是不可点击的,部分概念可 Google 了解。


姚家艺同学目前在爱奇艺任职,是一位热爱开源的小伙伴,曾对主流下拉刷新开源库的性能,进行过全面的对比分析,相当精彩;也解析过热门开源项目 Fresco 的源码,文章均发布在其 GitHub 上,深受大家欢迎,有兴趣的朋友可以前往关注:https://github.com/desmond1121,也可以点击文末阅读原文直接访问。

Jake Wharton的黄油刀ButterKnife,用过的人都说好。如果没有用过,请看这段代码:

public DemoActivity extends Activity{

   @Bind(R.id.text_view) TextView tv;
   @Bind(R.id.some_view) SomeView sv;
   
   @Override
   public void onCreate(Bundle savedInstanceState) {
       super.onCreate(savedInstanceState);
       setContentView(R.layout.demo_activity);
       // 自动帮你生成所有被@Bind注解
       // 的视图的findViewById(id)代码
       ButterKnife.bind(this);
       
       // 现在可以随意使用视图变量。
   }

   // 自动绑定OnClickListener
   @OnClick(R.id.submit)
   public void sayHi(Button button) {
       button.setText("Hello!");
   } }

更多炫酷的使用可以看Butter Knife官网!心动了吗?在复杂的页面中用上它不知道能为你省去多少样板代码,代码质量看起来就提高了一个档次! 这么厉害的手段不研究一下怎么行,赶紧来看看。

前言

首先介绍一些预备知识。

注解的解析方式

通过元注解@Retention来标识,有三种:

  • RetentionPolicy.SOURCE: 源码级别解析,例如@Override, @SupportWarnings。这类注解在编译成功后就不会再起作用,并且不会出现在.class文件中。

  • RetentionPolicy.CLASS: 编译时解析,默认的解析方式,会保留在最终的.class中,但是无法在运行时获取。

  • RetentionPolicy.RUNTIME: 运行时解析,会保留在最终的.class文件中。这类注解可以用反射API中的getAnnotations()获取到。

更多注解基础知识可以参考codekk公共技术点

http://a.codekk.com/detail/Android/Trinea/公共技术点之%20Java%20注解%20Annotation

编译时解析

编译时解析是注解强大的地方之一。你可以利用它在编译时帮你生成一些代码逻辑,避免了运行时利用反射解析所带来的性能开销。那么问题来了,注解是怎么在编译的时候被解析的呢?

Java 5带有Annotation Processing Tool(APT)。它能够提供一个编译时的注解处理,并且能够产生新的代码与文件,同时能够让java编译器将生成的代码和原来的代码一起编译!与之配套的还有Mirror API,它提供在编译时对程序结构的静态、只读的分析。这个功能很强大,但是处理起来略显麻烦。Java 6开始将这个功能整合进编译器中,你只要继承AbstractProcessor,并在javac中通过参数-processor指定注解处理就好了。

当然你也可以不指定具体的类,在META-INF/service/下创建文件javax.annotation.processing.Processor,在里面指定类名(全名)。Java的ServiceLoader会自己去找到这个类并编译。

说到这里,不得不说的两个库:

  • google-auto-service: 只要在你的Processor类上加上注解@AutoService(Processor.class),它自动帮你将这个类添加到META-INF/service/javax.naaotation.processing.Processor下,非常好用。

  • android-apt, 在gradle插件中整合了-processor-processorPath命令,还可以指定project,轻轻松松实现APT。

看完这段心里大概对实现原理有一个初步的猜测,那么开始挖掘代码吧!

从Bind开始

ButterKnife能够提供的注解类型太多了,本文就以解析@Bind注解为例。所有注解绑定都是通过一句ButterKnife.bind()函数开始的,我们先来看看它做了些什么

static void bind(Object target, Object source, Finder finder) {
   Class<?> targetClass = target.getClass();
   try {
       if (debug) Log.d(TAG, "Looking up view binder for " + targetClass.getName());
       ViewBinder<Object> viewBinder = findViewBinderForClass(targetClass);
       if (viewBinder != null) {
           viewBinder.bind(finder, target, source);
       }
   } catch (Exception e) {
       throw new RuntimeException("Unable to bind views for " + targetClass.getName(), e);
   }
}

这个findViewBinderForClass(),负责找到ViewBinder,我们可以看看ViewBinder是干嘛的:

public interface ViewBinder<T> {
   void bind(Finder finder, T target, Object source)
}

Finder是一个枚举,它的作用就是适配各类findViewByIdFinder.findView()函数上,比如你用ButterKnife.bind(Activity activity)时调用的就是bind(target, target, Finder.ACTIVITY)
可以看看Finder.ACTIVITY的声明:

ACTIVITY {
   @Override
   protected View findView(Object source, int id) {
       return ((Activity) source).findViewById(id);
   }
   
   @Override
   public Context getContext(Object source) {
       return (Activity) source;
   }
}

很清楚吧。那么我们话题回到ViewBinder上,你会发现全局实现它的只有一个啥也没干的类NOP_VIEW_BINDER。纳尼?好吧,我们先看看findViewBinderForClass是怎么去找的,希望从这寻求到突破点。

private static ViewBinder<Object> findViewBinderForClass(Class<?> cls)
   throws IllegalAccessException, InstantiationException
{
   // (省略代码)缓存中有就从缓存中取
   // (省略代码)"java."与"android."开头的包中的类,返回NOP_VIEW_BINDER  
   try
{
       Class<?> viewBindingClass = Class.forName(clsName + "$$ViewBinder");
       viewBinder = (ViewBinder<Object>) viewBindingClass.newInstance();
   } catch (ClassNotFoundException e) {
       viewBinder = findViewBinderForClass(cls.getSuperclass());
   }
   //(省略代码)将viewBinder放入缓存
   return viewBinder;
}

我刚开始看到这的时候心中打了一个大大的问号,啥玩意?Class.forName(clsName + "$$ViewBinder")??这是啥,好的吧,至此正常路线就看不到任何有价值的东西。那么问题留在这里,我们开始看Processor。

真相大白 - 注解处理与JavaPoet

实际上从前言中介绍的注解处理你已经心理有个谱,就是在注解处理器里面重写一下process方法。那么JW大神是怎么生成的代码?生成什么样的代码?我们还是应该去一探究竟。

首先看一下ButterKnifeProcessor这个类,顶上一个亮闪闪的@AutoService(Processor.class),从此不用再关心Processor被javac解析的事情了。先来看看process方法里干了什么吧:

@Override
public boolean process(Set<? extends TypeElement> elements, RoundEnvironment env) {

   Map<TypeElement, BindingClass> targetClassMap = findAndParseTargets(env);

   for (Map.Entry<TypeElement, BindingClass> entry : targetClassMap.entrySet()) {
       TypeElement typeElement = entry.getKey();
       BindingClass bindingClass = entry.getValue();
       try {
           bindingClass.brewJava().writeTo(filer);
       } catch (IOException e) {
           error(typeElement, "Unable to write view binder for type %s: %s", typeElement, e.getMessage());
       }
   }

   return true; }

这个函数信息量很大,我将它拆成两部分来说:

1. 解析所有注解 - findAndParseTargets.

private Map<TypeElement, BindingClass> findAndParseTargets(RoundEnvironment env) {

   Map<TypeElement, BindingClass> targetClassMap = new LinkedHashMap<>();
   Set<TypeElement> erasedTargetNames = new LinkedHashSet<>();

   for (Element element : env.getElementsAnnotatedWith(Bind.class)) {
       if (!SuperficialValidation.validateElement(element)) continue;
       
       try {
           parseBind(element, targetClassMap, erasedTargetNames);
       } catch (Exception e) {
           logParsingError(element, Bind.class, e);
       }
   }
   ...
}

这部分代码浅显易懂,解析所有的被@Bind所注解的Element(肯定是field,因为@Bind只能修饰field),并处理它。在parseBind中会初步判断Element的修饰符,不可以是privatestatic下的变量,不可以是私有类下的变量,也不可以是非类对象下的field(比如enum)。之后判断被注解的field是否List、Array,但是它们最终都会走向parseBindOne()函数(解析单个Bind)。那么我们来看看这个函数:

private void parseBindOne(Element element,
   Map<TypeElement, BindingClass> targetClassMap,
   Set<TypeElement> erasedTargetNames)
{
   // (省略代码)验证element是否继承自View
   // (省略代码)验证@Bind中是否至少含有一个id
   // (省略代码)如果上述两个验证失败,则退出。
   
   int
id = ids[0];
   
   // 判断element所在的类是否存在对应的BindingClass
   BindingClass bindingClass = targetClassMap.get(enclosingElement);
   if (bindingClass != null) {
       // (省略代码)判断待绑定的id已经被绑定过
   } else {
       // 创建一个对应类的BindingClass
       bindingClass = getOrCreateTargetClass(targetClassMap, enclosingElement);
   }
   
   String name = element.getSimpleName().toString();
   TypeName type = TypeName.get(elementType);
   boolean required = isFieldRequired(element);
   // 在对应BindingClass中添加FieldViewBinding&id组合
   FieldViewBinding binding = new FieldViewBinding(name, type, required);
   bindingClass.addField(id, binding);
   // 将绑定变量所在的类添加到待unBind序列中。
   erasedTargetNames.add(enclosingElement);
}

这里出现了两个新词汇:BindingClassFieldViewBinding,我们来看看它们都是啥。

  • BindingClass:所有应用ButterKnife的类都有一个单独的BindingClass与其挂钩,它里面包含了很多信息:Field绑定、Drawable绑定、Collection绑定等等很多信息。以类名+”$$ViewBinder”、包名标识。这个东西似乎前面看见过啊!有点激动,继续往下看。

  • FieldViewBinding:存放于BindingClass内,用于绑定变量View(与之对应还有绑定Drawable、Bitmap、List、Method等)。记录每个Field的绑定信息,如变量名、类名。每个FieldViewBinding都与一个id对应。同一个id添加第二个FieldViewBinding时会在之前出现的parseBindOne()函数中报错。

如果@Bind修饰符不为空,则意味着该FieldViewBinding需要绑定。

至此@Bind的解析工作就结束了,所有的信息都以FieldViewBinding的形式存在于注解所在的类对应的BindingClass中。

2. 生成代码

再次介绍一个大杀器javapoet,square出品的生成.java文件的Java API。通过清晰的语法来生成一个文件结构,无敌。

那我们再回到原先的注解处理process()方法中。它会遍历每一个生成的BindingClass,并调用bindingClass.brewJava().writeTo(filer);。其中filer是通过ProcessingEnvironment.getFiler()获取的,它的作用在注释中说了:

Returns the filer used to create new source, class, or auxiliary files.

就是用来生成代码的,APT允许你生成新的代码并且将他们一起编译。由此看来writeTo(filer)方法就是将生成的代码结构变成.java文件,看来奥秘应该是在brewJava()这个函数中!那我们赶紧来看看!

这个函数会做几件事情:

生成一个public class {CLASS}$$ViewBinder<T extends {CLASS}> implements ViewBinder<T>的类结构。其中{CLASS}代表了ButterKnife注解所在的类名。这就解释了我们最开始百思不得其解的Class<?> viewBindingClass = Class.forName(clsName + "$$ViewBinder")。同时它会在其中添加一个实现方法bind(finder, target, source),我们来看看它是怎么做的:

private MethodSpec createBindMethod() {
   //添加函数头
   MethodSpec.Builder result = MethodSpec.methodBuilder("bind")
       .addAnnotation(Override.class)
       .addModifiers(PUBLIC)
       .addParameter(FINDER, "finder", FINAL)
       .addParameter(TypeVariableName.get("T"), "target", FINAL)
       .addParameter(Object.class, "source");
   // (省略代码) 是否绑定资源
   // (省略代码) 是否调用父类的bind
   // If the caller requested an unbinder, we need to create an instance of it.
   if (hasUnbinder()) {
       final String statment;
       if (parentUnbinder != null) {
           // Explicitly call super in case this class has child's as well.
           statment = "$T unbinder = super.accessUnbinder($N)";
       else {
           statment = "$T unbinder = createUnbinder($N)";
       }
       result.addStatement(statment, unbinderBinding.getUnbinderClassName(), "target");
   }
   
   // 正主来了!
   if (!viewIdMap.isEmpty() || !collectionBindings.isEmpty()) {
       // 声明一个View供后续调用
       result.addStatement("$T view", VIEW);
       // 对每一个绑定过的id,添加绑定代码
       for (ViewBindings bindings : viewIdMap.values()) {
           addViewBindings(result, bindings);
       }
       // (省略代码) List/Array的绑定
   }
   // (省略代码) Resource/Bitmap/Drawable的绑定
   return result.build();
}

接下来计入到addViewBindings中,分作两部:找到View,将View赋值给变量。

  • 找到View:如果该ViewBinding需要绑定,则添加如下语句:

    result.addStatement("view = finder.findRequiredView(source, $L, $S)", bindings.getId(), asHumanDescription(requiredViewBindings));

    我们看看Finder.findRequiredView()里面是什么,它会添加到到时候执行的代码之中:

    public <T> T findRequiredView(Object source, int id, String who) {
       T view = findOptionalView(source, id, who);
       if (view == null) {
           //抛出异常
       }
       return view;
    }

    public
    <T> T findOptionalView(Object source, int id, String who) {
       View view = findView(source, id);
       return castView(view, id, who);
    }

    还记得开头出现的Finder.ACTIVITY吗!回去看看,恍然大悟有没有!绑定不同的Finder会有不同的代码加到生成的代码里去寻找View。JakeWharton真tm是个天才。

  • 赋值给原变量
    View找着了之后呢,肯定要把它赋值回原来的变量里:

    private void addFieldBindings(MethodSpec.Builder result, ViewBindings bindings) {
       Collection<FieldViewBinding> fieldBindings = bindings.getFieldBindings();

       for
    (FieldViewBinding fieldBinding : fieldBindings) {
           if (fieldBinding.requiresCast()) {
               result.addStatement("target.$L = finder.castView(view, $L, $S)", fieldBinding.getName(),
               bindings.getId(), asHumanDescription(fieldBindings));
           } else {
               result.addStatement("target.$L = view", fieldBinding.getName());
           }
       }
    }

上面这段代码已经非常简明易懂:如果需要Cast,就添加一个cast语句的赋值;否则直接赋值。

最终都会通过Javapoet生成一个文件流写入到Filer中,被javac编译。

这一切都只是通过一个注解和一句ButterKnife.bind()


如果喜欢这篇,欢迎帮助转发,点击文末阅读原文,可直接访问姚家艺同学GitHub主页。

继续滑动看下一个
Android程序员
向上滑动看下一个