首先需要了解什么是二进制重排, 二进制重排为什么能减少启动时间.
编译器把源文件编译成Mach-O可执行文件时, 是按照 Build Phases -> Compile Sources 中的文件顺序进行编译各个类文. 在 App 启动时, DYLD并不会把所有二进制都加载到内存中等待调用, 当调用某个方法或者函数时, 内存中已经存在的不需要重新加载, 如果不存在就去加载, 这个加载过程会堵塞主线程, 是个耗时过程, 这个加载过程叫缺页加载(Page Fault), 每次缺页加载大概是 6 - 8 ms(抖音给出的时间), 这个时间并不是确定的, 和程序的实际情况有关系. App 的启动时间是从点击 App 图标开始到第一个界面展示出来的时间, 可以划分为 main 函数之前和 main 函数之后, Page Fault 发生在 main 函数之后, 二进制重排可以让编译器 不 按照 Build Phases -> Compile Sources 中的文件顺序进行编译各个类文, 而是按照我们指定的顺序, 把 main 函数到第一个界面展示这之间用到的类文件等放在最前边, 尽量减少 Page Fault 次数, 心达到减少启动时间的目的.
Page Fault 自己百度,有时间我会补上这个点.
二进制重排是靠什么实现的呢 ?
需要在根目录放一个以 ".order" 为后缀的文件并在添加到配置中 , Build Setting 中搜索 order file, 添加这个文件的路径,如: ./xxx.order (根目录下), 这个文件里放的是我们指定的需要优化加载的符号, 此处只放 main 函数之后到第一外界面展示用到的符号. 有了这个文件, 编译器就会把文件里的符号放在最前边, 这样先用到的符号就会优化加载到内存中, 当使用的时候就会减少 Page Fault 次数.
lb.order
那么问题来了, 这个 "xxx.order" 文件从哪里来,
这就需要 Hook 到 App 在启动时到底调用了哪些方法或者函数等, 然后将这些符号写到文件中, 具体用哪种方案去 Hook 呢, 此处应该用 Clang 插桩技术. 理由如下:
runtime 的 Method Swizzling 只能 Hook 到 OC 的方法.
fishhook 只能 Hook 系统的 C 函数.
Clang 静态插桩可以 Hook 到所有 OC 方法, C 函数, Block, Swift 函数, 闭包.
写代码前需要添加两项配置,
- Build Setting -> Other C Flags 和 Build Setting -> Other C++ Flags 添加 fsanitize-coverage=func,trace-pc-guard
other c/c++ flags
- Build Setting -> Other Swift Flags 添加 -sanitize-coverage=func -sanitize=undefined
other swift flags
下面开始上代码,
#import "ViewController.h"
// swift 桥接文件
#import "ChaZhuangOCDemo-Swift.h"
// Clang 插桩需要的头文件
#import <dlfcn.h>
#import <libkern/OSAtomic.h>
@interface ViewController ()
@end
@implementation ViewController
- (void)setupUI {
self.view.backgroundColor = UIColor.whiteColor;
self.title = @"Clang 插桩测试 Demo";
}
- (void)viewDidLoad {
[super viewDidLoad];
[self setupUI];
[self testOne];
TwoController *twoVC = [[TwoController alloc] init];
twoVC.view.frame = [UIScreen mainScreen].bounds;
[self.view addSubview:twoVC.view];
}
+ (void)load {
}
//原子队列
static OSQueueHead symboList = OS_ATOMIC_QUEUE_INIT;
//定义符号结构体
typedef struct {
void * pc;
void * next;
} SymbolNode;
void __sanitizer_cov_trace_pc_guard_init(uint32_t *start,
uint32_t *stop) {
static uint64_t N; // Counter for the guards.
if (start == stop || *start) {
return; // Initialize only once.
}
printf("INIT: %p %p\n", start, stop);
for (uint32_t *x = start; x < stop; x++) {
*x = ++N; // Guards should start from 1.
}
}
// Hook 到 符号, 保存到原子队列中
void __sanitizer_cov_trace_pc_guard(uint32_t *guard) {
// if (!*guard) return; // Duplicate the guard check.
void *PC = __builtin_return_address(0);
SymbolNode * node = malloc(sizeof(SymbolNode));
* node = (SymbolNode) {
PC,
NULL
};
//入队
// offsetof 用在这里是为了入队添加下一个节点找到 前一个节点next指针的位置
OSAtomicEnqueue(&symboList, node, offsetof(SymbolNode, next));
}
// 从队列里取出 Hook 的符号,并写到 .order 文件中, 我用 lb.order 作为文件名.
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
NSMutableArray<NSString *> *symbolNames = [NSMutableArray array];
while (true) {
//offsetof 就是针对某个结构体找到某个属性相对这个结构体的偏移量
SymbolNode *node = OSAtomicDequeue(&symboList, offsetof(SymbolNode, next));
if (node == NULL) { break; }
Dl_info info;
dladdr(node->pc, &info);
NSString * name = @(info.dli_sname);
// block c函数 swift函数 添加下划线 _
BOOL isObjc = [name hasPrefix: @"+["] || [name hasPrefix: @"-["];
NSString * symbolName = isObjc ? name : [@"_" stringByAppendingFormat: @"%@", name];
if (![symbolNames containsObject: symbolName]) {
[symbolNames addObject: symbolName];
}
// 数组倒序
NSArray * symbolArray = [[symbolNames reverseObjectEnumerator] allObjects];
NSLog(@"symbolArray = %@", symbolArray);
// 写入文件
NSString * funcString = [symbolArray componentsJoinedByString: @"\n"];
NSLog(@"写入文件的字符串: %@", funcString);
NSString * filePath = [NSTemporaryDirectory() stringByAppendingPathComponent:@"lb.order"];
NSData * fileContents = [funcString dataUsingEncoding: NSUTF8StringEncoding];
BOOL result = [[NSFileManager defaultManager] createFileAtPath: filePath contents: fileContents attributes: nil];
if (result) {
NSLog(@"%@",filePath);
}else{
NSLog(@"文件写入出错");
}
}
}
- (void)testOne {
NSLog(@"I am testOne");
testCFunction();
}
void testCFunction() {
lldbBlock();
}
void (^lldbBlock)(void) = ^(void){
NSLog(@"I am block");
};
@end
TwoController 是个 swift 类, 代码如下:
import UIKit
class TwoController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .white
testSwiftFunc()
}
func testSwiftFunc() {
print("已经调用了 swift")
}
}
用 真机 把程序跑起来, 第一个界面出现之后, 点击屏幕, lb.order 文件就已经写在 tmp 文件夹下了,
把手机里的项目文件下载到电脑
找到下载的文件, 显示包内容, 搜索 lb.order, 将其放入根目录中, 下次编译时这些符号就会排列在前面, 这个文件打开查看是这样的, 这里边所有的符号都是 main 函数之后到 第一个界面展示 用到的所有符号.
lb.order
如何查看是否已经重排成功, 在 Build Setting 里 搜索 link map, 找到 Write Link Map File 改成 YES, 记得完成重排之后, 要改成 NO .
link map 配置
设置好之后再次编译就可以, 不需要 run, 然后找出 link map 文件, 找到 products 下的 app, 然后 show in finder, 往回找 两级就是 products 文件夹, 然后找到与 products 同级的 Intermediates.noindex , 按下图找到 link map 文件, txt 格式的,
项目名称.app
link map txt 文件
link map 文件打开之后找到 symbols 下面这部分, 对比 lb.order 和 此处的顺序是否一致, 一致就说明重振成功. 然后删除查找符号相关代码, 保留 lb.order 和 order file 配置.
link map symbols
注意: 如果 lb.order 中的符号在项目中找不到, 编译器会忽略掉, 并不会报错.
网友评论