Android资源注入框架分析(VitaminSaber)

引言

文章最开篇,我们先介绍一下这个注解框架的内容:VitaminSaber是一个轻量级的注解框架,也就算是ButterKnife和Dragge的一个功能点吧。它所要完成的任务是,通过注解的方式来对域进行资源注入。

这套注解框架的注解是CLASS级别的,后续我还会补上SOURCE和RUNTIME级别的注解框架的分析,RUNTIME级别的注解是可以通过反射API机制 + 动态代理的模式来处理的,甚至利用AspectJ框架处理的方式来完成注入,是一种很有技术的做法。后续也会利用这套方案做一个开源的项目来深入学习和分析。

下面通过一个实例来了解一下它的用法。


public class MainActivity extends AppCompatActivity {
@InjectResource(R.id.whiteColor)
int btnTextColor;

 @Override
 public void onCreate(Bundle savedInstanceState) {
      super.onCreate(savedInstanceState);
      setContentView(...);

      VitaminSaber.inject(this);   // like butterknife bind() method.
                ...
                Button btn = (Button) findViewById(R.id.btn);
                btn.setTextColor(btn);
 }

}


/**

  • Annotation for fields which indicate that it should be looked up in the activity intent's extras
  • or fragment arguments.
  • The extra will automatically be cast to the field type. If no key is provided, the variable name
  • will be used.
  • {@literal @}InjectExtra("key") String title;
  • {@literal @}InjectExtra String content; // "content" is the key for the extra

*/
@Retention(CLASS)
@Target(FIELD)
public @interface InjectResource {
int value() default 0;
}

这是一个只能为成员域注解的一条注解,同时,且虚拟机不会载入的注解。
就是通过一条注解和一条inject方法调用,资源就完成了对域的注入。那么,背后原理是怎样的? 现在开始娓娓道来!

一、框架目录结构。


-- vitaminSaber
------- src
--------- main
------------- java
---------------- com
------------------- w2ji
--------------------- vitaminsaber
------------------------- internal
----------------------------- FieldBinding.java // 被绑定域的描述类。
----------------------------- InjectResourceProcessor.java // 注解处理器的实现类
----------------------------- ResourceInjection.java // 被绑定域与资源id作为key的描述文件,关系 1(resid):n(fieldbinding)
----------------------------- ResourceInjector.java // 主要用来构建处理注入的Java文件

------------------------- InjectResource.java // 注解的源文件
------------------------- VitaminSaber.java // 注解处理器产生的注入处理和目标Activity等的Hook文件
---------------- resources
---------------- META-INF
---------------- services
-------------------- javax.annotation.processing.Processor

二、文件介绍

1.FieldBinding.java
FieldBinding是一个描述的实体类,它有两个成员,一个是String类型的name属性,一个TypeMirror类型的type类,这个类所要描述就是被绑定域的名字和类型是怎样的。
什么意思呢?根据我们上面提供的例子来说明:
btnTextColor就是我们想要绑定的一个类成员,所以name就是要描述这个类成员的名字,name = “btnTextColor”,那么TypeMirror又是一个什么东西呢?Java提供的文档是这么说明的:

javax.lang.model.type
Interface TypeMirror
Represents a type in the Java programming language. Types include primitive types, declared types, array types, type variables, and the null type.

上面这段话的意思就是,TypeMirror用来表示Java编程语言中的一种类型,在本例中,btnTextColor的TypeMirror就是int类型的。

2.InjectResourceProcessor.java
InjectResourceProcessor是整个注解框架的核心之一,它表示一个注解处理器。首先我们来介绍一下什么是注解处理器。


一条注解语句,如上述我们使用的一样,是不会对源码编译或者运行产生任何影响的,也就是说,无论给方法,域,构造器,包等加上一个注解,在没有注解处理器的情况下,这些注解是无法发挥作用的。
那么我就得知,注解处理的一个核心就是注解处理器。只有在注解处理器的解析处理下,注解才能发挥它强大的作用。

下面的解释引自《Java 核心技术 卷2 》

从Java SE6开始,我们可以将注解处理器添加到Java编译器中,编译器会定位源代码中的注解,然后可以选择相应的注解处理器,每个注解会依次执行。如果某个注解处理器创建了一个新的源文件,那么将重复执行这个处理过程,如果某次处理循环没有在产生任何新的源文件,那么所就编译所有的源文件。

以上这段话有这样几个点需要我们了解或者深入的。
1 注解处理器是如何添加到Java编译器?
2 Java编译器是如何选择注解处理器的?
3 如何利用注解处理器来创建一个文件?

对于问题1,我们需要看这个文件,resource/META-INF/services/java.annotation.processing.Processor,这个文件就是用来描述Processor所在的绝对路径。有了这个路径,编译器就可以定位到Processor所在的位置了。
对于问题2,暂时没有研究到!等深入学习之后,会再补充上。
对于问题3,在后面的代码中会提到,这里暂时先不讨论。

注解处理器是拓展自AbstractProcessor,首先我们需要在文件中指明Processor能支持哪些注解。


@SupportedAnnotationTypes("InjectResource") // 方法1:通过注解的方式
@SupportedSourceVersion(SourceVersion.RELEASE_7)
public class InjectResourceProcessor extends AbstractProcesor {

 // 方法2:通过方法的方式
 @Override
 public Set getSupportedAnnotationTypes() {
     Set supportTypes = new LinkedHashSet();
     supportTypes.add(InjectResource.class.getCanonicalName());
     return supportTypes;
 }

 @Override
 public SourceVersion getSupportedSourceVersion() {
      return SourceVersion.lastestSupported();
 }

}

接着,我们要指明注解处理器支持的JDK版本,也是通过两个方式,任选其一。
其实,注解处理器最重要的方法就是 process()方法,这里我们暂时先不讨论,等我们从实例入手,顺藤摸瓜,自然会说到process的作用和用法。

接下来,我们不去逐一的分析文件了,我们从用法开始,一点点往深入的探索它的原理,哪里用到什么,就说明什么。

我们在MainActivity中已经看过了它的用法。
利用VitaminSaber.inject(this);完成了初始化的工作,那么就看看这个inject方法到底做了什么。


pubilc static void inject(Activity activity) {
inject(activity, activity, ResourceFinder.CONTEXT);
}

这里是调用了inject的一个重载方法,那么就看看这个方法。


static void inject(Object context, Object target, ResourceFinder resourceFinder) {
Class targetClass = target.getClass();
try {
...
Method inject = findInjectorForClass(targetClass);
if (null != inject) {
inject.invoke(null, resourceFinder, target, context);
}
} catch (Exception e) {
...
}
}

这个三个参数是什么含义,我们看代码,他们在程序里分别做了什么,就知道他们的含义了。
从源码中,我们看到了这三个参数的作用,通过一个Method的反射调用作为参数传递了进去,那么这个inject方法是什么呢,ResourceFinder又是什么呢?我们接着向下看。
首先targetClass是通过target得到的一个class对象,从上面可以得知,target参数就是activity这个对象,那么也就是MainActivity的一个对象,那targetClass就是MainActivity的class对象。
接着,inject是通过findInjectorForClass()方法得到的,这个方法传入了一个targetClass,返回一个Method对象,那么就得知inject就是targetClass中的一个方法实例。


private static Method findInjectorForClass(Class targetClass) {
...
Class injector = Class.forName(clsName + InjectorResourceProceesor.SUFFIX);
inject = injector.getMethod("inject", ResourceFinder.class, cls, Object.class);
...
return inject;
}

首先这个方法已经触及了真整个注解框架的核心点之一。
clsName是targetClass得到的,我们知道了clsName就是“MainActivity”,那么clsName+InjectorResourceProcessor.SUFFIX = MainActivity$$ResourceInjector,那么Class.forName()就可以得到MainActivity$$ResourceInjector的一个实例了。
MainActivity$$ResourceInjector.class是通过Processor动态生成的源文件并且经过编译得到的字节码文件。这个我们会在Processor中的process方法中提到。

Class.forName方法通过传入的name,反射的方式生成了一个name类的实例,而这个实例就是MainActivity$$ResourceInjector的实例,然后在这个类实例中查找这样一个方法,name为inject的方法,参数为ResourceFinder, MainActivity, Object(resource),
inject的第三个参数就是Activity实例,得到这个method对象之后,return就ok了。

目前为止,MainActivity$$ResourceInjector.class中的inject(ResourceFinder, MainActivity, Object)已经调用了。那么我们来看一下MainActivity$$ResourceInjector里面到底是什么?


package hm.su.skinchange; // package name

import com.w2ji.vitaminsaber.VitaminSaber.ResourceFinder; // import
import hm.su.skinchange.MainActivity; // import

public class MainActivity$$ResourceInjector {
public MainActivity$$ResourceInjector() { // constructor
}

public static void inject(ResourceFinder finder, MainActivity target, Object resource) {
    Object object = finder.getResource(resource, 2131361811, "java.lang.Integer");
    if(object == null) {
        throw new IllegalStateException("Required resource with key \'2131361811\' for field \'btnTextColor\' was not found.");
    } else {
        target.btnTextColor = ((Integer)object).intValue();
    }
}

}

文件中的inject方法就是上文中我们提到的那个被调用的方法,看看里面的实现,


{
// 首先调用了finder的getResource()方法,target = resource, resourceId = 2131361811, variableType = Integer
// 这里保持两个疑问,R.java中的2131361811是从哪里来的?"javallang.Integer"从哪里来的?
Object object = finder.getResource(resource, 2131361811, "java.lang.Integer");
...
else {
target.btnTextColor = ((Integer)object).intValue();
}
}

那么ResourceFinder的作用在于此,利用ResourceFinder在对应的Resource下面找资源,ResourceFinder是一个enum枚举类,一共两个实例,
CONTEXT,FRAGMENT,
我们举例来看看CONTEXT的做法:


CONTEXT {
// target:activity 利用activity得到Resources对象,这是资源管理框架部分的知识。
// resourceId: 资源id
// variableType: 变量类型 资源的类型
@Overrdie
public Object getResource(Object target, int resourceId, String variableType) {
...
return activity == null ? null : findResourceType(activity.getResource(), resourceId, variableType);
}
}

我们可以看到,finder调用了getResource()方法,resource就是Resources的对象,2131361811是一个资源id,java.lang.Integer就是资源的类型了。接下来的判断就是注入的过程,在else中,target.btnTextColor被设置了一个值,这个值就是
object,也就是资源值。

由此,我们可以得出一个结论了,所谓的资源注入的浅层次含义就是给目标域赋值。
现在我们可以简单的梳理一下前端部分的流程。

  1. 首先,在MainActivity的onCreate方法中,完成inject方法调用。
  2. inject调用VitaminSaber类中的另一个重载方法。
  3. inject方法去创建MainActivity$$ResourceInjector实例,同时利用反射原理得到类中的一个inject方法。
  4. MainActivity$$ResourceInjector的inject方法完成资源的注入。
  5. 调用MainActivity$$ResourceInjector的inject方法完成注入动作。

1和2这两个流程没有什么问题,我们通过阅读源码就能弄懂在做什么,那么MainActivity$$ResourceInjector是怎么产生的?这就回到了我们需要再次说明Processor的process方法了。
process方法是AbstractProcessor中主要的方法之一,它要完成的任务就是处理注解并且可以完成例如文件创建、编译等工作。

接下来我们简单的说说注解的一些用法和语法。if(大神) 请忽略;
在Java中,注解是作为一个修饰符来使用的,它被置于被注解项之前,中间没有分号,注解是代码的一部分。注解不会改变程序的编译方式,Java编译器对于包含注解和不包含注解的代码会生成相同的虚拟机指令。
为了能够受益于注解带来的优势,我们需要选择一个处理工作,然后向处理工具可以理解的代码中插入注解,之后运用该处理工具处理代码。

三、注解的语法

modifiers @interface AnnotationName {
elementDeclaration1
elementDeclaration2
...
}
每个元素声明都具有下面的形式:
type elementName();
或者
type elementName() default value;

所有的注解都是隐式地扩展自java.lang.annotation.Annotation接口,这个接口是一个常规接口,不是一个注解接口。
注解接口中的元素声明(elementName())实际上方法声明,一个注解接口的方法不能有任何参数和任何的throws语句,并且他们也不能是泛型的。

注解元素的类型为下列之一:
基本类型(int short long byte char double float boolean)
String类型
Class(具有一个可选的类型参数,如Class)
enum类型
注解类型
由前面所述类型组成的数组


public @interface BugReport {
enum Status {CONFIRMED, FIXED, NOTBUG};
boolean showStopper() default false;
String assignTo() default "{none}";
Class testCase() default Void.class;
Status status() default Status.CONFIRMED;
Reference ref() default @Reference(); // an annotation type
String[] reportedBy();
}

注意,一个注解元素永远不能设置为null,甚至不允许将其默认值设置为null,这样在实际应用中会相当不方便,你必须使用其他的默认值,例如“” 或者 Void.class。

那么在Java语言中,那么类型可以使用注解呢?
包(Package)
类(Class) 包括enum
接口(Interface)包括注解接口
方法(Method)
构造器(Constructor)
实例域(Field) 包含enum常量
局部变量
参数变量

注意,局部变量的注解只能在源码级别上处理,类文件并不描述局部变量。因此,所有的局部变量注解在编译玩一个类的时候,就都被遗弃掉了。同样地,对包的注解不能在源码级别之外存在。
一个项可以具有多个注解,只要他们属于不同类型即可,当注解一个特定项的时候,不能多次使用同一个注解类型:


@BugReport(showStopeper = true, reportedBy = "Joe")
@BugReport(reportedBy = ["Harris"])
void method();

也就是说不能同时使用@BugReport两次,这是一个编译期的错误,对于这样的问题可以设计更简单的注解来处理:
@BugReports({
@BugReport(showStopper=true, reportedBy="Joe"),
@BugReport(reportedBy={"Harry", "Carl"})
})
void method();

元注解:
元注解用于描述接口的行为属性。元注解有:
@Target 指明可以应用这个注解的哪些项
@Retention 指明这个注解可以保留多久
@Document 指明这个注解应该包含在注解项的文档中
@Inherited 指明当这个注解应用于一个类的时候,能够自动被它的子类继承

@Target,这个是一个枚举类型的元注解,类型为ElementType,可以指定任意数量的元素类型,用括号括起来。
ANNOTATION_TYPE 注解类型的声明
PACKAGE 包
TYPE 类 包括enum和接口
METHOD 方法
CONSTRUCTOR 构造器
FIELD 成员域
PARAMETER 方法或构造器参数
LOCAL_VARIABLE 局部变量

@Retention元注解用于指定一条注解应该保留多长时间,单值,默认值为RetentionPolicy.CLASS。
SOURCE 不包括类文件中的注解
CLASS 包括类文件中的注解,但是虚拟机不需要将他们载入
RUNTIME 包括在类文件中的注解,并由虚拟机载入,通过反射API可以得到他们。

@Inherited元注解只能应用于类上,如果一个类具有继承注解,那么他们的子类都自动具有同样的注解。

注解的基本知识就先介绍到这里,以上的知识均来自于《java 核心技术 卷2》,接着来看AbstractorProcessor,官方文档给出了这样的描述,

四、Processor部分

This class examines annotation values to compute the options, annotations, and source version supported by its subtypes.
意思是说,AbstractProcessor会检查注解的值,来计算子类所支持的选项,注解,和源版本。


首先,子类的Processor需要初始化,那么提供初始化就是Processor中的void init(ProcessingEnvironment processingEnv)这个方法,看看这个方法。
// Initailizes the processor with the processing environment by setting the processingEnv field to the value of the processingEnv argument. An IllegalStateException will be thrown if this method is called more than once on the same object.
// 通过processingEnv来初始化processor。 如果这个方法被调用超过一次的话,将会抛出一个IllegalStateException异常。
// ProcessingEnvironment是一个接口,一个注解处理工具框架将会提供一个注解处理器,这个注解处理器就实现了这个接口,有了这个接口的帮助,processor就可以是使用该框架提供的功能来实施文件的编写,报告错误消息,并查找其他实用工具。
public void init(ProcessingEnvironment processingEnv) {
// 一般来说,我们需要在这里完成一些成员变量的初始化工作。
super.init(processingEnv);
elementUtils = processingEnv.getElementUtils(); // 操作元素的方法
typeUtils = processingEnv.getTypeUtils(); // 操作元素类型的方法
filer = processingEnv.getFiler(); // 返回的filer是用来创建新文件,类,或者备用文件
}

然后就是关键的process方法了,我们来看看process方法是如何定义且完成我们需要的任务的。


// 第一个参数annotations是一个Set类型的集合,泛型是TypeElement的子类,TypeElement是表示一个类或者一个接口的类型,如我们定义的注解一样,InjectResource注解的TypeElement就是InjectResource这个注解接口类型。 annotations官方文档给的解释是“请求去被处理的注解类型”。
// roundEnv:environment for information about the current and prior round.意思是当前一轮处理处理注解的环境和次轮处理注解的环境。
// 当我们创建了注解处理器,并且指明了注解处理器的路径后,在编译时编译器会定位源码中的注解,然后就可以选择对应的注解处理器,每个注解处理器会依次执行。如果某个注解处理器创建了一个新的源文件,那么将重复
// 这个执行过程,如果某次处理循环没有再产生任何新的源文件,那么就编译所有的源文件。
// 有了上面的解释,我们就不难理解RoundEnvironment的含义了。每一次有注解处理器处理注解的时候,就是一轮,如果新的文件产生,编译器就会在一次定位注解,如果有注解被处理,就是prior轮,次轮。
@Override
public boolean process(Set // 那么VitaminSaber的process方法都做了什么。
Map targetClassMap = findAndParseTargets(roundEnv);

 for(Map.Entry entry : targetClassMap.entrySet()) {
     TypeElement typeElement = entry.getKey();
     ResourceInjector extraInjector = entry.getValue();
     try {
          JavaFileObject jfo = filer.createSourceFile(extraInjector.getFqcn(), typeElement);
          Writer writer = jfo.openWriter();
          writer.write(extraInjector.brewJava());
          ...
     }
 }

 return true;

}

方法中的第一条语句就调用了一个findAndParseTargets(RoundEnvironment)方法,然后这个方法会返回一个Map,map的key是TypeElement,上面已经提到了TypeElement,表示一个类或者接口类型,value是ResourceInjector,表示一个资源注入器。 这里出现了一个ResourceInjector,那就进去看看它是什么样的一个类。


final class ResourceInjector {
// injectionMap 表示一个key为唯一资源id,value为资源注入实体的一个Map
private final Map injectionMap = new LinkedHashMap();
private final String classPackage; // 类所在的包
private final String className; // 类名
private final String targetClass; // 目标类的字符串表示
private String parentInjector;

ResourceInjector(String classPackage, String className, String targetClass) {
    this.classPackage = classPackage;
    this.className = className;
    this.targetClass = targetClass;
}

...

// 核心方法,利用StringBuilder来创建如MainActivity$$ResourceInjector的源码部分
String brewJava() {
    StringBuilder builder = new StringBuilder();
    builder.append("// Generated code from VitaminSaber. Do not modify!\n");
    builder.append("package ").append(classPackage).append(";\n\n");
    builder.append("import com.w2ji.vitaminsaber.VitaminSaber.ResourceFinder;\n\n");

    builder.append("public class ").append(className).append(" {\n");
    emitInject(builder);
    builder.append("}\n");
    return builder.toString();
}

...

}

在ResourceInjector的代码中,我们贴出了关键一个的方法,剩余的部分内容和含义相近,brewJava()意思为构建Java文件的一个方法,它所构建的就是上面我们给出的那个MainActivity$$ResourceInjector的类中的源码部分,但是我们看到,这个方法实际只是返回了一个StringBuilder的对象,真正创建出java文件并且得到编译的位置并不在这里,所以,我们可以得出ResourceInjector的这个类的主要作用,就是来构建Java文件,而这个Java就是我们的核心处理注入的文件,那么我们先回到process方法,然后再说ResourceInjector中引用到的其他类文件。

process方法的第一条语句是:
Map 也就是说一个ResourceInjector来构建一个java文件的源码部分,那么一个TypeElement表示一个类或者接口类型,那么Map中key存放的TypeElement到底是什么呢?我们还需要进入到方法里面去看看。


private Map ...
for (Element element : env.getElementsAnnotatedWith(InjectResource.class)) {
try {
parseInjectResource(element, targetClassMap, erasedTargetTypes);
} catch (Exception e) {
...
}
}
...
}

private void parseInjectResource(Element element, Map<TypeElement,
ResourceInjector&rt targetClassMap,
Set<TypeMirror&rt erasedTargetTypes) {
boolean hasError = false;
TypeElement enclosingElement = (TypeElement) element.getEnclosingElement();

    // Verify common generated code restrictions.
    hasError |= isValidForGeneratedCode(InjectResource.class, "fields", element);

    if (hasError) {
        return;
    }

    // Assemble information on the injection point.
    String name = element.getSimpleName().toString();
    int key = element.getAnnotation(InjectResource.class).value();
    TypeMirror type = element.asType();
    boolean parcel = isAnnotated(typeUtils.asElement(element.asType()), "org.parceler.Parcel");

    ResourceInjector extraInjector = getOrCreateTargetClass(targetClassMap, enclosingElement);
    extraInjector.addField(key, name, type);

    // Add the type-erased version to the valid injection targets set.
    TypeMirror erasedTargetType = typeUtils.erasure(enclosingElement.asType());
    erasedTargetTypes.add(erasedTargetType);

}

private ResourceInjector getOrCreateTargetClass(Map TypeElement enclosingElement) {
ResourceInjector extraInjector = targetClassMap.get(enclosingElement);
if (extraInjector == null) {
String targetType = enclosingElement.getQualifiedName().toString();
String classPackage = getPackageName(enclosingElement);
String className = getClassName(enclosingElement, classPackage) + SUFFIX;

        extraInjector = new ResourceInjector(classPackage, className, targetType);
        targetClassMap.put(enclosingElement, extraInjector);      // here is the put method. key is enclosingElement.
    }
    return extraInjector;
}

下面我们来分析一下这两个方法,首先,findAndParseTargets方法,这个方法有三个参数,element,targetClassMap,erasedTargetTypes,Element类型element表示的是被@InjectResource所注解的元素,假如是int btnTextColor,那么element就是描述这个域的一个对象,这个对象可以完整的描述这个btnTextColor,无论它的类型还是名字都可以得到。targetClassMap会依然向下传递,我们需要看到map使用put方法放入元素的时候,就会知道TypeElement是什么了,Set


TypeMirror erasedTargetType = typeUtils.erasure(enclosingElement.asType());
erasedTargetTypes.add(erasedTargetType);

在parseInjectResource中为什么要传入一个Set 我是这么认为的:首先,上面两行代码做了一个操作,就是擦除。泛型的擦除,在对java文件编译过后,泛型会被擦除并且获取一个泛型对应的原始类型,如果例如Set


这块我逻辑和细节比较多,我就按照步骤概括一下:
1.在process方法中,我们定位被注解标记的域。
2.拿到这些被标记的域的外层封装类。
3.针对于封装类创建对应的ResourceInjector,一个ResourceInjector就可以构建对应封装类的处理注入的源码文件。
4.将封装类装入map,key就是被注解域的外层封装类的类型。
5.那么process方法就利用JavaFileObject和Filer创建java的file对象。 如果有兴趣,可以研究研究Square的JavaPoet,编写源码的利器。
6.return true表示次轮注解处理已经完成,如果次轮没有需要处理的文件了,JavaCompiler就是执行编译的工作了,这里就不深入介绍了。

截至至此,Class级别的注解以及处理原理已经梳理的差不多了,如果疏漏或错误,请提出。
最后,附上一个相对完整的各部分以及整体的处理流程图,在梳理一下。

VitaminSaber处理结构

你可能感兴趣的:(Android资源注入框架分析(VitaminSaber))