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 种数据类型的字段:

public static final int
public static final int[]

只有 styleable 最为特殊,只有它里面有 public static final int[] 类型的字段定义,其它都只有 int 类型的字段。

此外,我们发现 R.java 类的代码行数最少也1000行了,这还只是一个简单的工程,压根没有任何业务逻辑。如果我们采用组件化开发或者在工程里创建多个 module ,你会发现在每个模块的包名下都会生成一个 R.java 文件。

为什么R文件可以删除

所有的 R.java 里定义的都是常量值,以 Activity 为例:

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) 替换成:

setContentView(213196283);

ProGuard对R文件的混淆

通常我们会采用 ProGuard 进行混淆,你会发现混淆也能删除很多 R$*.class,但是混淆会造成一个问题:混淆后不能通过反射来获取资源了。现在很多应用或者SDK里都有通过反射调用来获取资源,比如大家最常用的统计SDK友盟统计、友盟分享等,就要求 R 文件不能混淆掉,否则会报错,所以我们常用的做法是开启混淆,但 keep 住 R 文件,在 proguard 配置文件中增加如下配置:

-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 中引用的字段,例如:

<android.support.constraint.Groupandroid: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 的属性如下:

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

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)

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) + 1val values = mutableListOf<Int>()var i = vStartwhile (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

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[]

 public static int[] MsgView = { 0x7f040204, 0x7f040205, 0x7f040206, 0x7f040207, 0x7f040208, 0x7f040209 };

同样进行解析

对多余的R进行删除

寻找多余的R

   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文件进行扫描,并利用其进行修改

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 constantintFields.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 fieldsintArrayFields.forEach { field ->field.owner = "$appPackage/${field.owner.substring(field.owner.lastIndexOf('/') + 1)}"}}}

对这段代码进行debug

以androidx/appcompat/app/AlertController.java这个类为例子
通过如下方法过滤出可以内联的field

("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中确实有用到地方

  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进行内联替换
代码如下:

 method.instructions.insertBefore(field, LdcInsnNode(symbols.getInt(type, field.name)))method.instructions.remove(field)
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[]的修改就简单多了

intArrayFields.forEach { field ->field.owner = "$appPackage/${field.owner.substring(field.owner.lastIndexOf('/') + 1)}"}

直接将field.owner修改,从module的包路径改为app包名下主路径

Android Apk瘦身方案1——R.java文件常量内联相关推荐

  1. Android Apk瘦身方案2——gradle插件将png自动压缩为webp

    实现思路 在 mergeRes 和 processRes 任务之间插入 WebP 压缩任务,如下图所示: 使用开源框架Cwebp,使用命令行对所有的图片进行遍历处理,然后将结果输出 Google 官方 ...

  2. Android Apk瘦身指南大全

    Android Apk瘦身指南大全 前言 为什么Android应用Apk要瘦身?虽然安装包大小对应用使用没有影响,但应用的安装包越大,用户下载的门槛越高,安装等待时间也会越长,特别是在移动网络情况下, ...

  3. Android APK 瘦身

    Android APK 瘦身的几个方法 将项目中的图片由png.jpg转为webp格式.如下操作: 1.1选中图片或者含有图片的文件夹 右键选择Convert toWebP.. 1.2根据自身情况选择 ...

  4. Android apk瘦身讲解

    apk瘦身 想办法较减少apk大小 必要性 同样功能,apk越小越好,用户下载动机更大 了解apk的组成 classes.dex:是java源码编译后生成的java字节码文件 resources.ar ...

  5. Android APK 瘦身 - JOOX Music项目实战

    JOOX Music是腾讯海外布局的一个音乐产品,2014年发布以来已经成为5个国家和地区排名第一的音乐App.东南亚是JOOX Music的主要发行地区,由于JOOX Music所面对的市场存在很多 ...

  6. Android apk瘦身之使用TinyPng压缩图片

    1 前言 Android apk太大了怎么办,有的时候需要减小apk的体积,例如上传到服务器的时候有文件大小的限制.百度一下,发现大致有以下几种方法: 使用混淆,减少代码体积 删除不使用的资源 减少使 ...

  7. 导入android源码有错,R.java文件不能自动生成解决方法 http://caizi12.iteye.com/blog/975125

    最近几天学习android ,学习时候难免要导入一些示例,目的为了更加了解android各种API用法,顺便也可以学习下别人代码的写法.可是导入android源码后,基本都有错误,R.java也不会自 ...

  8. android APK瘦身全面总结——如何从32.6M到13.6M

    前言 之前我简单介绍了关于svg图片瘦身的问题,在公司,瘦身这个问题是我提出来的,所以这锅我背了.公司项目是32.6M,我给自己的要求就是低于20M.上周花了一个星期瘦身,至于为什么花了一周,主要是s ...

  9. Android APK瘦身

    随着项目不断地完善,资源不断地增多,导致apk大小越来越大,编译以及安装等待时间越来越长,虽然对应用使用并没有影响,但是用户会越不愿意下载.还有特别是在使用移动网络的时候,用户更不会浪费流量去下载,所 ...

最新文章

  1. cmake (4)引用子目录的库
  2. 携手长江天成转型 谋定·农业大健康-林裕豪:升级山茶油农产品
  3. Java内存模型与happens-before原则
  4. ITK:将图像从一种类型投射到另一种类型,但限制在输出值范围内
  5. 静态html引入js添加随机数后缀防止缓存
  6. Foundationd和Application Kit的类层次
  7. matlab晶闸管整流电路,采用Matlab/Simulink对三相桥式全控整流电路的仿真分析
  8. edge浏览器运行不流畅怎么办 提高edge浏览器速度的方法
  9. 乔帮主,iOS 9 关键源码被泄露了!
  10. 226.翻转二叉树 (力扣leetcode) 博主可答疑该问题
  11. 服务器fs改变文件内容,SeaweedFS文件系统
  12. 关于微信小程序web开发者工具模拟器出现空白问题
  13. 5g与计算机网络有什么关系,5G网络是什么意思 5G和4G网络有什么区别?
  14. 如何去掉vi下面显示的[converted]标志
  15. 苹果也被撕下遮羞布,iPhone14后继乏力,靠十三香维持销量
  16. python04-集合与字典
  17. 关于Element-ui中el-table在谷歌和360极速浏览器的兼容问题
  18. MyBatis 大于小于不等于的写法
  19. Service id not legal hostname (service_cmn)
  20. 【codeforces 794B】Cutting Carrot

热门文章

  1. 利用数字电子计数知识设计并制作的数字电子钟(含multisim仿真),该数字钟具有显示星期、24小时制时间、闹铃、整点报时、时间校准功能
  2. nexmo - 电话报警Alert
  3. 财报发布前获多家投行认可,借商流+物流闭环效应达达估值可期
  4. 海光服务器型号,中科海光CPU的首次评测:基于AMD架构,覆盖桌面服务器端
  5. 使用React,Twilio和Cosmic JS构建约会调度程序
  6. AcWing 487. 金明的预算方案 (有依赖关系的背包问题)
  7. Java中一个数的N次方
  8. 关于python无法显示中文的问题:SyntaxError: Non-ASCII character '\xe4' in file test.py on line 3
  9. [笔记]Pytorch框架下的入门应用:resnet34实现分类
  10. 6G八大关键技术(国泰君安团队)