本文为个人已知启动优化的总结,如有问题请指教
APP启动主要分为main函数前和main函数后
1. pre-main阶段:即main函数之前,操作系统加载App可执行文件到内存,执行一系列的加载&链接等工作,简单来说,就是dyld加载过程.
Edit Scheme -> Run -> Arguments ->Environment Variables点击+添加环境变量 DYLD_PRINT_STATISTICS 设为 1
说明
- dylib loading time:(加载动态库耗时)
- 系统自带的动态库,苹果都已经做过优化,所以不需要再进行优化.
- 手动添加的动态库,苹果建议不要超过6个.
- rebase/binding time(偏移修正/符号绑定耗时):
rebase(偏移修正):任何一个二进制文件,内部所有的方法、函数都是一个偏移地址。一旦运行到虚拟内存中,每次系统都会随机分配一个ASLR地址值,导致可执行文件和动态链接库在虚拟内存中的加载地址每次启动都不固定,所以需要在编译时来修复镜像中的资源指针,来指向正确的地址。即正确的内存地址 = ASLR地址 + 偏移地址。
binding(绑定):,例如NSLog方法,在编译时期生成的mach-o文件中,会创建一个符号!NSLog(目前指向一个随机的地址),然后在运行时(从磁盘加载到内存中,是一个镜像文件),会将真正的地址给符号(即在内存中将地址与符号进行绑定,是dyld做的,也称为动态库符号绑定)。- ObjC setup time:(OC类注册的耗时)OC类越多,越耗时
- initializer time:(执行+load和 C++构造函数的耗时)
针对pre_main的优化
- 尽量少用外部动态库,苹果官方建议自定义的动态库最好不要超过6个,如果超过6个,需要合并动态库
- 减少OC类,因为OC类越多,越耗时
- 将不必须在+load方法中做的事情延迟到+initialize中,尽量不要用C++虚函数
- 如果是swift,尽量使用struct
1. 对于必须在+load方法中实现的逻辑可用_attribute替代
以BeeHive举例
#define BeeHiveDATA(sectname) __attribute((used, section("__DATA, "#sectname" ")))
#define BeeHiveMod(name) \
class BeeHive; char * k##name##_mod BeeHiveDATA(BeehiveMods) = ""#name"";
#define BeeHiveService(servicename,impl) \
class BeeHive; char * k##servicename##_service BeeHiveDATA(BeehiveServices) = "{ \""#servicename"\" : \""#impl"\"}";
@BeeHiveMod(ModuleAModule)
输出:@class BeeHive; char * kModuleAModule_mod __attribute((used, section("__DATA, ""BeehiveMods"" "))) = """ModuleAModule""";
说明:__attribute((used, section("__DATA, ""BeehiveMods"" ")))表示在项目的mach-o文件的名字为__DATA的segment中添加一个名字为BeehiveMods的section,并将其值设置为字符串"ModuleAModule"
@BeeHiveService(ModuleAServiceProtocol, ModuleAService)
输出:@class BeeHive; char * kModuleAServiceProtocol_service __attribute((used, section("__DATA, ""BeehiveServices"" "))) = "{ \"ModuleAServiceProtocol\" : \"ModuleAService\"}";
说明:__attribute((used, section("__DATA, ""BeehiveServices"" ")))表示在项目的mach-o文件的名字为__DATA的segment中添加一个名字为BeehiveServices的section,并将其值设置为json格式的字符串"{ \"ModuleAServiceProtocol\" : \"ModuleAService\"}"
这样我们就可以优化大量的重复+load方法。而且使用__attribute属性为编译期间绑定注册信息,运行时读取速度快,注册信息在首次触发调用时读取,不影响pre-main时间
2.二进制重排
当进程访问一个虚拟内存Page而对应的物理内存却不存在时,会触发一次 缺页中断(Page Fault)。
二进制重排,主要是优化我们启动时需要的函数非常分散在各个页,启动时就会多次Page Fault造成时间的损耗
- 步骤一:添加 Build Setting 设置
Target -> Build Setting -> Custom Complier Flags ->
OC项目:Other C Flags 添加
-fsanitize-coverage=func,trace-pc-guard
Swift项目:Other Swift Flags 添加
-sanitize-coverage=func
-sanitize=undefined
如果项目有引用第三方,需要在Podfile中添加
post_install do |installer|
installer.pods_project.targets.each do |target|
target.build_configurations.each do |config|
config.build_settings['OTHER_CFLAGS'] = '-fsanitize-coverage=func,trace-pc-guard'
config.build_settings['OTHER_SWIFT_FLAGS'] = '-sanitize-coverage=func -sanitize=undefined'
end
end
end
- 步骤二:在调用最早的VC中添加代码
#import "dlfcn.h"
#import <libkern/OSAtomic.h>
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 list = OS_ATOMIC_QUEUE_INIT;
//定义节点结构体
typedef struct {
void *pc; //存下获取到的PC
void *next; //指向下一个节点
} Node;
void __sanitizer_cov_trace_pc_guard(uint32_t *guard) {
void *PC = __builtin_return_address(0);
Node *node = malloc(sizeof(Node));
*node = (Node){PC, NULL};
// offsetof() 计算出列尾,OSAtomicEnqueue() 把 node 加入 list 尾巴
OSAtomicEnqueue(&list, node, offsetof(Node, next));
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
NSMutableArray *arr = [NSMutableArray array];
while(1){
//有进就有出,这个方法和 OSAtomicEnqueue() 类比使用
Node *node = OSAtomicDequeue(&list, offsetof(Node, next));
//退出机制
if (node == NULL) {
break;
}
//获取函数信息
Dl_info info;
dladdr(node->pc, &info);
NSString *sname = [NSString stringWithCString:info.dli_sname encoding:NSUTF8StringEncoding];
printf("%s \n", info.dli_sname);
//处理c函数及block前缀
BOOL isObjc = [sname hasPrefix:@"+["] || [sname hasPrefix:@"-["];
//c函数及block需要在开头添加下划线
sname = isObjc ? sname: [@"_" stringByAppendingString:sname];
//去重
if (![arr containsObject:sname]) {
//因为入栈的时候是从上至下,取出的时候方向是从下至上,那么就需要倒序,直接插在数组头部即可
[arr insertObject:sname atIndex:0];
}
}
//去掉 touchesBegan 方法 启动的时候不会用到这个
[arr removeObject:[NSString stringWithFormat:@"%s",__FUNCTION__]];
//数组合成字符串
NSString * funcStr = [arr componentsJoinedByString:@"\n"];
//写入文件
NSString * filePath = [NSTemporaryDirectory() stringByAppendingPathComponent:@"link.order"];
NSData * fileContents = [funcStr dataUsingEncoding:NSUTF8StringEncoding];
// NSLog(@"%@", filePath);
[[NSFileManager defaultManager] createFileAtPath:filePath contents:fileContents attributes:nil];
}
- 步骤四:设置 order file
把 link.order 的路径放到工程根目录
Target -> Build Setting -> Linking -> Order File 设置路径./link.order
- 步骤五:clean后重新编译
验证:在Build Settings -> Write Link Map File 设置为YES,通过Path To Lingk Map File 查看文件中函数符号链接的顺序
2. main函数之后:即从main函数开始,到AppDelegate 的didFinishLaunching方法执行完成为止,主要是构建第一个界面,并完成渲染.
针对main函数之后的优化:
- 减少启动初始化的流程,能懒加载的懒加载,能延迟的延迟,能放后台初始化的放后台,尽量不要占用主线程的启动时间
- 优化代码逻辑,去除非必须的代码逻辑,减少每个流程的消耗时间
- 启动阶段能使用多线程来初始化的,就使用多线程
- 尽量使用纯代码来进行UI框架的搭建,尤其是主UI框架,例如UITabBarController。尽量避免使用Xib或者SB,相比纯代码而言,这种更耗时
- 删除废弃类、方法
- 将启动时非必要的操作延迟到首页显示之后加载
参考: