一、 背景
很早之前,大量书写findViewById的时候,有人就开始思考,如何将开发者从繁杂的重复的劳动中释放出来,于是,最初的大神jakewharton编写了一个ButterKnife框架,使用Java的APT技术,实现这个功能,让我们一起来看看这个的实现。
二、源码分析
自定义注解
我们先认识一下ButterKnife的目录结构,一般来说APT框架只需要三个包就可以了,一个注解包(java-library),一个api包(项目的入口),一个compiler包(注解使用)。
我们现在来说annotation包的相关东西:ButterKnife常用的注解包括BindView这个,是绑定findviewbyid的,我们从这个入手,我们看下他的形态:
@Retention(RUNTIME)
@Target(FIELD) // TYPE: 类或接口; FIELD: 成员变量; METHOD: 方法;
public @interface BindView {
/** View ID to which the field will be bound. */
@IdRes int value();
}
其中注解标准的形式:@interface这个就不用多说了。
1、RetentionPolicy.SOURCE:注解只保留在源文件,当Java文件编译成class文件的时候,注解被遗弃;
2、RetentionPolicy.CLASS:注解被保留到class文件,但jvm加载class文件时候被遗弃,这是默认的生命周期;3、RetentionPolicy.RUNTIME:注解不仅被保存到class文件中,jvm加载class文件之后,仍然存在; 这3个生命周期分别对应于:Java源文件(.java文件) ---> .class文件 ---> 内存中的字节码。明确生命周期长度 SOURCE < CLASS < RUNTIME ,所以前者能作用的地方后者一定也能作用。一般如果需要在运行时去动态获取注解信息,那只能用 RUNTIME 注解;如果要在编译时进行一些预处理操作,比如生成一些辅助代码,就用 CLASS注解;如果只是做一些检查性的操作,比如 @Override**和 **@SuppressWarnings,则可选用 SOURCE 注解。
这里之前是class,不知道在哪个版本就改成了RunTime,但是我们只在APT阶段使用,效果差不多
接受类型:
- 所有基本类型(int,float,boolean,byte,double,char,long,short)
- String
- Class
- enum
- Annotation,注解可以嵌套
初始化和APT
ButterKnife在初始化的时候使用了缓存技术。源码在ButterKnife这个文件中。SDK入口方法是:
ButterKnife.bind(this);
最后都会进入bind方法中,我们看下这里面干了什么:
public static Unbinder bind(@NonNull Object target, @NonNull View source) {
Class<?> targetClass = target.getClass();
Constructor<? extends Unbinder> constructor = findBindingConstructorForClass(targetClass);
if (constructor == null) {
return Unbinder.EMPTY;
}
return constructor.newInstance(target, source);
}
纯看代码也能看的出来,就是找到class(一般是Activity,在这里将butterknife与activity绑定),通过这个class的名字找到一个生成的类,最后反射一下创建实例(这是个常规操作,几乎所有APT的框架都存在这种实现,反射调用通过原className创建的一个文件)。这里很简单,我们直接进findBindingConstructorForClass这里看这里做了什么优化。
private static Constructor<? extends Unbinder> findBindingConstructorForClass(Class<?> cls) {
Constructor<? extends Unbinder> bindingCtor = BINDINGS.get(cls);
if (bindingCtor != null || BINDINGS.containsKey(cls)) {
return bindingCtor;
}
bindingCtor = (Constructor<? extends Unbinder>) bindingClass.getConstructor(cls, View.class);
BINDINGS.put(cls, bindingCtor);
return bindingCtor;
}
可以看到里面做了一个优化,就是加入了BINDINGS,他是一个map,保证只加载一次。这里也很简单,就是找到一个命名为classname+__ViewBinding的文件,并加载他的构造函数,且这个文件是继承Unbinder,我们看下Unbinder这个是什么东西:
public interface Unbinder {
@UiThread void unbind();
Unbinder EMPTY = () -> { };
}
这里面实际上就一个方法,解绑操作,这个就不用说了,肯定是为了优化相关的操作。看到这里初始化就没啥了,后面就是APT的部门了,如果我们也要写APT这样的框架,这种流程上的操作还是十分流水线且简单的。
APT:我们只需要继承AbstractProcessor这个类,并添加@AutoService(Processor.class)这个来实现注解处理器的注册,注册到 javac 后,在项目编译时就能执行注解处理器了,他是SPI的一种实现。这个在后面会引申一下。
我们继承AbstractProcessor,需要关心这几个实现:
初始化:init():注解处理器运行扫描源文件时,以获取元素(Element)相关的信息,维度:类、成员变量、方法
解析类:process() 处理解析相关流程
其它:getSupportedXXX(),一些配置:指定 java 版本,指定待解析的注解
我们需要关心的就是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) { }
}
return false;
}
第一句就是扫描相关的代码,findAndParseTargets,我们通过这个方法,找我们关心的bindview,先将扫描得到的注解相关信息保存到builderMap,最后对这些信息进行重新整理返回一个以TypeElement为 key 、BindingSet为 value 的 Map,其中TypeElement代表使用了 ButterKnife 的类,即 Activity、Fragment等。这个步骤就是为了生成文件前的准备。用来存储要生成类的基本信息以及注解元素的相关信息。
for (Element element : env.getElementsAnnotatedWith(BindView.class)) {
try {
parseBindView(element, builderMap, erasedTargetNames);
} catch (Exception e) {
logParsingError(element, BindView.class, e);
}
}
private void parseBindView(Element element, Map<TypeElement, BindingSet.Builder> builderMap,
Set<TypeElement> erasedTargetNames) {
// 首先要注意,此时element是VariableElement类型的,即成员变量enclosingElement是当前元素的父类元素,一般就是我们使用ButteKnife时定义的View类型成员变量所在的类,可以理解为之前例子中的MainActivity
TypeElement enclosingElement = (TypeElement) element.getEnclosingElement();
// 进行相关校验,不是重点,此处主要为一些限定性验证
boolean hasError = isInaccessibleViaGeneratedCode(BindView.class, "fields", element)
|| isBindingInWrongPackage(BindView.class, element);
TypeMirror elementType = element.asType();
.......
// 获得元素使用BindView注解时设置的属性值,即 View 对应的xml中的id
// Assemble information on the field.
int id = element.getAnnotation(BindView.class).value();
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) {return; }
} else {
builder = getOrCreateBindingBuilder(builderMap, enclosingElement);
}
}
看起来很繁琐,但是内部最核心的就是builder.addField(resourceId, new FieldViewBinding(name, type, required));只需要关注id,元素名,类型就行,最后生成的文件也就这几个主要元素,其他都是一些校验,这些校验踩坑才多了自然就知道了。这里面还存在很多AST,JavaC相关的知识,建议这个阶段只做了解,比如elementToId方法:
private Id elementToId(Element element, Class<? extends Annotation> annotation, int value) {
JCTree tree = (JCTree) trees.getTree(element, getMirror(element, annotation));
if (tree != null) {
rScanner.reset();
tree.accept(rScanner);
if (!rScanner.resourceIds.isEmpty()) {
return rScanner.resourceIds.values().iterator().next();
}
}
return new Id(value);
}
一般都是直接获取到id,但是这里又通过JCTree从新获取了一遍,理论上值是相同的(这个大家可以试验一下)。
好了,看到这里,大家肯定疑惑,好像只处理R.id,对于ButterKnife特有的R2.id是怎么处理的呢,就在下面
这里的Id是后面的获取的核心,
private static final ClassName ANDROID_R = ClassName.get("android", "R");
private static final String R = "R";
Id(int value, @Nullable Symbol rSymbol) {
this.value = value;
if (rSymbol != null) {
//这里默认取出R(在这里就抛弃了R2的概念),classname是包名.R.id这种数据
ClassName className = ClassName.get(rSymbol.packge().getQualifiedName().toString(), R,
rSymbol.enclClass().name.toString());
String resourceName = rSymbol.name.toString();
//将要使用的核心,code,将code拼成包名.R.id.XXX
this.code = className.topLevelClassName().equals(ANDROID_R)
? CodeBlock.of("$L.$N", className, resourceName)
: CodeBlock.of("$T.$N", className, resourceName);
this.qualifed = true;
} else {
this.code = CodeBlock.of("$L", value);
this.qualifed = false;
}
}
最后在BindingSet中生成了这个文件相关的东西,可以看到新建的文件名,这个就是之后创建的文件名:_ViewBinding
static ClassName getBindingClassName(TypeElement typeElement) {
String packageName = getPackage(typeElement).getQualifiedName().toString();
String className = typeElement.getQualifiedName().toString().substring(
packageName.length() + 1).replace('.', '$');
return ClassName.get(packageName, className + "_ViewBinding");
}
我看看下findviewbyid
private void addViewBinding(MethodSpec.Builder result, ViewBinding binding, boolean debuggable) {
if (binding.isSingleFieldBinding()) {
FieldViewBinding fieldBinding = requireNonNull(binding.getFieldBinding());
CodeBlock.Builder builder = CodeBlock.builder()
.add("target.$L = ", fieldBinding.getName());
boolean requiresCast = requiresCast(fieldBinding.getType());
if (!debuggable || (!requiresCast && !fieldBinding.isRequired())) {
if (requiresCast) {
builder.add("($T) ", fieldBinding.getType());
}
builder.add("source.findViewById($L)", binding.getId().code);//这里是核心,用上面的id里面的code直接替换就变成我们的想要的样子,这里的code就是android.R.color.transparent这种样式
} else {
builder.add("$T.find", UTILS);//构造了butterknife.internal.utils这个类
builder.add(fieldBinding.isRequired() ? "RequiredView" : "OptionalView");
if (requiresCast) {
builder.add("AsType");
}
builder.add("(source, $L", binding.getId().code);
if (fieldBinding.isRequired() || requiresCast) {
builder.add(", $S", asHumanDescription(singletonList(fieldBinding)));
}
if (requiresCast) {
builder.add(", $T.class", fieldBinding.getRawType());
}
builder.add(")");
}
result.addStatement("$L", builder.build());
return;
}
java代码生成
butterknife生成代码使用的是JavaPoet,这个不强制,你也可以用字符串写。使用Javapoet的好处就是他可以构造文件系统,生成的java程序是符合语法逻辑的。仅此而已。
JavaFile brewJava(int sdk, boolean debuggable) {
TypeSpec bindingConfiguration = createType(sdk, debuggable);//代码生成相关
return JavaFile.builder(bindingClassName.packageName(), bindingConfiguration)
.addFileComment("Generated code from Butter Knife. Do not modify!")
.build();
}
这东西没啥好说的,就是javapoet的使用。这种代码看起来其实没啥意思,就是javapoet的使用,我们可以直接看代码最后的生成,反推javapoet的写法,
public DrawerToggleActivity_ViewBinding(DrawerToggleActivity target, View source) {
this.target = target;
target.textview = Utils.findRequiredViewAsType(source, R.id.drawer_layout, "field 'drawerLayout'", DrawerLayout.class);
Context context = source.getContext();
target.guideColor = ContextCompat.getColor(context, R.color.half_transparent);
}
代码生成的比较统一。
三、源码拓展
AutoService是google提供的框架,其源码比较简单:
public class AutoServiceProcessor extends AbstractProcessor
//可以看到autoservice也是一个APT,就是套娃
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
try {
return processImpl(annotations, roundEnv);
} catch (Exception e) {
return true;}
}
private boolean processImpl(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
if (roundEnv.processingOver()) {
generateConfigFiles();
} else {
processAnnotations(annotations, roundEnv);
}
return true;
}
private void generateConfigFiles() {//就是一个创建文件的过程
Filer filer = processingEnv.getFiler();
for (String providerInterface : providers.keySet()) {
String resourceFile = "META-INF/services/" + providerInterface;
try {
SortedSet<String> allServices = Sets.newTreeSet();
try {
FileObject existingFile = filer.getResource(StandardLocation.CLASS_OUTPUT, "",
resourceFile);
Set<String> oldServices = ServicesFiles.readServiceFile(existingFile.openInputStream());
allServices.addAll(oldServices);
} catch (IOException e) {
log("Resource file did not already exist.");
}
allServices.addAll(newServices);
log("New service file contents: " + allServices);
FileObject fileObject = filer.createResource(StandardLocation.CLASS_OUTPUT, "",
resourceFile);
OutputStream out = fileObject.openOutputStream();
ServicesFiles.writeServiceFile(allServices, out);
out.close();
} catch (IOException e) {
return;
}
}
}
在官方文档上介绍的使用@AutoService()修饰后,会生成META-INF/services/javax.annotation.processing.Processor文件夹生成文件,这种实现就是JAVA原生的ServiceLoader是SPI的是一种实现,所谓SPI,即Service Provider Interface,用于一些服务提供给第三方实现或者扩展,可以增强框架的扩展或者替换一些组件,他可以自动加载这个类了
四、源码延伸
我们学习了butterknife的实现,那我们有两个问题:
1.如果实现相同的功能,我们还能怎么做
2.如果学习了类似的写法,我们能写出来什么样的功能。
相关功能的实现(其他方式构建findViewById)
1.kotlin的实现
kotlin是使用kotlin-android-extensions这个技术实现的,
使用:
import kotlinx.android.synthetic.main.xml文件的名称.*
我们通过反编译可以其实我们的源码又多了一些东西:
private HashMap _$_findViewCache;
protected void onCreate(@Nullable Bundle savedInstanceState) {
...
((TextView)this._$_findCachedViewById(id.helloTv)).setText((CharSequence)"Hello Kotlin!");
}
public View _$_findCachedViewById(int var1) {
if(this._$_findViewCache == null) {
this._$_findViewCache = new HashMap();
}
View var2 = (View)this._$_findViewCache.get(Integer.valueOf(var1));
if(var2 == null) {
var2 = this.findViewById(var1);
this._$_findViewCache.put(Integer.valueOf(var1), var2);
}
return var2;
}
public void _$_clearFindViewByIdCache() {
if(this._$_findViewCache != null) {
this._$_findViewCache.clear();
}
}
这个只能说在编译期层面使用插件(kotlin-android-extensions)上都做了相关的操作,最终的代码其实不是我们看到的那样(具体实现没有看过,插件代码太多了)。
2.AST的实现
AST是抽象语法树,计算机内存的一种树状数据结构,便于计算机理解和阅读。我们可以通过修改抽象语法树对源码的结构做增删改查,这个技术广泛应用于Lint上,这个的实现有兴趣可以研究一下,源码参考Lint相关实现。
3.ASM的实现
和上面的AST的机制差不多,他构建的是class的结构,ASM操作起来比较麻烦,需要考虑的东西也很多,这个的实现有兴趣可以研究一下,源码参考ByteX.
类似思想的实现(使用APT,我们能做什么)
Android中有个LocalBroadcast的东西,也就是本地广播,我们怎么通过利用本地广播来构造成EventBus的外表。我们可以利用APT的实现。
首先我们先构造初始化的方法,学习Butterknife一样,
private static Unbinder createBroadCast(@NonNull Object activity) {
Class<?> targetClass = activity.getClass();
if (debug) Log.d(TAG, "Looking up binding for " + targetClass.getName());
Constructor<? extends Unbinder> constructor = findBindingConstructorForClass(targetClass);
if (constructor == null) {
return Unbinder.EMPTY;
}
try {
return constructor.newInstance(activity);
} 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);
}
}
@Nullable
@CheckResult
@UiThread
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;
}
try {
Class<?> bindingClass = cls.getClassLoader().loadClass(clsName +BROCASE_SUFFIX);
bindingCtor = (Constructor<? extends Unbinder>) bindingClass.getConstructor(cls);
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;
}
可以看到代码差不多,核心就是APT的process方法里面的东西了,
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
mProxyMap.clear();
Set<? extends Element> elesWithBind = roundEnv.getElementsAnnotatedWith(LocalBind.class);
for (Element element : elesWithBind) {
checkAnnotationValid(element, LocalBind.class);
ExecutableElement variableElement = (ExecutableElement) element;
//class type
TypeElement classElement = (TypeElement) variableElement.getEnclosingElement();
//full class name
String fqClassName = classElement.getQualifiedName().toString();
LocalProxyInfo proxyInfo = mProxyMap.get(fqClassName);
if (proxyInfo == null) {
proxyInfo = new LocalProxyInfo(elementUtils, classElement);//构造生成类
mProxyMap.put(fqClassName, proxyInfo);
}
LocalBind bindAnnotation = variableElement.getAnnotation(LocalBind.class);
proxyInfo.setValus(bindAnnotation.value());
proxyInfo.injectVariables.put(fqClassName, variableElement);
}
for (String key : mProxyMap.keySet()) {
LocalProxyInfo proxyInfo = mProxyMap.get(key);
try {
JavaFileObject jfo = processingEnv.getFiler().createSourceFile(
proxyInfo.getProxyClassFullName(),
proxyInfo.getTypeElement());
Writer writer = jfo.openWriter();
writer.write(proxyInfo.generateJavaCode());//写入文件
writer.flush();
writer.close();
} catch (IOException e) {
error(proxyInfo.getTypeElement(),
"Unable to write injector for type %s: %s",
proxyInfo.getTypeElement(), e.getMessage());
}
}
return true;
}
可以看到,很简单的代码,先根据类名,参数等构造出文件生成器LocalProxyInfo:
public LocalProxyInfo(Elements elementUtils, TypeElement classElement) {
this.typeElement = classElement;
PackageElement packageElement = elementUtils.getPackageOf(classElement);
String packageName = packageElement.getQualifiedName().toString();
//classname
String className = ClassValidator.getClassName(classElement, packageName);
this.packageName = packageName;
this.proxyClassName = className + PROXY;
}
而proxyInfo.generateJavaCode()则是执行写入文件的入口了
public String generateJavaCode() {
StringBuilder builder = new StringBuilder();
builder.append("// Generated code. Do not modify!\n");
builder.append("package ").append(packageName).append(";\n\n");
builder.append("import com.longshihan.broca_api.*;\n");
builder.append("import android.content.IntentFilter;\n");
builder.append("import android.support.v4.content.LocalBroadcastManager;\n");
builder.append("import android.content.BroadcastReceiver;\n");
builder.append("import android.content.Context;\n");
builder.append("import android.content.Intent;\n");
builder.append("import android.support.annotation.UiThread;\n");
builder.append("import ").append(typeElement.getQualifiedName()).append(" ;\n");
builder.append('\n');
builder.append("public class ").append(proxyClassName).append(" implements Unbinder");
builder.append(" {\n");
builder.append("private LocalReceiver localReceiver;\n");
builder.append("private LocalBroadcastManager localBroadcastManager;\n");
builder.append(" private IntentFilter intentFilter;\n");
builder.append(" private ").append(typeElement.getQualifiedName()).append(" host;\n");
generateCMethods(builder);
generateMethods(builder);
generateDMethods(builder);
builder.append('\n');
builder.append("}\n");
return builder.toString();
}
我这里代码比较简单,直接用string生成代码就行了。当然,其他方式也行。
到这里一个功能就实现了,现在看下效果,猜也能猜出大概。
源文件MainActivity:
@LocalBind({"123", "345","4586"})
public void getBroadcastService(Intent intent) {
Log.d("打印Action", intent.getAction());
switch (intent.getAction()){
case "123":
break;
case "345":
break;
case "4586":
break;
}
}
生成的文件(build/generated/source/apt/debug/包名/MainActivity$BroadcastInject):
public class MainActivity$BroadcastInject implements Unbinder {
private LocalReceiver localReceiver;
private LocalBroadcastManager localBroadcastManager;
private IntentFilter intentFilter;
private com.longshihan.aopbrocad.MainActivity host;
@UiThread
public MainActivity$BroadcastInject(com.longshihan.aopbrocad.MainActivity host) {
localBroadcastManager = LocalBroadcastManager.getInstance(host);
intentFilter = new IntentFilter();
this.host=host;
intentFilter.addAction("123");
intentFilter.addAction("345");
intentFilter.addAction("4586");
localReceiver = new LocalReceiver();
localBroadcastManager.registerReceiver(localReceiver, intentFilter);
}
public class LocalReceiver extends BroadcastReceiver{
@Override
public void onReceive(Context context, Intent intent) {
host.getBroadcastService(intent);
}
}
@Override
public void unbind(){
localBroadcastManager.unregisterReceiver(localReceiver);
}
}
OK ,写到这里大家对于APT的使用也了然于心了吧,对于相关应用也有很多发散思维之处,比如Ali的ARouter,Dagger等等,这个可以交到大家之后学习了。