抖音研发实践:基于二进制文件重排的解决方案 APP启动速度提升超15%

尾声

在我的博客上很多朋友都在给我留言,需要一些系统的面试高频题目。之前说过我的复习范围无非是个人技术博客还有整理的笔记,考虑到笔记是手写版不利于保存,所以打算重新整理并放到网上,时间原因这里先列出面试问题,题解详见:


展示学习笔记

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化学习资料的朋友,可以戳这里获取

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

  • 如何重排 - 让链接器按照指定顺序生成Mach-O

  • 重排的内容 - 获取启动时候用到的函数


System Trace


日常开发中性能分析是用最多的工具无疑是Time Profiler,但Time Profiler是基于采样的,并且只能统计线程实际在运行的时间,而发生Page Fault的时候线程是被blocked,所以我们需要用一个不常用但功能却很强大的工具:System Trace。

选中主线程,在VM Activity中的File Backed Page In次数就是Page Fault次数,并且双击还能按时序看到引起Page Fault的堆栈:

System Trace

signpost


现在我们在Instrument中已经能拿到某个时间段的Page In次数,那么如何和启动映射起来呢?

我们的答案是:os_signpost

os_signpost是iOS 12开始引入的一组API,可以在Instruments绘制一个时间段,代码也很简单:

1os_log_t logger = os_log_create(“com.bytedance.tiktok”, “performance”); 2os_signpost_id_t signPostId = os_signpost_id_make_with_pointer(logger,sign); 3//标记时间段开始 4os_signpost_interval_begin(logger, signPostId, “Launch”,“%{public}s”, “”); 5//标记结束 6os_signpost_interval_end(logger, signPostId, “Launch”);

通常可以把启动分为四个阶段处理:

启动阶段

有多少个Mach-O,就会有多少个Load和C++静态初始化阶段,用signpost相关API对对应阶段打点,方便跟踪每个阶段的优化效果。

Linkmap


Linkmap是iOS编译过程的中间产物,记录了二进制文件的布局,需要在Xcode的Build Settings里开启Write Link Map File:

Build Settings

比如以下是一个单页面Demo项目的linkmap。

linkmap

linkmap主要包括三大部分:

  • Object Files 生成二进制用到的link单元的路径和文件编号

  • Sections 记录Mach-O每个Segment/p的地址范围

  • Symbols 按顺序记录每个符号的地址范围


ld

Xcode使用的链接器件是ld,ld有一个不常用的参数-order_file,通过man ld可以看到详细文档:

Alters the order in which functions and data are laid out. For each p in the output file, any symbol in that p that are specified in the order file file is moved to the start of its p and laid out in the same order as in the order file file.

可以看到,order_file中的符号会按照顺序排列在对应p的开始,完美的满足了我们的需求。

Xcode的GUI也提供了order_file选项:

order_file

如果order_file中的符号实际不存在会怎么样呢?

ld会忽略这些符号,如果提供了link选项-order_file_statistics,会以warning的形式把这些没找到的符号打印在日志里。

获得符号


还剩下最后一个,也是最核心的一个问题,获取启动时候用到的函数符号。

我们首先排除了解析Instruments(Time Profiler/System Trace) trace文件方案,因为他们都是基于特定场景采样的,大多数符号获取不到。最后选择了静态扫描+运行时Trace结合的解决方案。

Load

Objective C的符号名是+-[Class_name(category_name) method:name:],其中+表示类方法,-表示实例方法。

刚刚提到linkmap里记录了所有的符号名,所以只要扫一遍linkmap的__TEXT,__text,正则匹配("^\+\[.*\ load\]$")既可以拿到所有的load方法符号。

C++静态初始化

C++并不像Objective C方法那样,大部分方法调用编译后都是objc_msgSend,也就没有一个入口函数去运行时hook。

但是可以用-finstrument-functions在编译期插桩“hook”,但由于抖音的很多依赖由其他团队提供静态库,这套方案需要修改依赖的构建过程。二进制文件重排在没有业界经验可供参考,不确定收益的情况下,选择了并不完美但成本最低的静态扫描方案。

1. 扫描linkmap的__DATA,__mod_init_func,这个p存储了包含C++静态初始化方法的文件,获得文件号[ 5]

1//__mod_init_func 20x100008060    0x00000008  [  5] ltmp7 3//[  5]对应的文件 4[  5] …/Build/Products/Debug-iphoneos/libStaticLibrary.a(StaticLibrary.o)

2. 通过文件号,解压出.o。

1➜  lipo libStaticLibrary.a -thin arm64 -output arm64.a 2➜  ar -x arm64.a StaticLibrary.o

3. 通过.o,获得静态初始化的符号名_demo_constructor

1➜  objdump -r -p=__mod_init_func StaticLibrary.o 2 3StaticLibrary.o:    file format Mach-O arm64 4 5RELOCATION RECORDS FOR [__mod_init_func]: 60000000000000000 ARM64_RELOC_UNSIGNED _demo_constructor

4. 通过符号名,文件号,在linkmap中找到符号在二进制中的范围:

10x100004A30    0x0000001C  [  5] _demo_constructor

5. 通过起始地址,对代码进行反汇编:

1➜  objdump -d --start-address=0x100004A30 --stop-address=0x100004A4B demo_arm64  2 3_demo_constructor: 4100004a30:    fd 7b bf a9     stp x29, x30, [sp, #-16]! 5100004a34:    fd 03 00 91     mov x29, sp 6100004a38:    20 0c 80 52     mov w0, #97 7100004a3c:    da 06 00 94     bl  #7016  8100004a40:    40 0c 80 52     mov w0, #98 9100004a44:    fd 7b c1 a8     ldp x29, x30, [sp], #16 10100004a48:    d7 06 00 14     b   #7004

6. 通过扫描bl指令扫描子程序调用,子程序在二进制的开始地址为:100004a3c +1b68(对应十进制的7016)。

1100004a3c:    da 06 00 94     bl  #7016

7. 通过开始地址,可以找到符号名和结束地址,然后重复5~7,递归的找到所有的子程序调用的函数符号。

小坑

STL里会针对string生成初始化函数,这样会导致多个.o里存在同名的符号,例如:

1__ZNSt3__112basic_stringIcNS_11char_traitsIcEENS_9allocatorIcEEEC1IDnEEPKc

类似这样的重复符号的情况在C++里有很多,所以C/C++符号在order_file里要带着所在的.o信息:

1//order_file.txt 2libDemoLibrary.a(object.o):__GLOBAL__sub_I_demo_file.cpp

局限性

branch系列汇编指令除了bl/b,还有br/blr,即通过寄存器的间接子程序调用,静态扫描无法覆盖到这种情况。

Local符号

在做C++静态初始化扫描的时候,发现扫描出了很多类似l002的符号。经过一番调研,发现是依赖方输出静态库的时候裁剪了local符号。导致__GLOBAL__sub_I_demo_file.cpp 变成了l002。

需要静态库出包的时候保留local符号,CI脚本不要执行strip -x,同时Xcode对应target的Strip Style修改为Debugging symbol:

Strip Style

静态库保留的local符号会在宿主App生成IPA之前裁剪掉,所以不会对最后的IPA包大小有影响。宿主App的Strip Style要选择All Symbols,宿主动态库选择Non-Global Symbols。

Objective C方法

绝大部分Objective C的方法在编译后会走objc_msgSend,所以通过fishhook(https://github.com/facebook/fishhook) hook这一个C函数即可获得Objective C符号。由于objc_msgSend是变长参数,所以hook代码需要用汇编来实现:

1//代码参考InspectiveC 2__attribute__((__naked__)) 3static void hook_Objc_msgSend() { 4    save() 5    __asm volatile ("mov x2, lr\n"); 6    __asm volatile ("mov x3, x4\n"); 7    call(blr, &before_objc_msgSend) 8    load() 9    call(blr, orig_objc_msgSend) 10    save() 11    call(blr, &after_objc_msgSend) 12    __asm volatile ("mov lr, x0\n"); 13    load() 14    ret() 15}

子程序调用时候要保存和恢复参数寄存器,所以save和load分别对x0~x9, q0~q9入栈/出栈。call则通过寄存器来间接调用函数:

1#define save() \ 2__asm volatile ( \ 3"stp q6, q7, [sp, #-32]!\n"\ 4... 5 6#define load() \ 7__asm volatile ( \ 8"ldp x0, x1, [sp], #16\n" \ 9... 10 11#define call(b, value) \ 12__asm volatile ("stp x8, x9, [sp, #-16]!\n"); \ 13__asm volatile ("mov x12, %0\n" :: "r"(value)); \ 14__asm volatile ("ldp x8, x9, [sp], #16\n"); \ 15__asm volatile (#b " x12\n");

before_objc_msgSend中用栈保存lr,在after_objc_msgSend恢复lr。由于要生成trace文件,为了降低文件的大小,直接写入的是函数地址,且只有当前可执行文件的Mach-O(app和动态库)代码段才会写入:

iOS中,由于ALSR(https://en.wikipedia.org/wiki/Address_space_layout_randomization)的存在,在写入之前需要先减去偏移量slide:

Android高级架构师

由于篇幅问题,我呢也将自己当前所在技术领域的各项知识点、工具、框架等汇总成一份技术路线图,还有一些架构进阶视频、全套学习PDF文件、面试文档、源码笔记。

  • 330页PDF Android学习核心笔记(内含上面8大板块)

  • Android学习的系统对应视频

  • Android进阶的系统对应学习资料

  • Android BAT部分大厂面试题(有解析)

好了,以上便是今天的分享,希望为各位朋友后续的学习提供方便。觉得内容不错,也欢迎多多分享给身边的朋友哈。

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化学习资料的朋友,可以戳这里获取

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

的系统对应学习资料**

[外链图片转存中…(img-dH8AVlWL-1715336025730)]

  • Android BAT部分大厂面试题(有解析)

[外链图片转存中…(img-Hvba3MLW-1715336025731)]

好了,以上便是今天的分享,希望为各位朋友后续的学习提供方便。觉得内容不错,也欢迎多多分享给身边的朋友哈。

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化学习资料的朋友,可以戳这里获取

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值