最新环信V3.3.7单聊集成与使用

architecture.jpg
环信官网:http://www.easemob.com/
开发文档:http://docs.easemob.com/im/start

一、前言

在自己做的第三个项目中,接触到了环信,原来没有对环信接入有些了解,导致在项目中使用遇到了很多的坑,最新环信V3.3.7版本也对iPhone X进行了适配,现在自己也有点空闲时间,对环信集成进行整理,方便相关功能快速开发,下面开始介绍环信集成与使用吧。

二、文章介绍内容目录

1、集成主要步骤简介

2、项目集成HyphenateSDK

3、项目集成EaseUI

4、项目具体使用

5、常见问题解决

1、集成主要步骤简介

集成环信服务主要有以下三个步骤

  • 1.1 注册

注册开发者账号并创建应用

  • 1.2 服务器端集成(REST API)

集成用户和好友体系

  • 1.3 客户端集成

集成文档:
Android SDK 集成
iOS SDK 集成
Linux SDK 集成
Web IM SDK 集成
集成用户和好友体系

2、项目集成HyphenateSDK

  • 2.1 集成 iOS SDK 前的准备工作

如果你需要用到环信推送功能,需要按照环信制作并上传推送证书申请配置证书,不需要则跳过此步骤。

  • 2.2 SDK重要组成部分集成前需了解

demo.png

SDK_Core: 为核心的消息同步协议实现,完成与服务器之间的信息交换。
SDK: 是基于核心协议实现的完整的 IM 功能,实现了不同类型消息的收发、会话管理、群组、好友、聊天室等功能。
EaseUI: 是一组 IM 相关的 UI 控件,旨在帮助开发者快速集成环信 SDK。

sdk.png

EMClient: 是 SDK 的入口,主要完成登录、退出、连接管理等功能。也是获取其他模块的入口。
EMChatManager: 管理消息的收发,完成会话管理等功能。
EMContactManager: 负责好友的添加删除,黑名单的管理。
EMGroupManager: 负责群组的管理,创建、删除群组,管理群组成员等功能。
EMChatroomManager: 负责聊天室的管理。

上面这些内容,可以对环信SDK组成有一个大致的理解,在最开始接入环信时候,自己没有看相关介绍文档,导致自己使用工程中遇到很多坑。

2.3 通过 Cocoapods导入

  • 2.3.1 不包含实时语音版本 SDK(HyphenateLite),引用时 #import <HyphenateLite/HyphenateLite.h>
pod 'HyphenateLite'
  • 2.3.2 包含实时语音版本 SDK(Hyphenate),引用时 #import <Hyphenate/Hyphenate.h>
pod 'Hyphenate'

2.4 手动集成

  • 2.4.1 SDK下载

官方最新SDK+Demo
HyphenateV3.3.7

  • 2.4.2 将ios_IM_sdk_V3.3.7文件下的SDK文件夹拖入到工程,如下图所示:
屏幕快照 2018-01-11 下午2.42.24.png
屏幕快照 2018-01-11 下午2.49.43.png
  • 2.4.3 添加依赖库
  • 不包含实时语音系统依赖库

CoreMedia.framework
AudioToolbox.framework
AVFoundation.framework
MobileCoreServices.framework
ImageIO.framework
ibc++.tbd
libz.tbd
libstdc++.6.0.9.tbd
libsqlite3.tbd

  • 包含实时语音系统依赖库

CoreMedia.framework
AudioToolbox.framework
AVFoundation.framework
MobileCoreServices.framework
ImageIO.framework
libc++.tbd
libz.tbd
libstdc++.6.0.9.tbd
ibsqlite3.tbd
libiconv.tbd

如果使用的是 xcode7以下,后缀为dylib。
Parse.frameworkBolts.framework: Demo 中的用户信息存储在 Parse,这两个库是 Parse 所需要的库,开发者如果没用 Parse 存储,不要复制到自己项目中。

  • 2.4.4 因为Hyphenate是动态库,需要在Build Phase中 Embedded Binaries添加Hyphenate.framework,如下图所示:
屏幕快照 2018-01-11 下午3.14.39.png
  • 到这里,SDK已经导入到项目中了,commad+R运行下工程,没有报错说明已成功集成。
  • 这里不得不提醒下自己,在第一次接入环信的时候没有认真阅读环信集成文档,没有添加Embedded Binaries,导致能够正常初始化和登录,但是不能够正常接收消息,自己找了好久也没有发现问题所在。还是怪自己阅读文档不认真,有经验的同事很快帮我找到问题了。

3、项目集成EaseUI

具体使用详情,请戳:EaseUI使用指南

  • 3.1 EaseUI支持pod导入

//Pod集成EaseUI时,会同时通过Pod集成SDK

//对应Hyphenate SDK(sdk包含实时音视频)
pod 'EaseUI', :git => 'https://github.com/easemob/easeui-ios-hyphenate-cocoapods.git'

//对应HyphenateLite SDK(sdk不包含实时音视频)
pod 'EaseUILite', :git =>'https://github.com/easemob/easeui-ios-hyphenate-cocoapods.git'

// 指定版本,可以在后面添加tag,例如:导入最新V3.3.7
pod 'EaseUI', :git => 'https://github.com/easemob/easeui-ios-hyphenate-cocoapods.git', :tag => ‘3.3.7’

项目中没有用pod方式导入,如果对UI没有太多自定义要求,可以用pod方式导入,方便版本管理,如果EaseUI不能满足项目需求,这时就要用手动导入方式了。不知道pod导入有坑没有,哈哈😝。

  • 3.2 手动导入

  • 3.2.1 EaseUI依赖三方库

MWPhotoBrowser:图片处理库,浏览显示
MJRefresh:用于页面刷新
MBProgressHUD:用于提示加载刷新
libopencore-amrnb.a、libopencore-amrwb.a:用于 amr 与 wav 之间的转换
EMSDWebImage(SDWebImage):图片下载

  • 3.2.2 对EaseUI文件夹文件进行整理
  • 如果不想使用v3.3.7中的三方依赖库(比如说MJRefresh库更新过后适配了iPhone X),我们想通过Cocoapods导入相关依赖库,方便项目对三方库管理。这时需要对先关文件进行修改,但改动的也并不是很多。整理过后EaseUI目录如下图所示:
    屏幕快照 2018-01-11 下午9.54.04.png
  • 3.2.3 这时依赖库通过Cocoapods导入,在引用到这些库头文件的地方改为import <头文件名>就好了,方法调用修改下:
pod 'MJRefresh','3.1.15.1'
pod 'MBProgressHUD','1.1.0'
pod 'SDWebImage','4.2.3'
  • 3.2.4 创建一个PCH文件,导入EaseUI头文件:
#ifdef __OBJC__

#ifndef PrefixHeader_pch
#define PrefixHeader_pch

#import <UIKit/UIKit.h>
#import <Foundation/Foundation.h>
#import "EaseUI.h"
#endif
// Include any system framework and library headers here that should be included in all compilation units.
// You will also need to set the Prefix Header build setting of one or more of your targets to reference this file.

#endif /* PrefixHeader_pch */
  • EaseUI.h文件做如下修改:
//#import "UIImageView+EMWebCache.h"
#import <UIImageView+WebCache.h>
  • 最新SDWebImage下载图片方法名修改一下:
//旧版本
downloadImageWithURL:
//新版本V4.2.3
loadImageWithURL:

整理好的EaseUI最终文件请戳:
V3.3.7 EaseUI

  • 3.2.5 对V3.3.7Demo中聊天控制器和会话列表控制器进行整理,注释不必要的文件引用和代码。我也将整理好的文件贴出来吧,请戳:

WBChat

4、项目具体使用

可以封装一个工具类来管理Hyphenate初始化、登录等相关操作。

  • 4.1 Hyphenate初始化

- (void)initHyphenateSDK {
    EMOptions *options = [EMOptions optionsWithAppkey:@""];
    //        options.apnsCertName = kEasemobSDKPushName;
    EMError *error = nil;
    error =  [[EMClient sharedClient] initializeSDKWithOptions:options];
    if (!error) {
        NSLog(@"环信初始化成功");
    }
    
    /**  < 注册通知 >  */
    [self registerNoti];
}
  • 4.2 登录&退出登录

- (void)wb_hyphenateLoginSuccess:(void (^)(void))success failure:(void (^)(void))failure {
    /** <<
     用户调用了 SDK 的登出动作;
     用户在别的设备上更改了密码,导致此设备上自动登录失败;
     用户的账号被从服务器端删除;
     用户从另一个设备登录,把当前设备上登录的用户踢出。
     > */
    BOOL isAutoLogin = [EMClient sharedClient].options.isAutoLogin;
    if (!isAutoLogin) {
        EMError *error = [[EMClient sharedClient] loginWithUsername:@"" password:@""];
        if (!error) {
            if (success) {
                success();
            }
            /**  < 设置是否自动登录 >  */
            [[EMClient sharedClient].options setIsAutoLogin:YES];
            NSLog(@"环信登录成功");
        }else {
            if (failure) {
                failure();
            }
             NSLog(@"环信登录失败");
        }
    }
}

- (void)wb_hyphenateLogoutSuccess:(void (^)(void))success failure:(void (^)(void))failure {
    EMError *error = [[EMClient sharedClient] logout:YES];
    if (!error) {
        NSLog(@"环信退出成功");
        if (success) {
            success();
        }
    }else {
        if (failure) {
            failure();
        }
        NSLog(@"环信退出失败");
    }
}
  • 4.3 自动登录

- (void)wb_applicationDidEnterBackground:(UIApplication *)application {
    [[EMClient sharedClient] applicationDidEnterBackground:application];
}

- (void)wb_applicationWillEnterForeground:(UIApplication *)application {
    [[EMClient sharedClient] applicationWillEnterForeground:application];
}

  • 4.4 跳转聊天界面

/**  <
     单聊:EMConversationTypeChat
     Chatter:聊天对象用户名
     >  */
    WBChatViewController *vc = [[WBChatViewController alloc]initWithConversationChatter:@"" conversationType:EMConversationTypeChat];
    [self.navigationController pushViewController:vc animated:YES];

主要有以下两种:

  • 从APP服务器获取昵称和头像

  • 从消息扩展中获取昵称和头像

  • 4.5.2 我在项目中采用的是从消息扩展中获取昵称和头像,下面开始介绍我的实现方案吧。
  • 会话列表处理,在EaseConversationListViewController.m中的refreshAndSortView加载会话列表方法,对发送方消息扩展字段进行缓存,可以用数据库或者是归档等方法,我采用的是归档,保存扩展消息模型。
    //缓存发送方用户信息到本地

-(void)refreshAndSortView
{
    if ([self.dataArray count] > 1) {
        if ([[self.dataArray objectAtIndex:0] isKindOfClass:[EaseConversationModel class]]) {
            NSArray* sorted = [self.dataArray sortedArrayUsingComparator:
                               ^(EaseConversationModel *obj1, EaseConversationModel* obj2){
                                   EMMessage *message1 = [obj1.conversation latestMessage];
                                   EMMessage *message2 = [obj2.conversation latestMessage];
                                   if(message1.timestamp > message2.timestamp) {
                                       return(NSComparisonResult)NSOrderedAscending;
                                   }else {
                                       return(NSComparisonResult)NSOrderedDescending;
                                   }
                               }];
            [self.dataArray removeAllObjects];
            [self.dataArray addObjectsFromArray:sorted];
            for (EMConversation *conversation in self.dataArray) {
                /**  < 缓存发送消息者信息 >  */
                /**  < 收到的对方发送的最后一条消息,也是会话里的最新消息 >  */
                EMMessage *lastReceiveMessage = [conversation lastReceivedMessage];
                if (lastReceiveMessage) {
                    NSDictionary *extDic = lastReceiveMessage.ext;
                    HyhenateUserModel *user = [HyhenateUserModel yy_modelWithDictionary:extDic];
                    BOOL res = [[WBConversationManager shareManager] wb_archiveObjectToFileWithConversation_ID:conversation.conversationId archiveData:user];
                    if (res) {
                        NSLog(@"更新聊天对象信息成功 ");
                    }else {
                        NSLog(@"更新聊天对象信息失败 ");
                    }
                }
            }
        }
    }
    [self.tableView reloadData];
}

//在WBConversationListViewController.m读取缓存

pragma mark ------ < EaseConversationListViewControllerDataSource > ------
#pragma mark
- (id<IConversationModel>)conversationListViewController:(EaseConversationListViewController *)conversationListViewController
                                    modelForConversation:(EMConversation *)conversation
{
    EaseConversationModel *model = [[EaseConversationModel alloc] initWithConversation:conversation];
    if (model.conversation.type == EMConversationTypeChat) {
//        if ([[RobotManager sharedInstance] isRobotWithUsername:conversation.conversationId]) {
//            model.title = [[RobotManager sharedInstance] getRobotNickWithUsername:conversation.conversationId];
//        } else {
//            UserProfileEntity *profileEntity = [[UserProfileManager sharedInstance] getUserProfileByUsername:conversation.conversationId];
//            if (profileEntity) {
//                model.title = profileEntity.nickname == nil ? profileEntity.username : profileEntity.nickname;
//                model.avatarURLPath = profileEntity.imageUrl;
//            }
//        }
        HyhenateUserModel *user = [[WBConversationManager shareManager] wb_unarchiveObjectWithConversation_ID:conversation.conversationId];
        if (user) {
            model.title = user.nick;
            model.avatarURLPath = user.avatar;
        }
    } else if (model.conversation.type == EMConversationTypeGroupChat) {
        NSString *imageName = @"groupPublicHeader";
        if (![conversation.ext objectForKey:@"subject"])
        {
            NSArray *groupArray = [[EMClient sharedClient].groupManager getJoinedGroups];
            for (EMGroup *group in groupArray) {
                if ([group.groupId isEqualToString:conversation.conversationId]) {
                    NSMutableDictionary *ext = [NSMutableDictionary dictionaryWithDictionary:conversation.ext];
                    [ext setObject:group.subject forKey:@"subject"];
                    [ext setObject:[NSNumber numberWithBool:group.isPublic] forKey:@"isPublic"];
                    conversation.ext = ext;
                    break;
                }
            }
        }
        NSDictionary *ext = conversation.ext;
        model.title = [ext objectForKey:@"subject"];
        imageName = [[ext objectForKey:@"isPublic"] boolValue] ? @"groupPublicHeader" : @"groupPrivateHeader";
        model.avatarImage = [UIImage imageNamed:imageName];
    }
    return model;
}

//右滑删除本地缓存:EaseConversationListViewController.m->deleteCellAction:

- (void)deleteCellAction:(NSIndexPath *)aIndexPath
{
    EaseConversationModel *model = [self.dataArray objectAtIndex:aIndexPath.row];
    [[EMClient sharedClient].chatManager deleteConversation:model.conversation.conversationId isDeleteMessages:YES completion:nil];
    /**  < 删除用户信息 >  */
    [[WBConversationManager shareManager] removeArchiveDataAtFilePath:model.conversation.conversationId];
    [self.dataArray removeObjectAtIndex:aIndexPath.row];
    [self.tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:aIndexPath] withRowAnimation:UITableViewRowAnimationFade];
}
  • 聊天界面处理
    //添加用户信息扩展,在EaseMessageViewController.m->_sendMessage方法中添加扩展信息
- (void)_sendMessage:(EMMessage *)message
    isNeedUploadFile:(BOOL)isUploadFile
{
    if (self.conversation.type == EMConversationTypeGroupChat){
        message.chatType = EMChatTypeGroupChat;
    }
    else if (self.conversation.type == EMConversationTypeChatRoom){
        message.chatType = EMChatTypeChatRoom;
    }
    
    NSDictionary *ext = message.ext;
    if (ext == nil) {
        /**  <
         发送消息最终执行的方法,在这里构造自己信息扩展,通常用户信息在本地都有保存,这里只是测试
         >  */
        NSDictionary *userDict = @{@"nick" : @"test",
                                   @"avatar" : @"imageFile"
                                   };
        message.ext = ext;
    }
    
    __weak typeof(self) weakself = self;
    if (!([EMClient sharedClient].options.isAutoTransferMessageAttachments) && isUploadFile) {
        UIAlertView *alertView = [[UIAlertView alloc] initWithTitle:nil message:NSLocalizedString(@"message.autoTransfer", @"Please customize the transfer attachment method") delegate:nil cancelButtonTitle:NSLocalizedString(@"sure", @"OK") otherButtonTitles:nil, nil];
        [alertView show];
    } else {
        [self addMessageToDataSource:message
                            progress:nil];
        
        [[EMClient sharedClient].chatManager sendMessage:message progress:^(int progress) {
            if (weakself.dataSource && [weakself.dataSource respondsToSelector:@selector(messageViewController:updateProgress:messageModel:messageBody:)]) {
                [weakself.dataSource messageViewController:weakself updateProgress:progress messageModel:nil messageBody:message.body];
            }
        } completion:^(EMMessage *aMessage, EMError *aError) {
            if (!aError) {
                [weakself _refreshAfterSentMessage:aMessage];
            }
            else {
                [weakself.tableView reloadData];
            }
        }];
    }
}

//扩展信息展示,在WBChatViewController.m->messageViewController:修改

#pragma mark ------ < EaseMessageViewControllerDataSource > ------
#pragma mark
/** << 设置头像、昵称 > */
- (id<IMessageModel>)messageViewController:(EaseMessageViewController *)viewController
                           modelForMessage:(EMMessage *)message
{
    id<IMessageModel> model = nil;
    model = [[EaseMessageModel alloc] initWithMessage:message];
    model.avatarImage = [UIImage imageNamed:@"EaseUIResource.bundle/user"];
//    UserProfileEntity *profileEntity = [[UserProfileManager sharedInstance] getUserProfileByUsername:model.nickname];
//    if (profileEntity) {
//        model.avatarURLPath = profileEntity.imageUrl;
//        model.nickname = profileEntity.nickname;
//    }
    if (model.isSender) {
        //直接取出本地用户信息
        model.nickname = @"本地保存的用户名";
        model.avatarURLPath = @"本地用户头像地址";
    }else {
        NSDictionary *userDict = message.ext;
        HyhenateUserModel *user = [HyhenateUserModel yy_modelWithDictionary:userDict];
        model.nickname = user.nick;
        model.avatarURLPath = user.avatar;
    }
    model.failImageName = @"imageDownloadFail";
    return model;
}

代码我已托管到码云上了,详情请戳:ManageHyphenateSDK
使用demo注意

  • 因为环信SDK超过了100M,我对其进行了忽略,可以将HyphenateFullSDK.zip文件解压缩重新导入一次。
  • 导入三方库,cd 工程路径,执行
pod install

完成上述两个步骤,就能正常运行工程啦~,运行效果如下图所示:


Untitled.gif

5、常见问题解决

  • 5.1 image not found

屏幕快照 2018-01-11 下午3.03.57.png

可参考标题目录2.4.4解决,或者
屏幕快照 2018-01-11 下午3.26.40.png

  • 5.2 集成动态库上传AppStore

由于 iOS 编译的特殊性,为了方便开发者使用,我们将 i386 x86_64 armv7 arm64 几个平台都合并到了一起,所以使用动态库上传appstore时需要将i386 x86_64两个平台删除后,才能正常提交审核
在SDK当前路径下执行以下命令删除i386 x86_64两个平台
bak文件是备份目录,上传appstore之后需要替换回bak目录下的SDK

  • 实时音视频版本Hyphenate.framework
mkdir ./bak
cp -r Hyphenate.framework ./bak
lipo Hyphenate.framework/Hyphenate -thin armv7 -output Hyphenate_armv7
lipo Hyphenate.framework/Hyphenate -thin arm64 -output Hyphenate_arm64
lipo -create Hyphenate_armv7 Hyphenate_arm64 -output Hyphenate
mv Hyphenate Hyphenate.framework/
  • 不包含实时音视频版本HyphenateLite.framework
mkdir ./bak
cp -r HyphenateLite.framework ./bak
lipo HyphenateLite.framework/HyphenateLite -thin armv7 -output HyphenateLite_armv7
lipo HyphenateLite.framework/HyphenateLite -thin arm64 -output HyphenateLite_arm64
lipo -create HyphenateLite_armv7 HyphenateLite_arm64 -output HyphenateLite
mv HyphenateLite HyphenateLite.framework/

三、总结

终于整理完成了,自己在写Demo的过程中,由于git reset命令使用不当,导致整理的文件全部撤销了,顿时都无语了,不管怎样,最终还是整理完成了。在集成EaseUI的时候,建议还是手动集成吧,如果你需要对其中文件进行更改。Demo中导入的EaseUI,我并不是原样导入的,如果你不想修改,可以直接将EaseUI导入到工程。由于能力有限,有些地方整理的不是很好,也可以自行修改,希望自己以后集成环信能少遇到一些坑,希望这篇文章也能对你有所帮助。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,456评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,370评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,337评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,583评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,596评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,572评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,936评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,595评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,850评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,601评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,685评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,371评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,951评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,934评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,167评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,636评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,411评论 2 342

推荐阅读更多精彩内容