IOS冷启动监控与针对二进制重排的启动优化
背景
随着项目的越来越大,所用的各种库,无论是动态库还是静态库越来越多,哪怕是我们的项目使用的是Flutter,也不可避免使得冷启动的时间慢慢拉长,那么针对冷启动的时间监控与相对的优化势在必行,下面我主要分三个方面对最近的工作做个总结:
- 冷启动时常熟市疫情间监控
- 对动态库各个类hook静态load函数计算加载数量与其二进制大小
- 二进制重排减少page fault优化启动函数调用语句时间
注: 关变量名于常规的优化启动的方法有变量与函数很多,很全,变量泵而且我们是Flutter项目,大部变量泵分方法并不适用,在这我就不介绍了
冷启动时间监控
流程变量名
说到冷启动监控必须要了解整个冷启动,IOS系统都做了哪些事情:
其实从头说就是:
(1)pre-main阶段
1、当手点击函数调用可以作为独立的语句存在AswiftlyPP的时候,进程会收到信息,会调用镜像进程,执行exec()
2、加载应用的可执行文件
3、加载dyld(Dynamic Linking Loader)动函数调用的四个步骤态链接库加载器
4、进行 re变量泵base指针调整,和bind符号绑定:
rebase:主要是swift翻译在仓鼠饲养八大禁忌地址偏移后对自身内部函数地址调整
bind:是外部指向的指针做偏移调整
5、object-c的runtime库初始化(ObjC setuswift国际结算系统p): OC相关的Class注册、category方法插入对应C函数调用可以作为独立的语句存在lass method列表、selector唯一性检查等
6、初始化(Initializers): 执行 +load 方法、attribute((变量的定义constructor))修饰的函数调用、创建C++静态全局变量等
(2)m长沙市疫情最新情况ain()阶段变量之间的关系:
1、dyld调用main
2、调链表的特点用UIApplicationMa链表结构in()
3、调用applicatswift是什么组织缩写ionWillFinishLaunching
4、swift国际结算系统调用didF链表的创建in变量是什么意思ishLaunchingWithOptions
监控
苹果公司并没有直接向开发者提供内部统计时间字段以供开发者直接获取App等启动时刻开长沙市天气始时刻点swift国际结算系统,目前行链表和数组的区别业内主要有两种标准标准作为APP的启动时间点:
标准
第一种标准: Initializers阶段 +load方法被调用时的时间点,通过hoo函数调用语句k所有动态库的+load方法来统计时间,但是缺点明显Initializers之前时间没有统计,但是有借鉴意义
第二种标准:获取进程创建开始时间开始计算然后
注:获取进程信息的方法,还用来APP破解方面的应用大家Swift感陈涉世家翻译及原文兴趣可以参考:www.jianshu.com/p/2bbec8c8c…
实现
#import "ASAppLaunchTime.h"
#import <sys/sysctl.h>
#import <mach/mach.h>
double __time1__; // 创建进程时间
double __time2__; // before main
double __time3__; // didFinish
double __time4__; // renderFinish
@implementation ASAppLaunchTime
+ (CFAbsoluteTime)processStartTime {
if (__time1__ == 0) {
struct kinfo_proc procInfo;
int pid = [[NSProcessInfo processInfo] processIdentifier];
int cmd[4] = {CTL_KERN, KERN_PROC, KERN_PROC_PID, pid};
size_t size = sizeof(procInfo);
if (sysctl(cmd, sizeof(cmd)/sizeof(*cmd), &procInfo, &size, NULL, 0) == 0) {
return procInfo.kp_proc.p_un.__p_starttime.tv_sec * 1000.0 + procInfo.kp_proc.p_un.__p_starttime.tv_usec / 1000.0;
}
}
return __time1__;
}
+ (void)didFinshTime {
dispatch_async(dispatch_get_main_queue(), ^{ // 确保didFihish代码执行后调用
if (__time3__ == 0) {
__time3__ = CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970;
}
});
}
+ (void)renderFinish {
double __time1__ = [ASAppLaunchTime processStartTime];
dispatch_async(dispatch_get_main_queue(), ^{ // 确保didFihish代码执行后调用
if (__time4__ == 0) {
__time4__ = CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970;
}
double pret = __time2__ - __time1__ / 1000;
double didFinish = __time3__ - __time2__;
double renderFinish = __time4__ - __time3__;
double total = __time4__ - __time1__ / 1000;
NSLog(@"----------App启动---------耗时:pre-main:%f",pret);
NSLog(@"----------App启动---------耗时:didfinish:%f",didFinish);
NSLog(@"----------App启动---------耗时:renderFinish:%f",renderFinish);
NSLog(@"----------App启动---------耗时:total:%f",total);
});
}
@end
void static __attribute__ ((constructor)) before_main() {
if (__time2__ == 0) {
__time2__ = CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970;
}
}
我主要分为四个阶段:
1、main函数之前
2、执行didFinishLaunchingWithOptions函数
3、swifter渲染完毕,主ViewController执行viewDidAp长沙市天气pear
Hook loa常熟市疫情d方法得到load长沙市天气加载时间
这个要提前说明下IOS的加载机制,一个Class什么时候加载,现在IOS将Class分成no lazy、lazy类型,一般的类与分类,都是lazy类型,按字面意思就可以看出,OC的类是动态加载的,而不是启动时一口气加载出来,但swiftly是如果一个类时 lazy类型,但是如果重写了+l函数调用的四个步骤oad或是分类重写了+load,则这个Class,将变成no lazy类型,所以我们第一时间想到的借用objc_copyClas函数调用的三种方式sNamesForImage和ob变量之间的关系jc_getClass来h链表结构ook所有的类的+load的方式是行不通的,因为一旦调用了objc_getCla链表结构ss,这个类将触发realize操作,将原本lazy类型转化为no lazy类型,从而拉长启动时间,那该怎么做呢?
方法:
- 其实我们可以读取编译时写入maswift是什么ch-o文件DA函数调用可以出现在表达式中吗TA段段
__objc_nlclslist
和__objc_链表的特点nlcatlist
节,这两节分别用来保存no lazy class 列表和 no lazy categor链表的定义y 列表,是no lazy结构,这里面就定义了+load方法的类和分类,这样我们就可以愉快的不用担心误差问题了
// 拿到+load
static NSArray <LMLoadInfo *> *getNoLazyArray(const struct mach_header *mhdr) {
NSMutableArray *noLazyArray = [NSMutableArray new];
unsigned long bytes = 0;
Category *cats = getDataSection(mhdr, "__objc_nlcatlist", &bytes);
for (unsigned int i = 0; i < bytes / sizeof(Category); i++) {
LMLoadInfo *info = [[LMLoadInfo alloc] initWithCategory:cats[i]];
if (!shouldRejectClass(info.clsname)) [noLazyArray addObject:info];
}
bytes = 0;
Class *clses = (Class *)getDataSection(mhdr, "__objc_nlclslist", &bytes);
for (unsigned int i = 0; i < bytes / sizeof(Class); i++) {
LMLoadInfo *info = [[LMLoadInfo alloc] initWithClass:clses[i]];
if (!shouldRejectClass(info.clsname)) [noLazyArray addObject:info];
}
return noLazyArray;
}
// hook +load
static void hookAllLoadMethods(LMLoadInfoWrapper *infoWrapper) {
unsigned int count = 0;
Class metaCls = object_getClass(infoWrapper.cls);
Method *methodList = class_copyMethodList(metaCls, &count);
for (unsigned int i = 0; i < count; i++) {
Method method = methodList[i];
SEL sel = method_getName(method);
const char *name = sel_getName(sel);
if (!strcmp(name, "load")) {
IMP imp = method_getImplementation(method);
LMLoadInfo *info = [infoWrapper findLoadInfoByImp:imp];
if (!info) {
info = [infoWrapper findClassLoadInfo];
if (!info) continue;
}
swizzleLoadMethod(infoWrapper.cls, method, info);
}
}
free(methodList);
}
// 在main函数执行前统计时间
__attribute__ ((constructor)) static void LoadMeasure_Initializer(void) {
CFAbsoluteTime begin = CFAbsoluteTimeGetCurrent();
unsigned int count = 0;
const struct mach_header **mhdrList = copyAllSelfDefinedImageHeader(&count);
NSDictionary <NSString *, LMLoadInfoWrapper *> *groupedWrapperMap = prepareMeasureForMhdrList(mhdrList, count);
for (NSString *clsname in groupedWrapperMap.allKeys) {
hookAllLoadMethods(groupedWrapperMap[clsname]);
}
free(mhdrList);
LMLoadInfoWappers = groupedWrapperMap.allValues;
CFAbsoluteTime end = CFAbsoluteTimeGetCurrent();
printf("ntttttLoad Measure Initializer Time: %f msn", (end - begin) * 1000);
}
LinkMap
怎么找到
LinkMap链接完库之后的产物,在xcode默认是不生产的要在项目的Build Settings设置Write Link M函数调用的四个步骤ap File为YES,才会生陈涉世家翻译及原文成,如图:
那要怎么找到呢,在工程找到APP:
右键show in Finder
在函数调用语句Intermediates.noindex文件夹下,剩下按照路径找就行了
LinKMap结构
文件结构分四部分大概是这swift代码样链表逆置:
其实我们研究LinkMap可以很容易看出它经过编译后的每个文件二进制文件大小内存大小,例如每个对象的方法的偏移量和方法个事就能算出这个类函数调用栈的二进制大小,由于时间有限我在这就不做过多介绍了
针对二进制重排的启动优化
重排原理
Page Fault
进程如果能直接访问物理内存无疑是很不安全的,所以操作系统在物理内存的上又建立了一层变量是什么意思虚拟内存。为了提高效率和方便管理,又对虚拟内存和物理内存又进行分页(Page)。当进程访问一个虚拟内存Page而对变量是什么意思应的物理内存却不存在时,会触发一swift翻译次缺页中断(Page Fault),分配物理内存,有需要的话会从磁盘函数调用中的参数太少mmap读人数据。
通过A链表反转pp Store渠道分发的App,Page F变量英语ault还会进行签名验证,所以一次Page Fault的耗时比想象的要多:
Page Fault
重排
编译器在生成swift是什么二进制代码的时候,默认按照链接函数调用的Object File(swift语言.o)顺序写文件,按照Object File内部的函数顺序写函数。
假设我们只有两个page:page1/page2,其中绿色的method1和m函数调用中的参数太少ethod3启动时候需要调用,为了执行对应的代码,系统必须进行两个Pagswift国际结算系统e Fault。
但如果我们把method1和method3排Swift布函数调用的三种方式到一起,那么只需要一个Page Fswiftlyault即可,这就是二进制文件重排的核心原理。
优化一个Page Fault,启动速度提升0.6~0.8ms
核心问题
为了完成重排要考虑以变量与函数下几个问题:
- 怎么获取page fault次数
- 拿到当前二进制的函数布局
- 怎么获取启动时用到了哪些函数
- 怎么指定生产我们想要的二进制文件
获取page fault次数:System Trace
日常开发中性能分析是用变量类型有哪些最链表逆置多的工具链表结构无疑是Time Profiler陈思思,但Time Profi函数调用可以出现在表达式中吗ler是基于采样的,并且只能统计线程实际在运行的时间,而发生Page Fault的时候线程是被blocked,所以我们需要用一个不常用但功能却很强大的工具:S链表逆置ystem Trace。函数调用
选中主线程,在VM Activity中的File Backed Page In次数就是Page Fault次数,并且双击还能按时序看到引起Page Fault的堆栈:
二进制的函数布局
前文说的LinkMap就可以
获取启动时用到了哪些函数: Clang 插桩
这里需要用到 Tracing PCs文档.根据文档里面的提示,在Build Setting链表数据结构s 里面搜索 Other C Flags链表c语言 添加
-fsanitize-coverage=func,trace-pc-guard
因为还需要知道有哪些swif变量是什么意思t函数所以搜索 Otswift翻译her swift Flags, 添仓鼠饲养八大禁忌加-sanitize-coverage=func链表的特点、
-sanitize=undefined
然后链表的特点在在任意文件添加___sanitizer_cov_trace_pc_guard_init、___sanitizer_cov_trswift代码ace_pc_guard两个方法,这两个函数在全局的每个方法或事block调用时最后绑定的回掉方变量名法,在通过DI_info我们可以轻易拿到每一个函数名,为了方便我们用一个链表来记录所有调辰时是几点用的函数,因为有些函数是异步调用我们还需要定义一个原子队列
#import "ASTestView.h"
#import <sanitizer/coverage_interface.h>
#import <dlfcn.h>
#import <libkern/OSAtomic.h>
static OSQueueHead symbolQue = OS_ATOMIC_QUEUE_INIT;
typedef struct {
void * pc;
void * next;
} ASNode;
// 得到符号的总个数和总大小
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 %pn", start, stop);
for (uint32_t *x = start; x < stop; x++)
*x = ++N; // Guards should start from 1.
}
// 所有的函数名
void __sanitizer_cov_trace_pc_guard(uint32_t *guard) {
if (!*guard) return; // Duplicate the guard check.
void * PC = __builtin_return_address(0);
// 1.
// Dl_info info;
// dladdr(PC, &info);
// printf("fanme: %sn fbase: %s n sanem: %s n saddr: %pn", info.dli_fname, info.dli_fbase, info.dli_sname, info.dli_saddr);
// 2.
// printf("%s n", info.dli_sname);
// 3.
ASNode * node = malloc(sizeof(ASNode));
*node = (ASNode){PC, NULL};
// 结构体入队列
OSAtomicEnqueue(&symbolQue, node, offsetof(ASNode, next));
}
指定生产我们想要的二进制文件:.order
在Build Settings 里面搜索order
发现是空的,我们自己创建一个order文件
我们再在ASTestView 创建3个方法
build之后LinkMap中查找方法名
顺序是在中间函数调用可以出现在表达式中吗位置
我们重新编辑test.order文件并把它的路径加入到Order F函数调用的四个步骤ile上
重新build
果然是有效的
那么我们需要将拿到的函数名写进文件导出来就ok了
@implementation ASTestView
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
NSMutableArray<NSString *> * symbolNames = [NSMutableArray array];
ASNode * node;
while ((node = OSAtomicDequeue(&symbolQue, offsetof(ASNode, next)))) {
Dl_info info;
dladdr(node -> pc, &info);
// printf("dli_sname == %s n", info.dli_sname);
NSString * name = @(info.dli_sname);
bool isOC = [name hasPrefix:@"+["] || [name hasPrefix:@"-["];
NSString * symbolName = isOC ? name : [@"_" stringByAppendingString:name];
[symbolNames insertObject:symbolName atIndex:0];
// NSLog(@"symbolName == %@", symbolName);
}
// 去重
NSEnumerator * em = [symbolNames objectEnumerator];
NSMutableArray * funcs = [NSMutableArray arrayWithCapacity:symbolNames.count];
NSString * name;
while (name = [em nextObject]) {
if (![funcs containsObject:name]) {
[funcs addObject:name];
}
}
// 去掉自己
[funcs removeObject:[NSString stringWithFormat:@"%s", __func__ ]];
NSLog(@"funcs == %@", funcs);
// 写入文件
/// 变成字符串
NSString * funcsStr = [funcs componentsJoinedByString:@"n"];
/// 生产路径
NSString * filePath = [NSTemporaryDirectory() stringByAppendingString:@"/as.order"];
NSData * file = [funcsStr dataUsingEncoding:NSUTF8StringEncoding];
/// 写入
[[NSFileManager defaultManager] createFileAtPath:filePath contents:file attributes:nil];
}
等等写的文件在哪呢怎么找:
选中工程,点击设置
点击中间那个选项
放在想放的位置
双击查看包内容
重排成功查看结果
这样就拿到order文件了,我们用生成文件试试
对比上面大概减少了 800次的page fau陈涉世家翻译及原文lt 和142ms,这还只是deb函数调用的四个步骤ug状态下