美文网首页iOS-性能优化
二十六、 启动优化,二进制重排

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

作者: Mjs | 来源:发表于2020-11-23 15:26 被阅读0次

冷启动&热启动

  • 冷启动:内存中不包含相关数据,必须从磁盘载入到内存中,系统决定。内存覆盖时我们能决定,比如重启手机。
  • 热启动: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方法
  • 结束,打包上线

相关文章

网友评论

    本文标题:二十六、 启动优化,二进制重排

    本文链接:https://www.haomeiwen.com/subject/zstdiktx.html