美文网首页
启动优化3

启动优化3

作者: f8d1cf28626a | 来源:发表于2022-06-23 18:34 被阅读0次

二进制重排 & clang插桩

注意:在iOS生产环境的app,在发生Page Fault进行重新加载时,iOS系统还会对其做一次签名验证,因此 iOS 生产环境的 Page Fault 比Debug环境下所产生的耗时更多。

下面,我们来进行具体的实践,首先理解几个名词

Link Map

Linkmap是iOS编译过程的中间产物,记录了二进制文件的布局,需要在Xcode的Build Settings里开启Write Link Map File,Link Map主要包含三部分:

  • Object Files 生成二进制用到的link单元的路径和文件编号

  • Sections 记录Mach-O每个Segment/section的地址范围

  • Symbols 按顺序记录每个符号的地址范围

ld

ld是Xcode使用的链接器,有一个参数order_file,我们可以通过在Build Settings -> Order File配置一个后缀为order的文件路径。在这个order文件中,将所需要的符号按照顺序写在里面,在项目编译时,会按照这个文件的顺序进行加载,以此来达到我们的优化

所以二进制重排的本质就是对启动加载的符号进行重新排列

到目前为止,原理我们基本弄清楚了,如果项目比较小,完全可以自定义一个order文件,将方法的顺序手动添加,但是如果项目较大,涉及的方法特别多,此时我们如何获取启动运行的函数呢?有以下几种思路

  • 1、hook objc_msgSend:我们知道,函数的本质是发送消息,在底层都会来到objc_msgSend,但是由于objc_msgSend的参数是可变的,需要通过汇编获取,对开发人员要求较高。而且也只能拿到OC 和 swift中@objc 后的方法

  • 2、静态扫描:扫描 Mach-O 特定段和节里面所存储的符号以及函数数据

  • 3、Clang插桩:即批量hook,可以实现100%符号覆盖,即完全获取swift、OC、C、block函数

  • tracing PCs 跟踪CPU执行到的代码

  • 设置 Build Setting -> 搜索 Other c flag -> 找到Other C Flag 添加 -fsanitize-coverage=trace-pc-guard

Tracing PCs with guards
With -fsanitize-coverage=trace-pc-guard the compiler will insert the following code on every edge:

  • 报错 找不到以下两个函数

// 使用两个回调函数

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


// 1.这个函数可以获取项目中符号的个数(函数个数)
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.
}

// 2.这个函数可以拦截到当前项目(machO中)所有正在执行的函数,属性
void __sanitizer_cov_trace_pc_guard(uint32_t *guard) {

  if (!*guard) return;  // Duplicate the guard check.
  
// 返回地址return_address的意思是:调用了__sanitizer_cov_trace_pc_guard的函数,谁调用了__sanitizer_cov_trace_pc_guard,谁的地址就是return_address

  void *PC = __builtin_return_address(0);

  // Dl_info info;

  // dladdr(PC,&info);

  // 创建结构体
  SYNode *node = malloc(sizeof(SYNode));
  *node = (SYNode){PC,NULL}

  // 结构体入栈
  OSAtomicEnqueue(&symbolList,node,offsetof(SYNode,next));

  
  
  printf("%s\n %s\n %s\n %s\n",
                               info.dli_frame,
                               info.dli_fbase
                               info.dli_sname
                               info.dli_saddr);
  // info.bli_sname 是符号名称

  // 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);
}



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

// 定义一个符号的结构体
typedef struct {
  void *pc;
  void *next;
}SYNode;

// 取出symbolList中的数据


-(void)roc_getSymbolList{

   while(YES){

   SYNode *node = OSAtomicDequeue(&symbolList,offsetof(SYNode,next));
   
   if (node == NULL) break;

   Dl_info info;

   dladdr(node->pc,&info);

   printf("%s \n",info.dli_sname);

 }
}

   // 1.运行之后 死循环,OMG -> 永远在执行 打印roc_getSymbolList
   // 2.在 while(YES){} 循环体内部也进行了拦截,导致死循环
   // 3.解决:在 Build Setting / Other C Flag 中配置 `-fsanitize-coverage=func,trace-pc-guard`

start & stop debug

  • start数据

    • 断点调式打印内存 lldb x 0x1009914b8
start数据
  • stop 数据

    • 断点调式打印内存 lldb x 0x1009914f0
stop数据
  • 查看stop后一个数据

    • 断点调式打印内存 lldb x (0x1009914f0-4)
stop最后一个数据 stop读取图解
结果 0e 00 00 00 (14个符号,即`是dyld之后调用的函数个数`)

Hook all 的原理

  • __sanitizer_cov_trace_pc_guard (可拦截all函数)

    原理分析:
    

编译之前只要添加了clang插桩的标记:-fsanitize-coverage=trace-pc-guard

编译之后编译器就会在实现函数的edge插入一个函数,这个函数就是__sanitizer_cov_trace_pc_guard
so 起到拦截所有函数的效果,在执行函数内部内容之前

  • 相当于在ast(语法树)之后,在IR中每一个函数的函数中的内容之前插入__sanitizer_cov_trace_pc_guard
汇编形式的拦截原理

这也是苹果官方对app上线之前做代码审查的方案,所以在发布产品时记得去掉 -fsanitize-coverage=trace-pc-guard 配置

提示:如果需要制作乌龟壳这是至关重要的一部分

拦截获取函数符号

  • 打印出all已拦截的符号
已拦截的all符号
  • 哎妈妈呀,还需要照顾多线程循环体,巨大的坑

1.在 多线程 会影响执行的顺序,需要捋正

2.在 while(YES){} 循环体内部也进行了拦截,会导致死循环,这个坑好大吗

需要将函数符号按照顺序添加到other文件中
  • 解决 多线程 这个坑
通过原子队列保存符号(看上面代码部分的操作 OSAtomicEnqueue & OSAtomicDequeue)
  arm64汇编 `bl` == x86_64汇编 `call`
  • 解决 循环体 这个坑
在 Build Setting / Other C Flag 中配置 `-fsanitize-coverage=func,trace-pc-guard` 添加了`func,`
  • func,仅限方法的调用实现插桩,其它不管
func,
  • 查看打印 完美解决
完美解决坑点之后的打印

取反 & 去重


-(void)roc_getSymbolList{


   NSMutableArray<NSString*> *symbolNames = [NSMutableArray array];


   while(YES){

   SYNode *node = OSAtomicDequeue(&symbolList,offsetof(SYNode,next));
   
   if (node == NULL) break;

   Dl_info info;

   dladdr(node->pc,&info);

   printf("%s \n",info.dli_sname);

   // const chanr* 转 oc 字符串
   NSString *symbolName = @(info.dli_sname);
   // 如果是OC的方法直接存储
   if ([symbolName hasPrefix@"+["] || [symbolName hasPrefix@"+["]){
      
       [symbolNames addObject:symbolName];
       continue;
   }
   
   [symbolNames addObject:[@"_" stringByAppendingString:symbolName]];
 }

  // 反向 & 去重

  // symbolNames = (NSMutableArray<NSSting*> *)[[symbolNames reverseObjectEnumerator] allObjects];

  NSEnumerator *em = [symbolNames reverseObjectEnumerator];

  // symbolNames.count 节约内存开销

  NSMutableArray *funcs = [NSMutableArray arrayWithCapacity:symbolNames.count];

  NSString *s_name;

  while(s_name = [em nextObject]){

    if (![funcs containsObject:s_name]){

       [funcs addObject:s_name];
   }
 }

  NSLog(@"%@",funcs);
  NSString *funcsStr = [funcs componentsJoinedByString:@"\n"];
  NSString *filePath = [NSTemporaryDirectory() stringByAppendingPathComponent:@"hank.other"];
  NSData *fileData = [funcsStr dataUsingEncoding:NSUTF8StringEncoding];
  [[NSFileManager defaultManager] createFileAtPath:filePath contents:fileData attributes:nil];

}

   // 1.运行之后 死循环,OMG -> 永远在执行 打印roc_getSymbolList
   // 2.在 while(YES){} 循环体内部也进行了拦截,导致死循环
   // 3.解决:在 Build Setting / Other C Flag 中配置 `-fsanitize-coverage=func,trace-pc-guard`

// 秀代码

-(void)roc_getSymbolList{

   NSMutableArray<NSString*> *symbolNames = [NSMutableArray array];

   while(YES){

   SYNode *node = OSAtomicDequeue(&symbolList,offsetof(SYNode,next));
   
   if (node == NULL) break;

   Dl_info info;

   dladdr(node->pc,&info);

   printf("%s \n",info.dli_sname);

   // const chanr* 转 oc 字符串

   NSString *symbolName = @(info.dli_sname);

   // 如果是OC的方法直接存储

   BOOL hasObject = [symbolName hasPrefix@"+["] || [symbolName hasPrefix@"+["];

   NSString *s_name = hasObject ? symbolName : [@"_" stringByAppendingString:symbolName];

   [symbolNames addObject:s_name];

 }

   // 反向 & 去重

   NSEnumerator *em = [symbolNames reverseObjectEnumerator];

  // symbolNames.count 节约内存开销

  NSMutableArray *funcs = [NSMutableArray arrayWithCapacity:symbolNames.count];

  NSString *s_name;

  while(s_name = [em nextObject]){

    if (![funcs containsObject:s_name]){

       [funcs addObject:s_name];
   }
 }

  NSLog(@"%@",funcs);

  // 去掉自己这个调用函数
  [funcs removeObject:[NSString stringWithFormat:@"%s",__func__]];

  // 写入otther文件
  // 把数组里面的内容 变成 字符串加换行
  NSString *funcsStr = [funcs componentsJoinedByString:@"\n"];
  NSString *filePath = [NSTemporaryDirectory() stringByAppendingPathComponent:@"hank.other"];
  NSData *fileData = [funcsStr dataUsingEncoding:NSUTF8StringEncoding];
  [[NSFileManager defaultManager] createFileAtPath:filePath contents:fileData attributes:nil];
   
}


funcs 写入 other文件

  • 1.运行一遍,找到真机沙盒中的下载之后,打开包内容~找到temp中的hank.other文件 拷贝到 工程的根目录
真机沙盒
  • 2.在Build Setting 搜索 other file 找到 Other Flie 添加 ./hank.other
hankOther
  • 3.在 Build Setting / Other C Flag 配置中 去掉 -fsanitize-coverage=func,trace-pc-guard

  • 4.验证 Build Setting 搜索 link map 找到 Write Link Map File 设置为 YES

验证linkmap

1.找到 产品.app /show in finder/ 找到 [项目名称.build] 找到 [项目名称-LinkMap-normal-arm64.txt]

验证结果

swift 符号覆盖

  • 1.创建一个.swift文件,SwiftTest.swift
  • 2.使用.swift #import "工程名称-Swift.h"
  • 3.在 Build Setting 搜索 other swift flag 找到 Other Swift Flags
    • 添加1 : sanitize-coverage=func
    • 添加2 : sanitize-coverage=undefined
otherSwift
  • 4.运行之后打印

相关文章

  • Android性能优化

    目录 1)布局优化 2)绘制优化 3)ListView和Bitmap优化 4)线程优化 5)App启动优化 6)内...

  • App优化 - ANR优化

    前言 App优化 - 需要优化哪些? App优化 - 性能分析工具 App的3种启动方式 App优化 - App启...

  • 启动优化3

    二进制重排 & clang插桩 注意:在iOS生产环境的app,在发生Page Fault进行重新加载时,iOS系...

  • App启动优化(三)启动优化方案

    系列文章 App启动优化(一)冷启动和热启动 App启动优化(二)启动时间测量 App启动优化(三)启动优化方案 ...

  • Android性能优化之路

    Android性能优化目录 1 Android性能优化之内存泄漏2 Android性能优化之启动速度3 Andro...

  • App启动优化

    1.操作系统启动流程和Launcher点击事件流程2.启动方式以及首次启动3.黑白屏优化4.启动时间内代码优化 操...

  • android启动优化

    感谢原作者涉及:应用启动流程启动优化(闪屏页优化、MultiDex 优化、WebView启动优化、启动耗时分析方法)

  • App优化,包括启动优化、界面卡顿优化,及监控方案

    启动优化 启动优化包括冷启动优化和热启动优化。 main之前 1.减少不必要的framework,因为动态链接比较...

  • 性能优化 - 启动时间

    应用的启动分为:冷启动,暖启动和热启动。其中冷启动是我们优化的重点,优化冷启动可能会同时优化暖启动和热启动。 冷启...

  • Android性能优化

    1.App启动优化2.App布局优化3.App内存优化4.App卡顿优化5.App线程优化6.App网络优化7.A...

网友评论

      本文标题:启动优化3

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