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

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

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

七、总结

====

本文从背景知识和面临的实际问题出发,介绍了 __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移动开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。

img

img

img

img

既有适合小白学习的零基础资料,也有适合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岁后的你只会比周围的人更值钱。

《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值