1.冷发动
1.1 什么是冷发动?
冷发动是指内存中不包括该应用程序相关的数据,必需求从磁盘载入到内存中的发动进程。
注意:从头翻开 APP, 不一定便是冷发动。
- 当内存不足,APP被体系主动杀死后,再发动便是冷发动。
- 假如在从头翻开 APP 之前,APP 的相关数据还存储在内存中,这时再翻开 APP,便是热发动
- 冷发动与热发动是由体系决议的,咱们无法决议。
- 当然设备重启今后,第一次翻开 APP 的进程,一定是冷发动。
1.2 如何计算冷发动耗时?
一般来讲,计算 APP 发动时长,以main 函数为节点,分两个大阶段:
- main 函数之后的代码,是咱们自己写的,咱们能够自行计算进入 main 函数到第一个界面显示的耗时。
-
- 在main 函数里打印一下当时的时间,
- 在第一个要显示的控制器的viewDidLoad 办法中打印一下当时时间
- 两个时间的差值,即为main函数后的加载时长。
- main 函数之前,为pre-main阶段,由所以体系在做工作,这段时间的 耗时,咱们没办法直接计算,需求查 看体系给咱们的反应。
1.2.1 pre-main阶段都做了什么?
接下来看一下项目中的pre-main阶段的耗时。
- 检查体系给的反应需求 添加一个环境变量,
- 添加路径:在 Xcode -> Edit Scheme -> Run -> Arguments -> Environment Variables 中,
- 添加一个环境变量 DYLD_PRINT_STATISTICS:1。
下图是我项目的加载耗时:
耗时进程分为以下4部分:
- dylib loading time : 是指动态库加载的耗时,体系的动态库做过优化,耗时较少。 苹果官方推荐最多不要超过6个外部动态库,多余6个,需求考虑兼并动态库,兼并动态库对于发动时期的优化,十分有效。 像微信的动态库前期有八九个,现在也优化成6个了。
- rebase/binding
- rebase:是指地址的 偏移批改耗时。
-
- 在编译生成二进制文件的时分,每个函数都有一个地址,这个地址是相对于二进制文件的偏移地址。
- 在发动时,也便是在二进制文件在加载到虚拟内存的时分,为了安全起见,苹果有个安全机制(ASLR),会在整个二进制文件的最前面,随机加一个偏移值。
- 比方 A 函数,相对于二进制文件的偏移值是 0x003。 发动时,整个二进制文件被分配了一个随机值0x100。 那么A 函数在内存中的实际地址是 0x003 + 0x100 = 0x103。
- 偏移批改指的便是计算办法在虚拟内存中的地址的进程!
- binding: 动态库的办法绑定,是指将办法姓名与办法的完成进行绑定进程的耗时。
-
- 比方 NSLog 办法,在加载的时分需求先找到Foundation库,再找到库里的NSLog的办法的完成,将办法姓名和办法完成绑定在一起。
- Objc setup time: 注册一切 OC类 耗时, 类越多耗时越多,有人计算过2万个自界说的OC的类,大约耗时800毫秒。删去不用的类,能够削减耗时。
- initializer time:load办法 和 C++结构函数的耗时.削减重写load办法,尽量将工作延迟到 main 办法今后,能够削减耗时。
- slowest intializers : 指出了最耗时的几个库是下面的6个库(最后一个是我的项目)。
1.2.2 pre-main阶段耗时优化办法总结:
- 削减外部动态库的数量
- 不用的类和办法,删去
- 类尽量运用懒加载,也便是尽量不要重写load办法。
- 发动时加载的数据运用多线程
- 运用纯代码。不用xib storyboard(要额外进行代码解析转化和页面的渲染)
以上办法,都是和自己的项目代码息息相关的优化计划。不同项目具体是实施动作不相同。
还有一个优化办法,不管是什么项目,实施动作都相同 ,对什么项目都有效,那便是二进制重排!
2. 二进制重排
学习二进制重排,首先要知道数据是如何加载到内存中的 。
咱们已经知道数据加载到内存的进程,当虚拟内存页还没有对应的物理内存页时,会呈现缺页反常(PageFault)。
当冷发动时,物理内存中是没有数据的,这时会呈现很多的缺页反常,在iOS出产环境的app,在发生Page Fault进行从头加载时,iOS体系还会对其做一次签名验证,因此 iOS 出产环境的 Page Fault 比Debug环境下所产生的耗时更多
这儿有没有优化空间呢?接下来便是优化计划:二进制重排!
在了解二进制重排之前,再了解下在项目编译生成二进制文件的时分,类及其内部办法完成的摆放次序是什么样的呢?
2.2.1 二进制文件中办法完成排序是什么样的?
- 在 viewController 中,先随意写几个办法。
- 再看下源文件的编译次序
接下来检查 Link Map文件检查符号次序, 检查办法:
- 翻开link map
****
- 编译生成link map 文件
- 找到link map 文件
- 项目目录中,生成的 app 右键,show in Finder
- 找到 app 的上上级目录
- 进入Intermediates.noindex ->TraceDemo.build -> Debug-iphonesimulator -> TraceDemo.build -> TraceDemo-LinkMap-normal-x86_64.txt
- 翻开link map 文件,找到自己的类及办法的姓名
5.咱们能够直观的看出 link map中符号的次序,类是以源文件的编译次序,从上到下按序摆放。办法名是以类中办法的书写次序,由上到下排序。
2.2.2 为什么需求二进制重排?
从源码的履行次序上看,应该是 load -> test2 -> viewDidLoad -> test1.
可是二进制文件中符号的次序是办法从上到下的书写次序,没有依照调用次序去摆放。
在冷发动分页加载二进制文件时,发现很多页中都有发动时需求用到的办法,那么即使页里边也存在发动时不需求的办法,可是由于内存是分页管理的,要加载就要整页加载。这样就导致了很多不需求在 pre-main 阶段履行的办法,也会被加载到内存中,添加了发动的耗时。
例如,发动需求加载100个页,每个页能够包括20个办法。可是每个页里只有2个办法是发动时后用到的。这样实际上发动时必需求的办法是2 * 100 = 200个,假如将这200个办法紧挨着放在一起,那么只需求2页。比100个页,削减了98页。这样耗时就会大大下降。
2.2.3如何进行二进制重排?
1. 二进制重排的办法
在项目编译生成二进制文件的时分,找到发动时需求的办法,并且将它们放在一起 从头排序,这便是二进制重排。
两个要害点: 找到发动时需求办法 & 办法 的重排序
2.办法的重排序:
重排序其实很简单。xcode已经为咱们提供了这个机制,它运用的链接器叫做 ld, ld有一个参数叫做Order File, 咱们能够经过配置order文件,来使编译时生成的二进制的文件的Link Map种的符号次序,依照咱们指定的次序摆放生成。并且 libobjc 实际上也做了二进制重排 。
【第一步】在项目根目录下建一个xxx.order的文件,里边写上依照自己想摆放的次序,写上办法或许函数的姓名。(假如写了一个不存在的符号,也不会报错,会被主动过滤掉~)
【第二步】在 Build Settings搜索order file的文件。将项目根目录创立的文件,设置上去。
【第三步】从头编译,检查 Link Map 文件的次序,果然,依照咱们指定的次序摆放啦!
3. 静态插桩 – 找到冷发动时的一切办法
接下来,需求做的便是写入 order 文件里的符号了,咱们不可能手写上一切的发动时需求的履行的符号,这儿的一切符号包括,调用的办法、函数、C++结构办法、swift办法、block。
这儿运用LLVM 内置的简单代码覆盖率检测工具(SanitizerCoverage)。它在边际、 函数、基本块 等级上刺进对用户界说函数的调用。
-
edge
(默许):检测边际(一切的指令跳转都会被刺进对用户界说函数的调用, 如循环、分支判断、办法函数等)。 -
bb
:检测基本块。 -
func
:仅将检测每个 功能的输入块(这个便是咱们要重排序的符号)。
依照文档,
- 【第1步】搜索并设置Other C Flags/ Other C++ Flags为 ****-fsanitize-coverage=func,trace-pc-guard (这儿要用func, 不能用默许的edge, 不然会形成死循环)。
- 假如有swift ,需求设置Other Swift Flags设置为**** -sanitize-coverage=func -sanitize=undefined
- 【第2步】编译器将刺进对模块结构函数的调用,所以咱们要完成这个办法:
__sanitizer_cov_trace_pc_guard_init(uint32_t *start, uint32_t *stop);
经过打印start, stop 地址的内容,从 start 地址开始,到 stop 地址的前4位,存储的是 uint32 的1-19的数字。
咱们能够从这个函数中知道, 当时项目中自界说的功能输入块的数量。
- 【第3步】编译器会在生成二进制文件的时分,在每个func调用之初,刺进以下代码:
__sanitizer_cov_trace_pc_guard(&guard_variable)
也便是说,每个办法在履行的时分,都会调用上面这个办法。 接下来:
-
-
- 咱们要完成这个办法,并在这个办法里,获取到本办法结束后要回来的地址
-
// 获取到本办法结束后,要回来的地址去,这个地址包括在被hook的办法内部,但不是被hook 的办法的首地址
void *PC = __builtin_return_address(0);
-
-
- 并将地址保存一个体系的原子行列(( 底层实际上是个栈结构 , 使用行列结构 + 原子性来保证次序 ))中,运用原子行列,是为了避免多线程资源抢夺。原子行列的存值办法如下:
-
// 将结构体存入到原子行列中。
// offsetof(type,member) 回来结构体中成员的偏移值,由于指针PC是8字节,所以这儿回来8字节。
// 详见下图
OSAtomicEnqueue(&symbolList, node, offsetof(SYNode, next));
每个 SYNode 首地址都距离上一个偏移 PC 所占的字节数。这样做的妙处便是,每个SYNode 的next 的地址,恰巧便是下一个结构体的地址。这样方便获取行列里边的一切数据。
- 【第4步】咱们在点击屏幕的事件中
-
- 把存储到原子行列中的地址遍历出来,
- 根据地址获取当时地址地点的办法的名称并存入数组中,
typedef struct dl_info {
const char *dli_fname; /* 地点文件 */
void *dli_fbase; /* 文件地址 */
const char *dli_sname; /* 符号名称 */
void *dli_saddr; /* 函数起始地址 */
} Dl_info;
//这个函数能经过函数内部地址找到函数符号
int dladdr(const void *, Dl_info *);
-
- 由于原子行列是栈结构,先进后出,所以咱们需求将数组倒序摆放
- 由于办法可能会被屡次调用,咱们需求去重
- 再将最后咱们当时点击屏幕的办法删去掉
- 将办法姓名的数组,转成字符串,写到沙盒文件中
完整代码如下:
//
// ViewController.m
// TraceDemo
//
// Created by hank on 2020/3/16.
// Copyright 2020 hank. All rights reserved.
//
#import "ViewController.h"
#import <dlfcn.h>
#import <libkern/OSAtomic.h>
#import "TraceDemo-Swift.h"
@interface ViewController ()
@end
@implementation ViewController
+(void)initialize
{
}
void(^block1)(void) = ^(void) {
};
void test(){
block1();
}
+(void)load
{
}
- (void)viewDidLoad {
[super viewDidLoad];
[SwiftTest swiftTestLoad];
test();
}
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
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);
NSString * name = @(info.dli_sname);
BOOL isObjc = [name hasPrefix:@"+["] || [name hasPrefix:@"-["];
NSString * symbolName = isObjc ? name: [@"_" stringByAppendingString:name];
[symbolNames addObject:symbolName];
}
//取反
NSEnumerator * emt = [symbolNames reverseObjectEnumerator];
//去重
NSMutableArray<NSString *> *funcs = [NSMutableArray arrayWithCapacity:symbolNames.count];
NSString * name;
while (name = [emt nextObject]) {
if (![funcs containsObject:name]) {
[funcs addObject:name];
}
}
//移除本办法
[funcs removeObject:[NSString stringWithFormat:@"%s",__FUNCTION__]];
//将数组变成字符串
NSString * funcStr = [funcs componentsJoinedByString:@"\n"];
NSString * filePath = [NSTemporaryDirectory() stringByAppendingPathComponent:@"demo.order"];
NSData * fileContents = [funcStr dataUsingEncoding:NSUTF8StringEncoding];
[[NSFileManager defaultManager] createFileAtPath:filePath contents:fileContents attributes:nil];
NSLog(@"%@",funcStr);
}
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.
}
//原子行列
static OSQueueHead symbolList = OS_ATOMIC_QUEUE_INIT;
//界说符号结构体
typedef struct {
void *pc;
void *next;
}SYNode;
void __sanitizer_cov_trace_pc_guard(uint32_t *guard) {
// 会导致load 办法被return
// if (!*guard) return;
// 获取到本办法结束后,要回来的地址去,这个地址包括在被hook的办法内部,但不是被hook 的办法的首地址
void *PC = __builtin_return_address(0);
SYNode *node = malloc(sizeof(SYNode));
*node = (SYNode){PC,NULL};
//进入
OSAtomicEnqueue(&symbolList, node, offsetof(SYNode, next));
}
@end
2.2.4 如何验证二进制重排的作用?
1.检查缺页反常数量Page Fualt:
- 检查一下项目的缺页反常数量。注意需求卸载 APP 或许重启手机,来保证这个APP完全没有被加载到内存中,由于假如物理内存中有该APP的数据,
- 翻开 Instrument ->System Trace
3.挑选真机、项目、点击发动,当第一个页面显示出来后,点击中止。
- xcode 12搜索main thread, 挑选Virtual Memory,File Backed Page in 便是缺页反常的数量
优化前:项目的缺页遗产数量是427
优化后:
优化前:项目的缺页遗产数量是286
削减了发动时大约40%的缺页反常~
3.主动更新order 文件
随着代码迭代,order文件需求更新,每次手动更新很麻烦,所以需求主动更新。
brew install ios-deploy
APP_ORDER_DIR=appOrderDir
APP_ORDER=./$APP_ORDER_DIR/Documents/app.order
mkdir $APP_ORDER_DIR
ios-deploy --download=/Documents --bundle_id $PRODUCT_BUNDLE_IDENTIFIER --to ./$APP_ORDER_DIR
if [ -e $APP_ORDER ] ;then
cp -f $APP_ORDER ./Resource/app.order
fi
rm -r $APP_ORDER_DIR
【补充xcode13】检查缺页反常的办法
挑选真机、项目、点击发动,当第一个页面显示出来后,点击中止。