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

打印 上一主题 下一主题

主题 778|帖子 778|积分 2334

为什么移动 __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 为目标地址与源地址的距离。
然而该代码在计算 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]手动剖析,将主镜像名称当做参数传入即可。
自我介绍一下,小编13年上海交大结业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里不停到如今。
深知大多数初中级Android工程师,想要提升技能,往往是自己摸索成长大概是报班学习,但对于培训机构动则近万的学费,着实压力不小。自己不成体系的自学结果低效又漫长,而且极易碰到天花板技能停滞不前!
因此收集整理了一份《2024年Android移动开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻各人的负担。





既有得当小白学习的零基础资料,也有得当3年以上履历的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点,真正体系化!
由于文件比力大,这里只是将部分目录截图出来,每个节点内里都包罗大厂面经、学习条记、源码课本、实战项目、解说视频,而且会连续更新!
如果你觉得这些内容对你有帮助,可以扫码获取!!(备注:Android)
最后

总而言之,Android开发行业变化太快,作为技能职员就要保持终生学习的态度,让学习力成为焦点竞争力,所谓“活到老学到老”只有不断的学习,不断的提升自己,才气跟紧行业的步伐,才气不被时代所镌汰。
在这里我分享一份自己收录整理上述技能体系图相关的几十套腾讯、头条、阿里、美团等公司20年的面试题,把技能点整理成了视频和PDF(实际上比预期多花了不少精神),包罗知识脉络 + 诸多细节,由于篇幅有限,这里以图片的形式给各人展示一部分。



另有高级架构技能进阶脑图、Android开发面试专题资料,高级进阶架构资料 帮助各人学习提升进阶,也节流各人在网上搜刮资料的时间来学习,也可以分享给身边好友一起学习。
《互联网大厂面试真题剖析、进阶开发焦点学习条记、全套解说视频、实战项目源码课本》点击传送门即可获取!
)]
[外链图片转存中…(img-cmF9oY4O-1713504554102)]
[外链图片转存中…(img-lRPYpyZi-1713504554104)]
[外链图片转存中…(img-tRE0fr2v-1713504554105)]
[外链图片转存中…(img-9xObPZJ4-1713504554105)]
既有得当小白学习的零基础资料,也有得当3年以上履历的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点,真正体系化!
由于文件比力大,这里只是将部分目录截图出来,每个节点内里都包罗大厂面经、学习条记、源码课本、实战项目、解说视频,而且会连续更新!
如果你觉得这些内容对你有帮助,可以扫码获取!!(备注:Android)
最后

总而言之,Android开发行业变化太快,作为技能职员就要保持终生学习的态度,让学习力成为焦点竞争力,所谓“活到老学到老”只有不断的学习,不断的提升自己,才气跟紧行业的步伐,才气不被时代所镌汰。
在这里我分享一份自己收录整理上述技能体系图相关的几十套腾讯、头条、阿里、美团等公司20年的面试题,把技能点整理成了视频和PDF(实际上比预期多花了不少精神),包罗知识脉络 + 诸多细节,由于篇幅有限,这里以图片的形式给各人展示一部分。
[外链图片转存中…(img-MOq6t2xd-1713504554106)]
[外链图片转存中…(img-WCacWi0j-1713504554108)]
[外链图片转存中…(img-N79wUcCN-1713504554109)]
另有高级架构技能进阶脑图、Android开发面试专题资料,高级进阶架构资料 帮助各人学习提升进阶,也节流各人在网上搜刮资料的时间来学习,也可以分享给身边好友一起学习。
《互联网大厂面试真题剖析、进阶开发焦点学习条记、全套解说视频、实战项目源码课本》点击传送门即可获取!

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

悠扬随风

金牌会员
这个人很懒什么都没写!

标签云

快速回复 返回顶部 返回列表