前两个月,反馈群里逐渐开始透漏出app启动慢的问题,以前一直忙着做业务,对启动优化这块确实比较疏忽,又加上进入Q2以来,组内对项目的性能体验等方面要求愈发重视起来,以此为契机,开始着手整理启动优化这块。
一般而言,启动时间是指用户从点击APP那一刻开始到看到第一个界面时这中间的时间。
大家都知道 APP 的入口是 main 函数,在 main 之前,我们自己的代码是不会执行的。而进入到 main 函数以后,我们的代码都是从didFinishLaunchingWithOptions开始执行的。
这里我们要想知道哪些操作,或者说哪些代码是耗时的,我们需要一个打点计时器。通过打点计时器,对每个方法进行计时分析,再针对性处理。找到个挺好用的三方库:BLStopwatch
查看了BLStopwatch源码,也并不复杂,主要就是通过互斥锁来实现记录监测时间差。
简单分析下原理:
1. 单例创建,声明一个互斥锁
+ (instancetype)sharedStopwatch {
static BLStopwatch* stopwatch;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
stopwatch = [[BLStopwatch alloc] init];
});
return stopwatch;
}
- (void)dealloc {
pthread_mutex_destroy(&_lock);
}
- (instancetype)init {
self = [super init];
if (self) {
_mutableSplits = [NSMutableArray array];
pthread_mutex_init(&_lock, NULL);
}
return self;
}
2. 开启打点: 状态设置run 记录开始时间。
- (void)start {
self.state = BLStopwatchStateRuning;
self.startTimeInterval = CACurrentMediaTime();
self.temporaryTimeInterval = self.startTimeInterval;
}
3. 计算耗时
- (void)splitWithType:(BLStopwatchSplitType)type description:(NSString * _Nullable)description {
if (self.state != BLStopwatchStateRuning) {
return;
}
NSTimeInterval temporaryTimeInterval = CACurrentMediaTime();
CFTimeInterval splitTimeInterval = type == BLStopwatchSplitTypeMedian ? temporaryTimeInterval - self.temporaryTimeInterval : temporaryTimeInterval - self.startTimeInterval;
NSInteger count = self.mutableSplits.count + 1;
NSMutableString *finalDescription = [NSMutableString stringWithFormat:@"#%@", @(count)];
if (description) {
[finalDescription appendFormat:@" %@", description];
}
pthread_mutex_lock(&_lock);
[self.mutableSplits addObject:@{finalDescription : @(splitTimeInterval)}];
pthread_mutex_unlock(&_lock);
// 保存每次执行此方法后保存的临时时间
self.temporaryTimeInterval = temporaryTimeInterval;
}
type有两种枚举,BLStopwatchSplitTypeMedian为记录中间值,即上个方法到这个方法中间所耗时间。 BLStopwatchSplitTypeContinuous为记录连续值,即从开始计时到最后打印这期间所用的总时间。
在此方法里,记录当前瞬时时间,再根据type是中间值还是连续值来计算时间间隔,中间值就是当前瞬时时间减去每次执行此方法后保存的临时时间,连续值是当前瞬时时间减去开始时的时间。
然后再上锁,往数组里保存执行到的步骤和执行所耗时间,解锁。通过互斥锁来保证多线程操作时的数据安全。关于各种锁性能的测试,YYKit的作者ibireme大神在他的博客中进行了阐述。YYCache就是通过在方法中添加互斥锁的逻辑,来保证多线程操作缓存时数据的同步。
- (void)setObject:(id)object forKey:(id)key withCost:(NSUInteger)cost {
pthread_mutex_lock(&_lock);
//操作链表,写缓存数据
pthread_mutex_unlock(&_lock);
}
- (id)objectForKey:(id)key {
pthread_mutex_lock(&_lock);
//访问缓存数据
pthread_mutex_unlock(&_lock);
}
4. 弹框打印出各步骤耗时
- (void)stop {
self.state = BLStopwatchStateStop;
self.stopTimeInterval = CACurrentMediaTime();
}
- (void)reset {
self.state = BLStopwatchStateInitial;
pthread_mutex_lock(&_lock);
[self.mutableSplits removeAllObjects];
pthread_mutex_unlock(&_lock);
self.startTimeInterval = 0;
self.stopTimeInterval = 0;
self.temporaryTimeInterval = 0;
}
- (void)stopAndPresentResultsThenReset {
[[BLStopwatch sharedStopwatch] stop];
#ifdef DEBUG
[[[UIAlertView alloc] initWithTitle:@"App启动打点计时结果"
message:[[BLStopwatch sharedStopwatch] prettyPrintedSplits]
delegate:nil
cancelButtonTitle:@"确定"
otherButtonTitles:nil] show];
#endif
[[BLStopwatch sharedStopwatch] reset];
}
// 每个打印步骤展示的信息
- (NSString *)prettyPrintedSplits {
NSMutableString *outputString = [[NSMutableString alloc] init];
pthread_mutex_lock(&_lock);
[self.mutableSplits enumerateObjectsUsingBlock:^(NSDictionary<NSString *, NSNumber *> *obj, NSUInteger idx, BOOL *stop) {
[outputString appendFormat:@"%@: %.3f\n", obj.allKeys.firstObject, obj.allValues.firstObject.doubleValue];
}];
pthread_mutex_unlock(&_lock);
return [outputString copy];
}
调用stopAndPresentResultsThenReset来结束计时并打印出保存起来的每个步骤的计时结果。取结果时,声明字符串outputString,再遍历保存的数组 用outputString来拼接保存的每个步骤名称及对应的耗时,最后输出outputString。这其中也是用互斥锁来保证遍历时的数据安全。
这样,打点计时器就设计完成了。
怎么使用
然后在项目里使用,在didFinishLaunchingWithOptions里开启,在每个方法后面添加打印步骤,在首页加载完成时结束打印,即可看到App的启动时间。
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
[[BLStopwatch sharedStopwatch] start];
// 初始化程序配置
[self prepareCustomSetting];
[[BLStopwatch sharedStopwatch] splitWithDescription:@"初始化程序配置耗时y打印"];
// 注册Umeng,Growing,wxApi等
[self socialSetup];
[[BLStopwatch sharedStopwatch] splitWithDescription:@"注册Umeng,Growing,wxApi耗时打印"];
// bugly设置
[self setupBugly];
[[BLStopwatch sharedStopwatch] splitWithDescription:@"bugly耗时打印"];
// DoraemonKit设置
[self setupDoraemonKit];
[[BLStopwatch sharedStopwatch] splitWithDescription:@"DoraemonKit设置耗时打印"];
...
[[BLStopwatch sharedStopwatch] splitWithType:BLStopwatchSplitTypeContinuous description:@"didFinish完成花费时间打印"];
return YES;
}
// 首页didLoad
- (void)viewDidLoad {
[super viewDidLoad];
[[BLStopwatch sharedStopwatch] splitWithDescription:@"首页加载完成时间打印"];
[[BLStopwatch sharedStopwatch] splitWithType:BLStopwatchSplitTypeContinuous description:@"启动总时间打印"];
[[BLStopwatch sharedStopwatch] stopAndPresentResultsThenReset];
...
}
下面是在iPhone6sPlus上的打印测试图:
未优化时的耗时打印:
优化后的耗时打印:
性能好的手机效果更明显,基本实现秒开。
到这,这一部分暂时处理完了。
主要策略
主要耗时在didFinishLaunchingWithOptions和首页加载渲染两个地方。
didFinishLaunchingWithOptions里做的都是第三方SDK初始化,加载初始化资源,环境配置等这些。
我们可以根据轻重缓急,对其进行分配。
- 必须要在启动时加载的,仍然留在didFinishLaunchingWithOptions中,不过可以做异步处理。
- 不需要在启动完成的,做延迟处理--放在首页加载之后再去加载。这样就能很大部分时间。
首页加载渲染这部分,我们可以通过优化启动流程, 比如在UIApplicationDidFinishLaunching时初始化开屏广告,做到对业务层无干扰。还有开屏广告使用缓存数据,都能提高加载速度。
还有pre-main的那部分,因为比较难搞,有没有明显的效果,就没怎么处理了。等有时间再弄吧,目前的反馈效果比较良好了。