今日头条优化实践: iOS 包大小二进制优化,一行代码减少 60 MB 下载大小(1)

为什么移动 __TEXT 段会减少下载大小?下一小节会给出详细的解释。

注意,使用 -rename_p 需要关闭 Bitcode

3. 下载大小减少的原理

摘自苹果官方文档[3]:

When your app is approved for the App Store, it is encrypted with DRM and recompressed. The added encryption and DRM affects the ability to compress your binary, and as a result you may see a larger App Store file size for your binary than the binary you uploaded on App Store Connect. The exact final size for your app cannot be determined in advance to the accuracy of a single byte.

对项目工程进行 Archive 后会生成 .xcarchive 文件,该文件中包含了 App、dsYMS 以及其它信息。如图所示为 .xcarchive 文件中包含的内容:

.xcarchive 文件上传到 App Store Connect 后,苹果会对 App 中的可执行文件进行 DRM 加密,然后将 App 压缩成 ipa 文件,才发布到 App Store。加密对可执行文件的大小本身影响很小(对今日头条 App 的影响为 2 MB),但是它会严重影响可执行文件的压缩效率,导致压缩后的 ipa 大小增加,也就是下载大小增大。

实际上,这种加密几乎没有用,只要有越狱手机,使用市面上的脱壳工具就可以很容易地进行解密。

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 为目标地址与源地址的距离。

然而该代码在计算 srcAddrdstAddr 时,用的都是 offset,是相对距离:

  • atom->pOffset()target->pOffset() 都是 atom 相对于各自 Section 起始地址的距离。

  • fit->offsetInAtomaddend 都是 fixup 相对于各自 atom 的距离。

因此,算出来的 srcAddrdstAddr 都是 fixup 相对于各自所在 Section 起始地址的距离。而 displacement 又是根据 dstAddrsrcAddr 相减计算出来的,它的本意是要计算 dstAddrsrcAddr 之间的距离。在没有 -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

那么,这两种情况的跳转指令,在实际跳转中是否会出错?

  1. __TEXT,__stub_helper -> __TEXT,__stub_helper不会,因为__stub_helper的大小只有 28kb(在头条中),远小于 128M,所以它内部的指令再怎么跳都不会超出限制。

  2. __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]手动解析,将主镜像名称当做参数传入即可。

最后

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数初中级Android工程师,想要提升技能,往往是自己摸索成长,自己不成体系的自学效果低效漫长且无助

因此我收集整理了一份《2024年Android移动开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点!不论你是刚入门Android开发的新手,还是希望在技术上不断提升的资深开发者,这些资料都将为你打开新的学习之门

如果你觉得这些内容对你有帮助,需要这份全套学习资料的朋友可以戳我获取!!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!
的范围只有2047999(2.0 MB),这明显远小于主二进制文件中__text原本的大小 100 MB。这个 2.0 MB 的大小基本与__TEXT段被迁移后剩余的大小相符,因此猜测 Crash.log 在分析时取的是__TEXT段的大小,而我们把大部分__TEXT` 段都移走了。

所以当遇到一个符号落在 (2.0M, 100M] 的区间中时,Crash.log 就无法知道这个地址它到底是属于哪个镜像,它就会显示 ??? ,无法解析。

解决办法:这种 Crash.log 使用 atos 工具[11]手动解析,将主镜像名称当做参数传入即可。

最后

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数初中级Android工程师,想要提升技能,往往是自己摸索成长,自己不成体系的自学效果低效漫长且无助

因此我收集整理了一份《2024年Android移动开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。

[外链图片转存中…(img-kU0Tocb5-1715839494757)]

[外链图片转存中…(img-8MbmZhyo-1715839494758)]

[外链图片转存中…(img-9HTs1AlC-1715839494758)]

[外链图片转存中…(img-ZgHJZrk5-1715839494759)]

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点!不论你是刚入门Android开发的新手,还是希望在技术上不断提升的资深开发者,这些资料都将为你打开新的学习之门

如果你觉得这些内容对你有帮助,需要这份全套学习资料的朋友可以戳我获取!!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

  • 4
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值