问题
Annotation 中必须引用 final 的值(编译期已经有最终值),而 ButterKnife 中引用的 R (在 library 工程中)是非 final 的。
ButterKnife 的 tricks
生成 R2
既然 R 不是 final 的,生成一个呗。所以 butterknife 实现了一个 plugin,把 R 拷贝出了一个 final 版本: R2。这个非常直接有效,这个代码很短,只是一个 plugin 在每个配置上都加了一个拷贝 R 的 task。
这样就万事大吉了吗?完全不是,R2 只代表了在本 module 编译期间 R 的值,然而,在运行时 R 和 R2 完全对不上。
根据 R2 反查 R
看 ButterKnife 生成的代码,会发现里面并没有引用 R2,而是直接使用的 R。这就是为了解决运行期和编译期 R 值不一致的问题。怎么做到的呢?
在 apt 时,能得到的 Element、 annotation 都是返回值的。也就是说,并不知道当前传入的是 R2 的哪个 field。虽然 R2 和 R 在 module 编译期的值是一致的,也并不能直接找到。
这就衍生了用值构建 R 和 R2 对应关系的需求。这里需要用到一个新的 api:com.sun.source.util。这个库能够根据 Element 反查出真正 java 文件的树形结构,里面能够获取编译期对应的 AST 语言元素,比如调用、比较等等。这样就可以做赋值分析、代码进一步解构等工作。
ButterKnife 源码:
private void scanForRClasses(RoundEnvironment env) {
if (trees == null) return;
RClassScanner scanner = new RClassScanner();
// 所有 annotation 里的值 都是来自于 R2 的
for (Class<? extends Annotation> annotation : getSupportedAnnotations()) {
for (Element element : env.getElementsAnnotatedWith(annotation)) {
JCTree tree = (JCTree) trees.getTree(element, getMirror(element, annotation));
if (tree != null) { // tree can be null if the references are compiled types and not source
scanner.setCurrentPackage(elementUtils.getPackageOf(element));
tree.accept(scanner);
}
}
}
for (Map.Entry<PackageElement, Set<Symbol.ClassSymbol>> packageNameToRClassSet
: scanner.getRClasses().entrySet()) {
PackageElement respectivePackageName = packageNameToRClassSet.getKey();
for (Symbol.ClassSymbol rClass : packageNameToRClassSet.getValue()) {
parseRClass(respectivePackageName, rClass, scanner.getReferenced());
}
}
}
private void parseRClass(PackageElement respectivePackageName, Symbol.ClassSymbol rClass,
Set<String> referenced) {
TypeElement element;
// 各种奇技淫巧才能work
try {
element = rClass;
} catch (MirroredTypeException mte) {
element = (TypeElement) typeUtils.asElement(mte.getTypeMirror());
}
JCTree tree = (JCTree) trees.getTree(element);
if (tree != null) { // tree can be null if the references are compiled types and not source
IdScanner idScanner =
new IdScanner(symbols, elementUtils.getPackageOf(element), respectivePackageName,
referenced);
tree.accept(idScanner);
} else {
parseCompiledR(respectivePackageName, element, referenced);
}
}
// 针对 final 的
private void parseCompiledR(PackageElement respectivePackageName, TypeElement rClass,
Set<String> referenced) {
for (Element element : rClass.getEnclosedElements()) {
String innerClassName = element.getSimpleName().toString();
if (SUPPORTED_TYPES.contains(innerClassName)) {
for (Element enclosedElement : element.getEnclosedElements()) {
if (enclosedElement instanceof VariableElement) {
String fqName = elementUtils.getPackageOf(enclosedElement).getQualifiedName().toString()
+ ".R."
+ innerClassName
+ "."
+ enclosedElement.toString();
if (referenced.contains(fqName)) {
VariableElement variableElement = (VariableElement) enclosedElement;
Object value = variableElement.getConstantValue();
if (value instanceof Integer) {
int id = (Integer) value;
ClassName rClassName =
ClassName.get(elementUtils.getPackageOf(variableElement).toString(), "R",
innerClassName);
String resourceName = variableElement.getSimpleName().toString();
QualifiedId qualifiedId = new QualifiedId(respectivePackageName, id);
symbols.put(qualifiedId, new Id(id, rClassName, resourceName));
}
}
}
}
}
}
}
private static class RClassScanner extends TreeScanner {
// Maps the currently evaluated rPackageName to R Classes
private final Map<PackageElement, Set<Symbol.ClassSymbol>> rClasses = new LinkedHashMap<>();
private PackageElement currentPackage;
private Set<String> referenced = new HashSet<>();
@Override public void visitSelect(JCTree.JCFieldAccess jcFieldAccess) {
Symbol symbol = jcFieldAccess.sym;
// 这个是各种层次,具体不清楚 只能抄
if (symbol != null
&& symbol.getEnclosingElement() != null
&& symbol.getEnclosingElement().getEnclosingElement() != null
&& symbol.getEnclosingElement().getEnclosingElement().enclClass() != null) {
Set<Symbol.ClassSymbol> rClassSet = rClasses.get(currentPackage);
if (rClassSet == null) {
rClassSet = new HashSet<>();
rClasses.put(currentPackage, rClassSet);
}
referenced.add(getFqName(symbol));
rClassSet.add(symbol.getEnclosingElement().getEnclosingElement().enclClass());
}
}
Map<PackageElement, Set<Symbol.ClassSymbol>> getRClasses() {
return rClasses;
}
Set<String> getReferenced() {
return referenced;
}
void setCurrentPackage(PackageElement packageElement) {
this.currentPackage = packageElement;
}
}
private static class IdScanner extends TreeScanner {
private final Map<QualifiedId, Id> ids;
private final PackageElement rPackageName;
private final PackageElement respectivePackageName;
private final Set<String> referenced;
IdScanner(Map<QualifiedId, Id> ids, PackageElement rPackageName,
PackageElement respectivePackageName, Set<String> referenced) {
this.ids = ids;
this.rPackageName = rPackageName;
this.respectivePackageName = respectivePackageName;
this.referenced = referenced;
}
@Override public void visitClassDef(JCTree.JCClassDecl jcClassDecl) {
for (JCTree tree : jcClassDecl.defs) {
if (tree instanceof ClassTree) {
ClassTree classTree = (ClassTree) tree;
String className = classTree.getSimpleName().toString();
if (SUPPORTED_TYPES.contains(className)) {
ClassName rClassName = ClassName.get(rPackageName.getQualifiedName().toString(), "R",
className);
VarScanner scanner = new VarScanner(ids, rClassName, respectivePackageName, referenced);
((JCTree) classTree).accept(scanner);
}
}
}
}
}
private static class VarScanner extends TreeScanner {
private final Map<QualifiedId, Id> ids;
private final ClassName className;
private final PackageElement respectivePackageName;
private final Set<String> referenced;
private VarScanner(Map<QualifiedId, Id> ids, ClassName className,
PackageElement respectivePackageName, Set<String> referenced) {
this.ids = ids;
this.className = className;
this.respectivePackageName = respectivePackageName;
this.referenced = referenced;
}
@Override public void visitVarDef(JCTree.JCVariableDecl jcVariableDecl) {
if ("int".equals(jcVariableDecl.getType().toString())) {
String resourceName = jcVariableDecl.getName().toString();
if (referenced.contains(getFqName(jcVariableDecl.sym))) {
int id = Integer.valueOf(jcVariableDecl.getInitializer().toString());
QualifiedId qualifiedId = new QualifiedId(respectivePackageName, id);
ids.put(qualifiedId, new Id(id, className, resourceName));
}
}
}
}
我抄过来用的要简单很多:
public void onAnnotatedElement(Element field, Class<? extends Annotation> annotation) {
JCTree tree = (JCTree) mTrees.getTree(field, getMirror(field, annotation));
tree.accept(new TreeScanner() {
@Override
public void visitSelect(JCTree.JCFieldAccess jcFieldAccess) {
Symbol symbol = jcFieldAccess.sym;
if (symbol != null
&& symbol.getEnclosingElement() != null
&& symbol.getEnclosingElement().getEnclosingElement() != null
&& symbol.getEnclosingElement().getEnclosingElement().enclClass() != null) {
TypeElement rClass;
try {
rClass = symbol.getEnclosingElement().getEnclosingElement().enclClass();
} catch (MirroredTypeException mte) {
rClass = (TypeElement) mTypes.asElement(mte.getTypeMirror());
}
parseR2(rClass);
}
}
});
}
private void parseR2(Element r2Class) {
TypeElement r2Idclass = null;
for (Element idClass : r2Class.getEnclosedElements()) {
if (idClass.getKind() != ElementKind.CLASS || !idClass.toString().endsWith(".id")) {
continue;
}
r2Idclass = (TypeElement) idClass;
break;
}
if (r2Idclass == null) {
return;
}
if (!mParsedR.add((TypeElement) r2Class)) {
return;
}
TypeElement rClass = mElements.getTypeElement(mElements.getPackageOf(r2Class) + ".R.id");
for (Element idField : r2Idclass.getEnclosedElements()) {
if (idField.getKind() != ElementKind.FIELD) {
continue;
}
Optional.ofNullable(AptUtils.assignedValue(idField, mTrees))
.map(val -> Integer.parseInt(val))
.map(id -> {
return mId2NameMapping.put(id, new RClassField(idField.toString(), rClass));
});
}
}
public static String assignedValue(Element field, Trees trees) {
Tree tree = trees.getTree(field);
if (tree == null) {
return String.valueOf(((VariableElement) field).getConstantValue());
} else {
ExpressionTree expression = ((VariableTree) tree).getInitializer();
return Optional.ofNullable(expression)
.filter(e -> e instanceof LiteralTree)
.map(e -> e.toString()).orElse(null);
}
}
我的方案
其实 R2 不需要是 R 的翻版,而可以是 String 类型的、R 的对应关系。例如:
R2{
id{
final String some_id_in_r = "xxx.R.id.some_id_in_r";
}
}
这样完全不需要任何耗时耗力的反查工作,直接拿来用就好了。