iOS学习iOS底层性能调优、测试

iOS-底层原理32-启动优化二进制重排

2020-11-27  本文已影响0人  一亩三分甜

《iOS底层原理文章汇总》

上一篇文章《iOS-底层原理28-启动优化》介绍了二进制重排能减少缺页中断PageFault的页数,优化应用程序的启动时间,那启动时刻调用了哪些方法呢?此篇文章将分析启动时刻调用的方法。进而将启动时刻调用的方法尽量都放在集中的页中,从而减少启动时间。

clang插桩:参考官方文档Tracing PCs

fsanitize-coverage=trace-pc-guard@2x.png sanitizer_cov_trace_pc_guard@2x.png

报错的方法找不到的情况,删掉方法__sanitizer_symbolize_pc,注释掉不用的代码,能正常运行,此时运行,打印出结果,start和end的值分别为0x104ca1100和0x104ca1118

__sanitizer_symbolize_pc@2x.png 增加两个方法@2x.png 打印结果@2x.png

开始地址start为0x104919100,读取该开始地址下的内存值,一直读取16个字节,一排有16个字节,4个字节4个字节的读取,一直读到结尾,stop指向的地址为0x104919118,指向数据最后的端,要获取数据结尾处的数据应该往前再读4个字节,因为uint32_t是无符号整形占用4个字节,06表示的十进制数为6

(lldb) x 0x104919100
0x104919100: 01 00 00 00 02 00 00 00 03 00 00 00 04 00 00 00  ................
0x104919110: 05 00 00 00 06 00 00 00 78 2f a7 04 01 00 00 00  ........x/......
(lldb) x 0x104919118-0x4
0x104919114: 06 00 00 00 78 2f a7 04 01 00 00 00 00 00 00 00  ....x/..........
0x104919124: 00 00 00 00 43 75 91 04 01 00 00 00 00 00 00 00  ....Cu..........
stop往前读4个字节@2x.png

在ViewController增加一个方法touchBegin,此时发现结尾处的数据为07,这说明了啥?结尾处的数据存的就是方法的个数吗?继续在ViewController.m文件里面增加函数,block。无论是函数,方法,block都能获取到。


增加方法touchsBegan@2x.png 增加函数和block@2x.png

增加一个属性呢?增加了三个,setter,getter方法,还有一个cxx的析构函数。

增加属性@2x.png

1.获取符号地址:监听方法的调用

点击屏幕,调用了touchBegan方法,点一下监听一次

39.gif

若touchBegan中调用了其他方法,则继续监听到了其他方法,__sanitizer_cov_trace_pc_guard全免捕捉到了函数,方法,block的调用

tochBegan-test-block@2x.png 40.gif

那么__sanitizer_cov_trace_pc_guard为什么能捕捉到所有函数的调用呢,原理是什么?
进入汇编代码查看,sp操作栈空间,bl是跳转函数,断点进入test方法,查看汇编,发现只要是在Build Settings设置了让clang开辟这个功能-fsanitize-coverage=trace-pc-guard,clang在读取所有代码的时候生成ir(中间代码)之前,都将__sanitizer_cov_trace_pc_guard这个函数插入到每个函数、方法、block的开头或边缘,从而能进行监听,从而每一个方法,函数,block的调用都会来到此函数void __sanitizer_cov_trace_pc_guard(uint32_t *guard)

OtherCFlags@2x.png __sanitizer_cov_trace_pc_guard@2x.png 41.gif

避开load方法if (!*guard) return;,验证下,注释掉此行代码,在ViewController类中重写load方法,发现会多走一次方法void __sanitizer_cov_trace_pc_guard(uint32_t *guard),从而多打印一次guard: 0x100e99388 0 PC

避开load方法@2x.png 42.gif

我们可以利用上面的void __sanitizer_cov_trace_pc_guard(uint32_t *guard)来进行clang插桩,目标是拿到所有调用的方法名

获取方法名:引入void *PC = __builtin_return_address(0);,打印PC指针变量的地址,PC指向的地址0x0000000104b646f8和main函数的地址0x0000000104b646f8一致

INIT: 0x104b69388 0x104b693bc
(lldb) p PC
(void *) $0 = 0x0000000104b646f8
(lldb) bt
* thread #1, queue = 'com.apple.main-thread', stop reason = step over
  * frame #0: 0x0000000104b64264 TraceDemo`__sanitizer_cov_trace_pc_guard(guard=0x0000000104b693b8) at ViewController.m:38:34
    frame #1: 0x0000000104b646f8 TraceDemo`main(argc=0, argv=0x0000000000000000) at main.m:12
    frame #2: 0x000000018212256c libdyld.dylib`start + 4
(lldb) 
PC@2x.png PC和touchBegan内存地址@2x.png touchBegan.png touchBegan调用__sanitizer_cov_trace_pc_guard.png __sanitizer_cov_trace_pc_guard.png

void __sanitizer_cov_trace_pc_guard(uint32_t *guard)调用完毕,返回ret,还要返回到touchBegan方法中

返回touchBegan.png

函数调用栈:调用栈里面的内存地址并不是函数的开始位置,查看汇编代码可以得知函数栈中的touchBegan的地址为0x0000000102fa82f0,汇编代码中touchBegan的地址为0x102fa82c4,两者并不相等,所以函数栈中的地址并不是touchBegan的开始地址,而是上一个函数void __sanitizer_cov_trace_pc_guard(uint32_t *guard)调用后的返回地址

函数栈中地址并不是touchBegan的开始位置.png

这是有原因的,因为lldb的bt也是用的这个函数__builtin_return_address(0),验证如下:删除Other C Flags里面的哨兵函数标记-fsanitize-coverage=trace-pc-guard

删除guard监听.png
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    test();
    block();
}

void test(){
}
void (^block)(void)=^(void){
};
test.png touchBegan函数调用栈.png touchBegan调用test返回.png touchBegan调用block.png

blr

block中的地址.png

2.获取符号

综上,通过void *PC = __builtin_return_address(0);,PC为当前函数返回到上一个调用的地址,0代表我当前函数回到哪里去,1代表我上一个函数回到哪里去

address中参数为1返回上上个函数的地址@2x.png

引入#import <dlfcn.h>,通过dladdr(PC, &info);函数获取到函数所在的MachO文件名和地址,以及函数符号名和地址

touchBegan调用test()和block()@2x.png
void __sanitizer_cov_trace_pc_guard(uint32_t *guard) {
  if (!*guard) return;
  void *PC = __builtin_return_address(0);
    Dl_info info;
    dladdr(PC, &info);
    printf("fname:%s \nfbase:%p\nsname:%s \nsaddr:%p\n",info.dli_fname,info.dli_fbase,info.dli_sname,info.dli_saddr);
  char PcDescr[1024];
  printf("\nguard: %p %x PC %s\n", guard, *guard, PcDescr);
}

fname:/var/containers/Bundle/Application/D1D94998-5B0B-49FB-B5AF-87783F492AC7/TraceDemo.app/TraceDemo 
fbase:0x1003c4000
sname:-[ViewController touchesBegan:withEvent:] 
saddr:0x1003cc268

guard: 0x1003d13a0 5 PC X\260m\261�
fname:/var/containers/Bundle/Application/D1D94998-5B0B-49FB-B5AF-87783F492AC7/TraceDemo.app/TraceDemo 
fbase:0x1003c4000
sname:test 
saddr:0x1003cc00c

guard: 0x1003d1394 2 PC \224\302<
fname:/var/containers/Bundle/Application/D1D94998-5B0B-49FB-B5AF-87783F492AC7/TraceDemo.app/TraceDemo 
fbase:0x1003c4000
sname:block_block_invoke 
saddr:0x1003cc028

guard: 0x1003d1398 3 PC �m=
dis-s-地址@2x.png image-list@2x.png 动态库内存地址首地址@2x.png

3.符号拿到之后,生成相应的order文件

注意:在Build Settings ->Other C Flags,添加-fsanitize-coverage=trace-pc-guard后,在任何地方写这两个方法void __sanitizer_cov_trace_pc_guard_init(uint32_t *start,uint32_t *stop)void __sanitizer_cov_trace_pc_guard(uint32_t *guard)都能监听到,在main.m文件中实现都行,且只需要实现一次,在哪里结束写在哪里

取函数符号死循环.png 43.gif

因为while循环也被方法void __sanitizer_cov_trace_pc_guard(uint32_t *guard)hook了,循环一次hook一次,点击屏幕,在循环中进入汇编查看,多的一次__sanitizer_cov_trace_pc_guard属于循环,bl是条件跳转,b是无条件跳转,一次循环也会被hook一次,只要是跳转(bl和b的汇编指令),就会被hook

touchBeganhook三次@2x.png

解决办法:在Other C Flags中添加参数func,-fsanitize-coverage=func,trace-pc-guard,再次点击屏幕,不会产生循环,打印的方法如下

sname:-[ViewController touchesBegan:withEvent:] 
sname:-[AppDelegate window] 
sname:-[ViewController viewDidLoad] 
sname:-[AppDelegate window] 
sname:-[AppDelegate window] 
sname:-[AppDelegate application:didFinishLaunchingWithOptions:] 
sname:-[AppDelegate setWindow:] 
sname:-[AppDelegate window] 
sname:main 
sname:-[ViewController touchesBegan:withEvent:] 
sname:-[AppDelegate window] 
sname:-[ViewController viewDidLoad] 
sname:-[AppDelegate window] 
sname:-[AppDelegate window] 
sname:-[AppDelegate application:didFinishLaunchingWithOptions:] 
sname:-[AppDelegate setWindow:] 
sname:-[AppDelegate window] 
sname:main 
sname:+[ViewController load] 
     BOOL isObjc = [name hasPrefix:@"+["] || [name hasPrefix:@"-["];
     NSString * symbolName = isObjc ? name :[@"_" stringByAppendingString:name];
     [symbolNames addObject:symbolName];
     
     NSEnumerator * enumerator = [symbolNames reverseObjectEnumerator];
    //创建一个新的数组
    NSMutableArray * funcs = [NSMutableArray arrayWithCapacity:symbolNames.count];
    NSString *name;
    //去重!
    while (name = [enumerator nextObject]) {
        if (![funcs containsObject:name]) {//数组中不包含name
            [funcs addObject:name];
        }
    }
    //去除touchBegan
    [funcs removeObject:[NSString stringWithFormat:@"%s",__FUNCTION__]];
    //数组转成字符串
    NSString * funcStr = [funcs componentsJoinedByString:@"\n"];
    //字符串写入文件
    //文件路径
    NSString * filePath = [NSTemporaryDirectory() stringByAppendingPathComponent:@"cloud.order"];
    //文件内容
    NSData * fileContents = [funcStr dataUsingEncoding:NSUTF8StringEncoding];
    [[NSFileManager defaultManager] createFileAtPath:filePath contents:fileContents attributes:nil];
cloud.order放入工程根目录@2x.png Build Settings order file配置@2x.png

将Build Settings中Write Link Map File选项改为Yes,运行程序,在Product同级目录Intermediates.noindex/Demo.build/Debug-iphoneos/Demo.build/Demo-LinkMap-normal-arm64.txt中查看文件中每一个方法的排列顺序

函数按cloud.order文件执行顺序@2x.png 44.gif swift配置Other Swift Flags@2x.png 45.gif order文件中的swift函数@2x.png
上一篇 下一篇

猜你喜欢

热点阅读