ByteX-shrink_r源码解析
背景
为什么要对R文件内联处理?
这里首先说一下Android R文件的产生,对于Android开发者我们都知道,当我们要使用要使用一些布局文件,drawable等其他资源时,可以直接用 R.id. ``R.drawble
.等直接使用,而这个R.java
文件类的创建是在Android编译打包的过程中,位于res/
目录下的文件,就会通过AAPT
工具,对里面的资源进行编译压缩,从而生成相应的资源id,且生成R.java
文件,用于保存当前的资源信息,同时生成resource.arsc
文件,建立id
与其对应资源的值。
最终生成了如下图内容的代码
这里解释一下这个资源id:0x7f0600c9
的含义,由三部分组成:PackageId+TypeId+EntryId
,0x7f0600c9
可以拆解为0x7f
+06
+00c9
PackageId
:是包的Id
值,Android 中如果第三方应用的话,这个默认值是0x7f
,系统应用的话就是0x01
,插件的话那么就是给插件分配的id
值,占用一个字节。
TypeId
: 是资源的类型Id
值,一般有这几个类型:attr
,drawable
,layout
,anim
,raw
,dimen
,string
,bool
,style
,integer
,array
,color
,id
,menu
等。应用程序所有模块中的资源类型名称,按照字母排序之后。值是从1开支逐渐递增的,而且顺序不能改变(每个模块下的R文件的相同资源类型id
值相同)。比如:anim=0x01
占用1个字节,那么在这个编译出的所有R文件中anim
的值都是0x01
EntryId
: 是在具体的类型下资源实例的id
值,从0
开始,依次递增,他占用四个字节。
正常情况下APP的R文件就这样产生结束了,但是当我们的开发是多Module模式开发时问题就来了,module或者aar也会产生R文件,然后打包apk后的R文件格式会产生如下结构
可以每个moudle都有各自的R文件,同时上层R文件会融合下层的R文件资源。但是这会带来一个问题,就是
R文件越来越多是否冗余了,导致包大小增大
上层的R文件很容易出现R Field过多,导致MultiDex 65536的问题。(如果miniSDK>21可以忽略)
R文件内联
其实Android Studio在编译时已经为我们做了内联处理,比如我们看一下APP module的smail文件
源代码
反编译后smail代码
可以看到源码里的R.layout.activity_main
已经被替换成了资源id0x7f08007e
但是我们看一下MoudleA反编译后的smali代码
源代码
反编译后smail代码
可以看到module工程里的资源文件并没有被内联处理,为什么会这样?这是因为 module
的class文件,在主工程编译时,不会再次进行编译,module
的class文件原封不动的打包进apk。而资源id为常量是在主工程编译时才形成的,但module
生成class时,使用的是上面说到的变量,所以一直被保留了下来。
什么是shrink_r?
ByteX是字节团队开源的一个字节码插桩工具,而shrink_r是其中的一个插件是用来对
R文件常量内联,R文件瘦身;
无用Resource资源检查;
无用assets检查。
bytex.shrink_r就是为了解决上述问题中module工程里R文件没有被内联产生的一种方案,他通过ASM操作class文件进行操作对使用到R类变量的地方进行常量值替换,然后删除R文件从而达到减少包大小的目的。
使用收益
下面来看一下使用的前后效果收益对比
使用前
包大小
R文件数量
使用后
包大小
R文件数量
Moudle的R文件被删除了,然后module工程的也被内联替换成了资源id
shrink_r源码解析
由于shrink_r是用bytex框架,所以我们先从ShrinkRFilePlugin.traverse()
看起
traverse()
-第一次工程遍历
public void traverse(@NotNull String relativePath, @NotNull ClassVisitorChain chain) { super.traverse(relativePath, chain); if (Utils.isRFile(relativePath)) { chain.connect(new AnalyzeRClassVisitor(context)); } } 复制代码
traverse()
方法里判断如果是R文件
,则进入R class文件分析类AnalyzeRClassVisitor
,该类主要是用来保存需要替换R资源id的,主要看三个方法
visitField
访问变量,主要做了两件事保存需要替换的资源id
保存不需要替换的资源id
public FieldVisitor visitField(int access, String name, String desc, String signature, Object value) { if (TypeUtil.isPublic(access) && TypeUtil.isStatic(access) && TypeUtil.isFinal(access) && !context.shouldKeep(this.className, name)) { if (TypeUtil.isInt(desc) && value != null) { // 保存,需要替换的资源id context.addShouldBeInlinedRField(className, name, value); } } else { discardable = false; // 不需要替换,也保存id,做兜底判断 context.addSkipInlineRField(className, name, value); } return super.visitField(access, name, desc, signature, value); } 复制代码
visitMethod
访问方法,该方法主要就是判断是不是R$styleable
类的初始化方法,对于styleable
类特别处理
@Override public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) { MethodVisitor mv = super.visitMethod(access, name, desc, signature, exceptions); if (this.isRStyleableClass && Utils.isClassInit(name)) { if (discardable) { return new AnalyzeStyleableClassVisitor(mv, context); } } return mv; } 复制代码
visitEnd()
所有访问结束,判断当前类是否需要添加到替换的类集合
@Override public void visitEnd() { super.visitEnd(); if (discardable) { context.addShouldDiscardRClasses(className); } } 复制代码
transform()
- 第二次遍历,字节码转化
就是在该方法里做了对R类变量的地方进行常量值替换,该方法做了两件事
删除白名单外的R文件
替换R类变量
@Override public boolean transform(@NotNull String relativePath, @NotNull ClassVisitorChain chain) { if (context.discardable(relativePath)) { // 如果是白名单外的R文件,返回false删除 context.getLogger().d("DeleteRFile", "Delete R file: " + relativePath); return false; } // 不是R文件,变量类,进行R类变量替换 chain.connect(new ShrinkRClassVisitor(context)); return super.transform(relativePath, chain); } 复制代码
在 ShrinkRClassVisitor
类里对每个方法进行方法,然后对方法里使用R类变量的地方进行常量值替换处理
@Override public FieldVisitor visitField(int access, String name, String desc, String signature, Object value) { if (isRClass && context.shouldBeInlined(className, name)/* && !context.shouldKeep(className, name)*/) { // R文件且是删除需要变量,返回null,进行删除 context.getLogger().i("DeleteField", String.format("Delete field = [ %s ] in R class = [ %s ]", name, className)); return null; } else if (isRClass) { // 白名单的R文件,keep保留 context.getLogger().i("KeepField", String.format("Keep field = [ %s ] in R class = [ %s ]", name, className)); } return super.visitField(access, name, desc, signature, value); } 复制代码
visitMethod()
访问方法,对方法里使用R类变量的地方进行常量值替换处理,所有的替换都在ReplaceRFieldAccessMethodVisitor
处理了
@Override public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) { MethodVisitor mv = super.visitMethod(access, name, desc, signature, exceptions); if (!isRClass) { return new ReplaceRFieldAccessMethodVisitor(mv, context, name, className); } return mv; } 复制代码
下面看一下ReplaceRFieldAccessMethodVisitor.visitFieldInsn()
方法
public void visitFieldInsn(int opcode, String owner, String name, String desc) { // 判断是不是静态的filed if (opcode == Opcodes.GETSTATIC) { Object value = null; try { // 通过集合根据owner和name获取当前是否是R文件的资源id常量 value = context.getRFieldValue(owner, name); } catch (RFieldNotFoundException e) { context.addNotFoundRField(className, methodName, owner, name); } if (value != null) { if (value instanceof List) { // 替换styable资源 replaceStyleableNewArrayCode((List<Integer>) value); } else if (value instanceof Integer) { // 检查资源是否被使用, resManager.reachResource((Integer) value); // 替换成常量 mv.visitLdcInsn(value); } return; } } super.visitFieldInsn(opcode, owner, name, desc); } 复制代码
private void replaceStyleableNewArrayCode(List<Integer> valList) { int size = valList.size(); visitConstInsByVal(mv, size); mv.visitIntInsn(Opcodes.NEWARRAY, Opcodes.T_INT); for (int i = 0; i < size; i++) { mv.visitInsn(Opcodes.DUP); visitConstInsByVal(mv, i); mv.visitLdcInsn(valList.get(i)); mv.visitInsn(Opcodes.IASTORE); } } 复制代码
到这里资源id替换成常量就结束了
总结
总共流程如下
第一遍遍历traverse class获取到所有待替换R文件类变量的常量
第二遍遍历所有类,替换所有需要替换的R类变量为常量,并删除R文件
作者:JokAr
链接:https://juejin.cn/post/7171977380657889294