• 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值,一般有这几个类型:attrdrawablelayoutanimrawdimenstringboolstyleintegerarraycoloridmenu 等。应用程序所有模块中的资源类型名称,按照字母排序之后。值是从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文件

    • 源代码

    [(img-T9C7XRW6-1669856202728)(https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/5207e3f0b1c34b3fb99a412e529ec737~tplv-k3u1fbpfcp-zoom-1.image)]

    • 反编译后smail代码

    [(https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/daee163327b54ccd984385c6a1653de6~tplv-k3u1fbpfcp-zoom-1.image)]

    可以看到源码里的R.layout.activity_main已经被替换成了资源id0x7f08007e

    但是我们看一下MoudleA反编译后的smali代码

    • 源代码

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9LONcKLN-1669856202729)(https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/3b8d27f127614d08b8b95bc0f2396c52~tplv-k3u1fbpfcp-zoom-1.image)]

    • 反编译后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));
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    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);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 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;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • visitEnd()所有访问结束,判断当前类是否需要添加到替换的类集合
    @Override
    public void visitEnd() {
        super.visitEnd();
        if (discardable) {
            context.addShouldDiscardRClasses(className);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    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);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    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);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    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;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    下面看一下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) value);
                } else if (value instanceof Integer) {
                    // 检查资源是否被使用,
                    resManager.reachResource((Integer) value);
                    // 替换成常量
                    mv.visitLdcInsn(value);
                }
                return;
            }
        }
        super.visitFieldInsn(opcode, owner, name, desc);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    private void replaceStyleableNewArrayCode(List 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);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    到这里资源id替换成常量就结束了

    总结

    总共流程如下

    • 第一遍遍历traverse class获取到所有待替换R文件类变量的常量
    • 第二遍遍历所有类,替换所有需要替换的R类变量为常量,并删除R文件

  • 相关阅读:
    面向对象编程原则(06)——依赖倒转原则
    解析Spring中的循环依赖问题:初探三级缓存
    二叉树-输出二叉树的右视图
    flink groupby keyby区别
    .netcore+vue新生分班系统的设计与实现
    【Final Project】Kitti的双目视觉里程计(1)
    k8s(3)
    synchronized修饰类的注意事项
    门票赠送:诚邀您参加2023百度世界大会-大模型驱动产业发展论坛
    LVGL 入门使用教程
  • 原文地址:https://blog.csdn.net/a1018875550/article/details/128125908