二进制重排&优化启动

一 、了解自己项目冷启动阶段耗时。

1、配置 环境变量 获取 dyld 反馈。DYLD_PRINT_STATISTICS
  • Edit Scheme -> run - Arguments -> Environment Variables 填入环境变量 DYLD_PRINT_STATISTICS
2、分析环境变量打印信息。也就项目启动时刻耗时。
截屏2020-11-19 下午4.16.03.png
  • dylib loading time : 动态库耗时:
  • rebase/binding time: 偏移修正 / 符号绑定 耗时

偏移修正:任何一款app 生成的二进制文件 ; 在这个二进制文件里的、 方法、函数调用、都有一个地址 ; 这个地址就是在这二进制文件中的 偏移地址。 当我们运行的时候 ASLR会生成一个随机值插在 二进制文件的开始地址位置。也就是 随机值 偏移值 等于运行时确定的内存数值。也就是修正 值。

绑定: 拿 NSLog举例, 当前写一个NSLog的时候,此时build xcode 并不知道它真实的地址。因为NSlog实际在 Foundation库中,Foundation库是外部的 动态库。聪明的设计师,将Mach0已符号的形式来做。 这里就将 NSLog 符号 存入了MachO文件的 _DATA段儿。此时 这个符号指定的是一个没有意义的值。当我们运行的时候 此时 就会 将nslog符号 关联 真正的NSLog地址。这个关联 就是 绑定。

  • ObjC setup time: OC类注册的耗时。相比swift来说 swift要快很多。
  • initializer time: 执行load 和构造函数的耗时
  • slowest initializers: 举了几个最慢的例子。
3、根据列出的信息,我们来说优化方案:

(1)、动态库的耗时:我们app去载入动态库,这些动态库又有自己的依赖关系 ,花时间查找 啊,读取等操作。对于苹果官方的动态库,我们不用去担心,它做了高速的优化。对于一些自己自定义的,和cocopod导入的一些动态库,它花的时间就比较长了,所以我们应该尽量的减少它们。官方给出的建议 自定义的动态库最好是 6个。多余6个我们就要考虑合并动态库。合并动态库对于启动耗时 优化是非常之有效的。

(2)、偏移修正/符号绑定:不过多说。

(3)、类的注册耗时:减少 OC类 。如用swift开发的话 确实会高效许多。

(4)、执行load 和构造函数的耗时:减少load 非懒加载类 的产生 及 c++的构造函数使用,非必须 不这么做,可以考虑 在 initialize里做, 延迟在 main以后去做。

上面就是 启动时候的检测。

main函数以后的优化:

(1)、减少启动初始化的流程,能懒加载的就懒加载。
(2)、随着业务的不多堆叠,可能会遗留出许多未用到的类,这些类也是有一定的耗的。虽说没用但是它也参与了编译。所以 一定要删掉。
(3)、如果启动的时候,尽量使用多线程去做。在启动那一刻的,尽量使用多线程,把cpu的性能体现出来,来换取时间。
(4)、 我们启动时刻的那一些页面尽量 不用用xib,story.bord,最好用纯代码去做。因为 xib 也好 story.bord,它本身就得需要去做一层代码的解析页面的渲染。
(5)、其他的就是在业务层面的一些优化了。

下面就是在技术层面上对项目的启动优化,只要项目大 都有优化的空间,这就是二进制重排

二、二进制重排

1、需要知道的概念

虚拟内存,物理内存 以及进程和内存之间的关系。

我们简单的说一下

在早期的计算机中,程序是直接运行在物理内存中的,也就是说程序的运行时所访问的地址都是物理地址。如果说计算机只能同一时刻运行一个程序,那么这个程序所需要的物理空间,只要不超过物理内存的大小,就不会出问题。可以想象如果计算机是这样的。也会显得很笨,程序不能多开,cpu也无法展示出它的性能, 导致硬件资源的浪费及不好的体验。那怎样才能将有限的物理内存,可分配给多个程序使用呢? 假如 128MB 的内存条。 我们有 AB 两个程序 A程序 需要运行内存空间占用 10MB 、 B程序 需要运行内存空间 100MB。 这样也许我们就能同时跑两个 程序了。但是这样设计有很大的问题。

第一、程序之间没有隔离,都是直接运行在物理内存的。这样会导致数据之间的不安全。恶意程序轻轻松松就可以修改 其他程序的内存数据。非恶意的程序,可能不小心修改了其他程序数据,就会导致崩溃。这对于用户体验来说极差。

第二、内存使用效率低。 这时候 用户 打开了 一个C 程序 它需要的内存空间是 20 MB 。那么此时 物理内存 已经被 占用了 110MB 还有 18MB ,也满足不了 C程序 20MB的运行空间,那总得有办法解决吧。这时候就想到了,一种办法 可以先将 其他程序数据 暂时 写到磁盘里。用的时候在读取回来。那此时 A 程序 不满足条件, 那么只能 换 B了,先将 B 换到磁盘, 然后将 C 放到 内存开始运行。可以看到 在程序之间的切换,会有大量的数据来回换入 换 出。所以 导致效率十分低下。

第三、 程序编写麻烦。 “ 程序运行的地址不确定 因为程序每次需要装入运行时,我们都需要给它从内存中分配一块足够大的空闲区域,这个空闲区域的位置是不确定的”,“这给程序的编写造成了一定的麻烦”。

虚拟内存的由来

为了解决这些问题 ,就设计出了中间层,使用出一种间接地址访问 机制。 把程序给出的地址,看做是一种虚拟地址(Virtual Address),然后通过某种映射方法,在将这个虚拟地址转换成实际的物理地址。只要控制好 虚拟地址到物理地址的映射过程,就能保证 物理内存的 程序互不重叠,达到隔离的效果。而程序运行需要地址空间 在中间层这里 就是所说的 虚拟内存,通过映射的真实地址空间,就是物理内存。

“虚拟地址空间是指虚拟的、人们想象出来的地址空间,其实它并不存在,每个进程都有自己独立的虚拟空间,而且每个进程只能访问自己的地址空间,这样就有效地做到了进程的隔离。”

分段

“最开始人们使用的是一种叫做分段(Segmentation)的方法,基本思路是把一段与程序所需要的内存空间大小的虚拟空间映射到某个地址空间”。 “对于每个程序来说,无论它们被分配到物理地址的哪一个区域,对于程序来说都是透明的,它们不需要关心物理地址的变化,它们只需要按照从地址0x00000000到0x00A00000 或 0x00B00000 来编写程序、放置变量,所以程序不再需要重定位。” 例如 A程序 0x00000000 - 0x00A00000。程序B 地址 0x00000000 - 0x00B00000 ;

分段技术的出现 好像 解决了 上述 问题 1 和问题 3、但是还是没有解决使用效率的问题。
分段对内存区域的映射还是按照程序为单位,如果内存不足,被换入换出到磁盘的都是整个程序,这样势必会造成大量的磁盘访问操作,从而严重影响速度,这种方法还是显得粗糙,粒度比较大。”然而实际上一个程序在运行时,在某个时间段,它只是频繁的用到了一小部分数据。也就是说,程序的很多数据其实在某个时间段内并不会被用到,所以就出来了 更小粒度的 内存分割 和映射方法,“使得程序的局部性原理得到充分的利用,大大提高了内存的使用率。这种方法就是分页

分页

分页的基本方法是把地址空间人为地等分成固定大小的页,每一页的大小由硬件决定。或硬件支持多大小的页,由操作系统来决定页的大小。在 linux MacOS 一页数据为 4KB , 在iOS 一页为 16KB 。无论是 虚拟内存 还是 物理内存 。都是已分页来管理数据。每一页均为16KB。 一个app 虚拟地址空间 分为若干个 页 都是从0 为开始。而只有用到的页 才会存到 物理内存。

由于每个app都有一个虚拟的地址空间 都是从 0 开始的。那么为了安全 也就是 ASLR需要在运行时将随机值插入到前面。

MMU

我们从上面已经知道了 虚拟内存 就是一个虚拟的空间它就是一个映射表,可以想象成一本书的目录。来映射到真正的物理内存的地址。而这个映射过程 是由 操作系统 和硬件来寻址的。这个硬件就是MMU

Page Fault

当 一个进程 通过虚拟内存的 某一 页 进行映射寻址的过程, 当发现物理内存并没有,这时硬件就会捕捉到这个消息 ,这就是所谓的 缺页异常(Page Fault),接下来操作系统接管 从磁盘中将这部分数据加载到物理内存中 并和虚拟内存进行 映射关联。

内存回收

每一个操作系统都有一套自己的内存回收机制。iOS这里我们就会发现一个现象,为什么你多开几个应用。当一个后台应用长时间不用。程序就需要从新打开。其实 这就是 覆盖了内存区域中的不活跃的页。

2、那我们怎么优化?其原理是什么?

Page Fault 的概念我们知道了,说白了 就是 减少 页 的 缺页异常。如果只是少数的页,我们跟本就察觉不出来。但是多了 那一定会察觉。当什么时候会发生很多的 pageFault 那一定是冷启动的时候。这时候所有的数据均没有加载到物理内存。所以会大量的进行pageFault。那我怎么优化? 还记得上面所说的概念吗?一个应用在某个时间段儿,只有一部分页被加载到了物理内存, 所以这里我们只需要查找 启动这一刻 到你的 主页面 展示出来 这一段时间 所造成的pageFault.。 还记得 一页 是16kb 假如 从 启动 到 主页面完全展示出来。 我们调用了32个方法。 这32个方法 在虚拟空间 分别 被 安排了 32个页 的 。 这就会进行 32次的 pageFault。 那其实这 一页 16kb的空间 只存了一个启动时刻需要的方法。 如这一个启动时刻需要的方法只占用 1KB大小。那我们是不是可以将不是本次启动需要的方法向后移动,将启动时刻需要的方法尽量往一起凑,来减少 页的占用。
这就是我们的优化思路。 当按照我们的思路 排列后 以前启动 需要 32个页 进行 pageFault .而 优化后 仅需要 2个 。

3、看我们程序是如何排列的。
截屏2020-11-20 下午4.38.43.png

请问此时的执行顺序是什么样的 肯定是 load -> test2 -> viewDidLoad - >test1
那么此时的排列顺序是怎样呢?
首先打开 build Sttings -> 搜索 link Map -> Write Link Map File : Yes


截屏2020-11-20 下午4.43.20.png

打开之后 build
找到


截屏2020-11-20 下午4.46.05.png

找到

截屏2020-11-20 下午4.51.08.png
  • 这里面就是符号的顺序
截屏2020-11-20 下午4.53.08.png

多试几次 就会发下 里面的顺序 是编译文件的顺序(可从 Complie Sources中查看顺序)。然后在 书写的顺序 往下排列的。
这就是 我们mach-O 中的排列顺序。

这就看出来了,启动时刻的方法 被分别的分配到了 不同的页里。并没有集中分配。导致启动的时刻有大量的 pageFault。

将 所有启动时刻需要调用的方法,排列在一起! 这就是二进制重排。

Order File

前面知道了原理,及编译文件的顺序,那么怎么重排呢?其实苹果 给我们提供了相应的机制,只要是在 TARGETS -> Build Settings 搜索 order file 将 重排列的 .order文件 进行填入路径即可。


截屏2020-11-23 上午9.36.22.png

因为 xcode用的连接器 是ld, ld有个参数 叫 Order File ,所以我们就可以通过参数配置一个 后缀名为 order的文件路径。当build,Xcode会读取这个文件,打的二进制包就会按照这个文件中的符号顺序进行生成对应的 mach -O。

可以参考 源码libObjc项目,它就使用了二进制重排优化。

截屏2020-11-23 上午9.41.41.png

上面我们知道 程序没有做 二进制重排 mach-0 文件顺序 是 先已Complie Sources 顺序 在 已 每个文件的书写顺序排列的。当我们排列后,就不是这样了。就会已我们写在 order文件里的最先排列。没写的xcode自动补齐。

4、查看应用程序 在启动成功这一刻 的pageFault 次数。

打开 Instruments 找到 System Trace

截屏2020-11-23 上午9.05.08.png

找到要查看的项目,在启动成功那一刻暂停。


截屏2020-11-23 上午9.11.01.png

三、clang 插桩 找到启动-到启动完成阶段调用的方法。

1、Hook 一切的终极武器 Clang 插桩

工程配置

http://clang.llvm.org/docs/SanitizerCoverage.html

根据上面配置我们的工程 将 -fsanitize-coverage=trace-pc-guard 添加到 项目 里 的 Other C Flags

截屏2020-11-23 上午10.17.13.png

添加之后我们编译一下 运行报错 这个错误就是Undefined symbol: 找不到相应的符号

截屏2020-11-23 上午10.19.05.png
  • 向上面这种错误 一定是有地方调用 找不到这两个方法。但是奇怪的是我们这是一个空工程 根本,就没有做什么操作。
  • 这说明 一但 在Other C FLags 我配置了 -fsanitize-coverage=trace-pc-guard,它就会调用 报错的那两个函数。

下面实现这两个函数
官方示例代码:

// trace-pc-guard-cb.cc
#include <stdint.h>
#include <stdio.h>
#include <sanitizer/coverage_interface.h>

// This callback is inserted by the compiler as a module constructor
// into every DSO. 'start' and 'stop' correspond to the
// beginning and end of the section with the guards for the entire
// binary (executable or DSO). The callback will be called at least
// once per DSO and may be called multiple times with the same parameters.
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);
  for (uint32_t *x = start; x < stop; x++)
    *x = ++N;  // Guards should start from 1.
}

// This callback is inserted by the compiler on every edge in the
// control flow (some optimizations apply).
// Typically, the compiler will emit the code like this:
//    if(*guard)
//      __sanitizer_cov_trace_pc_guard(guard);
// But for large functions it will emit a simple call:
//    __sanitizer_cov_trace_pc_guard(guard);
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);
}

我们将上面代码粘贴到我们的项目中(将注释删掉、extern "C" 删掉)

截屏2020-11-23 上午10.32.08.png
  • 这时发现 还会有一个Undefined symbol: 我们看到 函数里有一个调用 先将他去掉。程序完美运行起来了。


    截屏2020-11-23 上午10.35.25.png

运行后 我们发现打印了了一堆东西看不懂


截屏2020-11-23 上午10.40.57.png
函数分析 __sanitizer_cov_trace_pc_guard_init

下面我们分析 一下 这个 start 和 stop 打印的值
首先 看 start 和 stop 是一个 指向 int类型的指针也就是占4个字节
我们lldb x 一下 start


截屏2020-11-23 上午10.56.34.png

在看一下 stop

截屏2020-11-23 上午11.00.56.png
  • 此时可以看到 stop并不是int类型的数值了。这时候 我们应该想到 一个 是开始位置 的内存地址,一个是 结束时候的内存地址。所以 我们需要 左移 4位来看最后一位数值。


    截屏2020-11-23 上午11.04.15.png
  • 这里可以看出我们获取到了 和 start一样类型的数据。也是最后一个数据。(这里的 stop 类似 数组 index -1)

回到 项目 我们新写一个方法


截屏2020-11-23 上午11.08.01.png

运行后点暂停 继续 lldb x stop

截屏2020-11-23 上午11.09.47.png

回到 项目 我们在写一个 c函数


截屏2020-11-23 上午11.13.18.png

运行后点暂停 继续 lldb x stop

截屏2020-11-23 上午11.14.52.png

回到 项目 我们在写一个 block


截屏2020-11-23 上午11.20.45.png

运行后点暂停 继续 lldb x stop


截屏2020-11-23 上午11.22.13.png

看到这里想必大家明白了什么 __sanitizer_cov_trace_pc_guard_init 无论是 方法 还是 函数 还是block 这家伙都能获取到,也就是记录这些方法 函数 block 的符号 个数。添加一个方法 就会加一次 stop最后一个值 我们把从一开始打印 stop 数值 用便签记录下来更能直观的看到


截屏2020-11-23 上午11.33.41.png
  • 知道了__sanitizer_cov_trace_pc_guard_init 方法 虽说能记录个数,但是好像并不能解决什么 我们要的是 方法啊。那么进行下面函数的讲解
函数分析 __sanitizer_cov_trace_pc_guard

我们运行项目 会发现许多 guard的调用


截屏2020-11-23 上午11.47.24.png

先将其打印清空 。我们点击屏幕发现又会调用

截屏2020-11-23 上午11.48.53.png
  • 这里注意 touch begin方法里并未实现什么操作。

我们将touch begin 方法里调用 test 函数 运行 并点击屏幕


截屏2020-11-23 上午11.51.44.png
  • 发现调用了两次
    我们将touch begin 方法里调用 test 函数 在 test函数中 在调用 block 运行 并点击屏幕
截屏2020-11-23 下午12.16.46.png
  • 发现调用了3次
  • 此时此刻我们就明白了 __sanitizer_cov_trace_pc_guard 函数捕获的正是 方法的调用,你调用一个函数,它来,你调用一个 方法 它来,你调用一个 block 它也来。
  • __sanitizer_cov_trace_pc_guard作用就是 全面 hook到了,我们所有的 方法 函数 还有block 调用

那它是怎么调用的呢?原理什么? 我们打开Always Show Disassembly查看汇编 ,首先在 touchbegin 方法打下断点 ,test 下断点 block 下断点。点击断点来到 test 函数 (此时为真机环境)

首先来到 touch begin 方法

截屏2020-11-23 下午3.21.02.png
  • bl 的正是我们代码里写的 __sanitizer_cov_trace_pc_guard

来到 test 方法


截屏2020-11-23 下午12.30.55.png
  • 对于混编可能大家晦涩难懂,但是没关系 我们逐条分析 当看到 sp这样的操作 这其实是一个函数调用必要的一些函数处理,向内存啊,参数啊,这些东西先进行处理 才回执行函数的代码。
    然后结束的时候也会看到一些对sp的处理 比如清空内存啊等。回到 test(){ } 是不是有一个 作用域。如在作用域里有局部变量,出了就被清空了。
  • 所以就是 一个函数 开始的时候 会开辟一个栈空间,当结束的时候会释放

sp : 栈空间寄存器,它指向了栈空间的位置,栈顶。

回到真正做操作的哪里我们发现 bl


截屏2020-11-23 下午12.46.14.png
  • 这个bl 是什么意思? 一般情况 看到 bl它就是在发生跳转,跳转是什么 就是调用方法吗
  • 跳转的也是我们代码里写的 __sanitizer_cov_trace_pc_guard

来到 block

截屏2020-11-23 下午3.21.32.png
  • 跳转的也是我们代码里写的 __sanitizer_cov_trace_pc_guard
总结

看到这里 突然觉得 这个 clang 插桩好屌啊。 只要我在 Other C FLags 添加 -fsanitize-coverage=trace-pc-guard标记 clang 就会接收到、clang 在读取我们所写的代码,读完代码要生成 中间代码 ir. 在生成 ir 的时候,会在每个函数 block ,方法 的边缘的位置 插入 一行 __sanitizer_cov_trace_pc_guard 代码。这就是clang 前端帮我们做的 clang 插桩。这就相当于 hook ,而且是编译期的 hook。这就是它做到了全局的百分百的覆盖,静态插入这条汇编代码,做到全局AOP.

2、拿到函数返回地址

我们已经知道 每个方法函数 block 都会调用 __sanitizer_cov_trace_pc_guard 所以在这里我们开始做事情。
1、我们怎么知道 谁是谁?
2、我们的目标是什么?拿到所有的符号,去做order文件

回到__sanitizer_cov_trace_pc_guard 函数

void __sanitizer_cov_trace_pc_guard(uint32_t *guard) {
  if (!*guard) return;
  void *PC = __builtin_return_address(0);
  char PcDescr[1024];
//  __sanitizer_symbolize_pc(PC, "%p %F %L", PcDescr, sizeof(PcDescr));
  printf("guard: %p %x PC %s\n", guard, *guard, PcDescr);
}

解读 __builtin_return_address我们进行断点调试 打印 PC

截屏2020-11-23 下午3.54.15.png

我们过断点 继续 p PC 在 bt 当前 调用栈


截屏2020-11-23 下午3.57.47.png

在继续过 重复操作,你会发现 此时 __builtin_return_address 获取的就是当前调用栈的地址,那这个地址 是 方法的 首地址?还是啥地址?我们看下汇编。


截屏2020-11-23 下午4.30.26.png
  • 由此我们可以知道 __builtin_return_address 并不是 调用方法的首地址 而是,当前函数返回到上一个调用的地址 。
  • 此时 address有个参数 0 为 我回哪里去, 1 为 我上一个函数回哪里去。

3、通过返回地址 拿到调用者的符号

首先加入头文件 #include <dlfcn.h>
我们看到里面有一个 Dl_info 结构体

typedef struct dl_info {
        const char      *dli_fname;     /* Pathname of shared object */
        void            *dli_fbase;     /* Base address of shared object */
        const char      *dli_sname;     /* Name of nearest symbol */
        void            *dli_saddr;     /* Address of nearest symbol */
} Dl_info;

然后写代码

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 \fbase:%p \n nsname:%s \n sadder:%p\n\n",info.dli_fname,info.dli_fbase,info.dli_sname,info.dli_saddr);
 
//  char PcDescr[1024];
////  __sanitizer_symbolize_pc(PC, "%p %F %L", PcDescr, sizeof(PcDescr));
//  printf("guard: %p %x PC %s\n", guard, *guard, PcDescr);
}

运行


截屏2020-11-23 下午5.22.47.png
  • 此时此刻 符号我们已经拿到,接下来就是 生成 order文件

3、生成 order file 文件

由于__sanitizer_cov_trace_pc_guard(uint32_t *guard) 是在子线程 搞的事情,所以为了线程安全 搞个队列
首先加头文件<libkern/OSAtomic.h>

///定义原子队列
static OSQueueHead symbolList = OS_ATOMIC_QUEUE_INIT;


///定义符号结构体

typedef struct {
    void *pc;
    void *next;
}SYNode;

此时 __sanitizer_cov_trace_pc_guard 函数其实我们就直接存储数据就好了 在合适的地方将所有的符号 拿出 或者写入文件。

void __sanitizer_cov_trace_pc_guard(uint32_t *guard) {
  if (!*guard) return;
  void *PC = __builtin_return_address(0);
   
   ///创建结构体
    
    SYNode * node =   malloc(sizeof(SYNode));
    *node = (SYNode){PC,NULL};
    /// 加入队列
    OSAtomicEnqueue(&symbolList, node, offsetof(SYNode, next));
 
}

我们在 touch began方法 将所有符号取出

-(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 \n",info.dli_sname);
    }
}

此时我们运行


截屏2020-11-23 下午5.49.59.png
  • 发现遇到了bug 死循环了

此时我们进行 混编查看 发现 在 touch began 方法 里发现被hook 了多次


截屏2020-11-23 下午6.08.56.png
  • 在这里被hook一次 我们能理解,因为 我们主动触发了

往下看

截屏2020-11-23 下午6.26.46.png
  • 我们发现 循环一次 也会被 HOOK一次!!
  • 所以这里并不是 函数 方法 block 等被 hook 本质 它其实只要有 b bl 的 指令并且是自定义的就会 hook。

解决办法


截屏2020-11-23 下午6.36.29.png
  • 在 targets -> Other C Flags 之前写的标记处 -fsanitize-coverage=trace-pc-guard 改为-fsanitize-coverage=func,trace-pc-guard就完美解决了。

运行

截屏2020-11-23 下午6.39.56.png

此时此刻 我们看到 还需要 注意几个问题 第一 函数 是反 的 应该倒序排序出 、第二 没有去重 、第三 load函数貌似没有 第四 如果是函数 还要加上一个下划线。最后写入到沙盒

  • 先看 第三个问题 load
    没有 load 是因为 要屏蔽掉 if (!*guard) return; 这段代码
void __sanitizer_cov_trace_pc_guard(uint32_t *guard) {
///  if (!*guard) return;
  void *PC = __builtin_return_address(0);
   
   ///创建结构体
    
    SYNode * node =   malloc(sizeof(SYNode));
    *node = (SYNode){PC,NULL};
    /// 加入队列
    OSAtomicEnqueue(&symbolList, node, offsetof(SYNode, next));
 
}

最后完整代码

-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
    //定义数组
    NSMutableArray<NSString *> * symbolNames = [NSMutableArray array];
    
    while (YES) {//一次循环!也会被HOOK一次!!
       SYNode * node = OSAtomicDequeue(&symbolList, offsetof(SYNode, next));
        
        if (node == NULL) {
            break;
        }
        Dl_info info = {0};
        dladdr(node->pc, &info);
//        printf("%s \n",info.dli_sname);
        NSString * name = @(info.dli_sname);
        free(node);
        
        BOOL isObjc = [name hasPrefix:@"+["]||[name hasPrefix:@"-["];
        NSString * symbolName = isObjc ? name : [@"_" stringByAppendingString:name];
        //是否去重??
        [symbolNames addObject:symbolName];
        /*
        if ([name hasPrefix:@"+["]||[name hasPrefix:@"-["]) {
            //如果是OC方法名称直接存!
            [symbolNames addObject:name];
            continue;
        }
        //如果不是OC直接加个_存!
        [symbolNames addObject:[@"_" stringByAppendingString:name]];
         */
    }
    //反向数组
//    symbolNames = (NSMutableArray<NSString *>*)[[symbolNames reverseObjectEnumerator] allObjects];
    NSEnumerator * enumerator = [symbolNames reverseObjectEnumerator];
    
    //创建一个新数组
    NSMutableArray * funcs = [NSMutableArray arrayWithCapacity:symbolNames.count];
    NSString * name;
    //去重!
    while (name = [enumerator nextObject]) {
        if (![funcs containsObject:name]) {//数组中不包含name
            [funcs addObject:name];
        }
    }
    [funcs removeObject:[NSString stringWithFormat:@"%s",__FUNCTION__]];
    //数组转成字符串
    NSString * funcStr = [funcs componentsJoinedByString:@"\n"];
    //字符串写入文件
    //文件路径
    NSString * filePath = [NSTemporaryDirectory() stringByAppendingPathComponent:@"HEWS.order"];
    //文件内容
    NSData * fileContents = [funcStr dataUsingEncoding:NSUTF8StringEncoding];
    [[NSFileManager defaultManager] createFileAtPath:filePath contents:fileContents attributes:nil];
}

  • 最后去沙盒里去拿到相应的order文件 就算完成了。

4、给项目指定order 文件 并 查看link Map 文件 看是否有重排成功

5、应用到自己的项目

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,390评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,821评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,632评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,170评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,033评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,098评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,511评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,204评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,479评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,572评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,341评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,213评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,576评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,893评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,171评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,486评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,676评论 2 335

推荐阅读更多精彩内容