首先感谢掘金的这篇文章,从中学到了很多关于R文件的知识和调试方法 https://juejin.cn/post/7289748806438420480
背景
近期提供了一个Apk给业务应用接入,业务应用表示接入SDK以后包体积膨胀过大(208KB),需要优化,最好是能优化到100KB以下。由于我是第一次碰到这样的问题,所以只能一边查资料一边摸索
体积膨胀的原因
最初,我以为是我的SDK中的依赖项比较多,导致APK接入以后引入了过多的依赖,进而导致体积增长。但是和业务的APK开发沟通以后,发现SDK和APK中的依赖基本是重复的,只有很少的新增。而这些新增很难达到200KB的体积。这一个方向走不通。
随后,我通过ApkTool反编译了业务的APK,这一下发现了异常:业务的Apk反编译以后得到了很多smali_classes目录,进入到这些dex目录中,我发现每一个包名的路径下都会有很多R$*.smali文件;每个路径下这些文件的体积加在一起都有几百KB之多。随后我自己打包了一些Demo Apk然后使用同样的操作反编译,并没有发现产物中有这些R文件,所以初步认为这些被打包进Apk中的R文件就是导致体积增长的直接原因。
-a--- 2024/9/19 16:53 2714 R$anim.smali
-a--- 2024/9/19 16:53 2291 R$animator.smali
-a--- 2024/9/19 16:53 62649 R$attr.smali
-a--- 2024/9/19 16:53 496 R$bool.smali
-a--- 2024/9/19 16:53 93277 R$color.smali
-a--- 2024/9/19 16:53 47894 R$dimen.smali
-a--- 2024/9/19 16:53 11621 R$drawable.smali
-a--- 2024/9/19 16:53 25507 R$id.smali
-a--- 2024/9/19 16:53 2424 R$integer.smali
-a--- 2024/9/19 16:53 1172 R$interpolator.smali
-a--- 2024/9/19 16:53 9441 R$layout.smali
-a--- 2024/9/19 16:53 348 R$plurals.smali
-a--- 2024/9/19 16:53 8853 R$string.smali
-a--- 2024/9/19 16:53 67585 R$style.smali
-a--- 2024/9/19 16:53 118393 R$styleable.smali
-a--- 2024/9/19 16:53 643 R$xml.smali
根因
查询资料我了解到:
- R文件是一个类,类中存储的是变量到资源ID的映射;平时开发Android的时候我们调用
R.id.XXX
,其实是调用的R这个类中的id这个类中的XXX变量(或常量,后续会有详细说明),这个变量帮助我们在APK中索引我们需要的资源; - 在library(aar)或者application(apk)中,R文件的表现是不一样的。我们先理解这么一个结论:对于安卓工程来说,APK才是最终的产物,aar只是一种中间产物。所以aar中的资源最终都是要被打包到APK中的,APK中的资源才是生效的资源。
- aapt在处理资源文件的时候,会将所有aar中的资源文件和apk本身的资源文件合并到一个R文件中,然后再将这个R文件中的所有变量设置为常量。然后,代码编译的过程中,引用到R文件的地方直接内联为常量值
按照以上这些说法,在APK反编译以后,应该是不会看到如此多的R文件反编译得到的字节码的,至少不应该每个包名下都有存在R文件字节码,因为R文件在打包的时候就已经合并成了一份了。所以业务的Apk在打包的过程中,并没有将所有aar的R文件合并。顺着这个思路,我要到了业务的混淆配置文件,果然在其中发现了这样一段配置:
-keep public class **.R$* {
public static final int *;
}
这段配置导致APK工程中所有包的R文件都不会被混淆或者压缩,我猜测这就是导致R文件的字节码被打包进APK的根因。随后我在Demo工程中也添加了这一段混淆配置进行验证,果然如此。
验证
随后我进行了一系列验证,来搞明白我在研究这个问题的过程中产生的一系列疑问:
1. 这一段配置的作用是什么?
考虑到混淆一个类通常是因为要使用反射调用,因此我让业务的开发帮忙去掉这段配置打了一个包,果不其然,在一些场景下APK无法正常运行了,其中有一个反射报错堆栈中有一行是这样的:
com.xxx.xxx.module.common.webview.BaseJsBridgeFragment$a.invoke(BaseJsBridgeFragment.kt:95)
于是对业务Apk反编译,找到对应的包名下的这个方法,找到了这样一段字节码:
Method declaredMethod = ((Class) type2).getDeclaredMethod("inflate", LayoutInflater.class);
...
Object invoke = declaredMethod.invoke(null, aVar2);
看起来,这段代码尝试反射调用一个类中涉及到布局的方法(inflate),那么我们可以猜测是由于R文件被混淆,导致布局操作无法获取到资源文件,于是抛出了异常。
2. 这一段配置是如何影响APK产物的
我的Demo Apk中有这样一段代码(一段很普通的在onCreate创建视图的代码):
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.container_web_activity);
然后分别打包两个Demo Apk,一个开启混淆并且配置keep R规则,另一个开启混淆但是不配置keep R规则,然后分别查看这段代码对应的字节码是怎样的。首先是没有keep R的Apk:
...
.method public onCreate(Landroid/os/Bundle;)V
.registers 8
invoke-super {p0, p1}, Landroidx/fragment/app/o;->onCreate(Landroid/os/Bundle;)V
const v0, 0x7f0b0020
invoke-virtual {p0, v0}, Le/h;->setContentView(I)V
...
可以看到,在调用setContentView这个方法之前,v0就已经被设置为常量0x7f0b0020了 接下来看配置了keep R的Apk:
...
.method public final onCreate(Landroid/os/Bundle;)V
.registers 5
.param p1 # Landroid/os/Bundle;
.annotation build Landroidx/annotation/Nullable;
.end annotation
.end param
.line 1
invoke-virtual {p0}, Ljava/lang/Object;->getClass()Ljava/lang/Class;
.line 2
.line 3
.line 4
move-result-object v0
.line 5
invoke-virtual {v0}, Ljava/lang/Class;->getName()Ljava/lang/String;
.line 6
.line 7
.line 8
move-result-object v0
.line 9
invoke-static {v0}, Lcom/networkbench/agent/impl/instrumentation/NBSTraceEngine;->startTracing(Ljava/lang/String;)V
.line 10
.line 11
.line 12
invoke-super {p0, p1}, Landroidx/fragment/app/FragmentActivity;->onCreate(Landroid/os/Bundle;)V
.line 13
.line 14
.line 15
sget v0, Lcom/hihonor/hm/h5/container/R$layout;->container_web_activity:I
.line 16
.line 17
invoke-virtual {p0, v0}, Landroidx/appcompat/app/AppCompatActivity;->setContentView(I)V
...
重点关注最后两行:倒数第二行的作用,是从Lcom/hihonor/hm/h5/container/R$layout这个类中获取到container_web_activity的值,也就是资源的ID,然后放入寄存器v0中;然后最后一行,才是将v0中的值作为setContentView方法的入参并调用这个方法。
比较配置keep R前后的字节码,可以发现8对R文件的默认处理方式,就是我们上文提到的,将资源ID内联到字节码中;但是当我们配置了keep R的规则以后,R文件中的ID就不会进行内联操作了,代码执行到需要获取资源ID的时候,虚拟机还是需要从指定的类文件中获取资源ID
结尾😊
第一次深入到字节码领域,曾经高深莫测的知识现在正在一点一点地揭开它的面纱,这感觉不错
另外,至于为什么我会自己花时间来研究这个混淆规则,而不是直接问业务的开发。是因为这段代码是21年从LDJ带来的,后来的人没有人知道这段代码当初为什么写了。在我看来这段规则的最佳实践应该是:aar中如果需要做反射操作,则aar的开发应该自行keep关键的类,而不是在最终的APK中把所有的资源都keep。但是现在代码既然已经这么写了,估计很难再有人推动去做改动了。
无所谓了,草台班子而已,演戏比做事重要。台下的人看的满意了就行,幕后再怎么破败,又有什么影响呢