iOS IM启动逻辑梳理及优化

2021-03-22  本文已影响0人  某非著名程序员

一、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. 通讯之前有哪些逻辑?

  1. 请求LBS接口(获取socket连接地址)
  2. 拿到IP、端口后socket连接

2. 一款IM软件,启动时需要哪些逻辑。

  1. 获取服务端时间
  2. 同步拉取个人信息、名片信息、同步个人设置(3个接口)
  3. 同步好友信息、同步最近联系人(2个接口)
  4. 同步组织信息
  5. 同步群组资料信息
  6. 同步群成员列表及禁言信息(2个接口)

思考及优化

  1. LBS接口的作用
    LBS仅返回一个地址,首先必须返回是IP地址,避免DNS解析耗时。
    LBS接口还有一个作用,就是负载均衡,服务端有多台服务器,但那台服务器处于空闲,由服务端返回最优的IP.

  2. LBS的接口是HTTP的,在网络不稳定时,也是非常耗时的
    对LBS返回的ip做缓存,当ip连接失败时,再请求LBS接口

  3. 第二步的逻辑有些多
    接口能否合并,如个人信息与个人设置:对于功能不同的接口更愿意遵循单一职责原则,合并到一起反而显得臃肿。可以采用第4步的方式来解决这个问题。
    是否所有消息都是必须拉取,如群成员列表是否进入会话才关注。
    已经加载过的,考虑增量拉取。

  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];
});

修改完后:

  1. 从所有串行到并发三条线:同步账户信息;同步好友信息;群组及群成员(必须串行)
  2. 同步账户信息:同步拉取个人信息、名片信息、同步个人设置,在个人信息返回后就算完成,名片信息和同步个人设置慢慢拉取。
  3. 获取服务器时间,放在最后。前面的接口都有返回时间。
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->首页消息展示经过了哪些过程。

  1. 请求lbs
  2. 链接socket
  3. socket链接后接收服务端推送的消息
  4. 收到消息后进行首屏会话消息渲染与红点展示

思考

偶现的问题。听到反馈,先确定原因。
排查原因:首先希望测试能复现;其次从代码角度排查。

  1. 打印出各时间段耗时与总耗时,总时间超过5s进行上报。
  2. 服务端从socket连接上、到消息推送耗时加日志。

解决

  1. 发现最耗时的时间段:在socket连接成功后到消息推到客户端占了5s多。
  2. 服务端逻辑问题:先查询所有会话,再去过滤未读的会话推送给客户端。当会话量大时,耗时严重。
  3. 服务端直接查询未读的会话,再做其他处理。限制在400ms。

四、启动拉取个人信息偶现闪退

1.dispatch_group_t

场景:一般在并发多个网络请求都返回时,处理逻辑会用到。

dispatch_group_enter与dispatch_group_leave需要成对出现。但dispatch_group_leave出现比dispatch_group_enter多时会崩溃。
例如在网络异常时,出现超时重试多次回调了block。这种场景不是必现,也不好复现,bugly监控只能看到在某个函数崩溃。我认为直接使用系统提供的是不安全的。


group使用崩溃.png

我的解决方案:

  1. 封装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
  1. 业务上的处理
    找出dispatch_group_leave为什么会出现两次。
上一篇下一篇

猜你喜欢

热点阅读