App的启动分为冷启动和热启动两种方式
- 冷启动: App 点击启动前,它的进程不在系统里,需要系统新创建一个进程分配给他启动的的情况, 这是一次完整的启动过程
- 热启动: App冷启动后, 将App进入后台. 此时App的进程还在系统中, 用户重新进入App, 这个过程做的事情非常少, 称之为热启动
App启动速度的优化主要是针对冷启动时的优化. 用户感知到启动速度慢主要是因为主线程的卡顿, 在主线程中进行了大量文件读写操作, 或者屏幕渲染周期中进行了大量的计算等等. 然而, 即便处理了首屏之前所有的主线程操作, 有时候App的启动仍然慢于其他竞品App, 这个时候我们就需要对App首屏启动前的流程进行一个分析, 找出可以被优化的方向.
一般而言, App的启动时间是指, 从用户点击App开始, 到用户看到App首屏画面中间的这段时间, 这期间主要包括三个阶段:
- main() 函数执行前
- main() 函数执行后
- 首屏渲染完成后
在main() 函数执行前, 系统会做的几件事包括:
- 加载可执行文件(.o)文件(编译时, 先将文件预编译, 替换宏, 删除注释, 展开头文件等生成预编译.i文件, 然后编译生成.s文件, 后将.s文件编译成机器可以识别的.o文件)
- 加载动态链接库, rebase指针调整, bind符号绑定
- Objc运行时的初始处理, 包括相关类的注册, category的注册, selector的唯一性检查等等
在这个时期, 我们可以做的对启动速度的优化操作包括:
- 减少不必要的动态库的加载, 尽量将多个动态库进行合并. 数量上, 苹果公司最多支持6个非系统动态库合并为一个
- 减少在启动后不回去使用的类或者方法
- +load()方法里的内容可以等到首屏渲染以后再去执行, 或者使用initialize() 方法去替换, 因为在每个+ load()方法中, 进行运行时方法替换都会带来4毫秒的消耗, 积少成多, 大量的+load方法对启动会起到很大的影响
- 控制C++全局变量的数量
在main()函数执行后(即main()函数开始执行到appDelegate的didFinishlaunchingWithOpitons方法里首屏渲染相关方法执行完成)首页的业务代码都是在这个阶段, 也就是首屏渲染完成前执行的, 主要包括
- 首屏初始化所需配置的文件的读写操作
- 首屏列表大数据读取
- 首屏渲染的大数据计算
在这个时期应该确定, 那些初始化方法需要在这个阶段执行, 那些方法可以再首屏渲染完成后执行, 梳理完后将初始化功能放到合适的阶段
首屏渲染完成后, 此时主要完成的就是:
- 其他非首屏业务模块的初始化
- 监听注册, 配置文件的读取
此时用户已经可以看到App首页的信息了, 从代码上看则是以首屏渲染完成后到didFinishLaunchingWithOptions方法作用域结束.
功能级别的启动优化就要从main()函数后的阶段入手.
- 通过监听runloop根据时间间隔判断卡顿:
ObserverMonitor.h
//
// ObserverMonitor.h
// StackCatch
//
// Created by 阮皓 on 2019/3/14.
// Copyright © 2019 阮皓. All rights reserved.
//
#import <Foundation/Foundation.h>
@interface ObserverMonitor : NSObject
+ (instancetype)monitor;
+ (void)start;
@end
ObserverMonitor.m
//
// ObserverMonitor.m
// StackCatch
//
// Created by 阮皓 on 2019/3/14.
// Copyright © 2019 阮皓. All rights reserved.
//
#import "ObserverMonitor.h"
#include <libkern/OSAtomic.h>
#include <execinfo.h>
@interface ObserverMonitor () {
CFRunLoopObserverRef _observer;
dispatch_semaphore_t _semaphore;
CFRunLoopActivity _activity;
}
@end
@implementation ObserverMonitor
+ (instancetype)monitor {
static ObserverMonitor *monitor;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
monitor = [[ObserverMonitor alloc] init];
});
return monitor;
}
+ (void)start {
[[ObserverMonitor monitor] startObserver];
}
- (void)startObserver {
CFRunLoopObserverContext context = {
0,
(__bridge void *)(self),
&CFRetain,
&CFRelease,
NULL
};
_observer = CFRunLoopObserverCreate(NULL, kCFRunLoopAllActivities, YES, 0, &runLoopObserverCallBack, &context);
CFRunLoopAddObserver(CFRunLoopGetMain(), _observer, kCFRunLoopCommonModes);
_semaphore = dispatch_semaphore_create(0);
dispatch_async(dispatch_get_global_queue(0, 0), ^{
while (YES) {
long result = dispatch_semaphore_wait(self->_semaphore, dispatch_time(DISPATCH_TIME_NOW, 50 * NSEC_PER_MSEC));
// Returns zero on success, or non-zero if the timeout occurred.
if (result != 0) {
// 说明一个runloop超时
if (self->_activity == kCFRunLoopBeforeSources || self->_activity == kCFRunLoopAfterWaiting) {
// 说明这个超时的loop不是沉睡状态
[self logStack];
}
}
}
});
}
static void runLoopObserverCallBack(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info) {
ObserverMonitor *monitor = (__bridge ObserverMonitor *)info;
monitor->_activity = activity;
dispatch_semaphore_signal(monitor->_semaphore);
}
- (void)logStack {
void *callstack[128];
int frames = backtrace(callstack, 128);
char **strs = backtrace_symbols(callstack, frames);
int i;
NSMutableArray *backtrace = [NSMutableArray arrayWithCapacity:frames];
for (i = 0; i < 4; i++) {
[backtrace addObject:[NSString stringWithUTF8String:strs[i]]];
}
free(strs);
NSLog(@"====================堆栈\n %@ \n",backtrace);
}
@end
然而, 这种方式可以检测出runloop的卡顿, 却不能精确地指出到底是哪一个方法导致了runloop的卡顿. 这时候可以使用对objc_msgSend方法进行hook来掌握所有方法的执行耗时.