二进制重排,不求甚解,例子是 MJRefresh

3,901 阅读3分钟

本文记录下二进制重排的常见操作手法

本文采集函数调用的方法是 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 内设置

link map 设置 YES.png

文件夹中,找着

link map 路径.png

二进制重排的关键是,把启动的函数调用栈,整理出来

面试中回答,手动整理,那 gg

因为调用会存在分支, 又存在重复调用

有一定的复杂性

本文采用 clang 插桩

clang flag.png

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;

    1. 去除重复

去除搜集到的重复符号

  • 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, 点击设备

dump app 里面的数据, 1 _ 3.png

  • 2, 选择应用

dump app 里面的数据, 2 _ 3.png

  • 3, 下载 app 沙盒信息

dump app 里面的数据, 3 _ 3.png

  • 4, 取出写入沙盒的文件

取 app 的数据两步, 1 _ 2.png

order.file 的设置

  • 一般,与工程 xcodeproj 平级,比较简单

order file location.png

  • 写入编译配置文件

order file set.png

检查结果

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

删除后,运行几个其他程序,刷新硬件,再跑程序测试

github repo

llvm 的文档