R.java 文件结构
R.java 是自动生成的,它包含了应用内所有资源的名称到数值的映射关系。先创建一个最简单的工程,看看 R.java 文件的内容:
R文件生成的目录为app/build/generated/not_namespaced_r_class_sources/xxxxxDebug/processXXXXDebugResources/r/com/xxx/xxx/R.java
R.java 内部包含了很多内部类:如 layout、mipmap、drawable、string、id 等等
这些内部类里面只有 2 种数据类型的字段:
代码语言:javascript复制public static final int
public static final int[]
只有 styleable 最为特殊,只有它里面有 public static final int[] 类型的字段定义,其它都只有 int 类型的字段。
此外,我们发现 R.java 类的代码行数最少也1000行了,这还只是一个简单的工程,压根没有任何业务逻辑。如果我们采用组件化开发或者在工程里创建多个 module ,你会发现在每个模块的包名下都会生成一个 R.java 文件。
为什么R文件可以删除
所有的 R.java 里定义的都是常量值,以 Activity 为例:
代码语言:javascript复制public class MainActivity extends AppCompatActivity {
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
}
R.layout.activity_main 实际上对应的是一个 int 型的常量值,那么如果我们编译打包时,将所有这些对 R 类的引用直接替换成常量值,效果也是一样的,那么 R.java 类在 apk 包里就是冗余的了。
前面说过 R.java 类里有2种数据类型,一种是 static final int 类型的,这种常量在运行时是不会修改的,另一种是 static final int[] 类型的,虽然它也是常量,但它是一个数组类型,并不能直接删除替换,所以打包进 apk 的 R 文件中,理论上除了 static final int[] 类型的字段,其他都可以全部删除掉。以上面这个为例:我们需要做的是编译时将 setContentView(R.layout.activity_main) 替换成:
代码语言:javascript复制setContentView(213196283);
ProGuard对R文件的混淆
通常我们会采用 ProGuard 进行混淆,你会发现混淆也能删除很多 R$*.class,但是混淆会造成一个问题:混淆后不能通过反射来获取资源了。现在很多应用或者SDK里都有通过反射调用来获取资源,比如大家最常用的统计SDK友盟统计、友盟分享等,就要求 R 文件不能混淆掉,否则会报错,所以我们常用的做法是开启混淆,但 keep 住 R 文件,在 proguard 配置文件中增加如下配置:
代码语言:javascript复制-keep class **.R$* {
*;
}
-dontwarn **.R$*
-dontwarn **.R
ProGuard 本身会对 static final 的基本类型做内联,也就是把代码引用的地方全部替换成常量,全部内联以后整个 R 文件就没地方引用了,就会被删掉。如果你的应用开启了混淆,并且不需要keep住R文件,那么app下的R文件会被删掉,但是module下的并不会被删掉,因为module下R文件内容不是static final的,而是静态变量。
如果你的应用需要keep住R文件,那么接下来,我们学习如何删除所有 R 文件里的冗余字段。
删除不必要的 R
对于 Android 工程来说,通常,library 的 R 只是 application 的 R 的一个子集,所以,只要有了全集,子集是可以通通删掉的,而且,application 的 R 中的常量字段,一旦参与编译后,就再也没有利用价值(反射除外)。在 R 的字段,styleable 字段是一个例外,它不是常量,它是 int[]。所以,删除 R 之前,我们要弄清楚要确定哪些是能删的,哪些是不能删的,根据经验来看,不能删的索引有:
1.ConstraintLayout 中引用的字段,例如:
代码语言:javascript复制<android.support.constraint.Group
android:id="@ id/group"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="visible"
app:constraint_referenced_ids="button4,button9" />
其中,R.id.button4 和 R.id.button9 是必须要保留的,因为 ContraintLayout 会调用 TypedArray.getResourceId(int, int) 来获取 button4 和 button9 的 id 索引。
总结下来,在 ConstraintLayout 中引用其它 id 的属性如下:
代码语言:javascript复制constraint_referenced_ids
layout_constraintLeft_toLeftOf
layout_constraintLeft_toRightOf
layout_constraintRight_toLeftOf
layout_constraintRight_toRightOf
layout_constraintTop_toTopOf
layout_constraintTop_toBottomOf
layout_constraintBottom_toTopOf
layout_constraintBottom_toBottomOf
layout_constraintBaseline_toBaselineOf
layout_constraintStart_toEndOf
layout_constraintStart_toStartOf
layout_constraintEnd_toStartOf
layout_constraintEnd_toEndOf
也就是说系统通过反射来获取的,包含反射属性的R是不能进行删除的,不然就会获取不到
因此,采用了解析 xml 的方式,从 xml 中提取以上属性。
其它通过 TypedArray.getResourceId(int, int) 或 Resources.getIdentifier(String, String, String) 来获取索引值的资源 针对这种情况,需要对字节码进行全盘扫描才能确定哪些地方调用了 TypedArray.getResourceId(int, int) 或 Resources.getIdentifier(String, String, String),考虑到增加一次 Transform 带来的性能损耗, 可以提供通过配置白名单的方式来保留这些资源索引
删除不必要的 Field
由于 Android 的资源索引只有 32 位整型,格式为:PP TT NNNN,其中:
PP 为 Package ID,默认为 0x7f; TT 为 Resource Type ID,从 1 开始依次递增; NNNN 为 Name ID,从 1 开始依次递增;
为了节省空间,在构建 application 时,所有同类型的资源索引会重排,所以,library 工程在构建期间无法确定资源最终的索引值,这就是为什么 library 工程中的资源索引是变量而非常量,既然在 application 工程中可以确定每个资源最终的索引值了,为什么不将 library 中的资源索引也替换为常量呢?这样就可以删掉多余的 field 了,在一定程度上可以减少 dex 的数量,收益是相当的可观。
我们可以看一下,这个是app module下的R.java
这个是module下的R.java
可以很明显发现app下是常量,library下是静态的变量
在编译期间获取索引常量值有很多种方法:
1)反射 R 类文件 2)解析 R 类文件 3)解析 Symbol List (R.txt) 经过 测试发现,解析 Symbol List 的方案性能最优,因此,在 Transform 之前拿到所有资源名称与索引值的映射关系。
关于解析 Symbol List (R.txt)的思路来源,可以参考gradle源码 TaskManager#createNonNamespacedResourceTasks
代码语言:javascript复制private void createNonNamespacedResourceTasks(
@NonNull VariantScope scope,
@NonNull File symbolDirectory,
InternalArtifactType packageOutputType,
@NonNull MergeType mergeType,
@NonNull String baseName,
boolean useAaptToGenerateLegacyMultidexMainDexProguardRules) {
File symbolTableWithPackageName =
FileUtils.join(
globalScope.getIntermediatesDir(),
FD_RES,
"symbol-table-with-package",
scope.getVariantConfiguration().getDirName(),
"package-aware-r.txt");
final TaskProvider<? extends ProcessAndroidResources> task;
File symbolFile = new File(symbolDirectory, FN_RESOURCE_TEXT);
BuildArtifactsHolder artifacts = scope.getArtifacts();
if (mergeType == MergeType.PACKAGE) {
// MergeType.PACKAGE means we will only merged the resources from our current module
// (little merge). This is used for finding what goes into the AAR (packaging), and also
// for parsing the local resources and merging them with the R.txt files from its
// dependencies to write the R.txt for this module and R.jar for this module and its
// dependencies.
// First collect symbols from this module.
taskFactory.register(new ParseLibraryResourcesTask.CreateAction(scope));
// Only generate the keep rules when we need them.
if (generatesProguardOutputFile(scope)) {
taskFactory.register(new GenerateLibraryProguardRulesTask.CreationAction(scope));
}
// Generate the R class for a library using both local symbols and symbols
// from dependencies.
task =
taskFactory.register(
new GenerateLibraryRFileTask.CreationAction(
scope, symbolFile, symbolTableWithPackageName));
} else {
// MergeType.MERGE means we merged the whole universe.
task =
taskFactory.register(
createProcessAndroidResourcesConfigAction(
scope,
() -> symbolDirectory,
symbolTableWithPackageName,
useAaptToGenerateLegacyMultidexMainDexProguardRules,
mergeType,
baseName));
if (packageOutputType != null) {
artifacts.createBuildableArtifact(
packageOutputType,
BuildArtifactsHolder.OperationType.INITIAL,
artifacts.getFinalArtifactFiles(InternalArtifactType.PROCESSED_RES));
}
// create the task that creates the aapt output for the bundle.
taskFactory.register(new LinkAndroidResForBundleTask.CreationAction(scope));
}
artifacts.appendArtifact(
InternalArtifactType.SYMBOL_LIST, ImmutableList.of(symbolFile), task.getName());
// Synthetic output for AARs (see SymbolTableWithPackageNameTransform), and created in
// process resources for local subprojects.
artifacts.appendArtifact(
InternalArtifactType.SYMBOL_LIST_WITH_PACKAGE_NAME,
ImmutableList.of(symbolTableWithPackageName),
task.getName());
}
就是会在以下路径app/build/intermediates/symbols/debug/R.txt生成文件,我们打开这个文件查看
可以看到R.txt里就有资源和索引的对应关系
代码实现
通过编写gradle插件,在 这里代码分析实现都是参考开源项目booster下代码 如何解析Symbol List (R.txt)
代码语言:javascript复制fun from(file: File) = SymbolList.Builder().also { builder ->
if (file.exists()) {
file.forEachLine { line ->
val sp1 = line.nextColumnIndex(' ')
val dataType = line.substring(0, sp1)
when (dataType) {
"int" -> {
val sp2 = line.nextColumnIndex(' ', sp1 1)
val type = line.substring(sp1 1, sp2)
val sp3 = line.nextColumnIndex(' ', sp2 1)
val name = line.substring(sp2 1, sp3)
val value: Int = line.substring(sp3 1).toInt()
builder.addSymbol(IntSymbol(type, name, value))
}
"int[]" -> {
val sp2 = line.nextColumnIndex(' ', sp1 1)
val type = line.substring(sp1 1, sp2)
val sp3 = line.nextColumnIndex(' ', sp2 1)
val name = line.substring(sp2 1, sp3)
val leftBrace = line.nextColumnIndex('{', sp3)
val rightBrace = line.prevColumnIndex('}')
val vStart = line.skipWhitespaceForward(leftBrace 1)
val vEnd = line.skipWhitespaceBackward(rightBrace - 1) 1
val values = mutableListOf<Int>()
var i = vStart
while (i < vEnd) {
val comma = line.nextColumnIndex(',', i, true)
i = if (comma > -1) {
values.add(line.substring(line.skipWhitespaceForward(i), comma).toInt())
line.skipWhitespaceForward(comma 1)
} else {
values.add(line.substring(i, vEnd).toInt())
vEnd
}
}
builder.addSymbol(IntArraySymbol(type, name, values.toIntArray()))
}
else -> throw MalformedSymbolListException(file.absolutePath)
}
}
}
}.build()
结合debug
代码语言:javascript复制int anim abc_slide_in_bottom 0x7f010006
其实就是解析第一个看是int还是int[] 然后解析出type=anim,name=abc_slide_in_bottom,value=0x7f010006.toInt,然后构建IntSymbol,然后添加到一个list中 val symbols = mutableListOf<Symbol<*>>()
如果是int[]
代码语言:javascript复制 public static int[] MsgView = { 0x7f040204, 0x7f040205, 0x7f040206, 0x7f040207, 0x7f040208, 0x7f040209 };
同样进行解析
对多余的R进行删除
寻找多余的R
代码语言:javascript复制 private fun TransformContext.findRedundantR(): List<Pair<File, String>> {
return artifacts.get(ALL_CLASSES).map { classes ->
val base = classes.toURI()
classes.search { r ->
r.name.startsWith("R") && r.name.endsWith(".class") && (r.name[1] == '$' || r.name.length == 7)
}.map { r ->
r to base.relativize(r.toURI()).path.substringBeforeLast(".class")
}
}.flatten().filter {
it.second != appRStyleable // keep application's R$styleable.class
}.filter { pair ->
!ignores.any { it.matches(pair.second) }
}
}
这里过滤掉了application’s R$styleable.class,还有白名单的 可以从debug中看到多余的R文件有哪些
对R常量内联
通过ASM对所有的class文件进行扫描,并利用其进行修改
代码语言:javascript复制private fun ClassNode.replaceSymbolReferenceWithConstant() {
methods.forEach { method ->
val insns = method.instructions.iterator().asIterable().filter {
it.opcode == GETSTATIC
}.map {
it as FieldInsnNode
}.filter {
("I" == it.desc || "[I" == it.desc)
&& it.owner.substring(it.owner.lastIndexOf('/') 1).startsWith("R$")
&& !(it.owner.startsWith(COM_ANDROID_INTERNAL_R) || it.owner.startsWith(ANDROID_R))
}
val intFields = insns.filter { "I" == it.desc }
val intArrayFields = insns.filter { "[I" == it.desc }
// Replace int field with constant
intFields.forEach { field ->
val type = field.owner.substring(field.owner.lastIndexOf("/R$") 3)
try {
method.instructions.insertBefore(field, LdcInsnNode(symbols.getInt(type, field.name)))
method.instructions.remove(field)
logger.println(" * ${field.owner}.${field.name} => ${symbols.getInt(type, field.name)}: $name.${method.name}${method.desc}")
} catch (e: NullPointerException) {
logger.println(" ! Unresolvable symbol `${field.owner}.${field.name}`: $name.${method.name}${method.desc}")
}
}
// Replace library's R fields with application's R fields
intArrayFields.forEach { field ->
field.owner = "$appPackage/${field.owner.substring(field.owner.lastIndexOf('/') 1)}"
}
}
}
对这段代码进行debug
以androidx/appcompat/app/AlertController.java这个类为例子 通过如下方法过滤出可以内联的field
代码语言:javascript复制("I" == it.desc || "[I" == it.desc)
&& it.owner.substring(it.owner.lastIndexOf('/') 1).startsWith("R$")
&& !(it.owner.startsWith(COM_ANDROID_INTERNAL_R) || it.owner.startsWith(ANDROID_R))
例如过滤出上面这个field 查看AlertController.java中确实有用到地方
代码语言:javascript复制 private static boolean shouldCenterSingleButton(Context context) {
final TypedValue outValue = new TypedValue();
context.getTheme().resolveAttribute(R.attr.alertDialogCenterButtons, outValue, true);
return outValue.data != 0;
}
即可以对R.attr.alertDialogCenterButtons进行内联替换 代码如下:
代码语言:javascript复制 method.instructions.insertBefore(field, LdcInsnNode(symbols.getInt(type, field.name)))
method.instructions.remove(field)
代码语言:javascript复制context.getTheme().resolveAttribute(R.attr.alertDialogCenterButtons, outValue, true);
1.通过symbols.getInt(type, field.name)获取R.attr.alertDialogCenterButtons对应的常量值
2.通过ASM在R.attr.alertDialogCenterButtons前插入这个常量值即method.instructions.insertBefore(field, LdcInsnNode(symbols.getInt(type, field.name)))
3.删除R.attr.alertDialogCenterButtons
对于int[]的修改就简单多了
代码语言:javascript复制intArrayFields.forEach { field ->
field.owner = "$appPackage/${field.owner.substring(field.owner.lastIndexOf('/') 1)}"
}
直接将field.owner修改,从module的包路径改为app包名下主路径