关于我们 | 联系我们

yobo体育网页版|下载官网

当前位置:主页 > 新闻资讯 > 行业新闻 >

今日头条优化实践:iOS 包巨细二进制优化

本文摘要:摘要苹果对 iOS App 巨细有严格限制:下载巨细超限会阻碍用户在蜂窝网络下载 App ,直接影响新用户转化;可执行文件超限将导致 App 审核被拒,直接影响上架。今日头条探索实践 __TEXT 段迁移技术,乐成减小下载巨细 32%,而且解决了可执行文件巨细受限问题。 一、配景知识1. 下载巨细限制App 巨细有下载巨细和安装巨细的观点。

yobo体育网页版

摘要苹果对 iOS App 巨细有严格限制:下载巨细超限会阻碍用户在蜂窝网络下载 App ,直接影响新用户转化;可执行文件超限将导致 App 审核被拒,直接影响上架。今日头条探索实践 __TEXT 段迁移技术,乐成减小下载巨细 32%,而且解决了可执行文件巨细受限问题。

一、配景知识1. 下载巨细限制App 巨细有下载巨细和安装巨细的观点。下载巨细是指 App 压缩包(也就是 .ipa 文件)所占的空间,用户在下载 App 时,下载的是压缩包,这样做可以节约流量;当压缩包下载完成后,就会自动解压,解压历程也就是通常所说的安装历程;安装巨细就是指压缩包解压后所占用的空间。

安装巨细在 App Store 上就可以瞥见 ,通常它会影响用户的下载意愿:而下载巨细只有研发人员在 App Store Connect 后台才可以看,用户看不见,它影响的是下载消耗的流量和时长:若下载巨细凌驾限制,将无法使用蜂窝网络下载 App( iOS 13 之前),会收到文件容量太大的提示,需通过 Wi-Fi 网络下载。如下,为苹果历年来对 App 下载巨细限制的变化情况:2008 年 7 月,搭载了 App Store 的 iPhone 3G 正式发售,下载限制仅为 10 MB2010 年 2 月,苹果将 iPhone 3G 的下载限制从 10 MB 提升到 20 MB2012 年 3 月,iOS 5.1 正式版后,下载限制从 20 MB 提升到 50 MB2013 年 9 月,iOS 7 正式版后,下载限制从 50 MB 提升至 100 MB2017 年 9 月,iOS 11 正式版后,下载限制从 100 MB 提升至 150 MB2019 年 5 月,下载限制从 150 MB 提升至 200 MB2019 年 9 月,iOS 13 正式版后,若下载巨细凌驾 200 MB,用户可选择是否使用蜂窝网络下载如今,App 下载巨细超出 200 MB 时 ,会泛起两种情况:iOS 13 以下的用户,无法通过蜂窝数据下载 AppiOS 13 及以上的用户,需要手动设置才可以使用蜂窝网络下载 App2. 可执行文件巨细限制凭据最大构建版本文件巨细[1]形貌,苹果对可执行文件巨细亦有明确限制,凌驾该限制会导致 App 审核被拒:ERROR:ERRORITMS-90122:"InvalidExecutaBeSize.Thesizeofyourapp'sexecutaBefile'News.app/News'is68534272bytesforarchitecture'arm64',whichexceedsthemaximumallowedsizeof60MB."详细限制如下:iOS 7 之前,二进制文件中所有的 __TEXT 段总和不得凌驾 80 MBiOS 7.X 至 iOS 8.X ,二进制文件中,每个特定架构中的 __TEXT 段不得凌驾 60 MBiOS 9.0 之后,二进制文件中所有的 __TEXT 段总和不得凌驾 500 MB二、面临问题随着网络普及、流量用度降低,苹果已经放宽了限制。

但下载巨细若超出 200 MB,可以肯定对新增仍会有一定影响。这对上亿级用户的 App 来说是庞大的损失,而且本着追求极致、在竞品中拔得头筹的理念,我们认为下载巨细 200 MB 是包巨细的一根红线。今日头条 App 的下载巨细已经靠近 180 MB,而经由了多年的极致优化(包罗但不限于代码/图片/其它资源的优化、编译/链接参数的优化、推进无用业务下线、准入卡口等),已经很难再有较大幅度的淘汰。

为此平台和各方都投入了极大的人力、甚至牺牲了业务的迭代空间来优化/抑制下载巨细。2020 年下半年,我们另辟蹊径探索实践了 __TEXT 段迁移的方法:将可执行文件的 __TEXT 段中的部门节移动到其它的段,避开苹果的加密机制,提高了可执行文件的压缩效率,使 App 的下载巨细淘汰了 60 MB。

该方案彻底解决了下载巨细限制的问题,同时还解决了仍在支持 iOS 8.X 的 App 面临的可执行文件巨细限制问题。三、技术原理1. Mach-O 文件花样简介iOS 可执行文件是 Mach-O 花样,主要由 Header、Load Commands、Data 三部门。可以简朴认为:Header 形貌了文件的或许信息。

Load Commands 由多条 Load Command 组成,它们形貌了 Data 在二进制文件和虚拟内存中的结构信息,有了这个结构信息就能够知道 Data 在二进制文件中和虚拟内存中是怎样排布的,它相当于修屋子时的图纸一样。Data 存储了实际的内容,主要是法式的指令和数据,它们的排布完全依照 Load Commands 的形貌。Mach-O 文件中的 Data 部门主要是以 Segment(中文翻译为段)和 Section (中文翻译为节)的方式来组织内容的,好比学校中有年级和班级、公司中有部门和小组一样,把有配合特点的内容组织到一块,可以利便治理,提高效率。使用 $ xcrun size -lm <binary-path> 指令可以检察 Mach-O 文件 Data 部门的结构和各 Segment/Section 的巨细信息(该 Mach-O 文件由 Xcode 的 iOS App 模板工程构建而来)。

在不需要更详细的信息时,这条下令很利便。上图就展示了 Data 中的内容排布的基本信息。由该图可知,在该文件中:Data 部门中有 5 个 Segment,依次是:__PAGEZERO__TEXT__DATA_CONST__DATA__LINKEDIT除 __PAGEZERO 和 __LINKEDIT外,每个段中有多个 Section。

注意:Data 与 __DATA 是差别的两个观点。Data 是 Mach-O 文件中的一部门,包罗多个段。

__DATA 只是 Data 中的一个段。__PAGEZERO 的巨细是 4 GB,但并不是它在 Mach-O 文件中的真实巨细。

这 4 GB 是 Mach-O 加载进内存后, __PAGEZERO 在内存中占中的巨细,它不行读,不行写,主要用来捕捉 NULL 指针的引用。如果会见 __PAGEZERO 段,会引起 EXC_BAD_ACCESS 错误。

__PAGEZERO 在 Mach-O 中实际上并不占用 Data 部门的空间。__TEXT、__DATA_CONST、__DATA 用于生存法式的代码指令和数据。__LINKEDIT 包罗启动 App 需要的信息,好比 bind & rebase 的地址,代码签名,符号表等。

2. __TEXT 段迁移的原理法式的构建历程包罗 预处置惩罚 -> 编译 -> 汇编 -> 链接 等 4 个主要阶段,完成之后就会获得 Mach-O 可执行文件。通过 $ man ld ,可以发现链接器有一个参数: -rename_section orgSegment orgSection newSegment newSection。

使用该参数可以将 orgSegment/orgSection 的名称修改为 newSegment/newSection。可以在 Other Linker Flags 中通报该参数。

如:-Wl,-rename_section,__TEXT,__text,__BD_TEXT,__text-Wl,-segprot,__BD_TEXT,rx,rx其中 -Wl 的作用是告诉 Xcode 它后面的参数是添加给 Ld 链接器的,这些参数将在链接阶段生效。第一行参数会新建立一个 __BD_TEXT 段,并把 __TEXT,__text 移动到 __BD_TEXT,__text。

第二行参数是给 __BD_TEXT 赋予可读和可执行权限。构建完成后再来看一下移动 __TEXT,__text 后的 Mach-O 文件:可以看到 __TEXT,__text 已经被移动到了 __BD_TEXT 中去了,它的地址也由起始的 0x100005e5c 变为了 0x100010000。

此时法式仍可以正常的运行,这是因为操作系统只体贴段的读/写/执行权限,并不体贴段或节的名称。即即是使用了 -rename_section 移动 Segment/Section,各符号的地址也会由链接器修正好,因此段移动后法式也可以正常运行。在最低支持 iOS 8 的时代,许多大型 App 都遇到过可执行文件中 __TEXT 段超 60 MB 的问题。facebook[2] 其时接纳了 -rename_section 的技术来制止该问题。

他们使用的链接参数为:-Wl,-rename_section,__TEXT,__cstring,__RODATA,__cstring-Wl,-rename_section,__TEXT,__gcc_except_tab,__RODATA,__gcc_except_tab-Wl,-rename_section,__TEXT,__const,__RODATA,__const-Wl,-rename_section,__TEXT,__objc_methname,__RODATA,__objc_methname-Wl,-rename_section,__TEXT,__objc_classname,__RODATA,__objc_classname-Wl,-rename_section,__TEXT,__objc_methtype,__RODATA,__objc_methtype参数的作用是将 __TEXT 中的 __cstring、__gcc_except_tab、__const、__objc_methname、__objc_classname、__objc_methtype 等 6 个节移动到 __RODATA去。由于这 6 个节是只读的,所以他们将新段取名为 __RODATA,意为只读段。这样做之后,__TEXT 的巨细就会被减小,而苹果只会扫描 __TEXT 段,所以当 __TEXT 段减小到 60 MB 以下时,就制止了 __TEXT 段凌驾 60 MB 的问题,该方案其时在海内大型 App 上也很常见。

今日头条 App 在 2018 年 5 月遇到此问题后也接纳了该方案,其时是为了制止 __TEXT 段超 60 MB 的问题。现在测试发现,以上参数也对下载巨细有 12 MB 的优化。

为什么移动 __TEXT 段会淘汰下载巨细?下一小节会给出详细的解释。注意,使用 -rename_section 需要关闭 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 是否被加密过。Loadcommand13cmdLC_ENCRYPTION_INFO_64cmdsize24cryptoff16384cryptsize101695488cryptid1pad0其中 cryptoff 表现加密字段位于文件中偏移 16384 个字节;cryptsize 表现加密内容长度 101695488 个字节;cryptid 表现加密方法为 1,如果为 0 表现不加密。

检察 LC_SEGMENT_64 中 __TEXT 段的规模Loadcommand1cmdLC_SEGMENT_64cmdsize1432segname__TEXTvmaddr0x00000001000000004294967296vmsize0x0000000006100000101711872fileoff0filesize101711872依据上述效果可以算出加密的内容实际上都位于 __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. CrashCrash 的原因是执行代码时找不到指定的节。在原理中说到:操作系统只体贴段的读/写/执行权限,并不体贴段或节的名称。

即即是使用了-rename_section 移动 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_section 移动 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_section 不会改变 Section 的类型。那为何会-rename_section 后会导致 branch out of range 的异常?主要是两个原因:1. Branch Island 算法的检查逻辑没有适配到 Section 被移动的情况。在分析 Mach-O 文件时只先容了 Segment/Section,实际链接器认为在 Section 中还存在 atom(链接的基本单元),在 atom 中还存在 fixup(用于形貌差别 atom 之间的引用关系。

yobo体育网页版

)如图所示为 ld64-530 的 branch_island.cpp 文件中 Branch Island 算法中的一部门代码。该片段是要判断跳转指令跳转的距离是否超出限制,如果凌驾限制就会对该跳转指令做掩护,否则就不做。srcAddr 为跳转指令所在的源地址,dstAddr 为目的地址,displacement 为目的地址与源地址的距离。

然而该代码在盘算 srcAddr 和 dstAddr 时,用的都是 offset,是相对距离:atom->sectionOffset() 和 target->sectionOffset() 都是 atom 相对于各自 Section 起始地址的距离。fit->offsetInAtom 和 addend 都是 fixup 相对于各自 atom 的距离。

因此,算出来的 srcAddr 和 dstAddr 都是 fixup 相对于各自所在 Section 起始地址的距离。而 displacement 又是凭据 dstAddr 和 srcAddr 相减盘算出来的,它的本意是要盘算 dstAddr 与 srcAddr 之间的距离。

在没有 -rename_section 的情况下,这种盘算方式没有问题;在使用-rename_section 的情况下,会导致盘算出来的距离 displacement 禁绝确,会使在预期对跳转指令做掩护的场景实际没做掩护。2. Branch Island 算法不会掩护自界说 Section。Branch Island 算法只会对 typeCode 的 Section 做掩护,而自界说 Section 的类型是 typeUnclassified,如果自界说 Section 中的代码使用了跳转指令,而且该跳转指令的跳转距离超出规模,那么无论是否-rename_section 都市泛起链接失败的问题。下面联合 3 个场景,来详细分析 Branch Island 算法的缺陷。

2.3.1 场景一__TEXT,__text 移不洁净导致链接失败。__text 节在 __TEXT 段中所占比例庞大,要想到达优化效果,必须把它移走,否则险些没有任何优化效果。头条最开始时,使用-Wl,-rename_section,__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 __text2007-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_section 之后,因此我们使用-rename_section,__TEXT,__text,__BD_TEXT,__text 没有将它俩移走。要让 __TEXT,_text 移洁净,只需要把它俩也-rename_section。使用如下设置就可以了:-Wl,-rename_section,__TEXT,__text,__BD_TEXT,__text,-Wl,-rename_section,__TEXT,__textcoal_nt,__BD_TEXT,__text,-Wl,-rename_section,__TEXT,__StaticInit,__BD_TEXT,__text注:字符串 __BD_TEXT 中的 BD 是 ByteDance 的缩写,__BD_TEXT 只是一个名称,可以随意更改。如果你的 App 中使用-rename_section,__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, 而且这个 section 在 __TEXT,__stubs 的前面。这种算法在正常的 Mach-O 文件中是完全可行的,但我们如果移动了 Segment/Section,就不切合它的设定了,就会导致问题。

因此需要添加如下参数,将 __TEXT,__stubs 也移走:-Wl,-rename_section,__TEXT,__stubs,__BD_TEXT,__stubs,-Wl,-rename_section,__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_section,__TEXT,__cstring,__RODATA,__cstring-Wl,-rename_section,__TEXT,__gcc_except_tab,__RODATA,__gcc_except_tab-Wl,-rename_section,__TEXT,__const,__RODATA,__const-Wl,-rename_section,__TEXT,__objc_methname,__RODATA,__objc_methname-Wl,-rename_section,__TEXT,__objc_classname,__RODATA,__objc_classname-Wl,-rename_section,__TEXT,__objc_methtype,__RODATA,__objc_methtype2.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_section->__TEXT,__text原因是 Branch Island 只会对类型是 typeCode 的 Section 中的跳转指令做检查 ,而只有 __TEXT,__text 的类型是 typeCode。那么,这两种情况的跳转指令,在实际跳转中是否会堕落?__TEXT,__stub_helper -> __TEXT,__stub_helper不会,因为__stub_helper的巨细只有 28kb(在头条中),远小于 128M,所以它内部的指令再怎么跳都不会超出限制。__TEXT,__custom_section -> __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_section (含跳转指令的自界说 Section)也必须随着移动,让它保持在 __text 的下面,保持它们原有的相对位置。照此分析,__TEXT 中的自界说 Section 不被 Branch Island 掩护,如果二进制文件足够大,而这个 Section 又有跳转指令,当跳转距离凌驾 128 MB 时,也会链接失败,与是否移动 __text 无关。要移走自界说 Section,需要再添加如下设置:-Wl,-rename_section,__TEXT,__custom_section,__CUSTOM_TEXT,__custom_section这里必须要使用新的段 __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_section,__TEXT,__cstring,__RODATA,__cstring,-rename_section,__TEXT,__objc_methname,__RODATA,__objc_methname,-rename_section,__TEXT,__objc_classname,__RODATA,__objc_classname,-rename_section,__TEXT,__objc_methtype,__RODATA,__objc_methtype,-rename_section,__TEXT,__gcc_except_tab,__RODATA,__gcc_except_tab,-rename_section,__TEXT,__const,__RODATA,__const,-rename_section,__TEXT,__text,__BD_TEXT,__text,-rename_section,__TEXT,__textcoal_nt,__BD_TEXT,__text,-rename_section,__TEXT,__StaticInit,__BD_TEXT,__text,-rename_section,__TEXT,__stubs,__BD_TEXT,__stubs,-rename_section,__TEXT,__picsymbolstub4,__BD_TEXT,__picsymbolstub4,-segprot,__BD_TEXT,rx,rx如果你是没有使用这种方式,在 Other Linker Flags 中逐行添加以下设置即可:-Wl,-rename_section,__TEXT,__cstring,__RODATA,__cstring-Wl,-rename_section,__TEXT,__objc_methname,__RODATA,__objc_methname-Wl,-rename_section,__TEXT,__objc_classname,__RODATA,__objc_classname-Wl,-rename_section,__TEXT,__objc_methtype,__RODATA,__objc_methtype-Wl,-rename_section,__TEXT,__gcc_except_tab,__RODATA,__gcc_except_tab-Wl,-rename_section,__TEXT,__const,__RODATA,__const-Wl,-rename_section,__TEXT,__text,__BD_TEXT,__text-Wl,-rename_section,__TEXT,__textcoal_nt,__BD_TEXT,__text-Wl,-rename_section,__TEXT,__StaticInit,__BD_TEXT,__text-Wl,-rename_section,__TEXT,__stubs,__BD_TEXT,__stubs-Wl,-rename_section,__TEXT,__picsymbolstub4,__BD_TEXT,__picsymbolstub4,-Wl,-segprot,__BD_TEXT,rx,rx如果你的二进制文件中存在自界说 Section 的话,好比使用了类似__attribute__((section("__TEXT,__custom_section")))的方式建立了自界说 Section,则可能需要做如下的设置以移走自界说 Section,详细见 「2.3.3 场景三」 的详细分析。

APP_THIN_LINK_FLAGS=-Wl,-rename_section,__TEXT,__cstring,__RODATA,__cstring,-rename_section,__TEXT,__objc_methname,__RODATA,__objc_methname,-rename_section,__TEXT,__objc_classname,__RODATA,__objc_classname,-rename_section,__TEXT,__objc_methtype,__RODATA,__objc_methtype,-rename_section,__TEXT,__gcc_except_tab,__RODATA,__gcc_except_tab,-rename_section,__TEXT,__const,__RODATA,__const,-rename_section,__TEXT,__text,__BD_TEXT,__text,-rename_section,__TEXT,__textcoal_nt,__BD_TEXT,__text,-rename_section,__TEXT,__StaticInit,__BD_TEXT,__text,-rename_section,__TEXT,__stubs,__BD_TEXT,__stubs,-segprot,__BD_TEXT,rx,rx,-rename_section,__TEXT,__picsymbolstub4,__BD_TEXT,__picsymbolstub4,-rename_section,__TEXT,__custom_section,__CUSTOM_TEXT,__text,-segprot,__CUSTOM_TEXT,rx,rx六、答疑1. 为什么不把 __TEXT 段中的所有 Section 都移走,这样不是更好吗?并不是移走的段越多,压缩就越有效,而是得看移走的巨细。例如下面虽然有 15 个 Section,可是它们的巨细加起来 578 KB,移走它们对压缩后的下载巨细险些零提升。Section__stubs:28488(addr0x105f21644offset99751492)Section__stub_helper:28428(addr0x105f2858coffset99779980)Section__swift5_typeref:2216(addr0x105f2f498offset99808408)Section__swift5_fieldmd:1272(addr0x105f2fd40offset99810624)Section__swift5_types:120(addr0x105f30238offset99811896)Section__const:64184(addr0x105f302b0offset99812016)Section__ustring:281012(addr0x105f3fd68offset99876200)Section__swift5_reflstr:796(addr0x105f84720offset100157216)Section__swift5_capture:376(addr0x105f84a3coffset100158012)Section__swift5_builtin:120(addr0x105f84bb4offset100158388)Section__swift5_assocty:312(addr0x105f84c2coffset100158508)Section__swift5_proto:308(addr0x105f84d64offset100158820)Section__swift5_protos:40(addr0x105f84e98offset100159128)Section__u__selector:36(addr0x105f84ec0offset100159168)Section__eh_frame:184708(addr0x1060cf018offset101511192)而且,有的 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 团队,在性能优化、基础组件、业务架构、研发体系、宁静合规、线下质量基础设施、线上问题定位归因平台等偏向深耕,卖力保障和提升今日头条、西瓜视频和番茄小说的产物质量与开发效率,聚焦于此的同时向外延伸。

如果你对技术充满热情,喜欢追求极致,盼望用自己的代码改变数亿用户的体验,接待加入我们。我们期待你与我们配合发展。现在我们在北京、深圳均有招聘需求,简历投递邮箱: tech@bytedance.com ;邮件标题: 姓名 - 事情年限 - GIP - 客户端平台架构 - iOS/Android。参考文献[1] 最大版本构建巨细https://help.apple.com/app-store-connect/#/dev611e0a21f[2] 分析 facebook Apphttps://blog.timac.org/2016/1018-analysis-of-the-facebook-app-for-ios/[3] App Store Connect Helphttps://help.apple.com/app-store-connect/en.lproj/static.html[4] iOS 13 更新日志https://support.apple.com/en-us/HT210393#13[5] dyld 检查 Section 的源码https://opensource.apple.com/source/dyld/dyld-750.6/src/dyldExceptions.c.auto.html[6] ARM 架构https://en.wikipedia.org/wiki/ARM_architecture#Thumb[7] ld64-530 OutputFile 源码https://opensource.apple.com/source/ld64/ld64-530/src/ld/OutputFile.cpp[8] Branch Island 算法https://opensource.apple.com/source/ld64/ld64-133.3/src/ld/passes/branch_island.cpp.auto.html[9] Ld 更新日志https://opensource.apple.com/source/ld64/ld64-97.2/ChangeLog[10] xcconfig 先容https://nshipster.com/xcconfig/[11] atos man pagehttps://www.manpagez.com/man/1/atos/更多分享抖音 Android 性能优化系列:Java 内存优化篇西瓜视频稳定性治理体系建设一:Tailor 原理及实践基于有限状态机与消息行列的三方支付系统补单实践UME - 富厚的Flutter调试工具接待关注「 字节跳动技术团队 」简历投递联系邮箱「 tech@bytedance.com 」。


本文关键词:今日,头条,yobo体育网页版,优化,实践,iOS,包巨细,包,巨细,摘要

本文来源:yobo体育网页版-www.thoth1961.com

Copyright © 2000-2022 www.thoth1961.com. yobo体育网页版科技 版权所有 备案号:ICP备20004369号-6