内容简介:知识库:搞事情系列文章主要是为了继续延续自己的 “T” 字形战略所做,同时也代表着毕设相关内容的学习总结。本文章是快速对接即时通讯完成需求,主要是记录在集成即时通讯的过程中遇到的一些问题和总结。接入即时通讯是大一的比赛作品“
知识库: PJ 的 iOS 开发之路
搞事情系列文章主要是为了继续延续自己的 “T” 字形战略所做,同时也代表着毕设相关内容的学习总结。本文章是快速对接即时通讯完成需求,主要是记录在集成即时通讯的过程中遇到的一些问题和总结。
前言
接入即时通讯是大一的比赛作品“ 大学+ ”,当时和另外一个小伙伴一起写下第一行代码,到靠着这个作品砍下了一些小奖,同时也让当时的自己快速的入门了与 iOS 开发相关一部分内容。
现在要在毕设中同样接入 IM,调研了目前比较流行的 IM 服务提供商后,最终选择了 融云 负责即时聊天业务。在调研的过程中除了能够提供稳定的基础聊天服务,最好还要有个 UIKit
,因为自己的时间并不多,想着直接在 IM 服务提供商所带的 UIKit
做二次开发。
IM 服务提供商调研
阿里云
不知为何,我对阿里云的产品总是提不起来兴趣。最开始是接入了阿里云短信做验证码,在对接的过程中我不是很喜欢阿里云的做法,阿里云短信的 server SDK 只提供一个跟运营商的通道,至于短信验证码的内容,需要我们自己做维护,包括验证码的生成、匹配和过期。
而相对我之前一直在使用 mob 来说,同样可以选择 client 触发短信验证码的发送,而 server 要做的事情仅仅只是匹配而已,不需要对验证码的生成和过期做处理。
当然这一点看法智者见智,对于我个人来说,短信验证码并不是核心业务,虽然整个对接过程也不复杂,但整体情况对比来看我不是很舒服。最重要的是如果你要测试阿里云短信必须得先充钱,这其实就陷入了一个死循环“我的逻辑还没跑通,凭什么先交钱?不交钱怎么开通服务?”,一条短信虽然也没几个钱,但确实会让人不太爽。反观 mob 提供了开发环境每天 20 条免费短信用于测试。
经过接入阿里云短信的过程后,我对阿里云系产品就失去了兴趣,包括阿里云通信。
腾讯云
在调研腾讯云 IM 的过程中,官网上的这句宣传语真是直击内心。
腾讯是国内最大也是最早的即时通讯开发商,QQ 和微信已经成为每个互联网用户必不可少的应用。现在,腾讯将高并发、高可靠的即时通讯能力进行开放,开发者可以很容易的根据腾讯提供的 SDK 将即时通讯功能集成入 App 中。
这还有什么好挑的?当时决定立马接入,其它不调研了。
文档???
腾讯云通信的 iOS SDK 应该是去年 8 月份左右做了更新,感觉很踏实。当初始化完 AppKey
后准备接入“消息列表 VC”时我死活找不到官网文档上描述的类。
后来我怀疑估计是偶然问题,凭着自己的经验,猜出了正确的“消息列表 VC”类,并成功的初始化,接着开始对接“会话界面 VC”,也就是 AddC2CController
,一开始 Xcode 并没有进行代码补全的提示,以为是 Xcode 本身的问题,开始的清缓存、重启 Xcode 等操作,把工程恢复到了最佳,可当我最后一次敲下 AddC2CController
时,依然没有提示。
翻了一遍 pods 中 TUIKit
中的所有类,惊奇的发现居然没有 AddC2CController
这个类!反复从官方文档中上下求索,可最终的结果是,我又凭着自己的经验找到了相似的类名,但初始化完成后,并不是我想要的结果,总不能把所有类都初始化一遍吧?
最后无法忍受,很不开心的发了工单。等待了一个星期后,文档依旧没有更新,我彻底放弃了。刚才又去看了一眼,嗯,依旧没有更新......
网易云信
个别大佬不推荐使用,据说要凉了,那我就算了吧。
leanCloud
之前就听说了 leanCloud 全家桶很香。本来也打算上 leanCloud 全家桶,但粗略的文档看过去怎么好像都跟其云数据库绑定到了一起,跟之前大一时我和另外一个小伙伴不会写数据库,使用了当时比较火的云数据库提供商 Bmob 做法类似,再加上被前面腾讯云搞得有些疲惫了,对全新事物已经很难提起兴趣了,只想着能够越快解决这个问题就好。
融云
最后再三思考后,还是回到了融云上。刚开始也确实打算直接使用融云的 UIKit
,但仔细对比了融云 UIKit
能够提供定制化的地方和 UI 设计图最终的效果差距甚远,遂放弃,准备只接入融云的核心通信库,使用第三方 IM UI 库完成。
UI 库调研
最开始我是想省事直接用 IM 服务提供商的 UIKit
,但在看过了腾讯云和融云提供的 UI 定制太局限了,而且不管怎么做,都很难复刻出跟设计图一样的效果。
MessagerKit
github 地址: github.com/steve228uk/… 。
一开始看上了这个库,基本上把大部分功能都实现了,但是跟设计图上的一些细节还是有差距,比如说需要自己的做拓展支持语音、地图等自定义消息体、消息体框特殊圆角。这部分工作是清明节回家做的,整体上对接完成后其实还算 OK。
MessageKit
直到有一天中午,突然看到了 MessageKit 这个库!几乎完成了所有功能,把我开心坏了!立马着手开始全部切换。
等到调好了一切细节后,发现这个库有一个坑爹的地方,点击输入框整个聊天界面的 collectionView
会上移一个固定距离,不管我怎么调,甚至把官方 demo 放到我的工程里也同样会出现这个问题,继续折腾了将近一个小时后,放弃了。无缘无故用户在点击输入框的时候整个聊天界面多往上移动大概 40px 的距离,不能忍。
MessagerKit
嗯,我又换回来了 :sweat_smile:,最终决定还是用回第一次的库。来来回回将近三四天的时间都在切换这两个 UI 库上,基本上都是快写完了才发现有些奇怪的地方,然后全部推翻再重来。
接入融云
首先按照融云的官方文档进行账号的注册和应用的创建。拿到 Appkey
,集成 RongCloudIM/IMLib
到工程中。
登录
官方文档并不推荐在客户端生成 token
进行融云 SDK 的登录,因为生成 token
的过程涉及到的 AppSecret
的固定,如果 app 被反编译则有极大可能导致泄漏。但是如果你心够大或者只是做个 demo 玩玩,在客户端本地请求生成 token
也不是不可以,以下是基于融云 server python sdk 的 token
生成代码:
@decorator.request_methon('GET') @decorator.request_check_args([]) def getRCToken(request): from rongcloud import RongCloud uid = request.GET.get('uid') nick_name = request.GET.get('nick_name') app_key = settings.RC_APP_KEY app_secret = settings.RC_APP_SECRET rcloud = RongCloud(app_key, app_secret) r = rcloud.User.getToken(userId=uid, name=nick_name, portraitUri='https://avatars0.githubusercontent.com/u/15074681?s=460&v=4') r_json = eval(str(r.response.content, encoding='utf-8')) if r_json['code'] == 200: json = { 'token': r_json['token'] } return utils.SuccessResponse(json, request) else: masLogger.log(request, 2333, str(r.response.content, encoding='utf-8')) return utils.ErrorResponse(2333, 'RCToken error', request) 复制代码
在客户端上进行请求生成 token
的接口即可
发送消息
发送消息主要是使用如下方法:
- (RCMessage *)sendMessage:(RCConversationType)conversationType targetId:(NSString *)targetId content:(RCMessageContent *)content pushContent:(NSString *)pushContent pushData:(NSString *)pushData success:(void (^)(long messageId))successBlock error:(void (^)(RCErrorCode nErrorCode, long messageId))errorBlock; 复制代码
关于该方法的使用在注释中已经写的很明白,我们需要做的就是把它进行一个封装,使其对外更好的使用:
/// 发送文本消息 func sendText(textString: String, userID: String, complateHandler: @escaping ((Int) -> Void), failerHandler: @escaping ((RCErrorCode) -> Void)) { let text = RCTextMessage(content: textString) RCIMClient.shared()?.sendMessage(.ConversationType_PRIVATE, targetId: userID, content: text, pushContent: nil, pushData: nil, success: { (mesId) in complateHandler(mesId) }, error: { (errorCode, mesId) in failerHandler(errorCode) }) } 复制代码
以上为发送文本消息的方法。需要注意的是,在调用该方法之前必须确定要消息体的类型等前置条件,必须得先确定要发送的消息体类型来调用不同的方法,比如图片、语音和视频等,包括自定义消息体,地图等。
接收消息
关于消息的接收,融云并没有限制消息监听器的类型,只要你是 NSObject
子类就可以实现代理方法接收消息。所以,我把消息接收稍微封装了一下:
extension PJIM: RCIMClientReceiveMessageDelegate { func onReceived(_ message: RCMessage!, left nLeft: Int32, object: Any!) { print(message.objectName) switch message.objectName { case "RC:TxtMsg": let text = message.content as! RCTextMessage let m = Message(type: .text, textContent: text.content, audioContent: nil, sendUserId: message.senderUserId, msgId: message.messageId, msgDirection: message.messageDirection, msgStatus: message.sentStatus, msgReceivedTime: message.receivedTime, msgSentTime: message.sentTime) getMsg?(m) print(m.textContent!) case "RCImageMessage": break default: break } } } 复制代码
其中 Message
是我根据业务自建的一个结构体,因为 RCMessage
的属性太多了,很多都用不到,当然你也可以选择不封装:
extension PJIM { enum MessageType { case text case audio } struct Message { var type: MessageType var textContent: String? var audioContent: Data? var sendUserId: String var msgId: Int var msgDirection: RCMessageDirection var msgStatus: RCSentStatus var msgReceivedTime: Int64 var msgSentTime: Int64 } struct MessageListCell { var avatar: Int var nickName: String var uid: String var message: Message? } } 复制代码
至此,我们通过了两个方法就完成了消息的发送和接收~可以愉快的玩耍一番了!
获取消息列表
如果你是免费用户,那么从融云获取消息列表只是本地数据,如果用户更换了设备、重装了 app 等都 会导致消息列表的丢失 ;如果你是收费用户,从融云服务器上拉取到的消息列表貌似 只有区区七天(再长也是多几天而已) ,所以如果对消息列表有追求的同学需要注意了。
我的消息列表还涉及到了用户信息的获取,这部分是异步请求,结合融云的同步获取本地消息列表,这就形成了一个异步操作保持顺序性的问题。为了到达“简洁”的操作,我只使用了“信号量”的方法完成。
/// 获取本地会话列表 func getConversionList(_ complateHandler: @escaping (([MessageListCell]) -> Void)) { let cTypes = [NSNumber(value: RCConversationType.ConversationType_PRIVATE.rawValue)] let cList = RCIMClient.shared()?.getConversationList(cTypes) as? [RCConversation] var msgListCells = [MessageListCell]() guard cList != nil else { return complateHandler(msgListCells)} if cList?.count != 0 { var cIndex = 0 for c in cList! { let currentMessage = RCMessage(type: .ConversationType_PRIVATE, targetId: c.targetId, direction: c.lastestMessageDirection, messageId: c.lastestMessageId, content: c.lastestMessage) currentMessage?.sentTime = c.sentTime currentMessage?.receivedTime = c.receivedTime currentMessage?.senderUserId = c.senderUserId currentMessage?.sentStatus = c.sentStatus if currentMessage != nil { let message = getMessage(with: currentMessage!) if message == nil { break } // 获取用户信息,可以替换为你的,如果不需要获取用户信息,可以删除 PJUser.shared.details(details_uid: c.targetId, getSelf: false, completeHandler: { let msgCell = MessageListCell(avatar: $0.avatar!, nickName: $0.nick_name!, uid: $0.uid!, message: message!) msgListCells.append(msgCell) if cIndex == cList!.count - 1 { var finalCells = [MessageListCell]() for cell in cList! { _ = msgListCells.filter({ if $0.uid == cell.targetId { finalCells.append($0) return true }; return false }) } complateHandler(finalCells) } cIndex += 1 }) { print($0.errorMsg) } } } } else { complateHandler(msgListCells) } } 复制代码
PJIM
结合融云形成一个简单数据服务就写好了,通过单例在任何你想要进行消息的发送和接收,完整代码如下。其中有一部分是业务耦合较为严重的方法不方便展开,看着替换即可。
// // PJIM.swift // PIGPEN // // Created by PJHubs on 2019/4/9. // Copyright © 2019 PJHubs. All rights reserved. // import Foundation @objc class PJIM: NSObject { var getMsg: ((Message) -> Void)? private static let instance = PJIM() class func share() -> PJIM { return instance } override init() { super.init() RCIMClient.shared()?.setReceiveMessageDelegate(self, object: nil) } /// 发送文本消息 func sendText(textString: String, userID: String, complateHandler: @escaping ((Int) -> Void), failerHandler: @escaping ((RCErrorCode) -> Void)) { let text = RCTextMessage(content: textString) RCIMClient.shared()?.sendMessage(.ConversationType_PRIVATE, targetId: userID, content: text, pushContent: nil, pushData: nil, success: { (mesId) in complateHandler(mesId) }, error: { (errorCode, mesId) in failerHandler(errorCode) }) } /// 获取本地会话列表 func getConversionList(_ complateHandler: @escaping (([MessageListCell]) -> Void)) { let cTypes = [NSNumber(value: RCConversationType.ConversationType_PRIVATE.rawValue)] let cList = RCIMClient.shared()?.getConversationList(cTypes) as? [RCConversation] var msgListCells = [MessageListCell]() guard cList != nil else { return complateHandler(msgListCells)} if cList?.count != 0 { var cIndex = 0 for c in cList! { let currentMessage = RCMessage(type: .ConversationType_PRIVATE, targetId: c.targetId, direction: c.lastestMessageDirection, messageId: c.lastestMessageId, content: c.lastestMessage) currentMessage?.sentTime = c.sentTime currentMessage?.receivedTime = c.receivedTime currentMessage?.senderUserId = c.senderUserId currentMessage?.sentStatus = c.sentStatus if currentMessage != nil { let message = getMessage(with: currentMessage!) if message == nil { break } PJUser.shared.details(details_uid: c.targetId, getSelf: false, completeHandler: { let msgCell = MessageListCell(avatar: $0.avatar!, nickName: $0.nick_name!, uid: $0.uid!, message: message!) msgListCells.append(msgCell) if cIndex == cList!.count - 1 { var finalCells = [MessageListCell]() for cell in cList! { _ = msgListCells.filter({ if $0.uid == cell.targetId { finalCells.append($0) return true }; return false }) } complateHandler(finalCells) } cIndex += 1 }) { print($0.errorMsg) } } } } else { complateHandler(msgListCells) } } private func getMessage(with rcMessage: RCMessage) -> Message? { switch rcMessage.objectName { case "RC:TxtMsg": let text = rcMessage.content as! RCTextMessage let m = Message(type: .text, textContent: text.content, audioContent: nil, sendUserId: rcMessage.senderUserId, msgId: rcMessage.messageId, msgDirection: rcMessage.messageDirection, msgStatus: rcMessage.sentStatus, msgReceivedTime: rcMessage.receivedTime, msgSentTime: rcMessage.sentTime) return m case "RCImageMessage": break default: break } return nil } } extension PJIM: RCIMClientReceiveMessageDelegate { func onReceived(_ message: RCMessage!, left nLeft: Int32, object: Any!) { print(message.objectName) switch message.objectName { case "RC:TxtMsg": let text = message.content as! RCTextMessage let m = Message(type: .text, textContent: text.content, audioContent: nil, sendUserId: message.senderUserId, msgId: message.messageId, msgDirection: message.messageDirection, msgStatus: message.sentStatus, msgReceivedTime: message.receivedTime, msgSentTime: message.sentTime) getMsg?(m) print(m.textContent!) case "RCImageMessage": break default: break } } } extension PJIM { enum MessageType { case text case audio } struct Message { var type: MessageType var textContent: String? var audioContent: Data? var sendUserId: String var msgId: Int var msgDirection: RCMessageDirection var msgStatus: RCSentStatus var msgReceivedTime: Int64 var msgSentTime: Int64 } struct MessageListCell { var avatar: Int var nickName: String var uid: String var message: Message? } } 复制代码
UI
经过之前的一番调整,即时聊天的数据源都准备好了,接下来就是要画界面了。关于 UI 库的选择上文已经说明经过了几番折腾后,最终的选择是 MessengerKit 。因为 UI 实现都很普通,没什么可以做拓展的地方,以下是一些我任何值得关注的地方:
ViewModel
融云提供的 RCMessage
类结构和 MessengerKit
所要求的数据类型不一样,需要我们单独针对 MessengerKit
做一个 ViewModel
喂食。
发送者和接收者
MessengerKit
聊天气泡的切换是根据“发送者”和“接收者”的 id
进行的,我们需要处理好从融云拉取过来的消息列表,根据“发送者” id
和“接受者” id
(也即 targetId
)进行分割为不同的 section
,以下是我的处理过程:
private func didSetMessageCell() { // 如果有未读消息数,进入聊天后就全部已读 let badge = RCIMClient.shared()!.getUnreadCount(.ConversationType_PRIVATE, targetId: messageCell!.uid) if (badge != 0) { RCIMClient.shared()!.clearMessagesUnreadStatus(.ConversationType_PRIVATE, targetId: messageCell!.uid) UIApplication.shared.applicationIconBadgeNumber -= Int(badge) } titleString = messageCell!.nickName friendUser = ChatUser(displayName: messageCell!.nickName, avatar: UIImage(named: "\(messageCell!.avatar)"), isSender: false) meUser = ChatUser(displayName: PJUser.shared.userModel.nick_name!, avatar: UIImage(named: "\(PJUser.shared.userModel.avatar!)"), isSender: true) func update(_ ms: [RCMessage]) { var m_index = 0 var tempMsgs = [MSGMessage]() var tempMsgUserId = messageCell?.uid messages.append(tempMsgs) // 便利所有消息,按照 sendId 和 targetId 进行分离 for m in ms { let text = m.content as! RCTextMessage if tempMsgUserId != m.senderUserId { tempMsgs.removeAll() messages.append(tempMsgs) m_index += 1 tempMsgUserId = m.senderUserId } let c_m: MSGMessage? if m.senderUserId != PJUser.shared.userModel.uid! { c_m = MSGMessage(id: m.messageId, body: .text(text.content), user: friendUser!, sentAt: Date(timeIntervalSince1970: TimeInterval(m.sentTime))) } else { c_m = MSGMessage(id: m.messageId, body: .text(text.content), user: meUser!, sentAt: Date(timeIntervalSince1970: TimeInterval(m.sentTime))) } // 设置消息已读状态 RCIMClient.shared()?.setMessageSentStatus(m.messageId, sentStatus: .SentStatus_READ) tempMsgs.insert(c_m!, at: 0) messages.insert(tempMsgs, at: m_index) messages.remove(at: m_index + 1) } messages.reverse() } let ms = RCIMClient.shared()?.getLatestMessages(.ConversationType_PRIVATE, targetId: messageCell?.uid, count: 30) as? [RCMessage] // 如果本地无消息,从融云服务器上拉取 if ms != nil { update(ms!) DispatchQueue.main.async { // reloadData 时主线程被占用,scrollToBottom 等待,reloadData 完成后,再执行 scrollToBottom self.collectionView.reloadData() DispatchQueue.main.async { self.collectionView.scrollToBottom(animated: false) } } } else { // TODO: 这部分有问题,需要交钱才能拉取到服务器上的历史消息 RCIMClient.shared()?.getRemoteHistoryMessages(.ConversationType_PRIVATE, targetId: PJUser.shared.userModel.uid!, recordTime: 0, count: 20, success: { (messages: [RCMessage]) in update(messages) } as? ([Any]?) -> Void, error: { (errorCode) in print(errorCode.rawValue) }) } } 复制代码
消息推送
这部分融云文档写得很好了~记得在 Xcode 中把 Background Modes
的“远程推送”打开。·
总结
以上是我完成了一期 IM 过程中的思考和总结,解决问题的方法还有些许不足,吸取了之前的做其它产品的教训,本次严格遵循 “MVP” 的开发流程,小步快跑,一期工作主要是先跑起来,让其它小伙伴聊起来。
以上所述就是小编给大家介绍的《搞事情之如何快速完成 IM》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!
猜你喜欢:- 记录最近的一些事情
- 搞事情之 Vapor 初探
- Angular-多级配置搞事情啦
- 搞点事情!死磕Java并发编程。
- 搞事情之 UML 的用例图
- 搞事情之 PJRulerPickerView 组件开发总结
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
Ajax模式与最佳实践
Christian Gross / 李锟、张祖良、蔡毅、赵泽欣 / 电子工业出版社 / 2007-3 / 49.80元
Ajax 正在将我们带入到下一代的网络应用中。 本书深入探讨了动态的网络应用,将Ajax和REST集成在一起作为单独的解决方案。一个很大的优势是,与Ajax相似,REST可以和现今存在的技术一起使用。现在上百万的客户端计算机都是基于Ajax的,上百万的服务器是基于REST的。 无论你是否已经开发过Ajax应用程序,这都是一本理想的书。因为这本书描述了各种各样的模式和最好的实践经验。通过此......一起来看看 《Ajax模式与最佳实践》 这本书的介绍吧!