面试官: ButterKnife为何执行效率为何比其余注入框架高?它的原理是什么java
心理分析: ButterKnife框架一直都是使用,不多又开发者对butterknife深刻研究的,既然你是面试Android高级岗位,天然须要有相应被问到原理的准备,面试官想问你对注解处理器了解多少,Android编译流程有多少认识android
**求职者:**应该从 注解处理器原理 与优点提及,确定注解处理器对解放生产力的做用。而后能够引伸常见的 Butterknife,Dagger2,DBFlow。这才是加分项git
优点github
接下来咱们一块儿来看注解处理的原理面试
在android开发中,比较经常使用到的第三方库中,有很多用到了 注解处理器(Annotation Processor)。 比较常见的就有 Butterknife,Dagger2,DBFlow 等。api
Java中存在很多关于注解的Api, 好比@Override
用于覆盖父类方法,@Deprecated
表示已舍弃的类或方法属性等,android中又多了一些注解的扩展,如@NonNull
, @StringRes
, @IntRes
等。bash
使用代码自动生成,一是为了提升编码的效率,二是避免在运行期大量使用反射,经过在编译期利用反射生成辅助类和方法以供运行时使用。app
注解处理器的处理步骤主要有如下:框架
Butterknife的注解处理器的工做方式以下:dom
Butterknife.bind(..)
方法。当你点击Android Studio的Build
按钮时,Butterknife先是按照上述步骤生成了对应的辅助类和方法。在代码执行到bind(..)
方法时,Butterknife就去调用以前生成的辅助类方法,完成对被注解元素的赋值操做。
了解了基本的知识点后,咱们应该尝试去使用这些技巧。 接下来是实践时间,咱们来开发一个简单的例子,利用注解处理器来自动产生随机数字和随机字符串。
1. 添加注解
在lib_annotations中添加两个注解:RandomString
, RandomInt
,分别用于生成随机数字和随机字符串:
@Retention(CLASS)
@Target(value = FIELD)
public @interface RandomString {
}
@Retention(CLASS)
@Target(value = FIELD)
public @interface RandomInt {
int minValue() default 0;
int maxValue() default 65535;
}
复制代码
public enum ElementType {
TYPE, //类
FIELD, //属性
METHOD, //方法
PARAMETER, //参数
CONSTRUCTOR, //构造函数
LOCAL_VARIABLE,
ANNOTATION_TYPE,
PACKAGE,
TYPE_PARAMETER,
TYPE_USE;
private ElementType() {
}
}
复制代码
public enum RetentionPolicy {
SOURCE, //被编译器所忽略
CLASS, //被编译器保留至类文件,但不会保留至运行时
RUNTIME //保留至类文件,且保留至运行时,能在运行时反射该注解修饰的对象
}
复制代码
真正处理注解并生成代码的操做都在这里。 在写代码以前咱们须要先导入两个重要的库,以及咱们的注解模块:
compile 'com.google.auto.service:auto-service:1.0-rc4'
compile 'com.squareup:javapoet:1.9.0'
implementation project(':lib_annotations')
复制代码
新建类RandomProcessor.java
:
@AutoService(Processor.class)
public class RandomProcessor extends AbstractProcessor{
@Override
public synchronized void init(ProcessingEnvironment processingEnvironment) {
super.init(processingEnvironment);
}
@Override
public SourceVersion getSupportedSourceVersion() {
return super.getSupportedSourceVersion();
}
@Override
public Set<String> getSupportedAnnotationTypes() {
return super.getSupportedAnnotationTypes();
}
@Override
public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
return false;
}
}
复制代码
META-INF/services
下生成javax.annotation.processing.Processor
文件,文件的内容为com.rhythm7.lib_compiler.RandomProcessor
复制代码
也就是说,你所声明的注解处理器都会在被写入这个配置文件中。 这样子,当外部程序装载这个模块的时候,就能经过该模块的jar包下的META-INF/services下找到具体的注解处理器的实现类名,并加载实例化,完成模块的注入。 注解处理器须要实现AbstractProcessor
接口,并实现对应的方法
processingEnvironment
对象,借由该对象能够获取到生成代码的文件对象, debug输出对象,以及一些相关工具类process()
方法所接收初始化
较详细代码以下:
private static final List<Class<? extends Annotation>> RANDOM_TYPES
= Arrays.asList(RandomInt.class, RandomString.class);
private Messager messager;
private Types typesUtil;
private Elements elementsUtil;
private Filer filer;
private TypeonProcess()per.init(processingEnv);
messager = processingEnv.getMessager();
typesUtil = processingEnv.getTypeUtils();
elementsUtil = processingEnv.getElementUtils();
filer = processingEnv.getFiler();
}
@Override
public SourceVersion getSupportedSourceVersion() {
return SourceVersion.latestSupported();
}
@Override
public Set<String> getSupportedAnnotationTypes() {
Set<String> annotations = new LinkedHashSet<>();
for (Class<? extends Annotation> annotation : RANDOM_TYPES) {
annotations.add(annotation.getCanonicalName());
}
return annotations;
}
复制代码
处理注解
在process()
方法中执行如下操做:
for (Element element : roundEnv.getElementsAnnotatedWith(RandomInt.class)) {
//AnnotatedRandomInt是对被RandomInt注解的Elment的简单封装
AnnotatedRandomInt randomElement = new AnnotatedRandomInt(element);
messager.printMessage(Diagnostic.Kind.NOTE, randomElement.toString());
//判断被注解的类型是否符合要求
if (!element.asType().getKind().equals(TypeKind.INT)) {
messager.printMessage(Diagnostic.Kind.ERROR, randomElement.getSimpleClassName().toString() + "#"
+ randomElement.getElementName().toString() + " is not in valid type int");
}
//按被注解元素所在类的完整类名为key将被注解元素存储进Map中,后面会根据key生成类文件
String qualifier = randomElement.getQualifiedClassName().toString();
if (annotatedElementMap.get(qualifier) == null) {
annotatedElementMap.put(qualifier, new ArrayList<AnnotatedRandomElement>());
}
annotatedElementMap.get(qualifier).add(randomElement);
}
复制代码
生成类文件
将以前以注解所在类为key的map遍历,并以key值为分组生成类文件。
for (Map.Entry<String, List<AnnotatedRandomElement>> entry : annotatedElementMap.entrySet()) {
MethodSpec constructor = createConstructor(entry.getValue());
TypeSpec binder = createClass(getClassName(entry.getKey()), constructor);
JavaFile javaFile = JavaFile.builder(getPackage(entry.getKey()), binder).build();
javaFile.writeTo(filer);
}
复制代码
生成类、构造函数、代码段以及文件都是利用到了javapoet
依赖库。固然你也能够选择拼接字符串和本身用文件IO写入,可是用javapoet
要更方便得多。
private MethodSpec createConstructor(List<AnnotatedRandomElement> randomElements) {
AnnotatedRandomElement firstElement = randomElements.get(0);
MethodSpec.Builder builder = MethodSpec.constructorBuilder()
.addModifiers(Modifier.PUBLIC)
.addParameter(TypeName.get(firstElement.getElement().getEnclosingElement().asType()), "target");
for (int i = 0; i < randomElements.size(); i++) {
addStatement(builder, randomElements.get(i));
}
return builder.build();
}
private void addStatement(MethodSpec.Builder builder, AnnotatedRandomElement randomElement) {
builder.addStatement(String.format(
"target.%1$s = %2$s",
randomElement.getElementName().toString(),
randomElement.getRandomValue())
);
}
private TypeSpec createClass(String className, MethodSpec constructor) {
return TypeSpec.classBuilder(className + "_Random")
.addModifiers(Modifier.PUBLIC, Modifier.FINAL)
.addMethod(constructor)
.build();
}
private String getPackage(String qualifier) {
return qualifier.substring(0, qualifier.lastIndexOf("."));
}
private String getClassName(String qualifier) {
return qualifier.substring(qualifier.lastIndexOf(".") + 1);
}
复制代码
经过以上几行代码,建立了类文件。在类的构造函数中添加参数(target)
, 并为每个被注解元素添加语句"target.%1$s = %2$s"
,最后经过javaFile.writeTo(filer)
完成文件写入。
在lib_api中新建一个类:RandomUtil.java
,添加注入方法:
public static void inject(Object object) {
Class bindingClass = Class.forName(object.getClass().getCanonicalName() + "_Random");
Constructor constructor = bindingClass.getConstructor(object.getClass());
constructor.newInstance(object);
}
复制代码
这里利用反射找到了以“Object类名_Random”命名的生成类,并调用它的构造方法。而在咱们以前的注解处理器中,咱们已在生成类的构造方法中实现了属性的赋值操做。
在app module中依赖刚才建立的库:
implementation project(':lib_annotations')
implementation project(':lib_api')
annotationProcessor project(':lib_compiler')
复制代码
在Activity中的使用
public class MainActivity extends AppCompatActivity {
@RandomInt(minValue = 10, maxValue = 1000)
int mRandomInt;
@RandomString
String mRandomString;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
RandomUtil.inject(this);
Log.i("RandomInt ==> ", mRandomInt + "");
Log.i("RandomString ==> ", mRandomString);
}
}
复制代码
编译,运行,查看结果:
RandomInt ==>: 700
RandomString ==>: HhRayFyTtt
复制代码
被注解的元素成功被自动赋值,说明注入成功。
注解处理器的debug跟普通的代码debug有点不一样:
在当前工程路径下输入命令
gradlew --no-daemon -Dorg.gradle.debug=true :app:clean :app:compileDebugJavaWithJavac
复制代码
并在Edit Configurations
中新添加一个远程配置(remote),名字随意,端口为5005。 而后点击debug按钮,就能够链接上远程调试器进行Annotation的调试了。