iOS 腾讯云(TIM)-即时通讯使用总结

开篇啰嗦:

好久没在简书写文章了,最近两个月断断续续的把腾讯云通信移植到工程里面来了。我们以前的工程是采用的混合开发(H5+),即时通讯用的环信那套,现在更换为腾讯云。先把其中的部分代码和经验分享给大家。希望大家能少走点弯路。

开发经费注意:环信的收费版是大概9000块一年,然后腾讯的收费版(超过100个用户):12000块一年。 我们公司采用腾讯云的初衷是看重其稳定,大厂的原因吧...(其实实际的功能需求来看的话,环信还做的更加直白简单些)

1.登录注册部分:

官方文档地址,文档怎么说呢,例子都有,但是还是有些坑。慢慢道来

登录注册的话有两种方式
一种是托管模式,那么就是注册和登录都由开发者调起API来进行注册和登录。账号也是由腾讯帮你管理的
另外一种就是独立模式:所谓独立模式就是需要你的后台也接入TIMSDK,账号的注册由后台来完成,你只需要负责在app端进行登录就好了

托管模式:

我只介绍了最简单的,就是用账号和密码进行注册和登录的情况:
注册:

//注册成功回调协议:
@interface DMRegisterController ()<TLSStrAccountRegListener>
//注册按钮:
- (IBAction)btnSubmitResponse:(UIButton *)sender {
  if(self.tfPassword.text.length < 5){
     NSLog(@"请输入8-16位密码");
  }else{
     [[TLSHelper getInstance] TLSStrAccountReg:self.tfUsername.text andPassword:self.tfPassword.text andTLSStrAccountRegListener:self];
  }

//协议回调
//注册成功
- (void)OnStrAccountRegSuccess:(TLSUserInfo *)userInfo{
  //注册成功后保存这个用户名在本地: 
  [userDefaults setObject:userInfo.identifier forKey:UserIdentifier];
  [self dismissViewControllerAnimated:YES completion:nil];
}
//注册失败
- (void)OnStrAccountRegFail:(TLSErrInfo *)errInfo{
  NSLog(@"注册错误信息--%@",errInfo.sErrorMsg);
}
 

登录:(登录就有个小坑,得登录两次)

一次是账号密码登录,一次是获得本地签名后的云通讯服务登录

普通登录:

//登录回调协议:
@interface DMLoginController ()<TLSPwdLoginListener>
//登录按钮响应
- (IBAction)btnLoginResponse:(UIButton *)sender {
 if (![self.tfUsername.text isEqualToString:@""] && ![self.tfPassword.text isEqualToString:@""]) {
   [[TLSHelper getInstance] TLSPwdLogin:self.tfUsername.text andPassword:self.tfPassword.text andTLSPwdLoginListener:self];
  }
//协议回调
//登录成功:
- (void)OnPwdLoginSuccess:(TLSUserInfo *)userInfo{
   [self TLSLoginMethod];
   [userDefaults setObject:userInfo.identifier forKey:UserIdentifier];
   DMTabBarController *tabBar = [[DMTabBarController alloc]init];
   UIWindow *window = [UIApplication sharedApplication].keyWindow;
   window.rootViewController = tabBar;
  
}
//登录失败
- (void)OnPwdLoginFail:(TLSErrInfo *)errInfo{
  NSLog(@"登录错误信息--%@",errInfo.sErrorMsg);
}

//登录腾讯云聊天:
- (void)TLSLoginMethod{
  TIMLoginParam *param = [[TIMLoginParam alloc]init];
  param.identifier = [userDefaults objectForKey:UserIdentifier];
  param.appidAt3rd = @"1400136431";
  param.userSig = [[TLSHelper getInstance] getTLSUserSig:[userDefaults objectForKey:UserIdentifier]];
  [[TIMManager sharedInstance] login:param succ:^{
    NSLog(@"登录腾讯云聊天成功");
    //获取会话列表
    //    [self getConversationList];
    
  } fail:^(int code, NSString *msg) {
    NSLog(@"登录腾讯云聊天失败:%@",msg);
  }];
}

TIMLoginParam 是TIM的登录信息类,放一些登录参数:

/**
 * 用户名
 */
@property(nonatomic,strong) NSString* identifier;

/**
 *  鉴权Token
 */
@property(nonatomic,strong) NSString* userSig;

/**
 *  App用户使用OAuth授权体系分配的Appid
 */
@property(nonatomic,strong) NSString* appidAt3rd;

至此,这个托管模式的账号注册和登录就搞定了。这种注册登录是最简单的,腾讯云还提供了手机号加短信验证码等注册登录方式,根据个人需求采用吧。


特别说明一下腾讯云这个用户鉴权Token:userSig. 其实就是一个签名认证Token,为安全性考虑的,不像环信直接账号密码登录的。

独立模式:

所谓独立模式其实就是账号的登录过程交给服务端的伙伴完成,我们只需要拿到服务端返回的鉴权Token进行登录就好了

-(void)loginTIMAsync:(PGMethod *)commands {
    NSString *cellnumber = [commands.arguments objectAtIndex:1];
    NSString *password = [commands.arguments objectAtIndex:2];
    NSString *imAccountSig = [commands.arguments objectAtIndex:3];
    //先保存,后面掉线使用
    [[NSUserDefaults standardUserDefaults] setValue:cellnumber forKey:@"cellnumber"];
    [[NSUserDefaults standardUserDefaults] setValue:password forKey:@"password"];
    [[NSUserDefaults standardUserDefaults] synchronize];
  //JJTIM-登录腾讯云
  NSLog(@"传过来的账号和密码,签名:是:%@,%@,%@",cellnumber,easeMobPassword,imAccountSig);
   [userDefaults setObject:cellnumber forKey:UserIdentifier];  
   [self TLSRealLoginMethod:imAccountSig];
  }

loginTIM方法简单说明下,因为我们是混合开发,所以这个账号和签名是H5端登录后写插件传到原生界面的。

- (void)TLSRealLoginMethod:(NSString *)imAccountSig{
  TIMLoginParam *param = [[TIMLoginParam alloc]init];
  param.identifier = [userDefaults objectForKey:UserIdentifier];
  param.appidAt3rd = @"1400136431";
  param.userSig = imAccountSig;
  [userDefaults setObject:imAccountSig forKey:ImAccountSig];
  [[TIMManager sharedInstance] login:param succ:^{
    NSLog(@"登录腾讯云聊天成功");
     //获取会话列表
     [self getConversationList];
    //获取自己的昵称和头像:
    [self getNickNameAndHeadImg];
    } fail:^(int code, NSString *msg) {
    NSLog(@"登录腾讯云聊天失败:%@",msg);
  }];
}

至此独立模式的登录TIM聊天服务也完成了,下面来说说这个收发消息和群管理方面的了

收发消息

发送消息:

对于我公司项目的聊天需求是有:文字,图片,地理位置,语音,文件,自定义表情这几种,然后加上我的聊天界面也是自己写的,所以我的DMMessageVC就显得相当庞大了。这里只介绍消息的发送用法,不会介绍具体的UI实现和功能实现(实在是写得有点乱,网上有很多写得不错的聊天界面可以参考学习)

/**
 发送消息的方法

 @param type 单聊/群聊
 @param chatId 聊天id
 @param messageType 消息类型
 @param textMessage 文本消息
 @param photoPath 照片消息
 @param location 位置信息
 @param file 文件信息
 */
- (void)sendMessage:(TIMConversationType)type
                 chatId:(NSString *)chatId
            messageType:(NSInteger)messageType
            textMessage:(NSString *)textMessage
              photoPath:(NSString *)photoPath
               location:(TIMLocationElem *)location
               file:(TIMFileElem *)file
              sound:(TIMSoundElem *)soundElem
               face:(TIMFaceElem *)faceElem{
  
  //获取会话对象:
  //单聊
  
  //chatName实际就是identifer
  TIMConversation *c2c_conversation = [[TIMManager sharedInstance] getConversation:type receiver:chatId];
  WEAKSELF;
  if (self.messageType == MessageType_Text) {
    //文本消息:
    TIMTextElem *text_elem = [[TIMTextElem alloc] init];
    [text_elem setText:textMessage];
    TIMMessage *msg = [[TIMMessage alloc] init];
    [msg addElem:text_elem];
    [c2c_conversation sendMessage:msg succ:^{
      NSLog(@"消息发送成功");
      [weakSelf sendNotiSuccessMessage];
      [weakSelf updateLocalMessageList:YES messageType:weakSelf.messageType];
    } fail:^(int code, NSString *msg) {
      NSLog(@"文本消息发送失败:%@,错误码:%d",msg,code);
      [weakSelf sendMessageFailMethod:code];
      [weakSelf.dataArr removeLastObject];
    }];
  }else if (self.messageType == MessageType_Photo){
    //图片信息
    TIMImageElem *image_elem = [[TIMImageElem alloc]init];
    image_elem.path = photoPath;
    TIMMessage *msg = [[TIMMessage alloc]init];
    [msg addElem:image_elem];
    
    [c2c_conversation sendMessage:msg succ:^{
      NSLog(@"图片消息发送成功");
      [weakSelf sendNotiSuccessMessage];
      [[NSNotificationCenter defaultCenter] postNotificationName:@"photoSuccess" object:nil userInfo:nil];
    } fail:^(int code, NSString *msg) {
       NSLog(@"图片消息发送失败:%@,错误码:%d",msg,code);
      //如果图片消息发送失败,需要删除self.dataArr的最后一个元素
        [weakSelf.dataArr removeLastObject];
        [weakSelf sendMessageFailMethod:code];
    }];
    
    
  }else if (self.messageType == MessageType_Location){
    TIMLocationElem *elem = location;
    TIMMessage *msg = [[TIMMessage alloc]init];
    [msg addElem:elem];
    [c2c_conversation sendMessage:msg succ:^{
      NSLog(@"地理位置消息发送成功");
      [weakSelf sendNotiSuccessMessage];
      [weakSelf updateLocalMessageList:YES messageType:weakSelf.messageType];
      
    } fail:^(int code, NSString *msg) {
      NSLog(@"地理位置消息发送失败:%@,错误码:%d",msg,code);
        [weakSelf.dataArr removeLastObject];
        [weakSelf sendMessageFailMethod:code];
    }];
    
    
  }else if (self.messageType == MessageType_Sound){
    //语音消息:
    TIMMessage *msg = [[TIMMessage alloc]init];
    [msg addElem:soundElem];
    [c2c_conversation sendMessage:msg succ:^{
      NSLog(@"语音消息发送成功");
      [weakSelf updateLocalMessageList:YES messageType:weakSelf.messageType];
      [weakSelf sendNotiSuccessMessage];
    } fail:^(int code, NSString *msg) {
      NSLog(@"语音消息发送失败:%@,错误码:%d",msg,code);
      [weakSelf.dataArr removeLastObject];
      [weakSelf sendMessageFailMethod:code];
    }];
    
  }else if (self.messageType == MessageType_File){
    //文件消息
    TIMMessage *msg = [[TIMMessage alloc]init];
    [msg addElem:file];
    [c2c_conversation sendMessage:msg succ:^{
      NSLog(@"文件消息发送成功");
      [weakSelf sendNotiSuccessMessage];
      [weakSelf updateLocalMessageList:YES messageType:weakSelf.messageType];
    } fail:^(int code, NSString *msg) {
      NSLog(@"文件消息发送失败:%@,错误码:%d",msg,code);
      [weakSelf.dataArr removeLastObject];
      [weakSelf sendMessageFailMethod:code];
    }];
  }else if (self.messageType == MessageType_Face){
    //专属表情消息
    TIMMessage *msg = [[TIMMessage alloc]init];
    [msg addElem:faceElem];
    [c2c_conversation sendMessage:msg succ:^{
                    NSLog(@"发送表情消息成功");
                    [weakSelf sendNotiSuccessMessage];
                    [weakSelf updateLocalMessageList:YES messageType:weakSelf.messageType];
   }fail:^(int code, NSString *msg) {
           NSLog(@"发送表情消息失败:%d,%@",code,msg);
                [weakSelf.dataArr removeLastObject];
                [weakSelf sendMessageFailMethod:code];
         }];
    }
 }

其实发送消息的方法很简单,主要是基于消息基类TIMElem进行各种消息的处理

    [weakSelf updateLocalMessageList:YES messageType:weakSelf.messageType];

更新tableView的数据处理就又是一大推了,这个不涉及到TIM的东西

收到消息:

首先需要写个DMMessageListenerlmpl类继承自NSObject,实现TIMMessageListener协议

@interface DMMessageListenerImpl : NSObject <TIMMessageListener>
/**收到的消息数组*/
@property (nonatomic, strong) NSMutableArray<TIMMessage *> *receiveArr;
- (void)onNewMessage:(TIMMessage *)msg;
@end

#import "DMMessageListenerImpl.h"
@implementation DMMessageListenerImpl
- (void)onNewMessage:(NSArray *)msgs{
  NSLog(@"收到的消息是:%@",msgs);
  [self assigmentMessageWithArr:msgs];
}

- (void)assigmentMessageWithArr:(NSArray *)msgs{
  self.receiveArr = [NSMutableArray array];
  if (msgs.count > 0) {
     [self.receiveArr addObject:msgs.firstObject];
  }
  NSMutableDictionary *dic = [NSMutableDictionary dictionary];
  [dic setValue:self.receiveArr forKey:RECEIVE_MSG_DIC];
  [[NSNotificationCenter defaultCenter] postNotificationName:RECEIVE_MSG_NOTI object:nil userInfo:dic];
}

其次调用配置方法加入TIMManager

//收到消息回调配置
  DMMessageListenerImpl *impl = [[DMMessageListenerImpl alloc] init];
  [[TIMManager sharedInstance] addMessageListener:impl];

这个放在appDelegate中就好了

我的收到消息回调处理是通过通知传到聊天界面在其中处理的:

- (void)receiveMessage:(NSNotification *)noti{
   NSMutableArray *arr = [[noti userInfo] objectForKey:RECEIVE_MSG_DIC];
  
  TIMMessage *msg = arr.firstObject;
  if ([self.currentMessage isEqual:msg]) {
    NSLog(@"重复消息");
    return;
  }else{
    self.currentMessage = msg;
  }
  NSString *sender = self.currentMessage.sender;
  int cnt = [msg elemCount];
  for (int i = 0; i < cnt; i++) {
    TIMElem *elem = [msg getElem:i];
    if ([elem isKindOfClass:[TIMTextElem class]]) {
      //文本消息
      TIMTextElem *text_elem = (TIMTextElem *)elem;
      [self updateReceiveTextMessage:text_elem sender:sender];
    }
    else if ([elem isKindOfClass:[TIMImageElem class]]){
      //图片消息
      TIMImageElem *image_elem = (TIMImageElem *)elem;
      [self updateReceivePhotoMessage:image_elem sender:sender];
    }else if ([elem isKindOfClass:[TIMLocationElem class]]){
      //地理位置消息
      TIMLocationElem *locationElem = (TIMLocationElem *)elem;
      [self updateReceiveLocationMessage:locationElem sender:sender];
    }else if ([elem isKindOfClass:[TIMSoundElem class]]){
      //语音消息:
      TIMSoundElem *soundElem = (TIMSoundElem *)elem;
      [self updateReceiveVoiceMessage:soundElem sender:sender];
    }else if ([elem isKindOfClass:[TIMFileElem class]]){
      //文件消息
      TIMFileElem *fileElem = (TIMFileElem *)elem;
      [self updateReceiveFileMessage:fileElem sender:sender];
    }else if ([elem isKindOfClass:[TIMFaceElem class]]){
      //专属表情消息
      TIMFaceElem *faceElem = (TIMFaceElem *)elem;
      [self updateReceiveFaceMessage:faceElem sender:sender];
    }
  }

}

1.收到消息第一个注意点是消息通过代理的消息回调,有时TIM会回调多次,造成重复接收一样的消息,我处理就是用一个currentMsg来过滤一下。

  NSString *sender = self.currentMessage.sender;

这个当前消息的发送者,就是当前发送消息人的账号,我这里获取这个是为群聊天中对方昵称和头像做准备。


至此简单的收发消息就可以了,但是光看我的代码可能会有点蒙,因为还有许多处理是和消息体的准备相关的(语音,文件,地理位置等),其次是各类型的消息cell的展现。所以说如果能找到合适的聊天框架,那么可以在此基础上替换和自定义,不然完全自己实现还是一个不小的工作量了。

用户登录状态监听:

这个状态监听,是我们项目以前就是用环信来监控这个设备二次登录的,所以TIM这边也要用。
首先也是要写个类继承TIMUserStatusListener协议:

@interface DMUserStatusListener : NSObject <TIMUserStatusListener>{
}
- (void)onForceOffline;
- (instancetype)initWithController:(UIViewController *)vc;

@interface DMUserStatusListener ()
/**vc*/
@property (nonatomic, strong) UIViewController *vc;
@end
@implementation DMUserStatusListener
- (instancetype)initWithController:(UIViewController *)vc{
  if(self = [super init]){
    self.vc = vc;
  }
  return self;
}
//被踢下线:
- (void)onForceOffline{
  
  UIAlertController *alertVc = [UIAlertController alertControllerWithTitle:@"下线通知" message:@"您的账号在其他设备登录" preferredStyle:UIAlertControllerStyleAlert];
  UIAlertAction *sureAction = [UIAlertAction actionWithTitle:@"重新登录" style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) {
  
     //重新登录TIM:
    TIMLoginParam *param = [[TIMLoginParam alloc]init];
    param.identifier = [userDefaults objectForKey:UserIdentifier];
    param.appidAt3rd = @"1400136431";
    param.userSig = [userDefaults objectForKey:ImAccountSig];
    [[TIMManager sharedInstance] login:param succ:^{
      NSLog(@"重新登录TIM成功");
    } fail:^(int code, NSString *msg) {
      NSLog(@"登录TIM失败:%@",msg);
    }];
  }];
  UIAlertAction *cancelAction = [UIAlertAction actionWithTitle:@"退出"  style:UIAlertActionStyleCancel handler:^(UIAlertAction * _Nonnull action) {
        NSLog(@"退出应用");
  }];
  [alertVc addAction:cancelAction];
  [alertVc addAction:sureAction];
  [self.vc presentViewController:alertVc animated:YES completion:nil];

}
//userSig过期
- (void)onUserSigExpired {
  NSLog(@"userSig expired");
}

然后是配置,配置就有点坑了,按照文档中就直接少了一步,加入配置的步骤,不知道是不是在其它地方有写到,耽搁了小半天。

 TIMManager *manager = [TIMManager sharedInstance];
  TIMSdkConfig *config = [[TIMSdkConfig alloc]init];
  config.sdkAppId = 1400136431;
  config.accountType = TIMAccountType;
  int statusNumber = [manager initSdk:config];
  if(0 == statusNumber){
    NSLog(@"初始化TIM成功");
    //成为腾讯云用户状态监听的代理:
    self.userConfig = [[TIMUserConfig alloc]init];
    self.lister = [[DMUserStatusListener alloc]initWithController:viewCon];
    self.userConfig.userStatusListener = self.lister;
    [[TIMManager sharedInstance] setUserConfig:self.userConfig];
    
  }else{
    NSLog(@"初始化TIM失败");
  }
  [[TIMManager sharedInstance] setUserConfig:self.userConfig];

官方文档中没有这一步,要注意。 其次设置的位置也有说明: 在initSdk:后调用,login:前调用


写到这里我发现其实TIM的集成也没有多复杂,API都挺简单的,主要工作量还是在聊天消息的准备和消息的解析处理以及聊天界面的一些处理。如果有什么疑问可以直接私信我,我们可以一起交流。 网上关于TIM的博客还是有点少。

推荐阅读更多精彩内容

  • 1、通过CocoaPods安装项目名称项目信息 AFNetworking网络请求组件 FMDB本地数据库组件 SD...
    爱运动爱学习阅读 15,687评论 3 114
  • 池上的美,不是用一两句话可以说清的。 几年前,我们几个伙伴乘车一路前往,时而,雨落梨花,窗外黑蓝色的低云翻滚变幻,...
    文案创意人阅读 256评论 0 0
  • 敬爱的李老师,智慧的班主任,亲爱的学兄们: 大家好!我是领航商贸有限公司的汪艳,今天(2018.10.19)是我日...
    领航商贸阅读 105评论 0 0
  • “世界儿童日”始于1954年,是联合国设立的一个国际日,定于每年的11月20日。在这一天,全世界儿童将共同欢庆,庆...
    C玉莹阅读 245评论 0 1