实际上,这种加密几乎没有用,只要有越狱手机,使用市面上的脱壳工具就可以很容易地进行解密。
Mach-O 文件代码的解密发生在 Mach-O 文件被加载的时候,由 Mach Loader 进行。Mach Loader 会读取 Mach-O 中的 LC_ENCRYPTION_INFO
这条 Load Command
来判断可执行文件是否加密。
所以,也可以通过 otool -l <binary-path>
的命令来查看 Mach-O 是否被加密过。
Load command 13
cmd LC_ENCRYPTION_INFO_64
cmdsize 24
cryptoff 16384
cryptsize 101695488
cryptid 1
pad 0
其中 cryptoff
表示加密字段位于文件中偏移 16384
个字节;cryptsize
表示加密内容长度 101695488
个字节;cryptid
表示加密方法为 1
,如果为 0
表示不加密。
查看 LC_SEGMENT_64
中 __TEXT
段的范围
Load command 1
cmd LC_SEGMENT_64
cmdsize 1432
segname __TEXT
vmaddr 0x0000000100000000 4294967296
vmsize 0x0000000006100000 101711872
fileoff 0
filesize 101711872
依据上述结果可以算出加密的内容实际上都位于 __TEXT
中。
可以认为苹果只会对 Mach-O 文件中的 __TEXT
段加密,而不会对其它段加密。只要能把 __TEXT
段中的节移到其它段,就能减少苹果的加密范围,从而使压缩效率提升,减小下载大小。这也解答上个小节提出的问题。
一般来讲,在 App 中可执行文件占 80% 的大小,而加密部分占可执行文件中的 70%,加密会影响 60% 的压缩率,因此移走该加密部分,会提升 34% 的下载大小。根据我们在多个 App 的实践,本方案可以减少 32~34% 的下载大小。
需要注意的是:
苹果在 iOS 13 已经对下载大小做了优化,所以本方案无法再对 iOS 13 的设备的下载大小进一步优化。
即,若用户的设备 < iOS 13,那么本方案可以减少该设备上 App 32~34%的下载大小;
若用户的设备 >= iOS 13,本方案不会对该设备的 App 的下载大小有进一步优化,也不会有负面影响。
因此,如果你看到 App Store Connect 后台展示的下载大小从 iPhone 11 开始大幅减小,不要惊讶,这是因为 iPhone 11 开始默认搭载的是 iOS 13+ 的系统。
目前推测苹果在 iOS 13 也是在针对压缩做了优化,可能是移除了加密或者是先压缩后加密。
苹果在 iOS 13 的更新日志[4]中描述到它们对包大小做了优化,如图:
四、实践
====
照着上面的思路来看,只要将 __TEXT
段中所有节都移走,就能够最大限度的减少下载大小。
这么简单就可以了吗?实际上并非如此。在小型 App 上,这么做没有任何问题。但在较大型 App 上,这并不是一件轻松的事情。
今日头条 App 在实践过程中解决了 Crash 和一个极为难缠的链接失败的问题。
1. Crash
Crash 的原因是执行代码时找不到指定的节。
在原理中说到:操作系统只关心段的读/写/执行权限,并不关心段或节的名称。即便是使用了-rename_p
移动 Segment/Section,各符号的地址也会由链接器修正好,因此段移动后程序也可以正常运行。
但是如果代码指明了要读取 __TEXT
中的某个 Section ,那么这个 Section 就不能够被移动,否则代码就无法读取到它,就会导致出错。
首先,dyld[5] 在启动阶段会检查 __unwind_info
和 __eh_frame
这两个 Section。如果移动这两个 Section,在启动后程序就会 Crash。
第二,Swift 相关的 Section 不能移动,否则会引起 Crash。
在使用 Swift 之后,二进制中会有一些 Swift 相关的 Section:
它们都不能够被移动,一共有下面这些 Section:
__TEXT,__swift5_typeref
__TEXT,__swift5_reflstr
__TEXT,__swift5_fieldmd
__TEXT,__swift5_types
__TEXT,__swift5_capture
__TEXT,__swift5_assocty
__TEXT,__swift5_proto
__TEXT,__swift5_protos
__TEXT,__swift5_builtin
第三,自己在代码中指明要读取的 Section。目前我们的代码中没有这种 Crash 情况,但是我们的某些脚本中有检测 __TEXT,__text
的代码,在 __TEXT
段迁移后,脚本受到了影响,因此需要重新适配这类脚本。
2. 链接失败
__TEXT
段迁移最难解决的问题是链接失败问题,是由 CPU 对寻址范围的限制以及 ld64 链接器的缺陷导致。
2.1 现象及原因概述
如果 Mach-O 文件足够大,贸然移动 Segment/Section 很容易引发 ld64 链接器异常。
想要让 CPU 工作就必须向它提供指令和数据,程序运行时指令和数据存放在内存中。CPU 通过地址总线来指定内存单元的的地址,地址总线的宽度决定了 CPU 的寻址能力,因此 CPU 对寻址范围有一定的限制。而不同 CPU 的地址总线宽度不同以及它们所采用的指令模式[6]也不一样,所以不同 CPU 的寻址范围也有差异。
B、BL 指令是 ARM 处理器中的跳转指令,可以让处理器跳转到指定的目标地址,从那里继续执行。由于寻址范围是受限的,所以跳转距离不能超出这个限制。ld64 链接器在最终 Output(写可执行文件)时,会对所有的跳转指令进行检查,若发现跳转距离超出限制就会立即抛出 ld: b(l) ARM64 branch out of range
异常,从而链接失败,就会出现了图上所示的现象。
在苹果开源的 ld64-530 OutputFile.cpp 文件[7] 中总结出来,常见 CPU 具体限制寻址范围如下:
2.2 ld64 链接器所做的事情
按照上面的描述,随着业务的扩张,代码的膨胀,Mach-O 文件会越来越大,那是不是 Mach-O 文件过大时程序就无法链接成功了?
当然不是!实际上 ld64 链接器知道会出现跳转距离超出限制的情况,所以它在链接过程中会做 Branch Island[8] 算法,对超限制的跳转指令加以保护。
// PowerPC can do PC relative branches as far as +/-16MB. (+/-16MB 可能是因为注释比较老)
// If a branch target is >16MB then we insert one or more
// “branch islands” between the branch and its target that
// allows island hopping to the target.
// Branch Island Algorithm
//
// If the __TEXT Segment < 16MB, then no branch islands needed
// Otherwise, every 14MB into the __TEXT Segment a region is
// added which can contain branch islands. Every out-of-range
// B instruction is checked. If it crosses a region, an island
// is added to that region with the same target and the B is
// adjusted to target the island instead.
//
// In theory, if too many islands are added to one region, it
// could grow the __TEXT enough that other previously in-range
// B branches could be pushed out of range. We reduce the
// probability this could happen by placing the ranges every
// 14MB which means the region would have to be 2MB (512,000 islands)
// before any branches could be pushed out of range.
从原理部分我们知道了 Mach-O 的 Data 部分有很多 Segment/Section。实际上 ld64 链接器还给每个 Section 归了类,归类的代码可以在苹果开源的 ld64-530 中的 ld.hpp 文件的第 547 行找到:
每个 Section 都属于其中一种类型。Branch Island 算法会对类型是 typeCode
的 Section 中的跳转指令做检查,如果跳转的距离超出限制,则会在它们之间插入 “branch islands”,跳转指令会先跳到一个 branch island ,再从这个 branch island 跳到目标地址,以此来保证其跳转距离不超过限制。此部分的代码在 branch_island.cpp 文件中可以找到。
__TEXT,__text
的类型是 typeCode
,因此,__TEXT,__text
中超出范围跳转指令都会被保护,在最后 Output 检查时,就不会出现 branch out of range
的异常。所以,正常构建的 App,即使很大也不会出现链接失败的问题,这都是归功于 Branch Island 算法。
在 Mach-O 文件中,只有 __TEXT,__text
的类型是 typeCode
(在使用-rename_p
移动 Segment/Section 之后,Section 的类型不会发生改变)。源地址在 __text
中的 跳转指令跳转的情况只有两种:__text -> __text
和 __text -> __stubs
。
所以 Branch Island 保护的 跳转指令的所在 Section ,与目标地址所在的 Section, 只有两种情况:
但实际上 Output 时 ld64 链接器会检查文件中所有的跳转指令,不仅限于源地址在__text
中的跳转指令。这意味会检查多种情况:
小结:Branch Island 算法仅会保护
__text
中超出限制的跳转指令。
Output 时,ld64 链接器会检查文件中所有的跳转指令是否超出限制。
2.3 Branch Island 算法的缺陷
既然 Branch Island 算法会保护类型是 typeCode
的 Section 中超限制的跳转指令,并且-rename_p
不会改变 Section 的类型。那为何会-rename_p
后会导致 branch out of range
的异常?
主要是两个原因:
1. Branch Island 算法的检查逻辑没有适配到 Section 被移动的情况。
在分析 Mach-O 文件时只介绍了 Segment/Section,实际链接器认为在 Section 中还存在 atom(链接的基本单元),在 atom 中还存在 fixup(用于描述不同 atom 之间的引用关系。)
如图所示为 ld64-530 的 branch_island.cpp 文件中 Branch Island 算法中的一部分代码。该片段是要判断跳转指令跳转的距离是否超出限制,如果超过限制就会对该跳转指令做保护,否则就不做。
srcAddr
为跳转指令所在的源地址,dstAddr
为目标地址,displacement
为目标地址与源地址的距离。
然而该代码在计算 srcAddr
和 dstAddr
时,用的都是 offset,是相对距离:
-
atom->pOffset()
和target->pOffset()
都是atom
相对于各自 Section 起始地址的距离。 -
fit->offsetInAtom
和addend
都是fixup
相对于各自atom
的距离。
因此,算出来的 srcAddr
和 dstAddr
都是 fixup
相对于各自所在 Section 起始地址的距离。而 displacement
又是根据 dstAddr
和 srcAddr
相减计算出来的,它的本意是要计算 dstAddr
与 srcAddr
之间的距离。在没有 -rename_p
的情况下,这种计算方式没有问题;在使用-rename_p
的情况下,会导致计算出来的距离 displacement
不准确,会使在预期对跳转指令做保护的场景实际没做保护。
2. Branch Island 算法不会保护自定义 Section。
Branch Island 算法只会对 typeCode
的 Section 做保护,而自定义 Section 的类型是 typeUnclassified
,如果自定义 Section 中的代码使用了跳转指令,并且该跳转指令的跳转距离超出范围,那么无论是否-rename_p
都会出现链接失败的问题。
下面结合 3 个场景,来详细分析 Branch Island 算法的缺陷。
2.3.1 场景一
__TEXT,__text
移不干净导致链接失败。
__text
节在 __TEXT
段中所占比例巨大,要想达到优化效果,必须把它移走,否则几乎没有任何优化效果。头条最开始时,使用-Wl,-rename_p,__TEXT,__text,__BD_TEXT,__text
来尝试迁移 __TEXT,__text
,但无论如何也移不干净,总有一小部分还留在 __TEXT,__text
中。
导致的问题就是,顶部的 __TEXT,__text
与底部的 __BD_TEXT,__text
中的跳转指令出现了跳转距离超出限制情况,ld64 链接器在 Output 的时候发现了这个错误,抛出异常,链接失败。
前面我们已经知道了 Branch Island 算法会对__text
中的跳转指令做保护,会在跳转距离超出限制时候插入 branch island。那为什么还会出现这种错误?
画图分析,假设在 Mach-O 文件中, __TEXT,__text
的总大小为 110 ,其中有 A、B 两个符号,跳转指令会从 A 跳转到 B,它们距离 Section __TEXT,__text
的 offset 分别是 30 和 90,它们的实际距离为 60。Branch Island 算法会对跳转指令进行保护,计算出 A、B 的间距 displacement 为 60,不会插入 branch island,在 Output 时,ld64 链接器检查出来它们的距离为 60,小于 128,不会抛出异常,链接成功。
在移走了其中 90 大小的 __TEXT,__text
后,__TEXT,__text
的大小变为了 20,B 被移到了 __BD_TEXT,__text
, A、B 相对于各自 Section 的 offset 大概也会发生变化(这个不重要),假设分别变成了 5 和 80。
此刻,A 和 B 的实际距离是 80 + 40 + 15 = 135。但是,Branch Island 算法在对跳转指令做保护时,还是依照它们相对各自 Section 的距离来计算,计算出来它们的距离是 80 - 5 = 75,没有插入 branch island。而实际 135 的大小在 arm64 和 armv7 的实际跳转时是会出错的。
在最后 Output 时,ld64 按照 A 和 B 在文件中的绝对地址来计算距离,算出来它们的距离是 135,超出了 128,检查出了这种由移动 Section 以及 Branch Island 算法缺陷导致的错误,抛出了 branch out of range
的异常,链接失败。
因此,若要移动 __TEXT,__text
,就必须保证把 __TEXT,__text
全都移走,否则就可能出现链接失败的问题。(如果你的 App 可执行文件比较小,跳转距离始终不会超过 128M 的话,则不会出现这种问题)
在 ld64-530 的 ld.cpp 文件中发现,__TEXT,_text
移不干净,是由 __TEXT, __textcoal_nt
和 __TEXT,__StaticInit
这两个 Section 导致的。在源码中有如下片段:
这段代码会把 __TEXT, __textcoal_nt
和 __TEXT,__StaticInit
都改名(merge)成 __TEXT,__text
,还留在 __TEXT,__text
中的部分就是它们。
在网上查询到 __textcoal_nt
是 gcc 产生的 Section,至少在 16 年的时候就已经废弃,但目前还是有不少库中携带的有这个 Section;__StaticInit
并没有查到更多信息。
我在苹果的 ld 更新日志[9]找到这两个 Section 的踪迹,苹果在 07、08 年的时候就已经会将这两个 Section merge 到 __text
中去。
2008-07-15 Nick Kledzik kledzik@apple.com
rdar://proBem/6061904 automatically order initializers to start of __TEXT
* src/MachOReaderRelocataBe.hpp: merge __StaticInit into __text
2007-04-30 Nick Kledzik kledzik@apple.com
rdar://proBem/5065659 unaBe to link VTK because __textcoal_nt too large
* src/MachOReaderRelocataBe.hpp: when doing a final link map __textcoal_nt to __text
但苹果的 merge 操作发生在我们-rename_p
之后,因此我们使用-rename_p,__TEXT,__text,__BD_TEXT,__text
没有将它俩移走。
要让 __TEXT,_text
移干净,只需要把它俩也-rename_p
。使用如下配置就可以了:
-Wl,-rename_p,__TEXT,__text,__BD_TEXT,__text,
-Wl,-rename_p,__TEXT,__textcoal_nt,__BD_TEXT,__text,
-Wl,-rename_p,__TEXT,__StaticInit,__BD_TEXT,__text
注:字符串
__BD_TEXT
中的 BD 是 ByteDance 的缩写,__BD_TEXT
只是一个名称,可以随意更改。
如果你的 App 中使用
-rename_p,__TEXT,__text,__BD_TEXT,__text
本身就能移干净的话,那说明它不包含__TEXT,__textcoal_nt
和__TEXT,__StaticInit
,可以不添加该配置。
2.3.2 场景二
不移动 __stubs
导致链接失败。
__TEXT
段迁移减少包大小的核心就是移走 __TEXT,__text
。但是由于存在__TEXT,__text -> __TEXT,__stubs
的这种跳转指令,所以如果只移动 __TEXT,__text
而不移动 __TEXT,__stubs
,就会出现和问题一中描述的类似的情况:Branch Island 算法检查的是__text
中的符号相对于 __BD_TEXT
的距离,__stubs
是相对于 __TEXT
的距离,该方式计算出来的 displacement 与它们的实际距离不符,在该插入 branch island 的地方没有插入,Output 时检查到错误,抛出异常。
但 __TEXT,__stubs
还有点不一样的地方:
根据源码的逻辑,已知图中框选分部中的 totalTextSize
是 __TEXT,__text
和 __TEXT,__stubs
的总大小。
代码逻辑描述的是:如果 Section 的类型是 typeStub
(arm64 中的__stubs
,armv7 中的__picsymbolstub4
),Branch Island 算法会令跳转指令的目标地址 dstAddr
为 totalTextSize
,然后以此来计算间距 displacement
。
这需要画图来分析:
如图,在一个正常的 Mach-O 文件中,__TEXT,__text
的大小是 110,__TEXT,__stubs
的大小是 20。A 符号存在于 __TEXT,__text
中,B 符号存在于 __TEXT,__stubs
中。A 距离 __TEXT,__text
的 offset 为 90,B 距离 __TEXT,__stubs
的 offset 为 10,A、B 的实际距离是 30。
在检查时,Branch Island 算法发现 B 位于 __TEXT,__stubs
,于是直接令 dstAddr = 130(110 + 20 = 130),然后计算出它们的距离 displacement = 130 - 90 = 40,不插入 branch island。在最终 Output 时检查出它们的实际距离为 30,小于 128 ,不会抛出异常,链接成功。
在移动 __TEXT,__text
之后,A 被移动到了 __BD_TEXT
,A、B 的实际距离变成了 10 + 40 + 90 = 140,但 Branch Island 算法计算方式 displacement = 130 - 90 = 40,没有插入 branch island,这会导致实际的跳转出错。ld64 链接器在最后的 Output 阶段检查出了这种错误,抛出异常,链接失败。
ld64 链接器知道 B 符号肯定位于__stubs
内,所以 Branch Island 算法的这种 dstAddr = totalSize
; 的做法只会令 dstAddr
比实际的大。这样可以保证__stubs
中距离超出的跳转指令都会被插入 branch island。但由于 dstAddr
偏大了一些,所以实际上也多保护了一部分 实际上并没有超出限制的跳转指令。
Branch Island 算法采用这种相对距离的计算方式,是因为在这个阶段它拿不到 A 和 B 符号的绝对地址(绝对地址是在 Output 前才确定的),所以它采用了取巧的办法,使用 A 和 B 相对于各自 Section 的 offset 来计算它们的距离。它默认了二进制文件中只有一个类型是typeCode
的 Section, 并且这个 p 在 __TEXT,__stubs
的前面。这种算法在正常的 Mach-O 文件中是完全可行的,但我们如果移动了 Segment/Section,就不符合它的设定了,就会导致问题。
因此需要添加如下参数,将 __TEXT,__stubs
也移走:
-Wl,-rename_p,__TEXT,__stubs,__BD_TEXT,__stubs,
-Wl,-rename_p,__TEXT,__picsymbolstub4,__BD_TEXT,__picsymbolstub4
在 arm64 中,该 Section 的名称叫做 __stubs
,在 armv7 中,该 Section 的名称叫做 __picsymbolstub4
。为了适配不同的架构,可以将这个 Section 同时-rename
。-rename
不存在的 Section 不会有问题,所以这种写法是可以的。
这种做法对类型是 typeStub 的 Section(arm64 中的__stubs
,armv7 中的__picsymbolstub4
) 有另一种限制,就是在移动后, __text
和 __stubs
或 __picsymbolstub4
之间不能有别的 Section,否则可能会出现错误,如图:
在正常 Mach-O 文件中,A 符号 相对于 __text
的 offset 为 0,B 符号相对于 __stubs
的 offset 为 17.5。在移动 __text
和 __stubs
后,如果我们还移动了其它的 Section,那么这个 Section 有可能会出现在 __BD_TEXT,__text
与 __BD_TEXT,__stubs
之间,这将导致错误:
Branch Island 算法的检查方式判断出 A 和 B 的距离为 (110 + 17.9) - 0 = 127.9,小于 128,因此没有插入 branch island 。但移动后它们的实际距离是 110 + 0.5 + 17.5 = 128 ,是会导致跳转出错的,所以 ld64 链接器会抛出异常,链接失败。
不过这种链接失败的情况比较苛刻,如果 A 的 offset 为 0, 那么它目标地址必须要落在 __stubs
中 [17.5, 17.90] 范围,才会出现链接失败,其余情况都不会出现,因为小于 17.50 的话,移动后 A 和 B 的实际距离也不会超出 128。并且 A 的 offset 必须要在 [0, 0.4] 范围内才会出现这种情况,A 如果大于 0.4 的话,那 __text
移动后,A 跳转到 B 的任意位置也不会超过 128M。
基于这一点,我们在移动 __cstring
、__gcc_except_tab
、__const
、__objc_methname
、__objc_classname
、__objc_methtype
这几个只读 Section 的时候,不能把它们移到 __BD_TEXT
段中去,否则它们会出现在 __BD_TEXT,__text
与 __BD_TEXT,__stubs
之间导致错误。
解决的办法就是使用原有的链接参数,将它们移动到另一个 Segment :__RODATA
,这样就可以避免这个问题:
-Wl,-rename_p,__TEXT,__cstring,__RODATA,__cstring -Wl,-rename_p,__TEXT,__gcc_except_tab,__RODATA,__gcc_except_tab -Wl,-rename_p,__TEXT,__const,__RODATA,__const -Wl,-rename_p,__TEXT,__objc_methname,__RODATA,__objc_methname -Wl,-rename_p,__TEXT,__objc_classname,__RODATA,__objc_classname -Wl,-rename_p,__TEXT,__objc_methtype,__RODATA,__objc_methtype
2.3.3 场景三
自定义 Section 的问题。
在 「2.2 ld64 链接器所做的事情」 中说到,跳转指令共有四种跳转情况。rangeCheck
检查这四种情况;但是 Branch Island 算法只会检查两种跳转情况,它只会保护 __text
中的跳转指令。
跳转指令的所有跳转情况:
第 4 种情况只在存在自定义 Section,并且自定义 Section 中有跳转指令时才会出现。
Branch Island 会保护的情况:
有两种情况, Branch Island 算法不会保护:
__TEXT,__stub_helper -> __TEXT,__stub_helper
__TEXT,__custom_p -> __TEXT,__text
原因是 Branch Island 只会对类型是 typeCode
的 Section 中的跳转指令做检查 ,而只有 __TEXT,__text
的类型是 typeCode
。
那么,这两种情况的跳转指令,在实际跳转中是否会出错?
-
__TEXT,__stub_helper -> __TEXT,__stub_helper
不会,因为__stub_helper
的大小只有 28kb(在头条中),远小于 128M,所以它内部的指令再怎么跳都不会超出限制。 -
__TEXT,__custom_p -> __TEXT,__text
,是有可能失败的。
关于自定义 Section ,我们遇到过两种情况。
A. 在一款 App 中有 __dof_RACSignal
和 __dof_RACCompou
两个 Section。
这两个 Section 是由 RAC 引入的,但是它们的 Number of Relocations 是 0,不涉及跳转指令,它们不用处理,不会有链接失败的问题。
B. 头条中有一个 __u_selector
Section:
它是依赖的某静态库引入的,__u_selector
中包含一个重定位符号 ___Symbol_A
,跳转指令会从它跳转到 __text
中的 ___Symbol_B
。
调试发现正常可执行文件中,它们之间的距离是 10M 左右。不会出现链接失败的。
可以推测___Symbol_B
其实位于__text
的底部, 而 __text
很大,如果把__text
移动到到 __u__selector
的下边去,那么这两个指令之间的距离就会增大,超过 128 MB 就会链接失败。如图:
所以在移动 __text
后,__custom_p
(含跳转指令的自定义 Section)也必须跟着移动,让它保持在 __text
的下面,保持它们原有的相对位置。
照此分析,
__TEXT
中的自定义 Section 不被 Branch Island 保护,如果二进制文件足够大,而这个 Section 又有跳转指令,当跳转距离超过 128 MB 时,也会链接失败,与是否移动__text
无关。
要移走自定义 Section,需要再添加如下配置:
-Wl,-rename_p,__TEXT,__custom_p,__CUSTOM_TEXT,__custom_p
这里必须要使用新的段 __CUSTOM_TEXT
,而不能把自定义 Section 放到 __BD_TEXT
中,否则自定义 Section 会出现在__text
与 __stubs
之间,导致出现 “场景二” 后半部分中描述的问题。
3. 设置段的权限
由于将可执行代码移动到了新的段 __BD_TEXT
和 __CUSTOM_TEXT
中。所以需要给这两个段添加可读和可执行权限,否则程序将无法运行:
-Wl,-segprot,__CUSTOM_TEXT,rx,rx
-Wl,-segprot,__BD_TEXT,rx,rx
五、一行代码
======
在今日头条 App 中是使用 xcconfig[10] 来管理构建参数的,如果你也使用该方式,那么使用下面这一行代码就能完成配置:
APP_THIN_LINK_FLAGS = -Wl,-rename_p,__TEXT,__cstring,__RODATA,__cstring,-rename_p,__TEXT,__objc_methname,__RODATA,__objc_methname,-rename_p,__TEXT,__objc_classname,__RODATA,__objc_classname,-rename_p,__TEXT,__objc_methtype,__RODATA,__objc_methtype,-rename_p,__TEXT,__gcc_except_tab,__RODATA,__gcc_except_tab,-rename_p,__TEXT,__const,__RODATA,__const,-rename_p,__TEXT,__text,__BD_TEXT,__text,-rename_p,__TEXT,__textcoal_nt,__BD_TEXT,__text,-rename_p,__TEXT,__StaticInit,__BD_TEXT,__text,-rename_p,__TEXT,__stubs,__BD_TEXT,__stubs,-rename_p,__TEXT,__picsymbolstub4,__BD_TEXT,__picsymbolstub4,-segprot,__BD_TEXT,rx,rx
如果你是没有使用这种方式,在 Other Linker Flags 中逐行添加以下配置即可:
-Wl,-rename_p,__TEXT,__cstring,__RODATA,__cstring
-Wl,-rename_p,__TEXT,__objc_methname,__RODATA,__objc_methname
-Wl,-rename_p,__TEXT,__objc_classname,__RODATA,__objc_classname
-Wl,-rename_p,__TEXT,__objc_methtype,__RODATA,__objc_methtype
-Wl,-rename_p,__TEXT,__gcc_except_tab,__RODATA,__gcc_except_tab
-Wl,-rename_p,__TEXT,__const,__RODATA,__const
-Wl,-rename_p,__TEXT,__text,__BD_TEXT,__text
-Wl,-rename_p,__TEXT,__textcoal_nt,__BD_TEXT,__text
-Wl,-rename_p,__TEXT,__StaticInit,__BD_TEXT,__text
-Wl,-rename_p,__TEXT,__stubs,__BD_TEXT,__stubs
-Wl,-rename_p,__TEXT,__picsymbolstub4,__BD_TEXT,__picsymbolstub4,
-Wl,-segprot,__BD_TEXT,rx,rx
如果你的二进制文件中存在自定义 Section 的话,比如使用了类似__attribute__((p("__TEXT,__custom_p")))
的方式创建了自定义 Section,则可能需要做如下的配置以移走自定义 Section,具体见 「2.3.3 场景三」 的详细分析。
APP_THIN_LINK_FLAGS = -Wl,-rename_p,__TEXT,__cstring,__RODATA,__cstring,-rename_p,__TEXT,__objc_methname,__RODATA,__objc_methname,-rename_p,__TEXT,__objc_classname,__RODATA,__objc_classname,-rename_p,__TEXT,__objc_methtype,__RODATA,__objc_methtype,-rename_p,__TEXT,__gcc_except_tab,__RODATA,__gcc_except_tab,-rename_p,__TEXT,__const,__RODATA,__const,-rename_p,__TEXT,__text,__BD_TEXT,__text,-rename_p,__TEXT,__textcoal_nt,__BD_TEXT,__text,-rename_p,__TEXT,__StaticInit,__BD_TEXT,__text,-rename_p,__TEXT,__stubs,__BD_TEXT,__stubs,-segprot,__BD_TEXT,rx,rx,-rename_p,__TEXT,__picsymbolstub4,__BD_TEXT,__picsymbolstub4,-rename_p,__TEXT, __custom_p,__CUSTOM_TEXT,__text,-segprot, __CUSTOM_TEXT,rx,rx
六、答疑
====
1. 为什么不把 __TEXT
段中的所有 Section 都移走,这样不是更好吗?
并不是移走的段越多,压缩就越有效,而是得看移走的大小。例如下面虽然有 15 个 Section,但是它们的大小加起来 578
KB,移走它们对压缩后的下载大小几乎零提升。
Section __stubs: 28488 (addr 0x105f21644 offset 99751492)
Section __stub_helper: 28428 (addr 0x105f2858c offset 99779980)
Section __swift5_typeref: 2216 (addr 0x105f2f498 offset 99808408)
Section __swift5_fieldmd: 1272 (addr 0x105f2fd40 offset 99810624)
Section __swift5_types: 120 (addr 0x105f30238 offset 99811896)
Section __const: 64184 (addr 0x105f302b0 offset 99812016)
Section __ustring: 281012 (addr 0x105f3fd68 offset 99876200)
Section __swift5_reflstr: 796 (addr 0x105f84720 offset 100157216)
Section __swift5_capture: 376 (addr 0x105f84a3c offset 100158012)
Section __swift5_builtin: 120 (addr 0x105f84bb4 offset 100158388)
Section __swift5_assocty: 312 (addr 0x105f84c2c offset 100158508)
Section __swift5_proto: 308 (addr 0x105f84d64 offset 100158820)
Section __swift5_protos: 40 (addr 0x105f84e98 offset 100159128)
Section __u__selector: 36 (addr 0x105f84ec0 offset 100159168)
Section __eh_frame: 184708 (addr 0x1060cf018 offset 101511192)
并且,有的 Section 是不能移走的,会引起 crash,有兴趣的读者可以自行尝试。
2. 出现 Crash.log 解析不了的情况怎么办?
在上线后,我们发现 Crash report 中的 Crash.log 中有一部分符号无法解析,如图中的 ???
。
出现这个问题的原因是,Crash.log 在分析主二进制镜像时,把它在虚拟内存中的地址范围取错了。
如图 0x100010000 - 0x100203fff
的范围只有 2047999(2.0 MB
),这明显远小于主二进制文件中__text
原本的大小 100 MB。这个 2.0 MB 的大小基本与 __TEXT
段被迁移后剩余的大小相符,因此猜测 Crash.log 在分析时取的是 __TEXT
段的大小,而我们把大部分 __TEXT
段都移走了。
所以当遇到一个符号落在 (2.0M, 100M]
的区间中时,Crash.log 就无法知道这个地址它到底是属于哪个镜像,它就会显示 ???
,无法解析。
解决办法:这种 Crash.log 使用 atos 工具[11]手动解析,将主镜像名称当做参数传入即可。
七、总结
====
本文从背景知识和面临的实际问题出发,介绍了 __TEXT 段迁移及减少下载大小的原理,描述了我们在实践过程中遇到的问题,并从源码的角度详细分析了问题产生的根本原因以及解决方式,解答了相关疑问和上线后遇到的问题。
目前,该方案已经在字节跳动多个大型 App 中应用,均对下载大小有 30% 以上的优化,且运行稳定。
八、加入我们
======
我们是字节跳动 General Information Platform - 客户端平台架构 iOS 团队,在性能优化、基础组件、业务架构、研发体系、安全合规、线下质量基础设施、线上问题定位归因平台等方向深耕,负责保障和提升今日头条、西瓜视频和番茄小说的产品质量与开发效率,聚焦于此的同时向外延伸。
如果你对技术充满热情,喜欢追求极致,渴望用自己的代码改变数亿用户的体验,欢迎加入我们。我们期待你与我们共同成长。目前我们在北京、深圳均有招聘需求,简历投递邮箱: t****ech@bytedance.com ;邮件标题: 姓名 - 工作年限 - GIP - 客户端平台架构 - iOS/Android 。
参考文献
====
[1] 最大版本构建大小
https://help.apple.com/app-store-connect/#/dev611e0a21f
自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。
深知大多数初中级Android工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则近万的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!
因此收集整理了一份《2024年Android移动开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点,真正体系化!
由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!
如果你觉得这些内容对你有帮助,可以扫码获取!!(备注:Android)
学习交流
群内有许多来自一线的技术大牛,也有在小厂或外包公司奋斗的码农,我们致力打造一个平等,高质量的Android交流圈子,不一定能短期就让每个人的技术突飞猛进,但从长远来说,眼光,格局,长远发展的方向才是最重要的。
35岁中年危机大多是因为被短期的利益牵着走,过早压榨掉了价值,如果能一开始就树立一个正确的长远的职业规划。35岁后的你只会比周围的人更值钱。
《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!
不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!**
因此收集整理了一份《2024年Android移动开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
[外链图片转存中…(img-o24TVQqp-1713504590538)]
[外链图片转存中…(img-M3R6eYqw-1713504590539)]
[外链图片转存中…(img-6Q73IGhg-1713504590540)]
[外链图片转存中…(img-I35gByWm-1713504590541)]
[外链图片转存中…(img-0OCHv65Y-1713504590542)]
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点,真正体系化!
由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!
如果你觉得这些内容对你有帮助,可以扫码获取!!(备注:Android)
学习交流
[外链图片转存中…(img-IDBhdKMb-1713504590543)]
[外链图片转存中…(img-A6xakeoN-1713504590544)]
群内有许多来自一线的技术大牛,也有在小厂或外包公司奋斗的码农,我们致力打造一个平等,高质量的Android交流圈子,不一定能短期就让每个人的技术突飞猛进,但从长远来说,眼光,格局,长远发展的方向才是最重要的。
35岁中年危机大多是因为被短期的利益牵着走,过早压榨掉了价值,如果能一开始就树立一个正确的长远的职业规划。35岁后的你只会比周围的人更值钱。
《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!