Android ButterKnife 的实现思路

  • 本文出自《Android源码设计模式解析与实战》中的第十六章。
    Android ButterKnife 的实现思路_第1张图片

在Android开发中,我们为了方便初始化Activity中的各种View,我们可能会使用到Jake Wharton的 ButterKnife库,这个库是针对View、资源id等进行注解的开源库,它能够去除掉一些丑陋不堪的样板式代码,使得我们的代码更加简洁、易于维护,同时基于APT也使得它的效率得到保证。
(如果你想快速了解ButterKnife的实现思路,可以先阅读 ExampleActivity$InjectAdapter类以及后续的结论,然后再回过头来阅读 )

下面我们来看看 ButterKnife 的简单使用。
首先我们看在没有使用ButterKnife时,我们初始化一个Activity中的各个控件的代码:

public class ExampleActivity extends Activity {
    TextView title;
    ImageView icon;
    TextView footer;

    @Override 
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.simple_activity);
        // 通过findViewById进行视图查找,然后进行类型转换
        title = (TextView) findViewById(R.id.title);
        icon = (ImageView) findViewById(R.id.icon);
        footer = (TextView) findViewById(R.id.footer);
    }
}

在ExampleActivity函数的onCreate函数中,我们通常会对各个子视图进行初始化,这些代码看起来重复性很高,而且丑陋不堪,几乎都要对View进行强转,当一个布局中含有十个以上的View时,再加上为某些View添加上事件处理等,这部分的代码将占用很大的篇幅。ButterKnife就是为了简化这些工作而出现的,让开发人员专注在真正有用的代码上。使用ButterKnife之后我们的代码变成了这样:

public class ExampleActivity extends Activity {
  @InjectView(R.id.title) TextView title;
  @InjectView(R.id.icon) ImageView icon;
  @InjectView(R.id.footer) TextView footer;

  @Override 
  public void onCreate(Bundle savedInstanceState) {
     super.onCreate(savedInstanceState);
     setContentView(R.layout.simple_activity);
     // 将Activity注入ButterKnife
     ButterKnife.inject(this);
  }
}

当运行完onCreate函数之后Activity中的几个View就已经被初始化了。findViewById、强制转换等样板代码被去除了,代码变得更加简单,使得我们可以更专注在代码逻辑的编写上,整个类型也更易于维护。

那么ButterKnife的原理是什么呢?@InjectView又是什么?ButterKnife的inject函数又有什么作用?

这是因为ButterKnife使用了一种叫做编译时注解的技术(即APT),代码在编译时会扫描AbstractProcessor的所有子类,并且调用这些子类的process函数,在这个函数就会将所有的代码元素传递进来。此时我们只需要在这个process函数中获取所有添加了某个注解的元素,然后对这些元素进行操作,使之能够满足我们的需求,这样我们就可以在编译期对源代码进行处理,例如生成新的类等。在运行时,我们通过一些接口对这些新生成的类进行调用以此完成我们的功能

说了这么多还是太抽象了,还是以小民的例子来为大家一一解除疑问吧。

小民自从知道ButterKnife之后也被它的魅力所吸引了,于是决定研究个究竟,经过一番搜索得知ButterKnife是基于编译时注解,然后通过APT生成辅助类,然后在运行时通过inject函数调用那些生成的辅助类来完成功能。小民决定自己写一个只支持View 的id注入的简版ButterKnife来深入学习,这个库被命名为SimpleDagger。

首先小民建了一个注解类,代码如下 : 
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.CLASS)
public @interface ViewInjector {
    int value();
}

因为我们的这个注解只支持View的id注入,因此它的目标元素是字段,它只存在在class文件中,因为一旦过了编译期我们就不再需要它了。关于注解方面的基础知识我们不做过多讲解,对这方面不了解的同学可以先阅读相关书籍,例如《Java编程思想》、《Java核心技术》。

在添加AbstractProcessor 之前,为了使Eclipse支持 APT 需要一些配置,可以参考 injectdagger。Android Studio要支持 APT则需要添加APT插件,有兴趣的同学可以自行搜索相关解决方案。

通过 APT 来生成辅助类型

添加这个注解之后,我们还需要在编译期对这个注解进行处理。上文说到,编译器会在编译时检测所有的AbstractProcessor并且调用它的process函数来让开发人员对代码元素进行处理。因此我们新建一个AbstractProcessor的子类,代码如下 :

@SupportedAnnotationTypes("org.simple.injector.anno.*")
@SupportedSourceVersion(SourceVersion.RELEASE_6)
public class ViewInjectorProcessor extends AbstractProcessor {

    //所有注解处理器的列表
    List<AnnotationHandler> mHandlers = new LinkedList<AnnotationHandler>();
    //类型与字段的关联表,用于在写入Java文件时按类型来写不同的文件和字段
    final Map<String, List<VariableElement>> map = new HashMap<String, List<VariableElement>>();
    // 生成辅助累的Writer类
    AdapterWriter mWriter;

    @Override
    public synchronized void init(ProcessingEnvironment processingEnv) {
        super.init(processingEnv);
        // 注册注解处理器
        registerHandlers();
        // 初始化代码生成器
        mWriter = new DefaultJavaFileWriter(processingEnv);
    }

    // 注册处理器
    private void registerHandlers() {
        mHandlers.add(new ViewInjectHandler());
    }

    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        // 迭代所有的注解处理器,使得每个注解都有一个处理器,
        for (AnnotationHandler handler : mHandlers) {
            // 关联ProcessingEnvironment
            handler.attachProcessingEnv(processingEnv);
            // 解析注解相关的信息
            map.putAll(handler.handleAnnotation(roundEnv));
        }
        // 将解析到的数据写入到具体的类型中
        mWriter.generate(map);
        return true;
    }
    // 代码省略
}

在ViewInjectorProcessor类的上面我们看到如下注解@SupportedAnnotationTypes(“org.simple.injector.anno.*”), 这个注解表明这个类只支持org.simple.injector.anno路径下的注解,我们的ViewInjector注解就是在这个包下。在该类的init函数中我们注册了一个注解处理器,也就是ViewInjectHandler类,该类实现了AnnotationHandler接口,该接口的声明如下 :

// 注解处理接口
public interface AnnotationHandler {
    // 关联ProcessingEnvironment
    void attachProcessingEnv(ProcessingEnvironment processingEnv);
    // 处理注解,将结果存储到Map中
    Map<String, List<VariableElement>> handleAnnotation(RoundEnvironment env);
}

该接口声明了两个函数,一个是关联ProcessingEnvironment,另一个是handleAnnotation函数,负责处理标识了ViewInjector注解的元素。小民的设计思路是定义一个AnnotationHandler接口,每个实现类处理一种类型的注解,例如ViewInjectHandler只处理ViewInject注解。下面我们看看ViewInjectHandler的核心代码 :

public class ViewInjectHandler implements AnnotationHandler {
    ProcessingEnvironment mProcessingEnv;

    @Override
    public void attachProcessingEnv(ProcessingEnvironment processingEnv) {
        mProcessingEnv = processingEnv;
    }

    @Override
    public Map<String, List<VariableElement>> handleAnnotation(RoundEnvironment roundEnv) {
        Map<String, List<VariableElement>> annotationMap = new HashMap<String, List<VariableElement>>();
        // 1、获取使用ViewInjector注解的所有元素
        Set<? extends Element> elementSet = roundEnv.getElementsAnnotatedWith(ViewInjector.class);
        for (Element element : elementSet) {
            // 2、获取被注解的字段
            VariableElement varElement = (VariableElement) element;
            // 3、获取字段所在类型的完整路径名,比如一个TextView所在的Activity的完整路径,也就是变量的宿主类
            String className = getParentClassName(varElement);
            // 4、获取这个宿主类型的所有元素,例如某个Activity中的所有注解对象
            List<VariableElement> cacheElements = annotationMap.get(className);
            if (cacheElements == null) {
                cacheElements = new LinkedList<VariableElement>();
            }
            // 将元素添加到该类型对应的字段列表中
            cacheElements.add(varElement);
            // 以宿主类的路径为key,所有字段列表为value,存入map.
            // 这里是将所在字段按所属的类型进行分类
            annotationMap.put(className, cacheElements);
        }

        return annotationMap;
    }
    // 代码省略
}

在handleAnnotation函数中小民获取了所有被ViewInject注解标识了的VariableElement元素,然后将这些元素按照宿主类进行分类存到一个map中,key就是宿主类的完整类路径,value就是这个宿主类中的所有被标识了ViewInject的VariableElement元素列表。例如将上述ExampleActivity的示例替换成小民的SimpleDagger,使用ViewInject注解标识中三个View,代码如下 :

package com.simple.apt;
public class ExampleActivity extends Activity {
  @ViewInject (R.id.title) TextView title;
  @ViewInject (R.id.icon) ImageView icon;
  @ViewInject (R.id.footer) TextView footer;

  @Override 
  public void onCreate(Bundle savedInstanceState) {
     super.onCreate(savedInstanceState);
     setContentView(R.layout.simple_activity);
     // 其他代码暂时省略
     SimpleDagger.inject(this);
  }
}

那么此时ExampleActivity的完整路径为com.simple.apt.ExampleActivity,这个完整路径我们可以通过VariableElement元素获取到,这些VariableElement就是代表了ExampleActiivty中的title、icon、footer三个对象。因此通过ViewInjectHandler的handleAnnotation处理之后我们的map中就含有了以com.simple.apt.ExampleActivity为key,以title、icon、footer三个成员变量对应的VariableElement列表为value的数据。

此时执行到process函数的最后一步,这里调用了AdapterWriter来生成辅助类,这个辅助类要生成的代码素材就是我们上述的VariableElement元素列表,调用的是AdapterWriter的generate函数,在AdapterWriter之下我们还建立了一个AbsWriter来封装一些通用逻辑,AbsWriter核心代码如下 :

public abstract class AbsWriter implements AdapterWriter {

    ProcessingEnvironment mProcessingEnv;
    Filer mFiler;
    // 代码省略

    @Override
    public void generate(Map<String, List<VariableElement>> typeMap) {
        Iterator<Entry<String, List<VariableElement>>> iterator = typeMap.entrySet().iterator();
        while (iterator.hasNext()) {
            Entry<String, List<VariableElement>> entry = iterator.next();
            List<VariableElement> cacheElements = entry.getValue();
            if (cacheElements == null || cacheElements.size() == 0) {
                continue;
            }

            // 取第一个元素来构造注入信息
            InjectorInfo info = createInjectorInfo(cacheElements.get(0));
            Writer writer = null;
            JavaFileObject javaFileObject;
            try {
                // 1、创建源文件,也就是生成辅助类
                javaFileObject = mFiler.createSourceFile(info.getClassFullPath());
                writer = javaFileObject.openWriter();
                // 2、写入package, import, class以及findViews函数等代码段
                generateImport(writer, info);
                // 3、写入该类中的所有字段到findViews方法中
                for (VariableElement variableElement : entry.getValue()) {
                    writeField(writer, variableElement, info);
                }
                // 4、写入findViews函数的大括号以及类的大括号
                writeEnd(writer);
            } catch (IOException e) {
                e.printStackTrace();
            } finally {
                IOUtil.closeQuitly(writer);
            }

        }
    }
    // 代码省略
}

在AbsWriter的generate函数中,我们定义了一个生成辅助类的逻辑骨架,分别为获取宿主类型的所有元素,并且通过第一个元素获取宿主类所在的包以及构建辅助类的类名等,然后创建一个新的java类,最后分别写入import、所有被注解的元素等信息写入到辅助类当中,所有生成的辅助类都是InjectAdapter的子类。实现代码如下的功能在DefaultJavaFileWriter类中,核心代码如下 :

public class DefaultJavaFileWriter extends AbsWriter {
  // 代码省略
    // 写入import以及类前面的类型声明
    @Override
    protected void generateImport(Writer writer, InjectorInfo info)
            throws IOException {
        writer.write("package " + info.packageName + " ;");
        writer.write("\n\n");
        writer.write("import org.simple.injector.adapter.InjectAdapter ;");
        writer.write("\n");
        writer.write("import org.simple.injector.util.ViewFinder;");

        writer.write("\n\n\n");
        writer.write("/* This class is generated by Simple ViewInjector, please don't modify! */ ");
        writer.write("\n");
        writer.write("public class " + info.newClassName
                + " implements InjectAdapter<" + info.classlName + "> { ");
        writer.write("\n");
        writer.write("\n");
        // 查找方法
        writer.write(" public void injects(" + info.classlName
                + " target) { ");
        writer.write("\n");
    }

    // 写入一个结尾的大括号
    @Override
    protected void writeEnd(Writer writer) throws IOException {
        writer.write(" }");
        writer.write("\n\n");
        writer.write(" } ");
    }

    // 写入字段
    @Override
    protected void writeField(Writer writer, VariableElement element, InjectorInfo info)
            throws IOException {
        ViewInjector injector = element.getAnnotation(ViewInjector.class);
        String fieldName = element.getSimpleName().toString();
        writer.write(" target." + fieldName + " = ViewFinder.findViewById(target, "
                + injector.value() + " ) ; ");
        writer.write("\n");
    }
}

在DefaultJavaFileWriter中分别写入了辅助类的各个部分,最终的是写入字段的部分,也就是writeField函数。在该函数中,小民获取了这个字段的名字,并且写下了一行如下一行代码 :

target.fieldName = ViewFinder.findViewBydId(target, ViewInject注解的值); 

其实这就是一个初始化某个View的语句,这个target在这个例子中就是ExampleActivity,这个ViewInject注解的值就是View的id,我们知道每个含有id的View最终都会在R类中生成一个整型的数值,这里的view id就是这个整型数值。需要注意的是这些被添加注解的字段都必须是非私有的,否则不能通过target.fieldName的形式直接访问。这些初始化代码都被写到了InjectAdapter子类的inject函数中,inject函数传递一个target参数,这个target就是元素所在的类,比如ExampleActivity,而生成的辅助类的名称格式为宿主类+” InjectAdapterExampleActivity InjectAdapter,它与ExampleActivity在同一个包中,因此可以访问到ExampleActivity的protected、package权限的字段。

InjectAdapter 接口

InjectAdapter的声明如下 :

public interface InjectAdapter<T> {
    void injects(T target);
}

ExampleActivity$InjectAdapter 类

这相当于我们为每个元素都生成一行初始化代码来替换手动在ExampleActiivty中进行findViewById,当我们在ExampleAcivity的onCreate函数中调用SimpleDagger的inject函数时,会将ExampleActivity传递到InjectAdapter中,因此最后为ExampleActivity生成的辅助类就成为了如下这样 :

public class ExampleActivity$InjectAdapter implements InjectAdapter<ExampleActivity> { 

  public void injects(ExampleActivity target)  { 
      target.title = ViewFinder.findViewById(target, 2131099648  ) ; 
      target.icon = ViewFinder.findViewById(target, 2131099649  ) ; 
      target.footer=ViewFinder.findViewById(target, 2131099332  ) ; 
  }
}

当调用SimpleDagger的inject时就会先通过传递进来的类名构建一个InjectAdapter子类的类名,例如传递进来的是ExampleActivity,那么此时的辅助类的类名为 ExampleActivity$InjectAdapter,它InjectAdapter的子类。拿到完整类名之后再反射构建一个对象,然后转换为InjectAdapter,最后调用inject函数。而这个生成的ExampleActivity$InjectAdapter的inject函数中又对每个View进行了findViewBydId,也就是对它们进行了初始化。至此,这些View字段就被自动初始化了!

我们最后再来捋一捋这个过程,大致分为如下几步 :

  1. 通过ViewInject注解标识一些View成员变量;
  2. 通过ViewInjecyProcessor捕获添加了ViewInject注解的元素,并且按照宿主类进行分类;
  3. 为每个含有ViewInject注解的宿主类生成一个InjectAdapter辅助类,并且在它的inject函数中生成初始化View的代码;
  4. 在SimpleDagger的inject函数中构建生成的辅助类,此时内部会它这个InjectAdapter辅助类的inject函数,这个函数中又会初始化宿主类中的View成员变量,至此,View就已经被初始化了。

SimpleDagger的完整代码在这里,有兴趣的同学可以下载下来进行学习以及扩展。

需要注意的是在eclipse中使用APT需要添加JRE库的引用,在Android Studio则需要引用APT的插件。

其他参考资料

  • Java注解处理器
  • 万能的APT!编译时注解的妙用

你可能感兴趣的:(设计模式,android,apt,ButterKnif,编译时注解)