iOS启动优化-二进制重排与Clang插桩

背景

  • 随着app的迭代,日常业务变多,项目复杂度变高,引起app的启动越来越缓慢
    那怎么优化app的启动呢?
    一般app启动分main函数之前, 和main函数之后,那么App的启动优化也可在main之前(pre-main)和main之后。
    最早执行代码的地方为+load方法。那么有什么方式可以控制这些顺序吗?
  • 大民哥带你认识iOS中二进制重排与Clang插桩

优化方案

Dyld反馈资源浪费:毫秒级
业务逻辑
Main函数之前 pre-main
Main函数之后

准备

  • objc源码
  • clang脚本

认识

插件安装

在这里插入图片描述

启动优化

  • iOS检测:dyld会把app启动耗时反馈给我们
    1,dyld重签名,然后设置PRINT_STSTISTICS
    请添加图片描述
    运行:
    请添加图片描述
    Dyld反馈等都是资源的浪费,基本上也是毫秒级的浪费!
    我们需要看的是
  • dylib loading:苹果建议的动态库载入不超过6个
  • rebase:虚拟内存,虚拟内存载入到物理内存发生缺页异常时,根据ASLR重写矫正地址,随机起始值 不同偏移量,ASLR+offsert
  • binding:绑定,以懒加载绑定
  • ObjC:OC类的注册,减少类的定义,分类的定义(实际项目中去除废弃不用的类)
  • initalizer:执行+load、构造函数的耗时
    下面的几个系统库已做了高度优化,不做研究。

请添加图片描述

操作系统

演进史

  • 物理内存的时代:内存不足,不安全,
  • 物理内存切片时代:使用懒加载,但内存不连续了,麻烦和不安全
  • 虚拟内存时代:使用虚拟内存 存放于映射表中,CPU的MMU(翻译地址)翻译成物理地址,然后到物理内存中找到物理内存Page(页表),PageiOS中16k,Mac中4k,解决内存不够用的问题,进程与进程之间的安全隔离保证了内存的安全,每次访问只访问虚拟内存对应的物理地址的那一页数据。
  • 按需加载,分页加载
PAGESIZE

请添加图片描述

  • 当虚拟页表中的内存在物理内存中没有的时候会进入缺页中断Pagefault
  • 这个时候新的虚拟内存要加入物理内存中,执行LRU算法,在物理内存中覆盖不活跃的进程
  • 虚拟页表中最后会有些空的内存空间,访问时候为NULL
  • 在64位系统里面,虚拟内存8G可以访问小于4G物理内存,因为有4个G的空间不让使用,因为要隔离兼容前面32位系统的4个G,
    请添加图片描述
    段:MachO文件格式,可变
    页:内存里面的单位,固定
    CPU数据吞吐量(数据总线),32位4字节,64位8字节

进程通信

  • 进程间通信,是通过kernel发信号,虚拟内存的共享缓存空间 访问物理内存的共享访问空间

二进制重排

如果同时有大量缺页异常,那就影响启动时间了(冷启动)

  • PageFault(缺页异常/中断)毫秒级

引入脚本appSign.sh,注入WeChat二进制文件.app
在这里插入图片描述
请添加图片描述
请添加图片描述
第一次启动(冷启动),我们看到缺页异常6000+次,启动耗时1秒多,当再启动时(热启动),缺页异常1000+次,启动耗时3.多毫秒。
那么我们怎么优化冷启动时间呢?(主要跟PageFault浪费有关)

在这里插入图片描述

请添加图片描述
在这里插入图片描述

  • 排列二进制时,把启动时需要调用的方法全部向前排,最大化优化启动时Pagefault次数
    请添加图片描述

.Order文件

在这里插入图片描述
order文件里的顺序就是给编译器看的编译顺序文件。

  • 第一步:工程目录文件建立.order文件
  • 然后,在.order文件中写入编译顺序
  • Xcode->Build Settings -> order File写入(./文件名称.order)

小节问题

二进制重排是没问题了,那么问题来了,App启动时方法的调用顺序又是什么呢?
还有OC与Swift混编的调用顺序呢?
方法,c函数,Block这些顺序怎么看呢?

  • hook objc_msgSend();能解决OC方法,但其他的又都hook不到,那么Clang就来了

Clang插桩

Clang会读程序中所有的代码

Clang插桩配置

Clang找到Trance PC
添加-fsanitize-coverage=trace-pc-guard到工程Other C Flag:
在这里插入图片描述

Clang插桩原理(获取及通过原子队列保存符号)

然后在ViewController里面声明头文件和实现:

#include <stdint.h>
#include <stdio.h>
#include <sanitizer/coverage_interface.h>
#import <dlfcn.h>
#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);
  //指针偏移4字节间隔,最后一个数据为stop - 4,防止内存溢出
  for (uint32_t *x = start; x < stop; x++)
    *x = ++N;
}

//启动时调用了哪些方法函数以及顺序,在这里都会被hook,一切的回调函数,无论线程
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};
    
    //结构体入栈,加入结构(链表结构)!- 列表头,存入node,标记SYNode类型,next:把下一个内存地址返回给node的next
    OSAtomicEnqueue(&symbolList, node, offsetof(SYNode, next));
}
//写入.order文件
-(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:@"hank.order"];
    //文件内容
    NSData * fileContents = [funcStr dataUsingEncoding:NSUTF8StringEncoding];
    [[NSFileManager defaultManager] createFileAtPath:filePath contents:fileContents attributes:nil];
}

在这里插入图片描述
监控到方法个数。
我们生成的项目方法函数调用及顺序表:
在这里插入图片描述

  • 只要添加Clang插桩标记,编译器会在所有方法,函数,block代码实现边缘添加一句代码__sanitizer_cov_trace_pc_guard,代表其方法的调用

坑点

坑点Clang在做代码跟踪时,不仅把方法,函数,block拦截,进入循环体内也进行了拦截,然后一直死循环,比如:touchesBegan
解决方式:
在Xcode->TARGETS->Build Settings的Other C Flags添加参数func设置仅拦截方法:
在这里插入图片描述

取反&去重

数组取反:

 NSEnumerator * enumerator = [symbolNames reverseObjectEnumerator];

去重:

//创建一个新数组
    NSMutableArray * funcs = [NSMutableArray arrayWithCapacity:symbolNames.count];
NSString * name;
    //去重!
    while (name = [enumerator nextObject]) {
        if (![funcs containsObject:name]) {//数组中不包含name
            [funcs addObject:name];
        }
    }

生成.order文件

生成文件:

//文件路径
    NSString * filePath = [NSTemporaryDirectory() stringByAppendingPathComponent:@"hank.order"];
    //文件内容
    NSData * fileContents = [funcStr dataUsingEncoding:NSUTF8StringEncoding];
    [[NSFileManager defaultManager] createFileAtPath:filePath contents:fileContents attributes:nil];

Swift方法处理(符号覆盖)

因为 Swift与OC不是同一个编译器。
创建添加.Swift文件,然后在OC代码中导入工程名- Swift.h并build

Swift环境配置

Xcode->Build Setting -> Other Swift Flags(添加-sanitize-coverage=func-sanitize=undefined)
在这里插入图片描述
然后编译:
在这里插入图片描述

  • 看到Swift相应的类和方法,由于做了混淆,优化了性能。
在Linux CentOS环境下,离线安装LLVM Toolset 7的Clang编译器通常需要一些额外的步骤,因为这涉及到下载编译器的源码包而不是直接的二进制包。以下是离线安装的基本流程: 1. **获取源码**: 首先,访问LLVM的官方网站(https://releases.llvm.org/download.html)找到对应的LLVM 7源码包的tarball,例如`llvm-project-7.x.y.src.tar.xz`。由于是离线安装,你需要提前将这个文件保存到本地。 2. **解压源码**: 使用`tar`命令解压缩下载的源码包: ``` tar -xvf llvm-project-7.x.y.src.tar.xz ``` 3. **进入目录**: 进入刚刚解压后的目录,例如`llvm-project-7.x.y.src`: ``` cd llvm-project-7.x.y.src ``` 4. **配置编译**: 编译前需要创建一个配置文件,告诉编译器在哪里找到必要的库和其他依赖。通常会创建一个`config.sh`文件,内容类似于: ```bash export LLVM_CONFIG_PATH=relative/path/to/your/llvm-config # 你需要的LLVM config的位置 export PATH=relative/path/to/your/bin:$PATH ./configure --prefix=/opt/llvm-toolset-7 --enable-optimized --disable-shared --enable-static ``` 然后替换`relative/path/to/your`为实际的路径。 5. **编译和安装**: 运行`make && sudo make install`来编译和安装LLVM和Clang。注意,这个过程可能会占用大量的磁盘空间和CPU时间。 6. **检验安装**: 安装完成后,你可以使用`which clang`检查Clang的路径,以及运行`clang --version`验证版本是否正确。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

☆MOON

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值