iOS IM启动逻辑梳理及优化
一、app启动原理
1.app启动分为冷启动和热启动。
App 的启动主要包括三个阶段:main() 函数执行前;main() 函数执行后;首屏渲染完成后。
1. main() 函数执行前;
加载Mach-o文件;加载动态库;Objc类、分类、方法初始化;+load()方法初始化。
可以优化的点:减少动态库加载,即合并动态库;采用懒加载的方式调用类或方法;+load()使用+initialize()方法替换;控制C++全局变量数量。
2. main() 函数执行后
从main()函数到didFinishLaunchingWithOptions方法里首屏渲染完成。
一般app的配置信息,初始化,信息上报等都在这里。放在了首屏渲染之前。
可以优化的点:按功能梳理出首屏必要的初始化功能。
3. 首屏渲染完成后
主要完成的是,非首屏其他业务服务模块的初始化、监听的注册、配置文件的读取等
问题:什么样的功能适合放在首屏渲染后呢?应该先检测方法耗时,按耗时严重的去重点关注。
app启动速度的监控
第一种:定时抓取主线程的方法调用栈,计算一段时间里的各个方法耗时。
第二种:对objc_msgSend方法进行hook来掌握所有的方法执行耗时。
自定义方法耗时工具
fishhook:在 iOS 上运行的 Mach-O 二进制文件中动态地重新绑定符号
二、启动时加载逻辑梳理
1. 通讯之前有哪些逻辑?
- 请求LBS接口(获取socket连接地址)
- 拿到IP、端口后socket连接
2. 一款IM软件,启动时需要哪些逻辑。
- 获取服务端时间
- 同步拉取个人信息、名片信息、同步个人设置(3个接口)
- 同步好友信息、同步最近联系人(2个接口)
- 同步组织信息
- 同步群组资料信息
- 同步群成员列表及禁言信息(2个接口)
思考及优化
-
LBS接口的作用
LBS仅返回一个地址,首先必须返回是IP地址,避免DNS解析耗时。
LBS接口还有一个作用,就是负载均衡,服务端有多台服务器,但那台服务器处于空闲,由服务端返回最优的IP. -
LBS的接口是HTTP的,在网络不稳定时,也是非常耗时的
对LBS返回的ip做缓存,当ip连接失败时,再请求LBS接口 -
第二步的逻辑有些多
接口能否合并,如个人信息与个人设置:对于功能不同的接口更愿意遵循单一职责原则,合并到一起反而显得臃肿。可以采用第4步的方式来解决这个问题。
是否所有消息都是必须拉取,如群成员列表是否进入会话才关注。
已经加载过的,考虑增量拉取。 -
不要把所有接口都串行,屡清几条线并发。之前的逻辑:
dispatch_group_async(syncGroup, syncQueue, ^{
BLLogDebug(@"sync");
[self getNetworkTimestamp:^{
// BLLogDebug(@"【INFO】1、获取服务器时间成功");
dispatch_semaphore_signal(syncSemaphore);
}];
dispatch_semaphore_wait(syncSemaphore, DISPATCH_TIME_FOREVER);
[self syncLoginUserProfileWithCallback:^{
// BLLogDebug(@"【INFO】2、同步账户信息成功");
dispatch_semaphore_signal(syncSemaphore);
}];
dispatch_semaphore_wait(syncSemaphore, DISPATCH_TIME_FOREVER);
[self syncFriendsWithCallback:^{
// BLLogDebug(@"【INFO】3、同步好友信息成功");
dispatch_semaphore_signal(syncSemaphore);
}];
dispatch_semaphore_wait(syncSemaphore, DISPATCH_TIME_FOREVER);
[self syncEnterprisesWithCallback:^{
// BLLogDebug(@"【INFO】4、同步组织信息成功");
dispatch_semaphore_signal(syncSemaphore);
}];
dispatch_semaphore_wait(syncSemaphore, DISPATCH_TIME_FOREVER);
[self syncImGroupWithCallback:^{
BLLogInfo(@"【INFO】5、同步群组信息成功");
dispatch_semaphore_signal(syncSemaphore);
}];
dispatch_semaphore_wait(syncSemaphore, DISPATCH_TIME_FOREVER);
[self syncImgroupMemberWithCallback:^{
BLLogInfo(@"【INFO】6、同步群组成员信息成功");
dispatch_semaphore_signal(syncSemaphore);
}];
dispatch_semaphore_wait(syncSemaphore, DISPATCH_TIME_FOREVER);
});
dispatch_group_notify(syncGroup, syncQueue, ^{
// BLLogDebug(@"【INFO】7、同步完毕");
[self syncComplete];
});
修改完后:
- 从所有串行到并发三条线:同步账户信息;同步好友信息;群组及群成员(必须串行)
- 同步账户信息:同步拉取个人信息、名片信息、同步个人设置,在个人信息返回后就算完成,名片信息和同步个人设置慢慢拉取。
- 获取服务器时间,放在最后。前面的接口都有返回时间。
dispatch_queue_t syncConcurrentQueue = dispatch_queue_create("sync", DISPATCH_QUEUE_CONCURRENT);
CFAbsoluteTime startTime = CFAbsoluteTimeGetCurrent();
BLGCDGroupManager * gcdGroup = [[BLGCDGroupManager alloc] initWithGroup:syncGroup queue:syncConcurrentQueue];
[gcdGroup enter];
dispatch_group_async(syncGroup, syncConcurrentQueue, ^{
[self syncLoginUserProfileWithCallback:^{
BLLogWarn(@"<<< t3_1_0 同步账户信息 %f",CFAbsoluteTimeGetCurrent()-startTime);
[gcdGroup leave];
}];
});
[gcdGroup enter];
dispatch_group_async(syncGroup, syncConcurrentQueue, ^{
[self syncFriendsWithCallback:^{
BLLogWarn(@"<<< t3_1_1 同步好友信息 %f",CFAbsoluteTimeGetCurrent()-startTime);
[gcdGroup leave];
}];
});
[gcdGroup enter];
dispatch_group_async(syncGroup, syncConcurrentQueue, ^{
[self syncImGroupWithCallback:^{
[self syncImgroupMemberWithCallback:^{
BLLogWarn(@"<<< t3_1_2 同步群组、群组成员信息 %f",CFAbsoluteTimeGetCurrent()-startTime);
[gcdGroup leave];
}];
}];
});
[gcdGroup enter];
dispatch_group_async(syncGroup, syncConcurrentQueue, ^{
[self syncEnterprisesWithCallback:^{
BLLogWarn(@"<<< t3_1_3 同步组织信息 %f",CFAbsoluteTimeGetCurrent()-startTime);
[gcdGroup leave];
}];
});
dispatch_group_notify(syncGroup, syncConcurrentQueue, ^{//7、同步完毕
BLLogWarn(@"<<< t3_1 同步登录信息、好友、群组、群成员耗时: %f",CFAbsoluteTimeGetCurrent()-startTime);
[self syncComplete];
[self getNetworkTimestamp:nil];
});
三、启动时消息加载慢问题排查
收到反馈:一条信息从启动或退到后台后再进入app,新消息展示大概6、7s。
一个IM软件,这肯定是不能容忍的。
新消息展示流程
启动几个时间段:如从didFinishLaunchingWithOptions->首页消息展示经过了哪些过程。
- 请求lbs
- 链接socket
- socket链接后接收服务端推送的消息
- 收到消息后进行首屏会话消息渲染与红点展示
思考
偶现的问题。听到反馈,先确定原因。
排查原因:首先希望测试能复现;其次从代码角度排查。
- 打印出各时间段耗时与总耗时,总时间超过5s进行上报。
- 服务端从socket连接上、到消息推送耗时加日志。
解决
- 发现最耗时的时间段:在socket连接成功后到消息推到客户端占了5s多。
- 服务端逻辑问题:先查询所有会话,再去过滤未读的会话推送给客户端。当会话量大时,耗时严重。
- 服务端直接查询未读的会话,再做其他处理。限制在400ms。
四、启动拉取个人信息偶现闪退
1.dispatch_group_t
场景:一般在并发多个网络请求都返回时,处理逻辑会用到。
dispatch_group_enter与dispatch_group_leave需要成对出现。但dispatch_group_leave出现比dispatch_group_enter多时会崩溃。
例如在网络异常时,出现超时重试多次回调了block。这种场景不是必现,也不好复现,bugly监控只能看到在某个函数崩溃。我认为直接使用系统提供的是不安全的。
group使用崩溃.png
我的解决方案:
- 封装dispatch_group_t使用,当leave出现异常时使用NSAssert处理
@interface BLGCDGroupManager()
{
dispatch_group_t _group;
dispatch_queue_t _queue;
NSInteger _count;
NSLock * _lock;
}
@end
@implementation BLGCDGroupManager
- (instancetype)initWithGroup:(dispatch_group_t)group
queue:(dispatch_queue_t)queue{
self = [super init];
if (self) {
_group = group;
_queue = queue;
_lock = [[NSLock alloc] init];
}
return self;
}
- (void)enter{
if (_group) {
[self add];
dispatch_group_enter(_group);
}
}
- (void)leave{
if (_group && _count > 0) {
[self sub];
dispatch_group_leave(_group);
}else{
NSAssert(@"leave使用出现异常", nil);
}
}
- (void)add{
[_lock lock];
_count ++;
[_lock unlock];
}
- (void)sub{
[_lock lock];
_count --;
[_lock unlock];
}
@end
- 业务上的处理
找出dispatch_group_leave为什么会出现两次。