做者: 张德帅, 时间: 2018.6.30 星期六 天气晴html
一眨眼功夫又到周末,总以为小日子过得不够充实(实际上是王者荣耀很差玩了...)。java
不过我能够跟Butterknife谈情说爱(RTFSC:Read The Fucking Source Code),本篇文章即是“爱的结晶” 。android
Android大神JakeWharton的做品(其它还有OKHttp、Retrofit等)。Butterknife使用注解代替findViewById()等,可读性高、优雅、开发效率提升等。这里就再也不列举Butterknife优势,相信各位老司机早就精通Butterknife使用。本篇主要学习Butterknife核心源码,万丈高楼平地起,直接杀入源码不免无头苍蝇,一脸懵逼,文章从如下几个部分聊起(瞎扯)。git
(强行插入表情包)github
Butterknife 基本使用姿式编程
一、在App build.gradle配置依赖缓存
dependencies {
implementation 'com.jakewharton:butterknife:8.8.1'
annotationProcessor 'com.jakewharton:butterknife-compiler:8.8.1'
}
复制代码
二、在Activity使用bash
public class MainActivity extends AppCompatActivity {
@BindView(R.id.tv_name)
TextView tvName;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
ButterKnife.bind(this);
}
}
复制代码
上面看到使用很是简单,几个注解就搞定以前一大堆findViewById()等代码,那么它是怎么作到的?答案是:butterknife会扫描这些自定义注解,根据注解信息生成Java文件,ButterKnife.bind(this)实际会执行自动生成的代码。红色框选部分文章后面会详细分析,能够了解到 在读Butterknife源码以前得先回顾Java基础-注解。app
目录:app\build\generated\source\apt\ (APT扫描解析 注解 生成的代码)。 框架
注解能够理解为代码的标识,不会对运行有直接影响。
Java内置几个经常使用注解,这部分标识源码,会被编译器识别,提示错误等。
@Override 标记覆盖方法
@Deprecated 标记为过期
@SuppressWarnings 忽略警告
假设咱们要自定义一个注解,这时候就须要用元注解去声明自定义注解,包括像注解做用域、生命周期等。
@Documented 能够被javadoc文档化。
@Target 注解用在哪
@Inherited
@Retention
使用元注解建立自定义注解
//直到运行时还保留注解
@Retention(RetentionPolicy.RUNTIME)
//做用域 类
@Target(ElementType.TYPE)
public @interface BindV {
int resId() default 0;
}
复制代码
在代码中定义了标识,如今想拿到这些信息,能够经过反射、APT获取注解的值。
经过反射解析注解(这里也不阐述什么是反射)
BindV.java 文件
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface BindV {
int resId() default 0;
}
复制代码
MainActivity.java 文件
@BindV(resId = R.layout.activity_main)
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
//这里是举个栗子反射当前类,设置布局文件,固然在实际中这部分代码多是通过封装的。
Class clz = MainActivity.class;
BindV bindV = (BindV) clz.getAnnotation(BindV.class);//拿到布局文件id
setContentView(bindV.resId());
}
}
复制代码
APT(Annotation Processing Tool)注解处理器,是在编译期间读取注解,生成Java文件。反射解析注解是损耗性能的,接下来经过APT来生成java源码,从而避免反射。
一、首先新建项目,再新建JavaLibrary。
JavaLibrary的Gradle配置,要想Java识别注解处理器,须要注册到META-INF,这里使用auto-service这个库实现自动注册。
apply plugin: 'java-library'
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation 'com.google.auto.service:auto-service:1.0-rc2'
}
sourceCompatibility = "1.7"
targetCompatibility = "1.7"
复制代码
二、新建BindLayout注解
package com.example.abstractprocessorlib;
public @interface BindLayout {
int viewId();
}
复制代码
三、新建AbstractProcessor子类
package com.example.abstractprocessorlib;
@AutoService(Processor.class)
public class MyProcessor extends AbstractProcessor {
private Types typesUtils;//类型工具类
private Elements elementsUtils;//节点工具类
private Filer filerUtils;//文件工具类
private Messager messager;//处理器消息输出(注意它不是Log工具)
//init初始化方法,processingEnvironment会提供不少工具类,这里获取Types、Elements、Filer、Message经常使用工具类。
@Override
public synchronized void init(ProcessingEnvironment processingEnvironment) {
super.init(processingEnvironment);
typesUtils = processingEnvironment.getTypeUtils();
elementsUtils = processingEnvironment.getElementUtils();
filerUtils = processingEnvironment.getFiler();
messager = processingEnvironment.getMessager();
}
//这里扫描、处理注解,生成Java文件。
@Override
public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
//拿到全部被BindLayout注解的节点
Set<? extends Element> elements = roundEnvironment.getElementsAnnotatedWith(BindLayout.class);
for (Element element : elements) {
//输出警告信息
processingEnv.getMessager().printMessage(Diagnostic.Kind.WARNING, "element name:" + element.getSimpleName(), element);
//判断是否 用在类上
if (element.getKind().isClass()) {
//新文件名 类名_Bind.java
String className = element.getSimpleName() + "_Bind";
try {
//拿到注解值
int viewId = element.getAnnotation(BindLayout.class).viewId();
//建立文件 包名com.example.processor.
JavaFileObject source = filerUtils.createSourceFile("com.example.processor." + className);
Writer writer = source.openWriter();
//文件内容
writer.write("package com.example.processor;\n" +
"\n" +
"import android.app.Activity;\n" +
"\n" +
"public class " + className + " { \n" +
"\n" +
" public static void init(Activity activity){\n" +
" activity.setContentView(" + viewId + ");\n" +
" }\n" +
"}");
writer.flush();
//完成写入
writer.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
return false;
}
//要扫描哪些注解
@Override
public Set<String> getSupportedAnnotationTypes() {
Set<String> annotationSet = new HashSet<>();
annotationSet.add(BindLayout.class.getCanonicalName());
return annotationSet;
}
//支持的JDK版本,建议使用latestSupported()
@Override
public SourceVersion getSupportedSourceVersion() {
return SourceVersion.latestSupported();
}
}
复制代码
二、在App的Gradle文件 添加配置
dependencies {
annotationProcessor project(path: ':AbstractProcessorLib')
implementation project(path: ':AbstractProcessorLib')
}
复制代码
三、在app\build\generated\source\apt\debug 目录下,能够看到APT 生成的文件。
能够在Activity使用刚才生成的文件
假设遇到这个错误,能够参考修改Grandle配置
Gradle配置
一、JavaPoet
从上面APT能够看到拼接Java文件是比较复杂的,好在Square开源了JavPoet这个库,否则整个文件全靠字符串拼接... 有了这个库生成代码,就像写诗同样。修改刚才process()方法
@Override
public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
Set<? extends Element> elements = roundEnvironment.getElementsAnnotatedWith(BindLayout.class);
for (Element element : elements) {
processingEnv.getMessager().printMessage(Diagnostic.Kind.WARNING, "element name:" +element.getSimpleName(), element);
if (element.getKind().isClass()) {
String className = element.getSimpleName() + "_Bind";
try {
int viewId = element.getAnnotation(BindLayout.class).viewId();
//获得android.app.Activity这个类
ClassName activityClass = ClassName.get("android.app", "Activity");
//建立一个方法
MethodSpec initMethod = MethodSpec.methodBuilder("init")
.addModifiers(Modifier.PUBLIC, Modifier.STATIC)//修饰符
.addParameter(activityClass, "activity")//参数
.returns(TypeName.VOID)//返回值
.addStatement("activity.setContentView(" + viewId + ");")//方法体
.build();
//建立一个类
TypeSpec typeSpec = TypeSpec.classBuilder(className)//类名
.addModifiers(Modifier.PUBLIC)//修饰符
.addMethod(initMethod)//将方法加入到这个类
.build();
//建立java文件,指定包名类
JavaFile javaFile = JavaFile.builder("com.example.processor", typeSpec)
.build();
javaFile.writeTo(filerUtils);
} catch (Exception e) {
e.printStackTrace();
}
}
}
return false;
}
复制代码
先到gayhub 下载源码,butterknife、butterknife-annotations、butterknife-compiler三个核心模块,也是主要阅读的部分。
一、首先来分析一下build\generated\source\apt**\MainActivity_ViewBinding.java 这个文件生成大概过程,以前咱们是用注解处理器生成代码,在butterknife-compiler模块的ButterKnifeProcessor.java类负责生成 ClassName_ViewBinding.java文件。 过程以下:
(为了方便快速阅读代码,把注释或代码强壮性判断移除)首先是init方法,没有过多复杂的,主要是工具类获取。
@Override
public synchronized void init(ProcessingEnvironment env) {
super.init(env);
elementUtils = env.getElementUtils();
typeUtils = env.getTypeUtils();
filer = env.getFiler();
trees = Trees.instance(processingEnv);
}
复制代码
getSupportedSourceVersion、getSupportedAnnotationTypes方法。
@Override
public SourceVersion getSupportedSourceVersion() {
return SourceVersion.latestSupported();
}
@Override
public Set<String> getSupportedAnnotationTypes() {
Set<String> types = new LinkedHashSet<>();
for (Class<? extends Annotation> annotation : getSupportedAnnotations()) {
types.add(annotation.getCanonicalName());
}
return types;
}
private Set<Class<? extends Annotation>> getSupportedAnnotations() {
Set<Class<? extends Annotation>> annotations = new LinkedHashSet<>();
annotations.add(BindAnim.class);
annotations.add(BindArray.class);
...
return annotations;
}
复制代码
接下来重点是process方法
@Override
public boolean process(Set<? extends TypeElement> elements, RoundEnvironment env) {
Map<TypeElement, BindingSet> bindingMap = findAndParseTargets(env);
for (Map.Entry<TypeElement, BindingSet> entry : bindingMap.entrySet()) {
TypeElement typeElement = entry.getKey();
BindingSet binding = entry.getValue();
JavaFile javaFile = binding.brewJava(sdk, debuggable);
try {
javaFile.writeTo(filer);
} catch (IOException e) {
error(typeElement, "Unable to write binding for type %s: %s", typeElement, e.getMessage());
}
}
return false;
}
复制代码
分析findAndParseTargets(env)这行,找到这个private Map<TypeElement, BindingSet> findAndParseTargets(RoundEnvironment env)这个方法核心部分
省略...
// 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, builderMap, erasedTargetNames); } catch (Exception e) { logParsingError(element, BindView.class, e); } } 省略... 复制代码
继续往下找 parseBindView()方法
private void parseBindView(Element element, Map<TypeElement, BindingSet.Builder> builderMap,
Set<TypeElement> erasedTargetNames) {
//当前注解所在的类
TypeElement enclosingElement = (TypeElement) element.getEnclosingElement();
//校验类、要绑定的字段 修饰符 private static,不能够在Framework层使用已java. android.开始的包名
boolean hasError = isInaccessibleViaGeneratedCode(BindViews.class, "fields", element)
|| isBindingInWrongPackage(BindViews.class, element);
// 要绑定的字段是否View的子类、或接口
TypeMirror elementType = element.asType();
if (elementType.getKind() == TypeKind.TYPEVAR) {
TypeVariable typeVariable = (TypeVariable) elementType;
elementType = typeVariable.getUpperBound();
}
Name qualifiedName = enclosingElement.getQualifiedName();
Name simpleName = element.getSimpleName();
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, qualifiedName, simpleName);
} else {
error(element, "@%s fields must extend from View or be an interface. (%s.%s)",
BindView.class.getSimpleName(), qualifiedName, simpleName);
hasError = true;
}
}
//校验不经过
if (hasError) {
return;
}
// Assemble information on the field.
//保存View ID之间映射
int id = element.getAnnotation(BindView.class).value();
BindingSet.Builder builder = builderMap.get(enclosingElement);//从缓存取出来
Id resourceId = elementToId(element, BindView.class, id);
if (builder != null) {//说明以前被绑定过
String existingBindingName = builder.findExistingBindingName(resourceId);
if (existingBindingName != null) {
error(element, "Attempt to use @%s for an already bound ID %d on '%s'. (%s.%s)",
BindView.class.getSimpleName(), id, existingBindingName,
enclosingElement.getQualifiedName(), element.getSimpleName());
return;
}
} else {
//构建要绑定View的映射关系,继续看getOrCreateBindingBuilder方法信息
builder = getOrCreateBindingBuilder(builderMap, enclosingElement);
}
String name = simpleName.toString();
TypeName type = TypeName.get(elementType);
boolean required = isFieldRequired(element);
//咱们Activity类有不少 BindView注解的控件,是以类包含字段,这里的builder至关于类,判断若是类存在就往里加字段。
builder.addField(resourceId, new FieldViewBinding(name, type, required));
// Add the type-erased version to the valid binding targets set.
erasedTargetNames.add(enclosingElement);
}
//直接看newBuilder
private BindingSet.Builder getOrCreateBindingBuilder(
Map<TypeElement, BindingSet.Builder> builderMap, TypeElement enclosingElement) {
BindingSet.Builder builder = builderMap.get(enclosingElement);
if (builder == null) {
builder = BindingSet.newBuilder(enclosingElement);
builderMap.put(enclosingElement, builder);
}
return builder;
}
//至关于建立一个类,类名是一开始看到的 类名_ViewBinding
static Builder newBuilder(TypeElement enclosingElement) {
TypeMirror typeMirror = enclosingElement.asType();
boolean isView = isSubtypeOfType(typeMirror, VIEW_TYPE);
boolean isActivity = isSubtypeOfType(typeMirror, ACTIVITY_TYPE);
boolean isDialog = isSubtypeOfType(typeMirror, DIALOG_TYPE);
TypeName targetType = TypeName.get(typeMirror);
if (targetType instanceof ParameterizedTypeName) {
targetType = ((ParameterizedTypeName) targetType).rawType;
}
String packageName = getPackage(enclosingElement).getQualifiedName().toString();
String className = enclosingElement.getQualifiedName().toString().substring(
packageName.length() + 1).replace('.', '$');
ClassName bindingClassName = ClassName.get(packageName, className + "_ViewBinding");
boolean isFinal = enclosingElement.getModifiers().contains(Modifier.FINAL);
return new Builder(targetType, bindingClassName, isFinal, isView, isActivity, isDialog);
}
回到process方法,这行就是将刚扫描的信息写入到文件
JavaFile javaFile = binding.brewJava(sdk, debuggable, useAndroidX);
复制代码
至此咱们知道*_ViewBinding文件生成过程,接下来看怎么使用,从Butterknife.bind()方法查看
public static Unbinder bind(@NonNull Activity target) {
View sourceView = target.getWindow().getDecorView();//拿到decorView
return createBinding(target, sourceView);
}
复制代码
进入createBinding()方法
private static Unbinder createBinding(@NonNull Object target, @NonNull View source) {
Class<?> targetClass = target.getClass();
if (debug) Log.d(TAG, "Looking up binding for " + targetClass.getName
//拿到类_ViewBinding的构造方法
Constructor<? extends Unbinder> constructor = findBindingConstructorForClass(targetClass);
if (constructor == null) {
return Unbinder.EMPTY;
}
//noinspection TryWithIdenticalCatches Resolves to API 19+ only type.
try {
return constructor.newInstance(target, source);//执行构造方法。
} catch (IllegalAccessException e) {
throw new RuntimeException("Unable to invoke " + constructor, e);
} catch (InstantiationException e) {
throw new RuntimeException("Unable to invoke " + constructor, e);
} catch (InvocationTargetException e) {
Throwable cause = e.getCause();
if (cause instanceof RuntimeException) {
throw (RuntimeException) cause;
}
if (cause instanceof Error) {
throw (Error) cause;
}
throw new RuntimeException("Unable to create binding instance.", cause);
}
}
复制代码
继续看findBindingConstructorForClass()方法,根据当前类先从缓存找构造方法,没有的话根据类名_ViewBinding找到构造方法。
private static Constructor<? extends Unbinder> findBindingConstructorForClass(Class<?> cls) {
Constructor<? extends Unbinder> bindingCtor = BINDINGS.get(cls);
if (bindingCtor != null) {
if (debug) Log.d(TAG, "HIT: Cached in binding map.");
return bindingCtor;
}
String clsName = cls.getName();
if (clsName.startsWith("android.") || clsName.startsWith("java.")) {
if (debug) Log.d(TAG, "MISS: Reached framework class. Abandoning search.");
return null;
}
try {
Class<?> bindingClass = cls.getClassLoader().loadClass(clsName + "_ViewBinding");
//noinspection unchecked
bindingCtor = (Constructor<? extends Unbinder>) bindingClass.getConstructor(cls, View.class);
if (debug) Log.d(TAG, "HIT: Loaded binding class and constructor.");
} catch (ClassNotFoundException e) {
if (debug) Log.d(TAG, "Not found. Trying superclass " + cls.getSuperclass().getName());
bindingCtor = findBindingConstructorForClass(cls.getSuperclass());
} catch (NoSuchMethodException e) {
throw new RuntimeException("Unable to find binding constructor for " + clsName, e);
}
BINDINGS.put(cls, bindingCtor);
return bindingCtor;
}
复制代码
//到这一步咱们清楚,是先生成文件,而后Butterknife.bind()方法关联生成的文件并执行构造。接下来看看生成的文件构造作了什么 //我在apt目录下找到一个文件SimpleActivity_ViewBinding.java文件,代码量多 我简化一下,直接看findRequireViewAsType()方法。
@UiThread
public SimpleActivity_ViewBinding(SimpleActivity target) {
this(target, target.getWindow().getDecorView());
}
@UiThread
public SimpleActivity_ViewBinding(final SimpleActivity target, View source) {
this.target = target;
View view;
target.title = Utils.findRequiredViewAsType(source, R.id.title, "field 'title'", TextView.class);
}
//继续看findRequiredView
public static <T> T findRequiredViewAsType(View source, @IdRes int id, String who,
Class<T> cls) {
View view = findRequiredView(source, id, who);
return castView(view, id, who, cls);
}
//findRequiredView()方法,能够看到其实仍是使用findViewByID()查找View只不过是自动生成。
public static View findRequiredView(View source, @IdRes int id, String who) {
View view = source.findViewById(id);
if (view != null) {
return view;
}
String name = getResourceEntryName(source, id);
throw new IllegalStateException("Required view '"
+ name
+ "' with ID "
+ id
+ " for "
+ who
+ " was not found. If this view is optional add '@Nullable' (fields) or '@Optional'"
+ " (methods) annotation.");
}
复制代码
//以上就是Butterknife基本原理,第一部分apt扫描注解信息生成文件,第二部分Butterknife.bind方法找到对于文件,执行构造,并执行findView获取view。
推荐一篇文章:Android主项目和Module中R类的区别 https://www.imooc.com/article/23756
最后:其实在MVVM、Kotlin等出来以后,Butterknife热度慢慢降低,但这根本不影响咱们去了解 这如此优秀的框架。