开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 3 天,点击查看活动详情
ButterKnife是用来解放开发者双手的依赖库,该库内通过注解替换了findViewById的操作,进一步提升开发人员编码效率,下面我们来看下,如果要实现一个ButterKnife(仅以findViewById为例,onClick等类型由大家自行探索),我们需要做些什么?
findViewById的实现要素
假设TestActivity的布局文件如下:
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".TestActivity">
<TextView
android:id="@+id/textview1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Hello"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</android.support.constraint.ConstraintLayout>
那么在Activity中引用TextView时,其实现代码如下所示:
public class TestActivity extends AppCompatActivity {
private TextView mTextView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_test);
mTextView = findViewById(R.id.textview1);
}
}
可以看到如果要自动完成findViewById的操作,我们主要要完成两项工作:
- 让layout中定义的ResId和java代码中定义的成员具有映射关系,一一对应
- 当遍历映射关系组时,调用activity.findViewById为Activity中定义的成员变量赋值,自动完成类成员的初始化
下面我们一起来看下,如何解决这两个问题。
resID-类成员映射关系
从前文可知,自定义注解可以携带value信息,那么我们只需要自定义一个注解,该注解的目标为成员属性,接受的取值为resId,当开发者在成员变量上,使用该注解时,即可搭建成员变量和resID之间的一一映射关系。
新建ButterKnifeDemo项目,在项目内创建命令为butterknife-annotation的java模块,在该模块内创建BindView注解,由于我们要求BindView注解的取值必须是资源id,所以我们可以引入androidx.annotation库借助其内部注解做限制,引入库的代码如下图所示:
随后编写BindView注解代价如下图所示:
随后让app模块依赖butterknife-annotation模块即可。
BindView注解解析
前文中我们已经利用BindView注解搭建了resID和成员变量之间的关系,接下来我们需要解决的是在发生注解的类中通过findViewById获取到View对象后将其赋值给Activity中的成员变量,伪代码如下:
public void init(Activity activity) {
// mTextView为注解修饰的变量名,resId为注解取值
activity.mTextView = activity.findViewById(resId);
}
那么如何实现该效果呢?有两种方案:
- 发现成员变量被@BindView注解后,在Activity onCreate函数中利用字节码插桩将findViewById这行代码写在onCreate函数内
- 生成中间类,该类持有Activity引用,要求开发者在Activity内调用初始化函数,我们在初始化函数内反射构建中间类,完成成员变量赋值
从两种方案可以看出,方案1虽然不需要开发者参与,但具有显式缺点:
- onCreate中插入字节码位置无法确定,开发者编写的View使用代码可能在我们插入的字节码前,也有可能在我们插入的字节码后
- ASM操作字节码会拉长编译时间
- ASM和字节码本身复杂度较高,实现难度较大
故我们选取第二种方案,生成中间类。
JavaPoet生成中间类
这里我们不妨手动完成中间类的实现,再去将其自动化,我们定义中间类的命令规则为Activity类名+ViewBinding,那么我们前面提到的TestActivity的中间类名应该为TestActivityViewBinding,其大致代码如下所示:
public class TestActivityViewBinding {
public TestActivityViewBinding(TestActivity activity) {
bindViews(activity);
}
private void bindViews(TestActivity activity) {
activity.mTextView = activity.findViewById(R.id.textview1);
}
}
// TestActivity.java onCreate函数
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_test);
// 构造TestActivityViewBinding对象即可
}
使用@BindView已知信息,我们可以替换中间类中信息,得到如下伪代码如下:
// 类名 = 包含注解的类名+ViewBinding
public class 类名 {
public 类名(使用注解的类名 activity) {
bindViews(activity);
}
private void bindViews(使用注解的类名 activity) {
activity.注解修饰的变量名 = activity.findViewById(注解的取值);
}
}
综上,只需要在解析到BindView注解时,生成一个这样的中间类即可,在Java中生成类需要借助javapoet库。
在butterknife-processor模块中添加javapoet依赖,按照描述设计process函数代码如下所示:
//第一个参数为该注解处理器支持的所有类型
@Override
public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
/**
* 从所有元素中查找使用了支持注解的元素
* 1.roundEnvironment.getRootElements() 获取所有的element对象
* 2.getTypeElementsToProcess中匹配支持的所有元素
*/
for (TypeElement typeElement : getTypeElementsToProcess(roundEnvironment.getRootElements(),set)){
// 从类元素中获取包名类名
String packageName = mProcessingEnvironment.getElementUtils().getPackageOf(typeElement).getQualifiedName().toString();
String typeName = typeElement.getSimpleName().toString();
ClassName className = ClassName.get(packageName, typeName);
// 构造生成的中间类名
ClassName generatedClassName = ClassName.get(packageName, typeName + "ViewBinding");
// 根据类名创建类构造器
TypeSpec.Builder classBuilder = TypeSpec.classBuilder(generatedClassName).addModifiers(Modifier.PUBLIC);
// 向类构造器中添加构造函数,构造函数的参数为类名,其内部调用bindViews函数
classBuilder.addMethod(MethodSpec.constructorBuilder()
.addModifiers(Modifier.PUBLIC)
.addParameter(className, "activity")
.addStatement("$N($N)",
"bindViews",
"activity")
.build());
// 构造bindViews函数实现,其使用private修饰,返回值是void
MethodSpec.Builder bindViewsMethodBuilder = MethodSpec
.methodBuilder("bindViews")
.addModifiers(Modifier.PRIVATE)
.returns(void.class)
.addParameter(className, "activity");
// 获取类的所有成员属性
for (VariableElement variableElement : ElementFilter.fieldsIn(typeElement.getEnclosedElements())) {
BindView bindView = variableElement.getAnnotation(BindView.class);
if (bindView != null) {
// 如果成员属性使用@BindView注解,就向bindViews函数内部加入一行
bindViewsMethodBuilder.addStatement("$N.$N = ($T) $N.findViewById($L)",
"activity",
variableElement.getSimpleName(),
variableElement,
"activity",
bindView.value());
}
}
// 将bindViews函数加入类构造器
classBuilder.addMethod(bindViewsMethodBuilder.build());
// 使用JavaFile将定义的类构造器写入真实文件中
try {
JavaFile.builder(packageName,
classBuilder.build())
.build()
.writeTo(mProcessingEnvironment.getFiler());
} catch (IOException e) {
mProcessingEnvironment.getMessager().printMessage(Diagnostic.Kind.ERROR, e.toString(), typeElement);
}
}
return true;
}
private Set<TypeElement> getTypeElementsToProcess(Set<? extends Element> elements,
Set<? extends TypeElement> supportedAnnotations) {
Set<TypeElement> typeElements = new HashSet<>();
for (Element element : elements) {
// 如果该元素是类类型元素
if (element instanceof TypeElement) {
boolean found = false;
// 获取该类内部所有的变量成员和方法
for (Element subElement : element.getEnclosedElements()) {
// 获取变量成员和方法上所使用的注解
for (AnnotationMirror mirror : subElement.getAnnotationMirrors()) {
for (Element annotation : supportedAnnotations) {
// 如果使用的注解包含在我们支持的注解内,则收集起来
if (mirror.getAnnotationType().asElement().equals(annotation)) {
typeElements.add((TypeElement) element);
found = true;
break;
}
}
if (found) break;
}
if (found) break;
}
}
}
return typeElements;
}
在app模块添加butterknife-processor依赖,运行项目,在build目录可以看到我们已经顺利生成了中间类,如下图所示:
反射初始化中间类
上文中我们已经成功生成了中间类,接下来我们提供中间类的初始化方法,以便在onCreate中调用,新建butterknife模块,其内部定义ButterKnife.java类,并使得app模块依赖该模块。
在ButterKnife.java类中我们提供bind方法,供Activity调用,在Activity调用该方法时,反射构造该类对应的中间类对象,在中间类对象的构造函数内会完成Activity成员变量的初始化,代码如下所示:
public class ButterKnife {
public static final String VIEW_BINDING_CLASS_SUFFIX = "ViewBinding";
private ButterKnife() {}
public static <T extends Activity> void bind(T activity) {
instantiateViewBinding(activity, VIEW_BINDING_CLASS_SUFFIX);
}
private static <T extends Activity> void instantiateViewBinding(T target, String suffix) {
Class<?> targetClass = target.getClass();
String className = targetClass.getName();
try {
Class<?> bindingClass = targetClass
.getClassLoader()
.loadClass(className + suffix); // dynamically loads Java class into memory...
Constructor<?> classConstructor = bindingClass.getConstructor(targetClass);
try {
classConstructor.newInstance(target);
} catch (IllegalAccessException e) {
throw new RuntimeException("Unable to invoke " + classConstructor, e);
} catch (InstantiationException e) {
throw new RuntimeException("Unable to invoke " + classConstructor, 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 instance.", cause);
}
} catch (ClassNotFoundException e) {
throw new RuntimeException("Unable to find Class for " + className + suffix, e);
} catch (NoSuchMethodException e) {
throw new RuntimeException("Unable to find constructor for " + className + suffix, e);
}
}
}
随后在TestActivity中调用ButterKnife.bind即可,运行发现,mTextView确实已赋值完成。
至此,我们自定义的ButterKnife就完成了,大家自己动手试试吧,可以尝试添加@OnClick,@OnLongClick等注解。