在android library项目里由于R类中变量不再是final类型而无法使用butterknife,为了解决此问题,Jakewharton大神引入了butterknife-gradle-plugin插件,用于生成变量类型为final的R2类。
此处为butterknife-gradle-plugin 8.4.0版本为例,介绍一下插件在library中的使用以及源码分析。
使用
在项目的build.gradle中添加classpath:
buildscript {
repositories {
jcenter()
google()
}
dependencies {
classpath 'com.android.tools.build:gradle:3.0.1'
classpath 'com.jakewharton:butterknife-gradle-plugin:8.4.0'
}
}
在library项目build.gradle中引入插件:
apply plugin: 'com.android.library'
apply plugin: 'com.jakewharton.butterknife'
在library项目build.gradle中添加依赖:
implementation 'com.jakewharton:butterknife:8.4.0'
annotationProcessor 'com.jakewharton:butterknife-compiler:8.4.0'
源码分析
插件主要有两个文件ButterKnifePlugin.groovy和FinalRClassBuilder.java
ButterKnifePlugin.groovy
public class ButterKnifePlugin implements Plugin<Project> {
@Override
void apply(Project project) {
if (!(project.plugins.hasPlugin(LibraryPlugin) || project.plugins.hasPlugin(AppPlugin))) {
throw new IllegalStateException('Butterknife plugin can only be applied to android projects')
}
def variants
if (project.plugins.hasPlugin(LibraryPlugin)) {
variants = project.android.libraryVariants
} else {
variants = project.android.applicationVariants
}
project.afterEvaluate {
variants.all { BaseVariant variant ->
variant.outputs.each { BaseVariantOutput output ->
output.processResources.doLast {
File rDir = new File(sourceOutputDir, packageForR.replaceAll('\\.',
StringEscapeUtils.escapeJava(File.separator)))
File R = new File(rDir, 'R.java')
FinalRClassBuilder.brewJava(R, sourceOutputDir, packageForR, 'R2')
}
}
}
}
}
}
在apply方法里首先判断项目是否存在LibraryPlugin(com.android.library)或AppPlugin(com.android.application),如不存在则抛出异常,然后获取对应的variants,在项目的processResources阶段,获取R.java文件的信息,sourceOutputDir对应输出目录如app\build\generated\source\r\release,packageForR为包名,如com.example.pengf.myapplication,则R.java的输出位置为app\build\generated\source\r\release\com\example\pengf\myapplication\R.java,然后调用FinalRClassBuilder类的brewJava方法生成R2.java文件
FinalRClassBuilder.java
用于根据R.java文件生成R2.java文件
public final class FinalRClassBuilder {
private static final String SUPPORT_ANNOTATION_PACKAGE = "android.support.annotation";
private static final String[] SUPPORTED_TYPES = {
"array", "attr", "bool", "color", "dimen", "drawable", "id", "integer", "string"
};
private FinalRClassBuilder() {
}
public static void brewJava(File rFile, File outputDir, String packageName, String className)
throws Exception {
CompilationUnit compilationUnit = JavaParser.parse(rFile);
TypeDeclaration resourceClass = compilationUnit.getTypes().get(0);
TypeSpec.Builder result =
TypeSpec.classBuilder(className).addModifiers(PUBLIC).addModifiers(FINAL);
for (Node node : resourceClass.getChildrenNodes()) {
if (node instanceof TypeDeclaration) {
addResourceType(Arrays.asList(SUPPORTED_TYPES), result, (TypeDeclaration) node);
}
}
JavaFile finalR = JavaFile.builder(packageName, result.build())
.addFileComment("Generated code from Butter Knife gradle plugin. Do not modify!")
.build();
finalR.writeTo(outputDir);
}
private static void addResourceType(List<String> supportedTypes, TypeSpec.Builder result,
TypeDeclaration node) {
if (!supportedTypes.contains(node.getName())) {
return;
}
String type = node.getName();
TypeSpec.Builder resourceType = TypeSpec.classBuilder(type).addModifiers(PUBLIC, STATIC, FINAL);
for (BodyDeclaration field : node.getMembers()) {
if (field instanceof FieldDeclaration) {
addResourceField(resourceType, ((FieldDeclaration) field).getVariables().get(0),
getSupportAnnotationClass(type));
}
}
result.addType(resourceType.build());
}
private static void addResourceField(TypeSpec.Builder resourceType, VariableDeclarator variable,
ClassName annotation) {
String fieldName = variable.getId().getName();
String fieldValue = variable.getInit().toString();
FieldSpec.Builder fieldSpecBuilder = FieldSpec.builder(int.class, fieldName)
.addModifiers(PUBLIC, STATIC, FINAL)
.initializer(fieldValue);
if (annotation != null) {
fieldSpecBuilder.addAnnotation(annotation);
}
resourceType.addField(fieldSpecBuilder.build());
}
private static ClassName getSupportAnnotationClass(String type) {
return ClassName.get(SUPPORT_ANNOTATION_PACKAGE, capitalize(type) + "Res");
}
private static String capitalize(String word) {
return Character.toUpperCase(word.charAt(0)) + word.substring(1);
}
}
brewJava方法主要是调用javapoet生成R2.java文件,支持的类型有array, attr, bool, color, dimen, drawable, id, integer, string,首先通过JavaParser类对R.java文件进行转换,然后依次读入R.java中每个节点,如该节点的类型为支持的类型,则将该节点下面的每个变量都写入到R2.java中,变量前加入final关键字,值为R.java中变量对应的值,同时为每个变量添加注解。最后将R2.java文件写入到指定输出目录。
如R.java里有以下内容:
public static final class bool {
public static int abc_action_bar_embed_tabs = 0x7f050001;
public static int abc_allow_stacked_button_bar = 0x7f050002;
public static int abc_config_actionMenuItemAllCaps = 0x7f050003;
public static int abc_config_showMenuShortcutsWhenKeyboardPresent = 0x7f050004;
}
则生成的R2.java文件如下,每一个变量前都加入了final关键字,其值为R.java中对应的值,还加入了android.support.annotation类中对应的注解
public static final class bool {
@BoolRes
public static final int abc_action_bar_embed_tabs = 0x7f050001;
@BoolRes
public static final int abc_allow_stacked_button_bar = 0x7f050002;
@BoolRes
public static final int abc_config_actionMenuItemAllCaps = 0x7f050003;
@BoolRes
public static final int abc_config_showMenuShortcutsWhenKeyboardPresent = 0x7f050004;
}