ByteX-shrink_r源码解析

news2024/11/25 18:46:49

背景

为什么要对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));
    }
}

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文件

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/50577.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Redis 内存管理

前言 Redis 的同学应该都知道&#xff0c;它基于键值对&#xff08;key-value&#xff09;的内存数据库&#xff0c;所有数据存放在内存中&#xff0c;内存在 Redis 中扮演一个核心角色&#xff0c;所有的操作都是围绕它进行。我们在实际维护过程中经常会被问到如下问题&#x…

详解设计模式:组合模式

组合模式&#xff08;Composite Pattern&#xff09;&#xff0c;又叫部分整体模式&#xff0c;是 GoF 的 23 种设计模式中的一种结构型设计模式。 组合模式 是用于把一组相似的对象当作一个单一的对象。组合模式依据树形结构来组合对象&#xff0c;用来表示部分以及整体层次。…

Codeforces Round #726 (Div. 2) E1. Erase and Extend (Easy Version)

翻译&#xff1a; 这是这个问题的简单版本。唯一的区别是&#x1d45b;和&#x1d458;上的约束。只有当所有版本的问题都解决了&#xff0c;你才能进行hack。 你有一个字符串&#x1d460;&#xff0c;你可以对它做两种类型的操作: 删除字符串的最后一个字符。 复制字符串:…

UI 智能化的原理和未来

本文将从 GUI 中用户体验的构建开始&#xff0c;用高质量、可调控、交互体验创新三个部分&#xff0c;分别介绍如何从传统 UI 一步步迈向 UI 智能化。最后&#xff0c;用如何实现 UI 智能化的一些思考收尾。 本文仅代表作者个人观点。前言&#xff1a;「UI 智能化才是用户体验的…

第十七章《MySQL数据库及SQL语言简介》第3节:数据库管理

17.2小节主要讲解的是MySQL数据库的下载、配置和安装。从严格意义来讲,17.2小节所做的工作是对“数据库管理系统”进行下载、安装和配置。本小节所要讲解的数据库管理是指如何用数据库管理系统新建、重命名和删除一个数据库。 程序员操作数据库管理系统主要有两种方式:1、通…

[附源码]Python计算机毕业设计SSM课程教学质量综合分析平台(程序+LW)

项目运行 环境配置&#xff1a; Jdk1.8 Tomcat7.0 Mysql HBuilderX&#xff08;Webstorm也行&#xff09; Eclispe&#xff08;IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持&#xff09;。 项目技术&#xff1a; SSM mybatis Maven Vue 等等组成&#xff0c;B/S模式 M…

企业里使用最广泛的技术之一SparkSQL

声明&#xff1a; 文章中代码及相关语句为自己根据相应理解编写&#xff0c;文章中出现的相关图片为自己实践中的截图和相关技术对应的图片&#xff0c;若有相关异议&#xff0c;请联系删除。感谢。转载请注明出处&#xff0c;感谢。 By luoyepiaoxue2014 B站&#xff…

mybatis实战:一、mybatis入门(配置、一些问题的解决)

出自《MyBatis从入门到精通》刘增辉&#xff0c;精简 1.pom.xml 1.设置源码编码方式为 UTF -8 2.设置编译源代码的 JDK 版本 3.添加mybatis依赖 4.还需要添加会用到的 Log4j JUnit ySql 驱动的依赖。 <?xml version"1.0" encoding"UTF-8"?> <pr…

麻雀算法(SSA)优化长短期记忆神经网络的数据回归预测,SSA-LSTM回归预测,多输入单输出模型。

clear all; close all; clc; %% 导入数据 P_train xlsread(data,training set,B2&#xff1a;G191); T_train xlsread(data,training set,H2&#xff1a;H191); % 测试集——44个样本 P_testxlsread(data,test set,B2:G45); T_testxlsread(data,test set,H2:H45); %% 优化参…

简单封装一个易拓展的Dialog

Dialog&#xff0c;每个项目中多多少少都会用到&#xff0c;肯定也会有自己的一套封装逻辑&#xff0c;无论如何封装&#xff0c;都是奔着简单复用的思想&#xff0c;有的是深层次的封装&#xff0c;也就是把相关的UI效果直接封装好&#xff0c;暴露可以修改的属性和方法&#…

【MySQL】数据库备份与容灾详解(实战篇)(MySQL专栏启动)

&#x1f4eb;作者简介&#xff1a;小明java问道之路&#xff0c;专注于研究 Java/ Liunx内核/ C及汇编/计算机底层原理/源码&#xff0c;就职于大型金融公司后端高级工程师&#xff0c;擅长交易领域的高安全/可用/并发/性能的架构设计与演进、系统优化与稳定性建设。 &#x1…

优质短视频的10个共同点,戳中两个就能提高爆款几率!

优质短视频的10个共同点&#xff0c;戳中两个就能提高爆款几率&#xff01; 01、引起共鸣和认同 : 观念、遭遇和经历 昨天随便搞了一个表情&#xff0c;配了一段核酸是辛酸的文案&#xff0c;30w播放。这就是共鸣。 02、引起好奇 :为什么、什么、何时、惊喜 ; 疑问或者悬疑式…

unity webgl开发踩坑——从开发、发布到优化

目录前言unity webgl的一些注意点videoplayer修改text修改——解决不能显示汉字问题制作、读取ab包unity audioclip减小建议减小包体 全过程记录webgl的buildwebgl部署到本地、云&#xff08;IIS&#xff09;webgl部署云如何提升加载速度总结参考前言 又是一个阳光明媚的早上&…

双十二买什么牌子电容笔?值得买的平价电容笔推荐

随着网络的迅速发展&#xff0c;人们开始使用移动电话、平板、笔记本电脑等。所以&#xff0c;在我们的日常生活中&#xff0c;电容笔的使用也日益频繁。我想&#xff0c;如果只把电容笔用在日常学习、记录或者其它一些简单的事情上&#xff0c;我们就不必再去买一支价格如此昂…

mp3转wav怎么转?

mp3可谓是被人们熟知的一种音频格式了&#xff0c;早在十几年前就出了一种很流行的音乐播放器&#xff0c;叫做mp3&#xff0c;其实是这种比播放设备只能播mp3格式的音频文件而已&#xff0c;所以俗称叫做mp3。这个就足以说明了mp3格式的音频文件应用的广泛性和普及性。mp3之所…

智云通CRM:CRM数据库在经营客户中有什么作用?

CRM客户数据库是信息的存储中心&#xff0c;由一条条记录构成&#xff0c;记载着相互联系的一组信息&#xff0c;数据库是面向主题的、集成的、相对稳定的、与时间相关的数据集合&#xff0c;能够及时反映市场的实际状况&#xff0c;是企业掌握市场的重要途径。 CRM客户数据库…

[附源码]计算机毕业设计springboot居家养老服务系统小程序

项目运行 环境配置&#xff1a; Jdk1.8 Tomcat7.0 Mysql HBuilderX&#xff08;Webstorm也行&#xff09; Eclispe&#xff08;IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持&#xff09;。 项目技术&#xff1a; SSM mybatis Maven Vue 等等组成&#xff0c;B/S模式 M…

[附源码]计算机毕业设计JAVA闲置物品交易管理系统

[附源码]计算机毕业设计JAVA闲置物品交易管理系统 项目运行 环境配置&#xff1a; Jdk1.8 Tomcat7.0 Mysql HBuilderX&#xff08;Webstorm也行&#xff09; Eclispe&#xff08;IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持&#xff09;。 项目技术&#xff1a; SSM my…

spring-boot-starter-data-redis 引发的一系列惨案

<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId></dependency> pom 引入jar包 &#xff0c;如果redis配置文件使用 lettuce &#xff0c;还需要引入 commons-pool2 &a…

iwebsec靶场 SQL注入漏洞通关笔记11-16进制编码绕过

系列文章目录 iwebsec靶场 SQL注入漏洞通关笔记1- 数字型注入_mooyuan的博客-CSDN博客 iwebsec靶场 SQL注入漏洞通关笔记2- 字符型注入&#xff08;宽字节注入&#xff09;_mooyuan的博客-CSDN博客 iwebsec靶场 SQL注入漏洞通关笔记3- bool注入&#xff08;布尔型盲注&#…