前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Android Apk瘦身方案1——R.java文件常量内联

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

作者头像
老马的编程之旅
发布2022-06-23 14:07:47
9400
发布2022-06-23 14:07:47
举报
文章被收录于专栏:深入理解Android

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包名下主路径

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2020-06-05,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • R.java 文件结构
  • 为什么R文件可以删除
  • ProGuard对R文件的混淆
  • 删除不必要的 R
  • 删除不必要的 Field
  • 代码实现
  • 对多余的R进行删除
  • 对R常量内联
相关产品与服务
腾讯云代码分析
腾讯云代码分析(内部代号CodeDog)是集众多代码分析工具的云原生、分布式、高性能的代码综合分析跟踪管理平台,其主要功能是持续跟踪分析代码,观测项目代码质量,助力维护团队卓越代码文化。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档