本文记录下二进制重排的常见操作手法
本文采集函数调用的方法是 clang 插桩
clang 插桩可以搞定:
Obj – C 的匿名函数 Block
Swift 代码方法
自定义的 C 函数 / 系统的 C 函数
我们的 / 系统的 Obj – C 方法
因为 clang 编译我们的代码,生成 IR 的过程中,
会有 AST 抽象语法树,方便处理调用相关
本文例子是 MJRefresh 的 demo, 针对的是 Obj – C 项目
背景简述
( 网上资料很多 )
操作系统存在 page fault, 一次 page fault ,约 4 ms, 用户无感知
app 启动的时候,发生了大量的 page fault, 用户易感知
感觉启动方法的默认编译顺序
使用 link map
- 看下面的符号
这个是默认的链接顺序,可看出于调用顺序无关
Address Size File Name
0x100005B5C 0x0000008C [ 1] -[MJRefreshBackFooter willMoveToSuperview:]
0x100005BE8 0x00000340 [ 1] -[MJRefreshBackFooter scrollViewContentOffsetDidChange:]
…
- 获取 link map
Xcode 内设置
文件夹中,找着
二进制重排的关键是,把启动的函数调用栈,整理出来
面试中回答,手动整理,那 gg
因为调用会存在分支, 又存在重复调用
有一定的复杂性
本文采用 clang 插桩
other C Flags 中,填入
-fsanitize-coverage=func,trace-pc-guard
设置后,
导入头文件 #include <sanitizer/coverage_interface.h>
会触发两个 C 函数
void __sanitizer_cov_trace_pc_guard_init(uint32_t *start, uint32_t *stop)
与
void __sanitizer_cov_trace_pc_guard(uint32_t *guard)
clang 插桩,把第二个方法,插入在我们关心的方法调用中
就拿到了上一个调用的地址
设置插桩,一般仅用于性能优化
平时开发与发生产,一般关闭插桩效果
下一步,拿到符号
导入 #import <dlfcn.h>
通过 dladdr
方法
注意:
- 1, 规避循环
-fsanitize-coverage=func
clang 插桩,默认把每一次的循环,也搜集到了
=func
, 有对插桩,忽略循环跳转的作用
- 2, 线程安全
可使用原子队列
导入 #import <libkern/OSAtomic.h>
static OSQueueHead symbolList = OS_ATOMIC_QUEUE_INIT;
-
- 去除重复
去除搜集到的重复符号
- 4, 符号处理
Obj-C 的类方法,带前缀 +[
Obj-C 的实例方法,带前缀 -[
C 方法,手动加前缀, _
- 5, 性能优化
插桩方法中,做的事情,越少越好
插桩方法中,只收集地址
完成符号收集后,统一解析处理
- 6, 细节
什么时候,算完成符号收集
二进制重排,做的是启动优化
首屏渲染出来后,
点击下屏幕,可以算
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
选一下列表,也可算
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
具体的方法实现,见下面的 github repo
order.file
长这样
+[UIViewController(Example) load]
_main
+[MJNavigationController initialize]
复制代码
// ...
复制代码
取出写入真机沙盒中的 order.file
本文通过 Xcode 取
- 1, 点击设备
- 2, 选择应用
- 3, 下载 app 沙盒信息
- 4, 取出写入沙盒的文件
order.file
的设置
- 一般,与工程 xcodeproj 平级,比较简单
- 写入编译配置文件
检查结果
main 函数以前的,时间统计
可使用环境变量
DYLD_PRINT_STATISTICS
= 1
main 函数后,至于首屏控制器 - (void)viewWillAppear:(BOOL)animated
的时间统计
main 文件中
CFAbsoluteTime StartTime;
int main(int argc, char * argv[]) {
StartTime = CFAbsoluteTimeGetCurrent();
@autoreleasepool {
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
}
}
复制代码
首屏控制器中
extern CFAbsoluteTime StartTime;
- (void)viewWillAppear:(BOOL)animated{
[super viewWillAppear: animated];
CFAbsoluteTime doneTime = CFAbsoluteTimeGetCurrent();
NSLog(@"AppLanuch-- main after 使用了 -- \n%f 秒 \n",(doneTime - StartTime));
}
复制代码
初步的结论: 可能存在微弱的提升
重排前
Total pre-main time: 229.55 milliseconds (100.0%)
2021-08-25 15:49:26.472878+0800 MJRefreshExample[8439:116264] AppLanuch--main after 使用了 --
0.551359 秒
复制代码
重排后
Total pre-main time: 227.41 milliseconds (100.0%)
2021-08-25 15:47:34.945831+0800 MJRefreshExample[8382:114265] AppLanuch--main after 使用了 --
0.406833 秒
复制代码
小项目,体现不明显
每次跑都不一样
步骤:Xcode Product clean 清空
真机删除 app
删除后,运行几个其他程序,刷新硬件,再跑程序测试