前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >进制重排

进制重排

原创
作者头像
花落花相惜
修改2021-11-23 13:15:48
6380
修改2021-11-23 13:15:48
举报

1. 虚拟内存

实际上我们平时所看到的进程中可以直接访问的连续内存空间`0x000000 ~

0xffffff`,只是一个虚拟地址,需要通过一张映射表映射后才可以获取到真实的物理地址。并不是所有的虚拟内存都会分配物理内存,只有那些实际使用的

虚拟内存 才分配 物理内存 ,并且分配后的物理内存,是通过内存映射来管理的。

虚拟内存工作原理

2. 虚拟内存分页

刚刚提到虚拟内存和物理内存通过映射表进行映射,但是这个映射并不可能是一一对应的,那样就太过浪费内存了。为了解决效率问题,实际上真实物理内存是分页的。而映射表同样是以页为单位的。换句话说,映射表只会映射到某一页,并不会映射到具体每一个地址。

Mac OS 、linux内存 4kb一页,iOS是16kb一页。可以使用 pagesize 命令,在终端直接查看。4096字节=4千字节。

0 和 1 代表当前地址有没有在物理内存中。 从上图我们也可以看出,进程的虚拟地址是连续的,但是实际物理内存地址并不是连续的,而是由若干完整的内存分页组成。 当应用被加载到物理内存中时 ,并不会将整个应用加载到物理内存中。只会放用到的那一部分。也就是懒加载,换句话说就是应用使用多少,实际物理内存就分配多少。

二、Page Fault

1. Page Fault产生原因

当应用访问到某个地址,映射表中为 0,也就是说它并没有被加载到物理内存中时,系统就会立刻阻塞整个进程,触发一个缺页中断,即 Page Fault。 当一个缺页中断被触发,操作系统会从磁盘中重新读取这页数据到物理内存上,然后将映射表中虚拟内存指向对应物理内存。 如果当前内存已满,操作系统会通过置换页算法找一页数据进行覆盖。这也是为什么开再多的应用也不会崩掉,但是之前开的应用再打开,就会重新启动的根本原因。

2. Page Fault影响

内存分页触发中断异常 Page Fault

后,会阻塞进程,这是会对性能产生影响的。并且在 iOS 系统的生产环境应用,在发生缺页中断进行重新加载时 ,iOS 系统还会对其做一次签名验证,因此

iOS 生产环境的 Page Fault 所产生的耗时要更多。

对用户而言,使用App时第一个直接体验就是启动 App 时间,而启动时期会有大量的分类三方等等需要加载和执行,此时大量`Page

Fault`所产生的的耗时往往是不能小觑的。

抖音团队分享的一个 Page Fault,开销在 0.6 ~ 0.8ms。实际测试发现不同页会有所不同 , 也跟 cpu 负荷状态有关,在 0.1 ~ 1.0 ms 之间。undefined 二进制重排这个方案最早也是 抖音团队 分享的。

三、二进制重排

1. 二进制重排原理

函数编译在mach-O中的位置是根据ld ( Xcode 的链接器)的编译顺序并非调用顺序来的,因此很可能这两个函数分布在不同的内存页上。

如上图 ,编译顺序是method1method2... 。启动时 page1page2 都需要从无到有加载到物理内存中,所以会触发两次Page Fault二进制重排的做法就是将method1method4放到一个内存页中,那么启动时则只需要加载一次 page 即可,也就是只触发一次Page Fault。 在实际项目中,我们可以将启动时需要调用的函数放到一起 ( 比如 前10页中 ) 以尽可能减少Page Fault,进而减少启动耗时。

2. 二进制重排操作

苹果已经给我们提供了这个机制,实际上 二进制重排就是对即将生成的可执行文件重新排列,这个操作发生在链接阶段。

2.1 Order File

Xcode用的链接器叫做 ldld有一个参数叫做Order File,我们可以通过这个参数配置一个 后缀名order的文件路径。在这个xxx.order文件中,将需要的符号按顺序写在里面,当工程build的时候,Xcode会读取这个文件,打的二进制包就会按照这个文件中的符号顺序进行生成对应的mach-O

2.2 Linkmap 查看二进制文件布局

Linkmap是iOS编译过程的中间产物,记录了 二进制文件的布局 ,开启步骤如下:

2.2.1 修改Write Link Map File为 YES,然后clean项目并重新编译
  • Products -> show in finder,上上层文件夹,然后找到一个xxx-LinkMap-normal-arm64.txt的txt文件
  • 这个文件的# Symbols:部分存储了所有符号的顺序,前面的 .o 等内容忽略,Address就是实际的物理地址,可用Mach-O工具查看
  • 我们发现符号顺序是按照Compile Sources的文件顺序来排列的

当我们调整Compile Sources中的文件顺序后,会发现符号顺序也有了变化。

2.3 二进制重排原理

我们二进制重排并非只是修改符号地址,而是利用符号顺序,重新排列整个代码在文件的偏移地址,将启动需要加载的方法地址放到前面内存页中,以此达到减少`page

fault`的次数从而实现时间上的优化。

3. 获取App启动时调用的所有方法(使用编译插桩)

备注:Clang插桩实际上就是一个代码覆盖工具

Clang插桩官网地址

要真正的实现二进制重排,我们需要拿到启动时的所有方法、函数等符号,并保存其顺序,然后写入xxx.order文件来实现二进制重排,获取的方案使用

Clang编译插桩

3.1 在Build SettingsOther C Flags添加编译配置`-fsanitize-

coverage=func,trace-pc-guard`。

3.2 添加完编译配置后,会发现编译报错,如下:

3.3 添加Clang函数

代码语言:txt
复制
#import "DZHomeViewController.h"
代码语言:txt
复制
#import <dlfcn.h> // 动态库的显式调用
代码语言:txt
复制
#import <libkern/OSAtomic.h> //
代码语言:txt
复制
/*
代码语言:txt
复制
 考虑到插桩方法会调用很多次,使用锁会影响性能,所以使用苹果底层的`原子队列`,其内部实际上是一个链表,遵循先进先出
代码语言:txt
复制
 **/
代码语言:txt
复制
static OSQueueHead symbolList = OS_ATOMIC_QUEUE_INIT;
代码语言:txt
复制
// 定义符号结构体
代码语言:txt
复制
typedef struct {
代码语言:txt
复制
    void *pc;
代码语言:txt
复制
    void *next;
代码语言:txt
复制
} PCNode;
代码语言:txt
复制
@interface DZHomeViewController ()
代码语言:txt
复制
@end
代码语言:txt
复制
@implementation DZHomeViewController
代码语言:txt
复制
void(^blockTest)(void) = ^(void) {
代码语言:txt
复制
};
代码语言:txt
复制
+ (void)load {
代码语言:txt
复制
}
代码语言:txt
复制
+ (void)initialize {
代码语言:txt
复制
}
代码语言:txt
复制
- (void)viewDidLoad {
    [super viewDidLoad];    
}
代码语言:txt
复制
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
  //    [self deziTest];
    NSMutableArray <NSString *> * symbolNames = [NSMutableArray array];
代码语言:txt
复制
    while (YES) {
代码语言:txt
复制
        PCNode *node = OSAtomicDequeue(&symbolList, offsetof(PCNode, next));
代码语言:txt
复制
        if (node == NULL) {
代码语言:txt
复制
            break;
代码语言:txt
复制
        }
代码语言:txt
复制
        Dl_info info;
代码语言:txt
复制
        dladdr(node->pc, &info);
代码语言:txt
复制
        NSString * name = @(info.dli_sname);
代码语言:txt
复制
        BOOL  isObjc = [name hasPrefix:@"+["] || [name hasPrefix:@"-["];
代码语言:txt
复制
        NSString * symbolName = isObjc ? name: [@"_" stringByAppendingString:name];
代码语言:txt
复制
        [symbolNames addObject:symbolName];
代码语言:txt
复制
    }
代码语言:txt
复制
    // 取反
代码语言:txt
复制
    NSEnumerator *emt = [symbolNames reverseObjectEnumerator];
代码语言:txt
复制
    //去重
代码语言:txt
复制
    NSMutableArray<NSString *> *funcs = [NSMutableArray arrayWithCapacity:symbolNames.count];
代码语言:txt
复制
    NSString *name;
代码语言:txt
复制
    while (name = [emt nextObject]) {
代码语言:txt
复制
        if (![funcs containsObject:name]) {
代码语言:txt
复制
            [funcs addObject:name];
代码语言:txt
复制
        }
代码语言:txt
复制
    }
代码语言:txt
复制
    //干掉自己!
代码语言:txt
复制
    [funcs removeObject:[NSString stringWithFormat:@"%s",__FUNCTION__]];
代码语言:txt
复制
    //将数组变成字符串
代码语言:txt
复制
    NSString *funcStr = [funcs  componentsJoinedByString:@"\n"];
代码语言:txt
复制
    NSString *filePath = [NSTemporaryDirectory() stringByAppendingPathComponent:@"fontResources.order"];
代码语言:txt
复制
    NSData *fileContents = [funcStr dataUsingEncoding:NSUTF8StringEncoding];
代码语言:txt
复制
    [[NSFileManager defaultManager] createFileAtPath:filePath contents:fileContents attributes:nil];
代码语言:txt
复制
    NSLog(@"%@",funcStr);
代码语言:txt
复制
}
代码语言:txt
复制
- (void)deziTest {
    blockTest();
}
代码语言:txt
复制
void __sanitizer_cov_trace_pc_guard_init(uint32_t *start, uint32_t *stop) {
代码语言:txt
复制
  static uint64_t N;  // Counter for the guards.
代码语言:txt
复制
  if (start == stop || *start) return;  // Initialize only once.
代码语言:txt
复制
  printf("INIT: %p %p\n", start, stop);
代码语言:txt
复制
  for (uint32_t *x = start; x < stop; x++)
代码语言:txt
复制
    *x = ++N;  // Guards should start from 1.
代码语言:txt
复制
}
代码语言:txt
复制
void __sanitizer_cov_trace_pc_guard(uint32_t *guard) {
代码语言:txt
复制
    /*  精确定位 哪里开始 到哪里结束!  在这里面做判断写条件!*/
代码语言:txt
复制
    void *PC = __builtin_return_address(0);
代码语言:txt
复制
    DeziNode *node = malloc(sizeof(DeziNode));
代码语言:txt
复制
    *node = (DeziNode){PC,NULL};
代码语言:txt
复制
    //进入
代码语言:txt
复制
    OSAtomicEnqueue(&symbolList, node, offsetof(DeziNode, next));
代码语言:txt
复制
    Dl_info info; // 动态链接库时 通过传递指针给Mach-O头部Mach-O header,引用一个Dl_info结构体
代码语言:txt
复制
    dladdr(PC, &info);
代码语言:txt
复制
    printf("----------------------------------------\nfname:%s \nfbase:%p \nsname:%s \nsaddr:%p\n",
代码语言:txt
复制
           info.dli_fname,
代码语言:txt
复制
           info.dli_fbase,
代码语言:txt
复制
           info.dli_sname,
代码语言:txt
复制
           info.dli_saddr);
代码语言:txt
复制
}
代码语言:txt
复制
@end
  • dl_info结构体
代码语言:txt
复制
typedef struct dl_info {
代码语言:txt
复制
    const char      *dli_fname;     /* 共享对象的路径名 */
代码语言:txt
复制
    void            *dli_fbase;     /* 共享对象的基本地址 */
代码语言:txt
复制
    const char      *dli_sname;     /* 最近的符号的名称 */
代码语言:txt
复制
    void            *dli_saddr;     /* 最近的符号地址 */
代码语言:txt
复制
} Dl_info;

3.4 汇编断点调试

  • 首先打开汇编调试
  • 在方法中加断点
  • 调试结果
  • 结论

* 由汇编断点调试可以发现在所有的方法函数里边插入这个方法__sanitizer_cov_trace_pc_guard,因此每次执行方法都会先执行插桩方法。所以在编译时刻,Clang插桩会静态加入汇编指令,做到全局AOP,Hook一切方法。

3.5 使用__sanitizer_cov_trace_pc_guard

  • 断点打印发现PC就是方法地址

void *PC = __builtin_return_address(0); 通过这个函数,拿到当前函数__sanitizer_cov_trace_pc_guard的下一个函数地址,也就是程序中的真实调用方法。

3.6 通过原子队列存取方法

  • 插桩时存
代码语言:txt
复制
void __sanitizer_cov_trace_pc_guard(uint32_t *guard) {
代码语言:txt
复制
    /* 定位插桩方法的下一个方法,也就是程序中的真实调用方法 */
代码语言:txt
复制
    void *PC = __builtin_return_address(0);
代码语言:txt
复制
    PCNode *node = malloc(sizeof(PCNode));
代码语言:txt
复制
    *node = (PCNode){PC,NULL};
代码语言:txt
复制
    // 进入 &symbolList链表表头,node节点数据,offsetof(PCNode, next) 下一个成员在链表中的偏移地址
代码语言:txt
复制
    OSAtomicEnqueue(&symbolList, node, offsetof(PCNode, next));
代码语言:txt
复制
}
  • 通过touchesBegan方法手动取出原子队列所存方法
代码语言:txt
复制
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    NSMutableArray <NSString *> *symbolNames = [NSMutableArray array];
代码语言:txt
复制
    while (YES) {
代码语言:txt
复制
        // &symbolList链表表头,
代码语言:txt
复制
        PCNode *node = OSAtomicDequeue(&symbolList, offsetof(PCNode, next));
代码语言:txt
复制
        if (node == NULL) {
代码语言:txt
复制
            break;
代码语言:txt
复制
        }
代码语言:txt
复制
        Dl_info info;
代码语言:txt
复制
        dladdr(node->pc, &info);
代码语言:txt
复制
        NSString * name = @(info.dli_sname);
代码语言:txt
复制
        BOOL isObjc = [name hasPrefix:@"+["] || [name hasPrefix:@"-["];
代码语言:txt
复制
        NSString *symbolName = isObjc ? name: [@"_" stringByAppendingString:name];
代码语言:txt
复制
        [symbolNames addObject:symbolName];
代码语言:txt
复制
    }
代码语言:txt
复制
    // 由于先进先出的特性,所以要取反
代码语言:txt
复制
    NSEnumerator *emt = [symbolNames reverseObjectEnumerator];
代码语言:txt
复制
    // 去重
代码语言:txt
复制
    NSMutableArray<NSString *> *funcs = [NSMutableArray arrayWithCapacity:symbolNames.count];
代码语言:txt
复制
    NSString *name;
代码语言:txt
复制
    while (name = [emt nextObject]) {
代码语言:txt
复制
        if (![funcs containsObject:name]) {
代码语言:txt
复制
            [funcs addObject:name];
代码语言:txt
复制
        }
代码语言:txt
复制
    }
代码语言:txt
复制
    // 去掉自己
代码语言:txt
复制
    [funcs removeObject:[NSString stringWithFormat:@"%s",__FUNCTION__]];
代码语言:txt
复制
    // 将数组变成字符串
代码语言:txt
复制
    NSString *funcStr = [funcs  componentsJoinedByString:@"\n"];
代码语言:txt
复制
    NSString *filePath = [NSTemporaryDirectory() stringByAppendingPathComponent:@"fontResources.order"];
代码语言:txt
复制
    NSData *fileContents = [funcStr dataUsingEncoding:NSUTF8StringEncoding];
代码语言:txt
复制
    [[NSFileManager defaultManager] createFileAtPath:filePath contents:fileContents attributes:nil];
代码语言:txt
复制
    NSLog(@"%@",funcStr);
代码语言:txt
复制
}
  • 将存入本地的fontResources.order文件取出,放在工程里
  • 配置工程的Order File文件
  • 二进制重排到此结束,对比前后xxx-LinkMap-normal-arm64.txt文件,我们会发现启动时调用的方法,已经被排到前边去了

二进制重排前

二进制重排后

四、使用 System Trace 来检验二进制重排结果

1. 那么如何衡量页的加载时间呢?这里就用到了Instruments中的System Trace工具。

首先,重新启动设备(冷启动)。⌘+I打开Instruments,选择System Trace工具。undefined 点击录制⏺后,出现第一个页面,马上停止⏹。过滤只显示Main Thread相关,选择Summary: Virtual Memory。

File Backed Page In次数就是触发Page Fault的次数了。 Page Cache Hit就是页缓存命中的次数了。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
作者已关闭评论
0 条评论
热度
最新
推荐阅读
目录
  • 1. 虚拟内存
  • 2. 虚拟内存分页
  • 二、Page Fault
    • 1. Page Fault产生原因
      • 2. Page Fault影响
      • 三、二进制重排
        • 1. 二进制重排原理
          • 2. 二进制重排操作
            • 2.1 Order File
            • 2.2 Linkmap 查看二进制文件布局
            • 2.3 二进制重排原理
          • 3. 获取App启动时调用的所有方法(使用编译插桩)
            • 3.1 在Build Settings中Other C Flags添加编译配置`-fsanitize-
            • 3.2 添加完编译配置后,会发现编译报错,如下:
            • 3.3 添加Clang函数
            • 3.4 汇编断点调试
            • 3.5 使用__sanitizer_cov_trace_pc_guard
            • 3.6 通过原子队列存取方法
        • 四、使用 System Trace 来检验二进制重排结果
          • 1. 那么如何衡量页的加载时间呢?这里就用到了Instruments中的System Trace工具。
          领券
          问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档