iOS启动优化-二进制重排与Clang插桩
二进制重排与Clang插桩
- 背景
- 优化方案
- 准备
- 认识
- 插件安装
- 启动优化
- 操作系统
- 演进史
- 进程通信
- 二进制重排
- .Order文件
- 小节问题
- Clang插桩
- Clang插桩配置
- Clang插桩原理(获取及通过原子队列保存符号)
- 坑点
- 取反&去重
- 生成.order文件
- Swift方法处理(符号覆盖)
- Swift环境配置
背景
- 随着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的nextOSAtomicEnqueue(&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相应的类和方法,由于做了混淆,优化了性能。
iOS启动优化-二进制重排与Clang插桩相关推荐
- iOS启动优化 —— 二进制重排
iOS启动优化 -- 理论 1. app启动 2. 虚拟内存 & 物理内存 3. 缺页中断(pagefault) 4. 二进制重排 1. app启动 启动的过程一般是指从用户点击app图标开始 ...
- APP启动优化——二进制重排,从入门到精通
一 理论介绍 1.1缺页中断 1.2 Linkmap 1.3 看二进制文件布局 二 探索重排方案 静态扫描+运行时trace. 思维方式,自顶向下的思维方式 Clang SanitizerCovera ...
- vm磁盘映射 不能启动_iOS 启动优化之Clang插桩实现二进制重排
前言 原文作者:李斌同学 原文链接:https://juejin.im/post/6844904130406793224 自从抖音团队分享了这篇 抖音研发实践:基于二进制文件重排的解决方案 APP启动 ...
- 抖音品质建设 - iOS启动优化《实战篇》
前言 启动是 App 给用户的第一印象,启动越慢,用户流失的概率就越高,良好的启动速度是用户体验不可缺少的一环.启动优化涉及到的知识点非常多,面也很广,一篇文章难以包含全部,所以拆分成两部分:原理和实 ...
- iOS 启动优化和安装包瘦身
iOS 启动优化和安装包瘦身 1 启动优化 在iPhone的启动方式中,分为冷启动和热启动两种方式: 1.冷启动(Cold Launch):从零开始启动APP ,需要系统新创建一个进程进行启动,这是一 ...
- iOS启动优化之——如何使用Xcode Log、App Launch、代码来计算启动时间 Launch Time
在iOS启动优化之--如何使用MetricKit 来计算启动时间 Launch Time ,我们提到,可以使用MetricKit 在Organizer中或者直接代码统计,那么还能用什么来统计呢? 配置 ...
- iOS 性能优化-启动优化、main函数之前优化-二进制重排
一个app的启动时间,很大程度会影响用户的体验,所以能优化还是尽量优化的.之前我们已经探究过dyld加载的流程,启动流程分为main函数之前和main函数之后.这里主要做main函数之前的优化建议. ...
- 抖音品质建设 - iOS启动优化之原理篇
前言 启动是 App 给用户的第一印象,启动越慢用户流失的概率就越高,良好的启动速度是用户体验不可缺少的一环.启动优化涉及到的知识点非常多面也很广,一篇文章难以包含全部,所以拆分成两部分:原理和实战. ...
- iOS启动优化(一)
1.启动优化 我们的App如果启动时间过长,会出现白屏的问题.在我们App中,我们一般会集成很多的功能,在启动时,会加载很多的组件以及初始化,这样耗费的时间越多,白屏时间就会越长,用户体验相对来说就会 ...
最新文章
- 在VMWare中配置SQLServer2005集群 Step by Step(四)——集群安装
- Docker 1.7.0 深度解析
- Java在MVC开发模式中使用try-catch以及throws避免踩坑
- SpringBoot上传图片的示例
- 塔式Server 服务器ESXI6.5安装
- nuxt解决首屏加载慢问题_一个 Node 脚本让你的前端项目加载速度飞起来
- [javascript|基本概念|Number]学习笔记
- 新计算机 安装win2000,图文教程!Windows 2000安装过程全接触
- Spring Cloud 服务消费者 Feign (三)
- js对本地文件进行加密_怎么对电脑文件进行加密
- Atitit 上传进度的实现与原理 目录 1.1. 前端	1 1.2. 读取进度	1 1.3. 后端 定时注入进度	1 1.1.前端 wind
- 【组合数学】递推方程 ( 特特解示例 1 汉诺塔 完整求解过程 | 特解示例 2 特征根为 1 的情况下的特解处理 )
- 十二导联动态心电图技术参数
- 【大数据语言】怎样利用Python爬虫,高效获取大规模数据
- 51单片机小车—循迹温湿度检测显示
- UltraISO软碟通安装与刻盘以及安装镜像
- PMP 项目沟通管理
- 微信小程序-JavaScript 3DES对称加密算法加密使用
- PHP - 性能测试工具
- css浮动清除以及BFC
热门文章
- 国网GIM设备三维模型要求细则 - 框架式电容器
- 利用Arduino uno控制24BYJ-48电机正反转停止( 不使用步进库实现方法二)
- G304电量查询与灯光讲解(驱动下载见上篇文章)
- 常用的设计模式(泡妞经典版)
- 人生苦短我用Python 五:ERROR: No matching distribution found for REfo==0.13
- word水印 禁止复制水印
- 修改了DNS服务器网速慢,如何修改DNS让网速快到飞起!教你两招让电视、盒子告别卡顿...
- apche和nginx分别与php的连接方式区别
- 收集的开源代码下载网站
- Android开发项目——智能农业(知识点整理回顾)