《Android资源优化陷阱:当shrinkResources遇上反射,如何避免资源误删?》

8 阅读4分钟

背景介绍:

包体积优化道路上的 "踩坑" 在接手项目之前, 那时候的APK包体积有110M左右, 接手之后,对项目做了一些包体积优化的策略, 目前的包体积在17M左右。 其中有一个策略就是通过配置: shrinkResources true 实现包体积优化, 而且包体积优化的效果非常显著。 但是,使用不当,会导致资源误删除。 我把此次踩坑的教训,分享给大家,希望能对大家有所帮助,同时,避免此类问题,重蹈覆辙。


shrinkResources的原理

Google官方提供的 资源缩减(Resource Shrinking) 工具链是我们的得力助手。通过在 build.gradle 文件中简单地设置 shrinkResources true,配合代码混淆(ProGuard/R8),构建系统就能自动移除项目中和库中未被引用的资源,为APK“瘦身”效果显著。 然而,静态分析的局限性 在动态技术面前暴露无遗。在实际项目中,我们常常会使用一些高级技巧,例如 反射(Reflection) 来动态地获取和使用资源(例如,通过 Resources.getIdentifier() 根据字符串名称查找资源ID,或在插件化、热修复框架中动态加载资源)。这些运行时行为,构建系统在静态分析阶段是无法准确追踪的。 这就导致了一个令人头疼的“优化陷阱”:那些在代码中被反射引用的宝贵资源,很容易被 shrinkResources 误判为“未使用”而遭到无情删除。结果就是,应用在开发调试阶段一切正常,但一旦打出发布包(Release APK),运行时就会抛出 Resources.NotFoundException 等异常,引发崩溃或功能缺失,且问题难以在编译期被发现。


如何开启ShrinkResource

buildTypes {

release {

ext.alwaysUpdateBuildId = false

minifyEnabled true

shrinkResources true

signingConfig signingConfigs.release

proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), '../proguard-rules.pro'

} }

shrinkResources配置为true只有在开启混淆后才生效


如何避免资源被误删除

由于开启shrinkResources后,编译生成APK的时候会移除无用资源。为了避免资源被误删除,总结了两种解决方案:

  1. 关闭shrinkResources

shrinkResources false 这种解决方式,不是正向解决问题。 虽然能解决资源误删除的问题, 同时带来的问题是包体积增大, 推荐的解决方案,配置白名单。

  1. 配置白名单

添加图片注释,不超过 140 字(可选)

在res目录下面创建raw目录, raw目录下面创建keep.xml文件。在keep.xml文件中配置白名单。 如果在代码中有通过反射取资源id, 则需要在keep.xml文件中,配置资源名,确保在编译生成apk时,不会删除此资源。


案例分享

最近因为这个事情,导致项目崩溃,影响还挺大的,警钟长鸣。 在公共组件库中,面向车型开发,建了一个如下的配置文件:

{ "default" : "widget_checkbox_selector_circle", "_F111": "widget_checkbox_selector_circle", "_F222" : "widget_checkbox_selector_circle_4dp"}

在上层的java代码中, 通过反射获取资源id:

getResourceIdByName这个方法就是通过反射获取资源ID:

项目在编Release的APK时,会开启混淆, 同时也会开启资源优化。由于没有配置白名单, 导致widget_checkbox_selector_circle, widget_checkbox_selector_circle_4dp两个资源图片会绕过检查,判定为未使用的资源图片,在编APK的时候会被优化处理。 反编译APK查看对应资源图片: 正常的xml文件内容如下:

<?xml version="1.0" encoding="utf-8"?> 
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@drawable/widget_radio_default_sel_circle" android:state_checked="true" android:state_enabled="true"></item>
<item android:drawable="@drawable/widget_radio_default_sel_dis_circle" android:state_checked="true" android:state_enabled="false"></item>
<item android:drawable="@drawable/widget_radio_default_nor_circle" android:state_checked="false" android:state_enabled="true"></item> 
<item android:drawable="@drawable/widget_radio_default_dis_circle" android:state_checked="false" android:state_enabled="false"></item>
</selector>

实际编译生成apk后查看对应文件的内容如下:

从上面的图片明显可以看出编译生成的apk查看对应的资源图片,虽然文件存在, 但是文件内容为空的。因此会导致:Resources.NotFoundException 因此程序会闪退。


总结

反射虽然能实现很多牛逼的功能, 同时也存在很多坑, 当开启混淆,资源优化时,不仅仅会出现: ResourceNotFoundException, 还有可能出向ClassNotFoundException等问题。因此, 当使用到反射时:

  1. 反射获取资源时, 需要在混淆规则文件配置R文件不参与混淆
  2. 反射获取资源时,并且开启shrinkResource时, 需要在keep.xml中配置白名单。
  3. 在java代码中,反射获取类时, 如: Class.forName("包名+类名"),需要在混淆规则文件中配置。