ButterKnife 是怎麼解決 library 的 R 問題的
阿新 • • 發佈:2019-01-30
問題
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";
}
}
這樣完全不需要任何耗時耗力的反查工作,直接拿來用就好了。