本文篇幅较长,预计阅读时长1-2h,欢迎收藏+点赞+关注。
这是《千亿级IM独立开发指南!全球即时通讯全套代码4小时速成》的第三篇:《APP 内部流程与逻辑》
系列文章可参考:
千亿级IM独立开发指南丨全球即时通讯全套代码4小时速成(一):功能设计与介绍
千亿级IM独立开发指南丨全球即时通讯全套代码4小时速成(二):UI设计与搭建
千亿级IM独立开发指南丨全球即时通讯全套代码4小时速成(三):App内部流程与逻辑(上)
千亿级IM独立开发指南丨全球即时通讯全套代码4小时速成(四):服务端搭建与总结
三、App内部流程与逻辑
7. 会话窗口消息展示
进入会话窗口前,需要先准备好本地已保存的历史消息。然后显示窗口界面的同时,异步线程开始拉取最新的历史消息,并且检查之前的历史消息是否有缺失,有缺失就会持续填补,直到填补完成,或者用户退出等引发的填补中断。
编辑 IMCenter.swift,加入 showDialogueView() 函数:
class func showDialogueView(contact: ContactInfo) {
//-- 仅能在主线程中调用
IMCenter.viewSharedInfo.targetContact = contact
IMCenter.viewSharedInfo.strangerContacts.removeAll()
IMCenter.prepareDialogueMesssageInfos(contact: contact)
IMCenter.viewSharedInfo.lastPage = IMCenter.viewSharedInfo.currentPage
IMCenter.viewSharedInfo.currentPage = .DialogueView
}
并修改 ContactItemView.swift 中 onTapGesture 行为为:
... ...
.onTapGesture {
IMCenter.showDialogueView(contact: contactInfo)
}
... ...
showDialogueView() 主要清理上一个(如果存在)会话的相关数据,并调用 prepareDialogueMesssageInfos() 函数,准备新的会话数据,然后显示会话窗口界面。
7.1 准备会话窗口数据
prepareDialogueMesssageInfos() 函数先加载本地保存的历史数据,并按时间排序;然后启动两个异步线程,一个清理未同步的未知联系人信息,一个检查并不断填充历史消息。
prepareDialogueMesssageInfos() 代码如下:
private class func soreChatMessages(messages: [ChatMessage]) -> [ChatMessage] {
if messages.count <= 1 {
return messages
}
return messages.sorted(by: { (m1, m2) -> Bool in
if m1.mtime < m2.mtime {
return true
}
if m1.mtime == m2.mtime {
if m1.mid < m2.mid {
return true
}
}
return false
})
}
class func prepareDialogueMesssageInfos(contact:ContactInfo) {
if contact.kind == ContactKind.Room.rawValue {
DispatchQueue.global(qos: .default).async {
IMCenter.client!.enterRoom(withId: NSNumber(value: contact.xid), timeout: 0, success: {
DispatchQueue.main.sync {
sendCmd(contact: contact, message: "\(getSelfDispalyName()) 进入房间")
}
}, fail: { _ in })
}
}
let chatMessages = IMCenter.db.loadAllMessages(contact:contact)
IMCenter.viewSharedInfo.dialogueMesssages = soreChatMessages(messages: chatMessages)
DispatchQueue.global(qos: .default).async {
let unknownContacts = pickupUnknownContacts(messages:chatMessages)
cleanUnknownContacts(unknownContacts: unknownContacts)
}
DispatchQueue.global(qos: .default).async {
refillHistoryMessage(contact:contact)
}
}
7.2 发送系统通知
当点击的联系人为房间类型时,为了简单起见,直接通过客户端进入房间。这时,我们需要告诉房间中的其他用户,谁进入房间了。因此,我们在这里用 RTM 预定义的 Cmd 类型消息发送系统通知:
private class func sendGroupCmd(contact:ContactInfo, message:String) {
IMCenter.client!.sendGroupCmdMessageChat(withId: NSNumber(value: contact.xid), message: message, attrs: "", timeout:0, success: { _ in
}, fail: {
_ in
})
}
private class func sendRoomCmd(contact:ContactInfo, message:String) {
IMCenter.client!.sendRoomCmdMessageChat(withId: NSNumber(value: contact.xid), message: message, attrs: "", timeout:0, success: { _ in
}, fail: {
_ in
})
}
class func sendCmd(contact:ContactInfo, message:String) {
if contact.kind == ContactKind.Group.rawValue {
sendGroupCmd(contact:contact, message:message)
}else if contact.kind == ContactKind.Room.rawValue {
sendRoomCmd(contact:contact, message:message)
} else { return }
objc_sync_enter(locker)
let mid = fakeMid
fakeMid += 1
objc_sync_exit(locker)
let curr = Date().timeIntervalSince1970 * 1000
let chatMessage = ChatMessage(sender: IMCenter.client!.userId, mid: mid, mtime: Int64(curr), message: message)
chatMessage.isChat = false
var chats = IMCenter.viewSharedInfo.dialogueMesssages
chats.append(chatMessage)
IMCenter.viewSharedInfo.dialogueMesssages = chats
}
因为用户发送消息是网络操作,而网络操作可能会失败,比如断网时。因此我们无法等服务器确认后,再将用户发送的内容显示到界面上。但 RTM 只有在返回后,才能获取到消息的 messageId。但显示消息需要使用 MessageId 初始化 ChatMessage 对象。于是我们采用了当年QQ的方法:直接本地显示(这方法现在微信也还在用)。为此,我们引入了一个虚假的MessageId:fakeMid:Int64 进行代替。
但是在其他情况下,比如修改信息或者其他情况,也需要发送系统通知。而且这些系统通知往往是在网络操作完成后,在其他线程内异步触发的。那就存在着 fakeMid 被并发读写的情况。于是我们需要添加一个锁对象 class Locker,并用其进行同步。
于是在 IMCenter.swift 中,增加以下代码:
... ...
class Locker {}
... ...
class IMCenter {
... ...
static var locker = Locker()
... ...
static var fakeMid:Int64 = 1
... ...
}
7.3 同步未知联系人
在一个群组,或者房间中,RTM系统推送过来的消息,只有发送人唯一数字ID,而其它的展示信息,则须要我们自己获取。无论是从我们自己的服务器上获取,还是从RTM服务器上获取。
此外,在获取的过程中,可能因为用户退出等原因,获取过程被中断,此时,App本地的数据中也将存在未知联系人。所以 pickupUnknownContacts() 便是从本地消息中筛查出未知联系人,然后交给 cleanUnknownContacts() 进行信息更新。
相关代码如下:
private class func syncQueryUsersInfos(type: Int, contacts:[ContactInfo]) {
var queryIds = [NSNumber]()
for contact in contacts {
queryIds.append(NSNumber(value: contact.xid))
}
let attriAnswer = IMCenter.client!.getUserOpenInfo(queryIds, timeout: 0)
let strangers = decodeAttributeAnswer(type: type, attriAnswer: attriAnswer)
DispatchQueue.main.async {
for stranger in strangers {
if let contact = IMCenter.viewSharedInfo.strangerContacts[stranger.xid] {
contact.nickname = stranger.nickname
contact.imageUrl = stranger.imageUrl
contact.showInfo = stranger.showInfo
} else {
IMCenter.viewSharedInfo.strangerContacts[stranger.xid] = stranger
}
}
}
}
private class func pickupUnknownContacts(messages:[ChatMessage]) -> [ContactInfo] {
var uids: Set<Int64> = []
for msg in messages {
if msg.sender != IMCenter.client!.userId {
uids.insert(msg.sender)
}
}
var contacts = [ContactInfo]()
if uids.isEmpty == false {
let allUsers = IMCenter.db.loadAllUserContactInfos()
for uid in uids {
if let contact = allUsers[uid] {
if contact.nickname.isEmpty || contact.imageUrl.isEmpty {
contacts.append(contact)
}
} else {
contacts.append(ContactInfo(xid: uid))
}
}
}
return contacts
}
private class func cleanUnknownContacts(unknownContacts:[ContactInfo]) {
//-- 查询 xname
var uids = [Int64]()
for contact in unknownContacts {
uids.append(contact.xid)
}
BizClient.lookup(users: nil, groups: nil, rooms: nil, uids: uids, gids: nil, rids: nil, completedAction: {
lookupData in
var strangers = [ContactInfo]()
for (key, value) in lookupData.users {
let contact = ContactInfo(type: ContactKind.Stranger.rawValue, xid: value)
contact.xname = key
strangers.append(contact)
}
DispatchQueue.main.async {
for stranger in strangers {
if let contact = IMCenter.viewSharedInfo.strangerContacts[stranger.xid] {
contact.xname = stranger.xname
} else {
IMCenter.viewSharedInfo.strangerContacts[stranger.xid] = stranger
}
}
}
}, errorAction: { _ in })
//-- 查询展示信息
if unknownContacts.count < 100 {
syncQueryUsersInfos(type: ContactKind.Stranger.rawValue, contacts: unknownContacts)
} else {
var queryContacts = [ContactInfo]()
for contact in unknownContacts {
queryContacts.append(contact)
if queryContacts.count == 99 {
syncQueryUsersInfos(type: ContactKind.Stranger.rawValue, contacts: queryContacts)
queryContacts.removeAll()
}
}
if queryContacts.count > 0 {
syncQueryUsersInfos(type: ContactKind.Stranger.rawValue, contacts: queryContacts)
}
}
}
7.4 填补空缺的历史消息
最后一步,是填补历史消息空缺。填补历史消息空缺其实也很简单。先从数据库中获取已经保存的历史消息检查点,按从新到旧进行排序。然后从新到旧,以10条为单位(RTM的默认限制),降序拉取当前会话的历史消息,然后逐条保存。一旦需要保存的历史消息已经在数据库中存在,则意味着已经填补了历史消息的最新一段空缺。则检查对应范围内是否存在历史消息检查点,如果存在则删除,不存在则以最新的历史消息检查点开始,继续从新到旧,以降序拉取历史消息。
如果这之间发现有未知联系人,则启动新的异步线程进行同步。
填补空缺历史消息的 refillHistoryMessage() 函数及相关代码如下:
private class func sortHistoryCheckpoint(checkpoints:[HistoryCheckpoint]) -> [HistoryCheckpoint] {
if checkpoints.count < 2 {
return checkpoints
}
return checkpoints.sorted(by: { (c1, c2) -> Bool in
if c1.ts > c2.ts {
return true
}
if c1.ts == c2.ts {
return c1.desc
}
return false
})
}
private class func refillHistoryMessage(contact:ContactInfo) {
var historyAnswer: RTMHistoryMessageAnswer? = nil
var begin: Int64 = 0
var end: Int64 = 0
var lastId: Int64 = 0
let fetchCount = 10
let nsXid = NSNumber(value: contact.xid)
let nsCount = NSNumber(value: fetchCount)
var checkpoints = db.loadAllHistoryMessageCheckpoints(contact:contact)
checkpoints = sortHistoryCheckpoint(checkpoints: checkpoints)
while (true)
{
if contact.kind == ContactKind.Friend.rawValue {
historyAnswer = IMCenter.client!.getP2PHistoryMessageChat(withUserId: nsXid, desc: true, num: nsCount, begin: NSNumber(value: begin), end: NSNumber(value: end), lastid: NSNumber(value: lastId), timeout: 0)
} else if contact.kind == ContactKind.Group.rawValue {
historyAnswer = IMCenter.client!.getGroupHistoryMessageChat(withGroupId: nsXid, desc: true, num: nsCount, begin: NSNumber(value: begin), end: NSNumber(value: end), lastid: NSNumber(value: lastId), timeout: 0)
} else if contact.kind == ContactKind.Room.rawValue {
historyAnswer = IMCenter.client!.getRoomHistoryMessageChat(withRoomId: nsXid, desc: true, num: nsCount, begin: NSNumber(value: begin), end: NSNumber(value: end), lastid: NSNumber(value: lastId), timeout: 0)
} else { return }
if historyAnswer != nil && historyAnswer!.error.code == 0 {
var chatMessages = [ChatMessage]()
for message in historyAnswer!.history.messageArray {
if message.messageType == 30 {
if IMCenter.db.insertChatMessage(type: contact.kind, xid: contact.xid, sender: message.fromUid, mid: message.messageId, message: message.stringMessage, mtime: message.modifiedTime, printError: false) == false {
break
}
} else {
if IMCenter.db.insertChatCmd(type: contact.kind, xid: contact.xid, sender: message.fromUid, mid: message.messageId, message: message.stringMessage, mtime: message.modifiedTime, printError: false) == false {
break
}
}
let chatMsg = ChatMessage(sender: message.fromUid, mid: message.messageId, mtime: message.modifiedTime, message: message.stringMessage)
if message.messageType != 30 {
chatMsg.isChat = false
}
chatMessages.append(chatMsg)
}
if chatMessages.count > 0 {
DispatchQueue.global(qos: .default).async {
let unknownContacts = pickupUnknownContacts(messages:chatMessages)