iOS性能提升探索 – 启动优化之二进制重排


二进制重排

介绍

上一年年底二进制重排的概念被世界厂带火了起来,出于学习的目的,综合网上已有材料并总结完结了下,以便对发动优化有更好的了解。

比照了网上的完结办法,抖音经过手动插桩获取的符号数据,$ 8 [ * Q包括C++静态初始化、+Load、Block等都需求针对性处理,就其复杂度来说感觉性价比不高;手淘的方案c l P w o比较特别,经过修正 .o 方针文件% Q w i &完结静态插桩,需求对方针代码较为熟悉,通用性不高;最终决定选用 clang 插桩的办法完结B ? C H J { e二进制重排。z w { D

先介绍一些根本的概念以便对完结有5 ( L | E # H K更好的了解。

App发动和内存加载

Linux 系统下,进程请求内存并不是直接物理内存给咱们运转,而是只标记当时进程拥有该段内存,当真正运用这段段内存时才会分配,此刻的内存是虚拟内存。

在虚拟内存出现前,程序指令必须都在物理内存内,使得物理内存能寄存的进程十分有限,并且因为是相邻存储,容易发生a E V D . H X T L越界拜访等情况。

虚拟内存是作为 内存的办理B 6 z h/ % U 2 9 3 C O保护东西 诞生的,为每个进程供给了一片接连完好的虚拟内存空间,运用时先经过界限寄存器判别拜访是否越界,再经过基址寄存器转换为实际内存地址。降低了内存办理的复杂度,保护每个进程的内存地址空间不会被其它进程损坏,并且完结了 同享缓存功用,拜访时先t 7 ! c : e判别是否已缓存到主存中才经过 CPU 寻址(虚拟] U 0 q | + f地址)拜访主存或a – i j ! *硬盘。

当咱们需求拜访一个内存地址时,假如虚拟内存地址对应的物理内存还未分配,CPU 会履行 page fault,将指令从磁e F 0 .盘加载到物理内存中并进行验# x L 3 { q 1 x k签操作(App Store 发布情况下)。

在App 发动进程中,会调b 1 Z m用各种函数,因为G g :这些函数散布在各个 TEXT 段中且不接 R & p 8 8 }连,此刻需求履行多次 page fault 创建分页,将代码读取到物理内存中,并且这I 7 8 R + & ? @ O些分页中的部分代码不会在发动阶! T y & D 9 @ 8 5段被调用。如下图所示,假设咱们在h o I m | A 8发动阶段需求调用 Func A、B、C,则需履行3次 page dQ ` y Y Fefault(包括首次读取),并运用3个分页。

iOS性能提升探索 - 启动优化之二进制重排

怎么优化?

优化的思r ( P ~ {路很简单,即把发动阶段需求用到的函数按次序排放,削减 pagey p k fault 履行次数和分页数量,并使 page fault 在相邻页履行,如下图所示,相较于之前,削减了一次 page fault 和分页加载,当工程复杂度高时,优化的效果就很客观了。

iOS性能提升探索 - 启动优化之二进制重排

Xc7 + U $ Kode 的链接器供给了一个 Order File 装备 5 6 O R & C Z,对应的文件中符号会依照次序写入二进制文+ k t ~ i {件中,咱们能( ; 5够将调用到的函数写到该文件,完结优化。

iOS性能提升探索 - 启动优化之二进制重排

完结详解

Link Map了解链接次序

LiQ j s E Z n g &nk Map 是 App 编译进程的中心产品,记载了二进制文件的布局,咱们能够j m S _ l经过 Link Map 文件剖析可履行文件的构成是怎样,里面的内容都是些什么,哪些库占用空间较高等等,需求手动在 Build SettV ! / V : E Y j Mings 将 Write Link Map File 设P = l置为 Yes。

默认生成的 Link Map 文件在 build 目录下,能够经过修正 Path To Link Map 指定寄存地址。

iOS性能提升探索 - 启动优化之二进制重排

以deml m M Co为例,文件中的内容如下,各部位T P * p J b ` 1意义见注释:

// Link Map对应安装包地址
# Path: /Users/yehuangbin/Library/Developer/Xcode/DerivedData/IOSDevelopTools-bpjwhcswecoziihayzwjgxztowne/Build/Products/Debug-iphoneos/IOSDevelopTools.app/IOSDevel_ Q Y UopTools
// 对应的架构
# Arch: arm64
// 编译后生成的.o文件列表,包括系统和用户自定的类,UIKit库等等。
# Obje& h x } D s v |ct files:
[  0] linker synthesized
[  1] /Users/yed a % ] R W W b ghuangbin/Library/D) V ] developer/Xcode/DerivedData/IOSDevelopTools-bpjwhcswecoziihayzwjgxztowne/Build/Intermediatep 4 & 7 J ds.noindex/IOSDevelopTools.build/Debug-iphoneos/IOSDevelopTools.build/ObK ^ Z : c 3 M . AjectC 5 Y } P  / Ws-norma5 ` 2 S r V Al/arm63 [ ^ s d4/YECallMonitor.o
[  2] /Users/yehuangbin/Library/Developer/Xcode/DerivedData/IOSDevelopTools-bpjwhcswecoziihayzwjgxztowne/Build/Intermediates.noindex/I8 8 VOS6 r #DevelopTools.build/DebF ~ - 6 n z J G ;ug-iphoneos/IOSDevelopTools.build/Objects-normal/ar1 2 ) ? $ h Ym# L / I64/YECallRecordCell.o
...
// Section是各种数据类型地点的内存空间,Section首要分为两大类,| l | = Q L [ `__Text和__DATA。__Text指的是程序代码,__DATA指的是现已初始化的变量等。
# Sections:
# Address	Size    	Se: L C zgment	Section
0x1S ) E0000572C	0x0000B184	__TEXT	__text
0x1000108B0	0x000002s w I @ r !C4	__T 7 & S l b 2 | EXT	__stubs
0x100010B74	0x000002D7 m ! eC	__TEXT	__stub_helper
0x100010E50	0x00000088	__TEXT	__const
0x100010ED8	0xB ^ 1 A L G 2000006EC	__TEXT	__cstring
...
// 变量名、类名、办法名等符号表
# Symbols:& } { f m )
# Address	Size    	File  Name
0x10000572C	0x0000008 H ` I0	[  1] +[g t P q N ^ : }YECallMonito{ W K l | ] ; ^ Pr sd * ^ n u B K }hareInstance]
0x1000057AC	0x0000005C	[  1] ___30+[YE` u r P CH b e [ 6allMonitorH _ r H ~ y shareInstance]_block_:  # g 0 ,invoke
0x100005808	0x00000024	[  1] -[YECallMonitor start]
0x10000582C	0x00000024	[  1] -[YECallMonitor stop]
...
# Dead Stripped Symbols:
#        	Size    	File  Name
<<dead>> 	0x00000008	[  2] 8-byte-literal
<<dead>> 	0x00000006	[  2o | ( ! & o] literal striU 4 u 4 {ng: depth
<<dead>> 	0x00000012	[  2] literal string:] # { F  stringWithFormat:
<<dead>> 	0x00000007	[  2] li) % [ Vterall m ` R W p C + string: string
<<dead>> 	0x00000034	[  2] li$ U 3 : = = ,teral string:p v 1 E stringByPaddingToLength:withString:startingAtIndex:
<<dead>> 	0x0000000E	[  2]= s 9 t W | f literal sf 4 3 x Z Z D #tring: appendString* # d D U:
<<dead>&[ @ ` ) : 0 { J /gt; 	0x00000004	[  2] literal string: cls
<<dead>> 	0x0000000E	[  2] lite. ] ^ D . D : k 8ral string: .cxx_destruct
...

能够看到此刻 S1 0 u cymbols 的符号表并不是依照发动时履行的函数次序加载的,而是依照库的编译次序全部载入。

SanitizerCoverage搜集调用函数信息

咱们经过 SanitizerCoverage 搜集调用函数信息, SanitizerCoverage 内置在LLVM中,能够在函数、根本块和鸿沟这些级别上刺进对用户界说函数的回调,归于静态插桩,代码会在编译进程中刺进到每个函数中,具体介绍能够在 Clang 11: c @ ] Z D documentation 找到。

在 build settings 里的 “Other C Fla7 $ W 2 o ? ) Fgs” 中增加 -fsanitize-coveraY g : f i 1 T ege=func,trace-pc-guar* / o O l Kd。假如含有 Swift 代码的话,还需求在 “Other Swift Flags” 中参加 -sanitize-cN B ! z moverage=func-sani! q b } H o # o rtize=undefined。需注意,一切链接到 App 中的二进制都需求敞开 SanitizerCoverage,这样才干彻底覆盖到一切调用。

敞开后,函数的调用 都会履行 void __sanitizer_cov_tra= / l . i i ce_pc_guard(uint32_t *guard) {} 回调,效果相似咱们对 objc_msgSend 进行 Hook插桩,但该回调不2 P 8 _ . S X止局限于 OC 函数,还包括 Swift、block、C等。

咱们在该回调中刺进自己的计算代码,搜集函数名,发动完结后再将数据导出。借鉴玉令天下的完结代码,稍微修正了下,如需自取 AppCallCollecter,完好代码如下:

static OSQueueHead qHead = OS_ATOMIC_QUEUE_INIT;
staticv [ 5 O & y / BOOL stopCollecting = NO;
typedef struct {
void *pointer;
void *next;
} PointerNode;
// start和sB b f _ E +top地址之间的差异保存工程一切符号的个数
void _0 ) L `_sanitizer_cov_trace_pc_guard_init(uint32_t *start,
uint32_t *stop) {
static uint32_t N;  // Counter for the guards.
if (start == stop || *start) return;  // Initialize only once.
printf("INIT:T & 1 !  A , + J %p %pn", start, stop);
for (uint3. 2 V w G g } Y2_t *x = start; x < stop; x++)
*x = ++N;V A 8 i E R C  // Guards should start from 1.
printf("tob 3 . N 1 itasl count %in", N);
}
// 每个函数调用时= 1 @ j X ,都会先跳转履行该函U * u 4 { ! z 6 t数
void __sanitizer_cov_trace_pc_ga D 6 9 8 Huard(uint32q $ q c H 5 [_t *guard) {
// +loadg % E办法先于guard_init调用,此刻guard为0
/G D y/    if(!*guard) { return }
if (stopCollectiO z ang) {
return;
}
// __builtin_return_address 获取当时调用栈信息,取榜首帧地址
void *PC = __builtin_return_address(0);
PointerNode *node = malloc(7 : . * ^ T ` Q ksizeof(PointerNode));
*node = (PointerNode){PC, NULL};
// 运用原子行列要存储帧地址
OSAtomicEnqueue(&qHead, node,+ R 4 : ! A offsetof(Poa ] 2 j TinterNode, next));
}
extern NSArrayu - t 8 I <NSString *> *getAllFunctions(NSString *currentFuncName) {
NSMutableSet&G X U 3lt;NSString *&( T G d , o Xgt; *unqSet = [NSMutableSet setWithObject:currentFuncName];
NSMutableArray <NSString *> *functions = [NSMu; + b J G ?tableArray array];
while (YES) {
PointerNode *front = OSAtomicDequeue(&qHead, offsetof(PointerNode, next));
if(front == NULL) {
break;
}
Dl_info info = {0};
// dladdr获取地址符号S F m信息
dladdr(front->pointer, &info);
NSString *name = @(info.dli_sname);
// 去除重复调用
if([unqSet conta_ U % Y L E @insObject:name]) {
contin` N @ w t v o l oue;
}
BOOL, Z H R ! A c + isObjc = [name hasPrefix:@"+["] || [name hasPre: # wfix:@"-["];
// ordQ g & Her文件格式要求C函数和block前需求增加_
NSString *symbolName = isObN m xjc ? name : [@"_" stringByAppendingString:name];
[unqSet addObject:name];
[| l ~ r 2 S B $functions addObject:symbolName];
}
return [[functions reverseObjectEnumerator] allObjects];;
}
#pragma mark - public
extern NSArray <NSString *> *getAppCalls(void) {
stopCollF a 6 : * .er k & B = 4ctii m j eng = YES;
__sync_synchronize();
NSString* curFuncationName = [NSString stringWithUTF8String:__FUNCTION__];
return getAllFunctions(curFuncationName);
}
exU ` 4 2 b y ) 8 2tern void appOr- $ LderFile(void(^completion)(NSString* orderFilePath)) {
stopCoS r 7  ~llecting =j I r { M / YES;
__sync_synchronize();
N 1 h gSString* curFs p F Z *uncationName = [NSString stringWithUTF8String:__FUNCTION__]= * ` x w _ _ 8 [;
// 异步存储到文件中
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.01 * NSEC_PER_SEC))8 W k [, dispatch_get_g* z !lobal_queue(DISPATCH_QUEUE_PRH ! B 0 IORITY_DEFAULT, 0), ^{
NSArray *functions = getAllFunctions(curFuncationName);
NSString+ * ( D } o 7 = z *orderFileContent = [f+ Z g K 5  {unctie q | e t ; )ons.reverseObjectEnumerator.allObjects componentsJoinedBQ ] 3 o P = QyString:@"n"];
NSLog(@4 c m , v |"[orderFile]: %@",ordeC B N ? KrFileContent);
NSString *filePath = [NSTemporaryDirectory() string? U .ByAppending4 T   r q c _PathComponent:@"orderFile.order"];
[orderX Q m n A ` , ~FileContent writeToFile:filePath
atomically:YES
encoding:Nr A O D oSUTF8StringEncoding[ ! I I ^
error:nil];
if(completion){
completion(filePath);
}
});
}

要害代码解析

这儿具体介绍下每个函数的效果。

voidJ S 5 __sanitizer_cov_trace_pc_guard_init(uint32_t *start,
uint32_t *stop) {
static uint32_t N;  // CounteM f y f 3 9 Or for the guards.
if (start == stop || *start) retuY 3 4 w ) U vrn;  // Initialize only once.
printf("INIT:+ } ` , %p %pn", start, stop);
for (uint32_t *x = start; x < stop; x++)
*x = ++N;  // Guards should sta7 3 1rt from 1.
p` f F ;rin8 S H @ [ y . I ?tf("totasl count %in", N);
}

dyld 每链接一个敞开 SanitizerCoverage 装备的 dylib 都会履行一次 __sanitizer_cov_trace_pc_guard_initstartstop 之间的区间保存了该 dylib 的符号个数,咱们经过设置静态全局变量 N 可计算一切 dylib 的符号。

假如不需求以上内容能够仅履行空函数 void __sanitizer_cov_trace_pcW b X ?_guard_inX o Mit(uint32_t *stk u i 7 ~ Zart, uint32_t *stop){},不会影响后面的调用。

// 每个函数调用时都会先跳转履行该函数
voidR $ A K 2 c __sanitizer_cov_trace_pc_guard(uint32_t *guard) {
// +load办法先于guard_initZ b z o Z G调用,此刻guard为0
//    if(!*guard) { return }
if (stopCollecting) {
r4 & W ! 5 Heturn;
}
// __builtin_r$ Q e o K l oeturn_address 获取当时调用栈的下一条指令地Q 6 f - j 5址
void *PC = __builtin_return_address(0);
PointerNode *node = malloc(sizeof(PointerNode));
*node = (PointerNode){PC, NULL};
// 运用原子行列要存储帧地址
OSAt, F iomicEnqueue(&qHead, node, offsetof(PointerNode, next));
}
iOS性能提升探索 - 启动优化之二进制重排

咱们经过汇编可发现L p y .,每个函数调用前M R | v 6 W都被刺进了__sanitizer_cov_1 o R W u % /trace_pc_guard,所以咱们在该函数中,利_ [ e V 6 C__builtin_return_address 获取运转栈的情况,保存榜首条指令地址,即函数地址。

注意,因为存在多线程调用的问题,此刻需求用锁来保证符号存储,这儿咱们运用原子行列,履行效率高且行列存储数据,不需求再额定加锁处理和创建数组。

extern NSArray <NSString *> *getAllT H Z d ] ~Functions(NSSt: X * 3 b c m vring *currentFuncName) {
NSMutableSet<NSString *> *unqSet = [NSMutableSet setWithObject:currentFuncName];
NSMutableArray <NSString *> *functionsO @ m & = [NSMutableA) ; ~ v O z d O rrray array];
while (YES) {
PointerNode *front = OSAtomicDequeue(&qHead, of; R k ]fsetof(PointerNode, next));
if(front == NULL) {
break;
}
Dl_info infoP D # o Q L  T = {0};
// dladdr获取地址符号信息
dladdr(front->pointer, &info);
NSString *name = @(info! _ ? F b & O H v.dli_sname);
// 去除重复调用
if([unq0 a 9 ( M @ ;Set containsObject:name]) {
continue;
}
BO| . s t [ R MOL isObjc = [name hasPrey T 0 Rfix:@"+["] || [name hasPrefix:@"-[z y i 8 y 6 t"];
// order文件格式要求C函数和block前需求增加_
NSString *symbolName = isObjc ? name : [@"_" stringByAppendingString:name];
[unqSet addObject:name];
[funcy i I P xtions addObject:symb3 F . G k wolName];
}
return [[functions reverseObjectEnumerator] allObjects];;
}
/*
*x K   7 v t Structure filled in by dladdr(h  W + 2).
*/
typedef struct dl_info {
const char      *dli_fname;     /* Pathname of shared object */
void            *dli_fbase;     /*n s a 0 R x u  P Base address of sha? r ` ;  | Rred object */
const char      *dli_sname;     /* Name of nearest0 w _ 3 y Q ( , symbol *M  y 7 O R % A/
void            *dli_saddr;     /* Address ofO } X t nearest symbol */
} Dl_info;
extern int dladdr(const void *, Dl_info *);

将搜集的函数地址从原子行列中取出,经过 dladdr 获取地址的对应符号信息,最终将数组排序反转即可得到按次序排序的调用函l D g f @数数组。

结果比照

在项目发动后调用 appOrderFile 办法,将调用列表写到沙盒中,经过在 Devices 下? T ; p l ] C q载 xcappdata 文件Z % t ? D即可获取该列表。

iOS性能提升探索 - 启动优化之二进制重排
iOS性能提升探索 - 启动优化之二进制重排

里面的内容便是发动进程被调用的函数p 1 K ?次序。

_getThreadMethodStack
_after_objc_msgSend
_before_objc_msgSe1 r Jnd
-[YECallMonitor ignor0 C c _ H N Y heClassArr]
-[YECallMonitor setFilterClassNames:]
_get_protection
_perform_rep p i a W # t - -binding_ws - . J K A (ith_section
_rebind_symbolsU P  p u_for_imaG - s G *ge
__rebind_symbols_for_imagW y { + @ g k Je
_prepend_rebindingc a m G 6s
_rebind_symbols
___startMonitor_block_invoke
_startMonitor
-[YECallMonitor start]
_setMinConsumeTime
-[YECallMonitor setMinTime:]
___30+[YECallMonitor shareInstance]_bloc8 L = J I A x 9 9k_invoke
+[YECallMonito3 ! ^ ; P O 2 Rr shareInstance]
-[* - 9 }AppDelegate application:didFinish{ L n ] ! ELaunchingWithOptions:]
-[AppDelegate setWindow:]
-[AppDelegate window]
_main

最终在 Order File 装备下文件地址,重新编译打包。

从重排后的 Link Map Symbols 部分能够看到此刻n _ r的载入次序跟咱们的 order file 文件是共同的。

...
# Symbols:
# Address	Size    	File  Name
0x100007CCC	0x000000AC	[  4] _getThreadMethodStack
0x100007D78	0x00000234	[  4] _after_objc_msgSend
0x100007FAC	0x0000016C	[  4] _beforr j s A 6 Z Fe_objc_msgSend
0x100008118	0x000001AC	[  1] -[YECallMonitor ignoreClassArr]
0x1000082C4	0x000002%  X98	[  1] -[YECallMonitor setFilterC+ M T G U W glassNames:]
0x10000855C	0x000000A0	[  5] _get_protecti9 $ * mon
0x1000085FC	0x000003D0	[  5] _performh v _ = s { [ K_rebinding_with_section
0x1000089CC	0x00000320	[  5] _rebind_symbols_for_image
0x100008CEC	0x00000058	[  5] __rebind_symboO L D 0 Y + e T %ls_for_image
0x100008D44	0x00000104	[  5]- u b a @ 5 K _prepend_rebindings
0x100008E48	0x000000F8	[  5] _rebind_k x : 9 G ~ h y +symbols
0x100008F40	0x000000E0	[  4] ___startMonitor_block_invoke
0x100009020	0x00000074	[  4] _startMoni3 _ d n X 7 g / ,tor
0x100009 u 6094	0x00000044	[  1] -[YECallMonitor start]
0x1000090D8	0x00000044	[  4] _setMinConsu9 s  v p S h Q NmeTime
0x10000911C	0x00000054	[  1] -[YECallMonr P ^ g Bitor setMinTime:]
0x100009170	0x00000074	[  1] ___30+[| 1 0 [ a UYECallMonitor shareInstance]_block_invoke
0x1000091E4	0x0000009C	[  1] +[YECallMonitorO Q ; } i P = shareInstance]
0x100009 ~ 0 u z k R i280	0x00000208	[ 11] -[AppDelegate application:didFinishLaunchiz 3 p * ^ 5ngWithOptions:]
0x100009488	0x00000070	[ 11] -[AppDelegate setWindow:]
0x1000094F8	0x00000058	[? 5 0 q 7 = - q 9 11] -[AppDele) e ~gate window]
0x100009550	0x000000D4	[  9] _main
...

经过 system trace 东西比照下优化前后的发动速度,因为 Demo 工程内容少,无法看出显着差异,这儿用公司项目作为比照:

iOS性能提升探索 - 启动优化之二进制重排
iOS性能提升探索 - 启动优化之二进制重排

能够看到履2 G u S : J X Dpage fault 少了将近 1/3,速度提升了 1/4,阐明对发动优化上仍是有一定效果,尤其是在大项目中。

总结

因为在 iOS 上,一页有16KB(Ma9 N f 2 V x r 9 hc 为4KB),能够寄存大量代码,所以在发动阶段履行 page faulS 6 G a R V _t 的次数并不会许多,二进制重排比较于其: : & M W Z T b他优化手法,提升效果不显着,应优先从其他方面去进行发动优化(关于这部分的文章近期就会发布),最终再考虑是否做重排优化,但从技能学习的层面仍是值得研讨的 。

参考

  • Improving App Performance with Order Files
  • App 二进制文件重排现已被玩坏了
  • 简谈二进制重排
  • 根据LinkMap剖析iOK – DSAPP各模块体积
  • 手淘架构组最新f t X s k { Y b实践& v d v | iOS根据静态库插桩的⼆进制重排发动优化
  • 抖音研发实践:根据二进制文件重排的解决方案 APP发动速度提升超15%

About Me

今年计划完结10个优异第三方源码解读,会连续提交到 iOS-Framework-Analysis ,欢迎 star 项目陪伴笔者一起提高前进,若有什么不足之处,敬请奉告 。

发表评论

提供最优质的资源集合

立即查看 了解详情