这次接入IM的感觉有点糟糕,LeanCloud提供的官方SDK并不是很完善,完整的SDK包含AVOSCloud(基础模块)、AVOSCloudIM(实时通信)、AVOSCloudCrashReporting(崩溃报告),虽然集成时,可以自行选择,但这些库只提供了基础的通信手段,没有集成UI,例如最基础的反馈页面也没有(属于双人聊天,提供发送文字信息,图片信息,查看发送历史的功能)。而这次接入的IM需要更多的功能,包括:会话列表(ConversationList)、发送文字与图片信息(TextMessage、ImageMessage)、消息提醒(Badge、Sound、APNS)。此外为了安全,应该只将聊天相关的数据存储在LeanCloud上,其余信息如:用户手机号码、用户详细信息(除用户名与头像等),etc。全部向自己的服务器请求。官方的基础SDK提供了通信手段,而ChatKit可以看作是一层封装,提供了UI。由于这次折腾了不少,所以记录下集成ChatKit的坑(感觉用“修改”比“集成”合适)。

1.大量使用Block

ChatKit中大量地使用了代码块,通过定义代码块类型,将它作为对象的属性保存,然后在需要调用时,先判断是否不存在代码块,若存在则执行代码块,格式如下。

1
!block?:block()

在传入的参数足够多时,可以让使用者实现更多的功能,但缺点比较多,也许是因为我不习惯使用如此多的Block,大量的回调,也许是因为功能比较完善,自然显得复杂,以LCCKConversationListViewController为例。下拉刷新时。

1.LCCKConversationListViewModel调用refresh,在refresh函数中调用

LCCKConversationListService单例的findRecentConversationsWithBlock,并在Block中处理返回值,执行为LCCKConversationListViewController设置的didSelectItemBlock。

2. findRecentConversationsWithBlock中调用selectOrRefreshConversationsWithBlock方法,设置Block,在Block中查询每个对话的最新消息,未读消息数,显示在对话列表上,这里的查询是从本地缓存(SQLite数据库)中查询。

3.selectOrRefreshConversationsWithBlock中,当首次进入对话列表时,调用fetchConversationsWithConversationIds,将本地的所有对话的id传递给它,设置blcok,在block中更新本地conversation的信息,之后再次调用时,直接获取本地所有对话,执行上一步中传递的Block。

3.fetchConversationsWithConversationIds中根据传递的conversationId,从服务器查询对话信息,执行上一步传递的Block。

从程序执行的角度讲,这些操作都是线性的,但是在编写、修改和调试的时候,就需要反着过来了,一个函数中调用起另一个,然后处理返回值,这些操作都通过Block传递,写法十分漂亮,但如果不使用Block,而是使用delegate,会有更清晰的参数传递,因为Block的参数显示依靠着自动补全,结果感觉陷入了回调地狱之中,更加恐怖的还在后头。

2. 新消息的接收 {.p1}

按照上面的逻辑,下拉刷新只刷新conversation的信息,并且更新的数据从本地来,那么新消息如何接收呢?

didreceimessage

在LCCKSessionService.m文件中,这个方法是所有消息的入口。

1
- (void)conversation:(AVIMConversation *)conversation didReceiveTypedMessage:(AVIMTypedMessage *)message

可以看到,当它被触发后,会判断对话是否是一个新建的对话,如果是的话,就去拉取最新消息,否则调用下一个方法,如下,我对它做了一些修改。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
- (void)receiveMessage:(AVIMTypedMessage *)message conversation:(AVIMConversation *)conversation {
    [[LCCKConversationService sharedInstance] insertRecentConversation:conversation];
    if (![[LCCKConversationService sharedInstance].chattingConversationId isEqualToString:conversation.conversationId]) {
        // 没有在聊天的时候才增加未读数和设置mentioned
        [[LCCKConversationService sharedInstance] increaseUnreadCountWithConversation:conversation];
        if ([self isMentionedByMessage:message]) {
            [[LCCKConversationService sharedInstance] updateMentioned:YES conversation:conversation];
        }
        [[NSNotificationCenter defaultCenter] postNotificationName:LCCKNotificationUnreadsUpdated object:nil];
    }
    NSString *remoteConv = [LCCKConversationService sharedInstance].remoteNotificationConversationId;
    if (![LCCKConversationService sharedInstance].chattingConversationId && !remoteConv) {
        if (!conversation.muted && ![LCChatKit sharedInstance].startFromInactitve) {
            [[LCCKSoundManager defaultManager] playLoudReceiveSoundIfNeed];
        }
        [LCChatKit sharedInstance].startFromInactitve = NO;
    }
    [[NSNotificationCenter defaultCenter] postNotificationName:LCCKNotificationMessageReceived object:message];
}

在这个方法中,当收到新消息时将它插入数据库,如果当前聊天对象不是新消息的发送者,那么就会增加消息未读数和设置被人@,如果未处于聊天状态或,则播放消息声音,同时分别发送两个NSNotification,这两个会触发LCCKConversationListViewModel的refresh方法。使用NSNotification间接触发Block。

3.消息推送

在正确启用了消息推送时,开始出问题了。接入时,IM并不是集成在App的首页,而是在主页增加了一个入口,点击后进入IM。

在未添加是否存在remoteNotificationConversationId,是否从后台进入应用的状态下,每次不论从消息栏或者应用图标点击进入时,都会播放两次叮的声音和接收到消息的声音,因为ChatKit中还使用了大量的单例,结合Block时,无法避免的会产生内存泄漏……结果就是即使退出了ConversationListViewController,也会播放声音,以及时不时触发didSelectItemBlock,直接push聊天界面。即使在添加了判断后,用户收到推送,进入IM的情况又有以下几种:

1.App未运行,用户点击消息栏或应用图标进入IM。此时launchoptions中会包含userinfo,里面含有推送消息,但是ChatKit会更早地触发接受消息的方法,结果是连续播放了两次提醒声音,一个是系统收到APNs,另一个是这个方法间接触发的。

1
- (void)conversation:(AVIMConversation *)conversation didReceiveTypedMessage:(AVIMTypedMessage *)message

2.App在后台运行,尚未被kill。用户点击消息栏进入IM,会触发

1
- (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo 

在这里可以取到userinfo,从其中可以取到推送过来的remoteNotificationConversationId,防止播放两次声音。而当用户点击图标进入IM时,则会依次调用

1
2
- (void)applicationWillEnterForeground:(UIApplication *)application
- (void)applicationDidBecomeActive:(UIApplication *)application

这时就无法拿到userinfo了,所以增加一个标志,来判断应用是否从点击图标进入。

剩下的其它问题,就是之前接入者产生的问题了,比如把用户基本信息存在conversation中……所以每次都需要优先拉取对话,再从对话中缓存用户信息,由于拉取对话和从网络查询对话的最新信息也是异步调用,在block中处理结果,最后只能使用信号量来做同步,还有用户头像都是原图……conversation偶尔变成三人聊天……接入完IM后,问题依旧多多……简直心塞。