二十六、 启动优化,二进制重排

冷启动&热启动

  • 冷启动:内存中不包含相关数据,必须从磁盘载入到内存中,系统决定。内存覆盖时我们能决定,比如重启手机。
  • 热启动:kill进程后,数据仍然存在,载入进程的时候,很多数据不用载入内存,而是通过表关联起来,不同系统有不同优化方案。

启动优化

测试APP启动一般通过main函数分割成两个阶段。main函数之前通过系统反馈给我们,main函数之后我们可以自己统计。

main函数之前

打印启动时间

  • 添加DYLD_PRINT_STATISTICS

    添加DYLD_PRINT_STATISTICS.png

    程序跑起来后,控制台就会自动打印耗时操作。
    main函数之前启动时间.png

  • dylib loading time:动态库载入耗时(优化:建议不要大于6个,合并动态库)

  • rebase/binding time:偏移修正/符号绑定(优化:减少OC类) 优化少

    • rebase:app的二进制文件中的方法、函数调用地址是偏移地址。比如funA 地址为0x000a,在运行到内存中时。通过安全机制随机的值(ASLR) 插入到二进制的开头 。比如分配到0xf000,funA的真实地址就是0xf00a
    • binding:Mach0文件中,创建一个符号,比如NSLog ,地址指向随机地址,在运行的时候通过dyld就将NSLog 和NSLog函数地址进行绑定。
    • ObjC setup time:OC类注册的耗时。(优化:减少OC类) 优化少
    • initializer time:执行load和构造的耗时。(优化:使用懒加载)
main函数之后
  1. 减少初始化流程,能懒加载就懒加载
  2. 减少遗留不用的代码
  3. 启动时候的加载放在多线程去做
  4. 启动的时候的页面最好用纯代码去做

iOS内存分为物理内存和虚拟内存
虚拟内存 : 解决安全问题、解决内存使用率问题

image
image
  • 解决安全问题:映射表(页表)(虚拟页表)
  • 解决内存使用率问题:内存分页管理。缺页中断(PageFault),然后加载到物理内存,加载之前会签名加载的页;如果启动的时候要加载的代码分别在不同的页,那么缺页中断时间就比较长,这时就出现了二进制重排(把启动要加载的代码放在前面几页)。使用内存分页后,就会导致代码的加载都是从0开始的,为了防止黑客,就出现了ASLR。

内存分页技术
分配4G大小假地址
MacOS 、linux (4K为一页)
iOS(16K为一页)

生成LinkMap文件

  • Build Settings 中搜索 link map,No改为Yes,然后Command+B,build一下,就会生成LinkMap文件

    image

打开LinkMap文件

  • Address: 函数真实实现的地址(汇编代码的地址)(代码的地址)
  • Size:函数的大小,写的代码的多与少
  • File:所在文件
  • Name: 方法名
image

file文件顺序就是在Compile Sources决定的

image
image
image
  • 0x0000000100d30000(ASLR)+00004848(偏移) = 0x100d34848

    image

程序启动调用的放法分别分布在不同的分页中,这就导致启动的时候有大量的缺页中断(PageFault)。

优化思路
将所有启动时刻需要调用的方法排列在一块。这就是二进制重排

查看PageFault(缺页中断)次数

1、command+I

image

2、选择System Trace

image

3、点击一下,第一个页面出现后,再点击一下

image
image

4、搜索Main Thread

image

5、选择Main Thread、选择Virtual Memory。File Backed Page in 就是PageFault

image

二进制重排优化是在链接阶段对即将生成的可执行文件进行重新排列

order_file

1、打开objc4-750源码

image

libobjc.order存放的是方法的调用顺序

libobjc.order.png

苹果公司对源码也进行了二进制重排,从这里就可以看到方法的调用流程。
现在我们对自己的程序进行重排。
我们通过终端进入当前项目目录下

touch hank.order

新建个hank.order文件,输入

_main
-[ViewController viewDidLoad]
-[hank hello]
_test1

Build Settings中搜索order file,将我们的文件添加进去。

orderFile.png

编译一下查看linkMap

重排后的linkMap.png

我们可以看到,现在的顺序是我们在order文件里添加的顺序,没有的方法会被忽略掉。

clang插庄

  • Build Settings 搜索 other c flags,添加-fsanitize-coverage=trace-pc-guard参数

    image
  • 粘贴实例代码如下代码到项目,根据编译报错去除多余的代码

extern "C" 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);
[图片上传中...(Snip20200420_13.png-54b663-1587378588681-0)]
  for (uint32_t *x = start; x < stop; x++)
    *x = ++N;  // Guards should start from 1.
}

extern "C" void __sanitizer_cov_trace_pc_guard(uint32_t *guard) {
  if (!*guard) return;  // Duplicate the guard check.
  // If you set *guard to 0 this code will not be called again for this edge.
  // Now you can get the PC and do whatever you want:
  //   store it somewhere or symbolize it and print right away.
  // The values of `*guard` are as you set them in
  // __sanitizer_cov_trace_pc_guard_init and so you can make them consecutive
  // and use them to dereference an array or a bit vector.
  void *PC = __builtin_return_address(0);
  char PcDescr[1024];
  // This function is a part of the sanitizer run-time.
  // To use it, link with AddressSanitizer or other sanitizer.
  __sanitizer_symbolize_pc(PC, "%p %F %L", PcDescr, sizeof(PcDescr));
  printf("guard: %p %x PC %s\n", guard, *guard, PcDescr);
}

image
  • 调试
    暂停,然后附加,然后x读start内存
读取头尾.png

读头 x 0x1051a34b0
读尾 x 0x1051a34f4-0x4,读尾需要减四个字节,因为一个方法占四个字节,而这个0x1051a34f4地址是尾方法结束的地址,所以如果需要读尾方法的地址,就需要减掉四个字节
我们看到现在最后一位是11,换算成十进制就是17,现在我们添加touchesBegan方法,这里就变成了12.所以只要我们添加OC方法、C/C++函数、block都会添加进去。
之前我们添加了touchesBegan,现在点击下屏幕,控制台就会打印guard: 0x1053ed4e8 9 PC .D\263�,只要我们调用了方法就是调用__sanitizer_cov_trace_pc_guard.

点击屏幕时候的汇编.png

现在我们目标是找到调用的所有方法名字,我们通过void *PC = __builtin_return_address(0);打印下PC可以看到这个地址是方法返回的地址

__sanitizer_cov_trace_pc_guard返回地址.png

这个时候我们需要导入#import <dlfcn.h>

void __sanitizer_cov_trace_pc_guard(uint32_t *guard) {
  if (!*guard) return;  // Duplicate the guard check.
    //当前函数返回到上一个调用的地址,参数0是当前返回的地址,1是上一层方法返回的地址
  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("guard: %p %x PC %s\n", guard, *guard, PcDescr);
}
···········
fname:/Users/jiangshicheng/Library/Developer/CoreSimulator/Devices/4AFEBD14-731B-416C-9454-CE8116BFFA17/data/Containers/Bundle/Application/D57CCD44-B02F-4A7D-938B-1B935AC2738F/Demo.app/Demo 
fbase:0x105b6b000 
sname:-[ViewController touchesBegan:withEvent:] 
saddr:0x105b6cec0 
guard: 0x105b714f0 9 PC .\324;  �

现在我们sname来进行操作。现在我们需要把保存到队列中,我们导入#import <libkern/OSAtomic.h>

//定义原子队列
static OSQueueHead symbolList = OS_ATOMIC_QUEUE_INIT;
//定义符号结构体
typedef struct{
    void *pc;
    void *next;
}SYNode;

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.
}
void __sanitizer_cov_trace_pc_guard(uint32_t *guard) {
  if (!*guard) return;  // Duplicate the guard check.
    //当前函数返回到上一个调用的地址,参数0是当前返回的地址,1是上一层方法返回的地址
  void *PC = __builtin_return_address(0);
    //创建结构体
    SYNode *node = malloc(sizeof(SYNode));
    *node = (SYNode){PC,NULL};
    
    //加入结构
    OSAtomicEnqueue(&symbolList, node, offsetof(SYNode, next));
 
  char PcDescr[1024];
//  printf("guard: %p %x PC %s\n", guard, *guard, PcDescr);
}

-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    while (YES) {
        SYNode *node = OSAtomicDequeue(&symbolList, offsetof(SYNode, next));
        if (node == NULL) {
            break;
        }
        Dl_info info = {0};
        dladdr(node->pc, &info);
        printf("%s",info.dli_sname);
    }
}

这个时候就会发生死循环,这是因为循环也会调用__sanitizer_cov_trace_pc_guard,我们就需要在Other C Flags添加个func条件,变成-fsanitize-coverage=func,trace-pc-guard,现在需要添加+load方法,把if (!*guard) return;注释掉就可以了.
接下来就是取反,去重,不是OC方法的添加下划线。
最终代码

#import "ViewController.h"
#include <stdint.h>
#include <stdio.h>
#include <sanitizer/coverage_interface.h>
#import <dlfcn.h>
#import <libkern/OSAtomic.h>
@interface ViewController ()

@end

@implementation ViewController




- (void)viewDidLoad {
    
    [super viewDidLoad];
}

//定义原子队列
static OSQueueHead symbolList = OS_ATOMIC_QUEUE_INIT;
//定义符号结构体
typedef struct{
    void *pc;
    void *next;
}SYNode;

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.
}
void __sanitizer_cov_trace_pc_guard(uint32_t *guard) {
//  if (!*guard) return;  // Duplicate the guard check.
    //当前函数返回到上一个调用的地址,参数0是当前返回的地址,1是上一层方法返回的地址
  void *PC = __builtin_return_address(0);
    //创建结构体
    SYNode *node = malloc(sizeof(SYNode));
    *node = (SYNode){PC,NULL};
    
    //加入结构
    OSAtomicEnqueue(&symbolList, node, offsetof(SYNode, next));
 
  char PcDescr[1024];
//  printf("guard: %p %x PC %s\n", guard, *guard, PcDescr);
}

-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    NSMutableArray<NSString *> *symbolNames = [NSMutableArray array];
    while (YES) {
        SYNode *node = OSAtomicDequeue(&symbolList, offsetof(SYNode, next));
        if (node == NULL) {
            break;
        }
        Dl_info info = {0};
        dladdr(node->pc, &info);
        NSString * name = @(info.dli_sname);
        BOOL  isObjc = [name hasPrefix:@"+["] || [name hasPrefix:@"-["];
        NSString * symbolName = isObjc ? name: [@"_" stringByAppendingString:name];
        [symbolNames addObject:symbolName];
    }
    //取反
    NSEnumerator * emt = [symbolNames reverseObjectEnumerator];
    //去重
    NSMutableArray<NSString *> *funcs = [NSMutableArray arrayWithCapacity:symbolNames.count];
    NSString * name;
    while (name = [emt nextObject]) {
        if (![funcs containsObject:name]) {
            [funcs addObject:name];
        }
    }
    //干掉自己!
    [funcs removeObject:[NSString stringWithFormat:@"%s",__FUNCTION__]];
    //将数组变成字符串
    NSString * funcStr = [funcs  componentsJoinedByString:@"\n"];
    
    NSString * filePath = [NSTemporaryDirectory() stringByAppendingPathComponent:@"dyz.order"];
    NSData * fileContents = [funcStr dataUsingEncoding:NSUTF8StringEncoding];
    [[NSFileManager defaultManager] createFileAtPath:filePath contents:fileContents attributes:nil];
}
  • 下载下来,显示包内容

    image
    image
    image
  • 项目中如果是OC和Swift混编
    1、在Build Settings搜索other swift flags,如果是OC项目,里面没有Swift文件,那么搜索不到Other Swift Flags,只有项目里面有了Swift文件才会搜索到。

    image
    image

    2、添加参数-sanitize-coverage=func-sanitize=undefined

    image

    3、导入Swift头文件#import "TraceDemo-Swift.h",调用Swift方法[SwiftTest swiftTestLoad];

    image

    4、Swift方法同样可以hook到

最后

  • 把得到的dyz.order文件拷贝到项目的根目录下。
  • Build Settings中搜索order file,添加dyz.order文件的地址(./dyz.order或者${SRCROOT}/dyz.order)
  • Build Settings 中搜索 link map,如果是Yes则改回No
  • 去掉 Other C Flags的参数 -fsanitize-coverage=func,trace-pc-guard
  • 去掉 Other Swift Flags的参数 -sanitize-coverage=func-sanitize=undefined
  • 注销__sanitizer_cov_trace_pc_guard_init和__sanitizer_cov_trace_pc_guard方法
  • 结束,打包上线
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,362评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,330评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,247评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,560评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,580评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,569评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,929评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,587评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,840评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,596评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,678评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,366评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,945评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,929评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,165评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,271评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,403评论 2 342

推荐阅读更多精彩内容