去年一全年的工做主要是進行一款移動辦公類產品的框架開發。核心功能點的話,框架層涉及到的全局字號的調整、App主題顏色的調整、路由;核心業務涉及到IM即時通信、Hybird 移動開發平臺、工做流、高仿朋友圈。
網頁端是使用的一款叫 LayIM 的聊天系統,但很惋惜,LayIM 沒有提供原生的移動SDK,因此好傢伙,只能本身造輪子了😂。
接下來主要是對 IM即時通信 開發中遇到的難點進行記錄。php
剛開始開發的時候,也確實是沒有比較好的思路,在網上查詢了一些資料 iOS即時通信實現IM,最後也敲定了具體的實現方案。
git
WebSocket協議是基於TCP的一種新的網絡協議。 咱們在應用層,使用socket,輕易的實現了進程之間的通訊。避免直面TCP/IP協議。
在這邊,我沒有直接基於OS底層Scoket去實現自定義封裝,而是使用了一個第三方框架 SocketRocket。
github
做爲即時通信客戶端基類,主要實現:
一、SRWebSocket 的初始化。
二、實現 SRWebSocketDelegate 並向外部提供,當前狀態,打開鏈接,斷開鏈接,收發消息。
數據庫
聊天業務的基類,主要實現:
一、連、斷、重連、當前狀態、用戶信息。
二、提供單聊、羣聊等業務方法。
三、封裝約定的消息格式,給服務端發送 LayIM 消息。
四、接收來自服務器端的消息,解析。
緩存
首先,咱們來看看 服務端與客戶端進行交互的消息格式:服務器
@property (strong, nonatomic) LKLayIMServeMsgMine *mine; /**<發送方*/
@property (strong, nonatomic) LKLayIMServeMsgTo *to; /**<接收方*/
複製代碼
@property (copy, nonatomic) NSString *id; /**<發送方ID*/
@property (copy, nonatomic) NSString *messNo; /**<消息主鍵*/
@property (copy, nonatomic) NSString *avatar; /**<發送方的頭像*/
@property (copy, nonatomic) NSString *content; /**<內容*/
@property (copy, nonatomic) NSString *username; /**<發送者名字*/
@property (assign, nonatomic) NSTimeInterval timestamp; /**<時間戳*/
複製代碼
@property (copy, nonatomic) NSString *avatar; /**< 接受者的頭像 */
@property (copy, nonatomic) NSString *name; /**< 接受者名字 */
@property (copy, nonatomic) NSString *id; /**< 接受者ID */
@property (copy, nonatomic) NSString *type; /**< 消息類型:group | friend | business */
複製代碼
App端在接收到Serve端的消息後,會解析成App端使用的數據格式:markdown
不管是我本身發送的消息,仍是別人發的消息,都會通過接收消息這個方法處理中,咱們在解析中須要注意如下幾個點:
一、mine是消息發送方,to消息接收方。
二、若是發送方的id和當前用戶的cliendid 一致, 那麼說明是我本身的消息。
三、羣聊和我發送的 消息msg 的 conversionId 要換成 to(接收方) 中的 id。
複製代碼
會話類網絡
@property (copy, nonatomic, nullable) NSString *conversationId; /**<會話ID*/
@property (copy, nonatomic) NSString *avatar;/**<羣聊頭像*/
@property (copy, nonatomic) NSString *name;/**<會話標題*/
@property (copy, nonatomic) NSString *type;/**<類型,group|friend|business|多是notification*/
@property (assign, nonatomic) LKChatConversationType conversationType; /**<type屬性的具現化*/
@property (assign, nonatomic) NSUInteger unread;/**<未讀數*/
@property (strong, nonatomic,nullable) LKLayIMMessage *lastMessage; /**<最新一條消息*/
@property (copy, nonatomic,nullable) NSString *lastMessageNoWhenClean;
@property (assign, nonatomic) BOOL muted; /**<是否免打擾*/
@property (nonatomic, copy) NSString *draft; /**<草稿*/
@property (assign, nonatomic) BOOL mentioned; /**<是否有人提到了你,配合 @ 功能。不能看最後一條消息。由於可能倒數第二條消息提到了你,因此維護一個標記。*/ // 暫時不處理
@property (assign, nonatomic) BOOL isWholeMsg; /**<消息鏈是否徹底*/
@property (assign, nonatomic) NSTimeInterval stickTime; /**<設置置頂的時間*/
@property (assign, nonatomic) BOOL isHidden; /**<是否被關閉了羣聊,關閉了的,不會在列表顯示*/
@property (strong, nonatomic) NSArray *members; /**<羣成員,暫時無值*/
複製代碼
其餘一些輔助方法:併發
一、展現 最新一條消息 的 本地時間, 相似 xxx分鐘前。
二、根據消息主鍵messNo,對消息進行升、降序。
三、獲取該會話的最近 limit 條消息。 剛進頁面第一次調用的方法。
四、查詢歷史消息,獲取某條消息或指定時間戳以前的 limit 條消息。
五、返回所有的本地緩存的消息
複製代碼
消息類框架
@property (nonatomic, copy) NSString *conversationId;/**<iOS客戶端,本身建立的通用會話主鍵*/
@property (copy, nonatomic) NSString *chatNo; /**<會話主鍵 */
@property (copy, nonatomic) NSString *type;/**<會話的類型,group|friend|business|多是notification*/
@property (assign, nonatomic) LKChatConversationType conversationType;/**<會話類型*/
@property (copy, nonatomic, nullable) NSString *messNo;/**<消息主鍵*/
@property (copy, nonatomic, nullable) NSString *uuid;/**<本地消息uuid*/
@property (copy, nonatomic) NSString *sendUsr; /**<消息發送人主鍵*/
@property (copy, nonatomic) NSString *avatar;/**<發送人頭像*/
@property (copy, nonatomic) NSString *username;/**<發送人的名字*/
@property (copy, nonatomic) NSString *content;/**<消息內容*/
@property (assign, nonatomic) NSTimeInterval sendDat;/**<時間戳*/
@property (assign, nonatomic) BOOL isMineMsg;/**<是否爲我發出的消息*/
@property (assign, nonatomic) LKChatMessageType messageType;/**<消息類型*/
@property (assign, nonatomic) LKChatMessageSendState messageStatus;/**<消息狀態*/
@property (assign, nonatomic) LKChatMessageOwnerType ownerType;/**<消息的擁有者類型*/
@property (copy, nonatomic) NSMutableAttributedString *attributedContent; /**<TEXT消息的富文本:暫時沒法存到本地*/
@property (strong, nonatomic) NSDictionary *fileDic; /**<文件消息的FileDic*/
@property (strong, nonatomic) NSDictionary *voiceDic; /**<語音Dic*/
@property (nonatomic, copy) NSString *voiceDuration; /**<語音長度*/
@property (strong, nonatomic) NSDictionary *locationDic; /**<位置Dic*/
@property (strong, nonatomic) NSDictionary *cardDic; /**<業務Dic*/
// 本地本身發的:
@property (strong, nonatomic, nullable) UIImage *previewImage; /**<預覽圖:視頻 | 圖片*/
@property (strong, nonatomic) NSValue *imageSize; /**<圖片的比較適中的大小*/
複製代碼
其餘一些輔助方法:
一、是否顯示時間軸Label。
二、建立時間戳這種系統消息。
三、獲取消息的惟一標識uuid。
四、獲取業務消息的具體內容(好比附件的主鍵,業務信息的數據字典)。
複製代碼
交互消息設計完以後,再來看看 IMConversionManager 主要實現的點:
一、當前正在聊天的會話對象:currentConversation。
二、數據庫操做,將磁盤緩存轉化爲內存緩存。以後的增刪改查都是對內存緩存的操做以後,開啓子線程對磁盤緩存進行處理。
三、會話業務:查找所有的會話、設置已讀、增長未讀數、插入/更新會話、會話置頂、會話靜音、設置 draft 草稿、取出會話的草稿、刪除最近會話。
四、消息業務:取出早/晚於msgId的消息、根據會話id查詢消息、根據消息主鍵查找消息對象、消息模糊查詢、插入消息、更新消息、異步插入/刪除消息。
複製代碼
消息 Cell 注意點:
一、使用工廠模式,進行cell的註冊和初始化。
二、將一些通用的view封裝到基類中,便於調整,好比消息狀態,是否已讀,背景,頭像等等。
三、因爲本身發送的消息和別人發送的消息,只有背景顏色和頭像位置等一些稍小差異,因此在註冊時給Identifier添加前綴來區分(_SELF,_OTHER,_SYSTEM,_BUSINESS等。),從而避免在cellForRow中對佈局約束不斷改動。
四、因爲系統設置中存在全局字號調整的功能,因此在佈局時所有使用的Masonry佈局,支持自適應高度。
複製代碼
lame 的使用
網上仍是有許多實現的方案的,個人話,是參照了 PPStickerKeyboard。
市面上App和Serve端約定的表情格式的話,均相似 [開心]、[憤怒] 這些,LayIM的話,使用的是face[開心]、face[憤怒],因此在App端,處理時,仍是要展現表情。
實現方案: 咱們設置到輸入框的NSAttributedString中的每個NSTextAttachment都有一個"隱藏的"屬性-—表情的文本描述,這裏對NSAttributedString進行拓展就能實現。lk_setTextBackedString能夠對NSAttributedString的指定range設置一個LKTextBackedString類型的屬性,而lk_plainTextForRange能拿到NSAttributedString指定range的純文本。
- (void)lk_setTextBackedString:(LKTextBackedString *)textBackedString range:(NSRange)range
{
if (textBackedString && ![NSNull isEqual:textBackedString]) {
[self addAttribute:LKTextBackedStringAttributeName value:textBackedString range:range];
} else {
[self removeAttribute:LKTextBackedStringAttributeName range:range];
}
}
複製代碼
LayIM的消息格式都是String,針對圖片、視頻、定位這些媒體信息,咱們實現這些業務消息的實現方案是:
一、將圖片這些附件,先上傳,服務端返回主鍵。
二、構造對應消息格式:img[%@],video[%@],location[%@]。
三、JSONString以後就是簡單的文本信息了。
因爲Serve端在處理消息是否已經發送接收上存在技術難點,目前App端的實現方案是,在發送消息以後,設置消息狀態爲 消息發送中,記錄發送時間,展現Loading狀態框。若是接收到了Serve端發送過來的本身的消息,那麼設置消息狀態爲 消息發送成功,隱藏Loading狀態框,關閉定時器。若是一直沒有收到了Serve端的消息,那麼當前時間減去發送時間超過3分鐘後,展現Failed狀態框。
UIImage (LKChatExtension)
- (CGSize)lkchat_getScaledSize{
CGFloat ow = CGImageGetWidth(self.CGImage);
CGFloat oh = CGImageGetHeight(self.CGImage);
CGSize kMaxImageViewSize = {.width = 240, .height = 240};
CGFloat aspectRatio = ow / oh;
CGFloat width;
CGFloat height;
CGSize limitSize = kMaxImageViewSize;
if (ow < limitSize.width && oh < limitSize.height) {
width = ow;
height = oh;
return CGSizeMake(width, height);
}
//胖照片
if (limitSize.width / aspectRatio <= limitSize.height) {
width = limitSize.width;
height = limitSize.width / aspectRatio;
} else {
//瘦照片
width = limitSize.height * aspectRatio;
height = limitSize.height;
}
return CGSizeMake(width, height);
}
複製代碼
將優化後的size記錄在消息中,緩存到本地。
其次,因爲加載圖片是耗時的,故,針對圖片也要進行緩存。
具體查看以前的 LayIMMessage。
當圖片加載完成前展現的是一份展位圖,加載完成後,經過代理方法告知聊天室VC去刷新頁面UI,滾動到頁面底部等等,告知數據類 IMConversionManager 去更新數據。
使用的是框架中的下載工具類。針對 LayIM 作了一個單例的分類,單獨緩存聊天中的下載文件,不作最大下載併發量的設置,緩存時對文件進行前綴標註,方便設置中心清除緩存。
以後單獨拿出來,整理一下。
功能點:保證App端的消息鏈是最完整的。
目前處理歷史消息鏈的時候,是有網狀態下,每次都是從服務端取消息,而後存儲到本地。無網狀態下,從本地數據庫中出取。
想要實現好方案的話:是在有網狀態下,會判斷本地消息鏈是不是連續的,非連續會去從服務端請求,再整合。
複製代碼
目前全部的業務功能都是寫在 聊天鍵盤類(ChatBar) 中的,沒有很好的作抽離解耦,後續增長功能,好比視頻會議等等這些業務功能的話,只能在 ChatBar 中添加代碼。須要考慮如何作出拓展。
複製代碼