目录

DebugInfo 复用

apk 包体积这块,因为网络传输速度不断提升和手机存储空间的不断提升,越来越有点不是那么重要了

主要最近一直有这方面的需求,所以研究了一下包体积相关的东西,对于 android 的编译流程,有了更多的了解

在看包体积相关优化手段的时候,看到了这样两篇文章,都是讲解 debugInfo 复用的

百度:https://mp.weixin.qq.com/s/zEAwm0om8MPle6hgaizFGw

支付宝:https://juejin.cn/post/6844903712201277448

也有一些文章针对于后面一篇支付宝的文章进行了二次分析和复制,似乎每篇文章都在纠结行号解析公式,贴出一段行号解析代码,然后就莫名结束了,让人头大,人云亦云,而两篇文章也没有具体的介绍

当然,其实这块东西我觉得如果不是对于包体积有极致的要求,也完全没有必要做···但是看一看原理还是有那么一点点收货的(我断断续续看了一周,我觉得性价比很低,没学到什么东西)

题外话:

支付宝的文章是很久以前的了,看得出对于优化方面,阿里似乎很多年期就是卷无可卷了···多年前就开始搞这些了···
再看看如今的 Android 面试,全是一些 性能优化啊、JVM/ART啊、插件化啊 等等,听起来很“高大上”的技术。 而往往这些东西,费劲千辛万苦卷出来以后,对于实际的作用并不大,或者随着 android 的快速变化,没法跟得上脚步,最终很多库就成了没人维护的状态~可以研究研究这些听起来“高大上”的技术,但是前提是基础知识足够扎实。当然了,如果你着急找工作,那赶紧背背“高大上”技术课文吧,不然似乎很难找得到工作~~
我现在总是发现很多“高工”对于计算机基础、或者对于一些 android 基础,甚至对于异步/同步,其实毛都不懂~~但是,你跟他聊聊“高大上”,他就可以蹦出几个“高大上”的词来,然后就可以开始一场驴头不对马嘴的“技术交流”,也是很滑稽。

DebugInfo 是什么

debugInfo 记录了,方法行号 和 dex指令 之间的关系 简单理解就是,debugInfo 可以用于还原行号(这里的行号,如果 R8 开启的话,还有一层映射关系,都在生成的 mapping 文件中)

R8 行号的映射关系

补充说明一下,R8 的行号问题。可以看一下 R8 生成的 mapping 文件:

1:1:void setLink(java.lang.String):58:58 -> setLink
1:1:void setSearchWord(java.lang.String):54:54 -> setSearchWord
1:1:void setType(java.lang.String):57:57 -> setType

当 R8 开启的时候,如果我们堆栈上报 setLink 这个方法的行号,实际上传的会是 1,解析平台会用 mapping 文件反解为 58 行。

DebugInfo 的真实面目

/img/in-post/debuginfo.png

上图就是 debugInfo 的真实数据结构(工具是 010 Editor),除了 line_start 和 parameters_size 两个值以外,其余都是叫做 opcode 的东西。

opcode 有4种: DBG_END_SEQUENCE, DBG_ADVANCE_PC, DBG_ADVANCE_LINE, special code

且此处的 opcode 已经解析好,其余的 opcode 比较好理解,DBG_END_SEQUENCE DBG_ADVANCE_PC 表示只会增加 address 值, DBG_ADVANCE_LINE 表示增加 line 值,而 special opcode 本身有自己的换算规则:

DBG_FIRST_SPECIAL = 10
DBG_LINE_RANGE = 15
DBG_LINE_BASE = -4

int adjopcode = opcode - DBG_FIRST_SPECIAL;
entry.address_ += adjopcode / DBG_LINE_RANGE;
entry.line_ += DBG_LINE_BASE + (adjopcode % DBG_LINE_RANGE);

所以 opcode = 0xF0 = 240,可以得到,adjopcode = 240 - 10 = 230, address = 230 / 15 = 15, line = -4 + 230 % 15 = 1

也就是图中解析好的 line + 1, address + 15,这个的含义是:源码中移动一行,但是字节码指令区间需要移动 15字节,也就是所谓的 dex_pc 需要移动 15字节,其实就是源码中的一行对应到字节码中多少个字节,那么执行下一条指令,也会偏移相应的字节。

具体解释一下:

首先,每个方法的代码的字节码指令在 dex 中就是一串数字,用一个数组存储 insns[],如下图:

/img/in-post/instructions.png

当我们要执行一个方法时,实际需要一个 address 指向这串字节码指令。

同时,我们的 dex 文件中因为保留了原行号(这里需要说明的是,R8其实也开启了 debugInfo 的优化,R8 的行号,每个方法都是从 1 开始,希望得以复用。所以,就像一开始说的 R8 的行号和真正的原始行号还有一个 mapping 关系,存储在了 mapping 文件中)

所以可以知道每一行代码,对应到 insns 数组中的那一块内容,可以使用 dexdump 查看,更明了:

/img/in-post/dexdump.png

红圈部分就说明了,每一行代码 和 insns 数组之间的关系

从第一行变为第二行时,其实也就是我们前面看到的 opcode:

line + 1, address + 15

行号 和 Proguard 之间的关系

proguard 中我们会 -keepattributes LineNumberTable,这是一条默认开启的规则,因为我们一直 keep 住了行号,所以我们可以反解出行号来。

而如果我们删除了 keep 规则,那我们的堆栈也就变成了没有行号信息的堆栈。

DebugInfo 复用的概念

这里一直在说 debugInfo 复用,为什么可以复用呢?

其实所有的 debugInfo 是存放在一个区域内的,可以简单的理解为,所有的 debugInfo 存放在 dex 的一片连续的区间内,由不同的方法分别指向不同的 debugInfo,从而获得方法的行号信息。

那么如果,我们可以让多个不同的方法,可以指向同一个 debugInfo,那 debugInfo 的数量会大大减少,从而实现了所谓的 “复用” 和 “包体积的优化”

但是,由前面可以知道,debugInfo 其实是跟方法体内的具体的每行方法转换成的字节码指令的长度相关的,除非一模一样的方法,不然很难让两个不同方法的 debugInfo 的内容完全一致。

DebugInfo 复用方案

百度和支付宝想到了能让 debugInfo 尽量复用的解决方法。

之前我们的 opcode 为: line + 1, address + 15,也就是说 insns 数组的前 15字节其实是第一行代码。那如果我们让 opcode 全部变为: line + 1, address + 1,是不是就 ok 了呢?

这是什么意思呢?

拿前面的 insns[] 那张图来说,图中会发现 insns 数组长度是 65,假设这个方法有 10 行,那么这个方法原本的 debugInfo 中的 debug_opcode 可能会有 11个(line_number + 1, 含有一个 end_sequence)。

但是,修改后,因为 line + 1, address + 1,我们的 debug_opcode 会有 66个(insns[].size + 1)

debug_opcode 变多了,怎么会反而有优化呢?

其实,虽然 debug_opcode 变多了,但是,只要是一个方法的 insns[] 长度 < 65,都可以让那个方法的 debugInfo 指向这个 debugInfo,从而达到了复用的效果

怎么理解呢?

假设我们有两个方法,funcA(inssn.size = 65) 和 funcB(insns.size = 10),假设修改前的 opcode 的情况是:

funcA:  
insns[0] ~ insns[14]  -> line1   第 n 行代码对应的 insns 区间
insns[15] ~ insns[19]  -> line2
···
insns[xx] ~ insns[64]  -> line10

funcB
insns[0] ~ insns[9]  -> line1

如果此时,虚拟机正在读取 funcA 或者 funcB 的 insns[8],那我们可以推断出 funcA 和 funcB 都是第一行。

而修改后,funcB 可以复用 funcA 的 opcode,都指向 funcA 的 opcode,情况如下:

funcA 和 funcB: 
insns[0]  -> line0
insns[1]  -> line1
insns[2]  -> line2
···
insns[64]  -> line64

line 行号直接变成了 insns 的偏移量,

如果有很多个方法的 insns 都小于 65,那么就可以有很多个方法公用这一个 debugInfo,所以 debugInfo 里面的内容是增多了,但是因为复用,debugInfo 的总量是极具减少的!

那么行号怎么还原呢?

如果此时,虚拟机正在读取 insns[8],我们只能推断出是第八行代码。

但是我们可以获取到 funcA 和 funcB 修改前 line 和 insns 的关系(也就是修改前的 opcode 的情况),进而还是可以反解出 insns8 表示 funcA 的第一行,也是 funcB 的第一行。

至此,我们就实现了 debugInfo 的复用。

这就是基本原理,很多文章并没有解释清楚。

当然,还有一些实际的细节问题,百度的文章里也已经提及,遇到同名的重载方法,需要区分 start_line。