将环信集成到已有的业务体系,后台用php写的接口
首先讲下版本,开始开始集成环信的应用的时间比较早,用的是2.1.6版本。
后台接口用的php和mysql。
下面,我就将应用本身的注册,登陆和环信的想结合的逻辑分享下。
1.登陆的逻辑是,先在app里面调用服务器接口,接口里面判断账号密码是否正确,(注意:在做环信的登陆的时候,为了安全,我并不是直接使用用户的账号密码登陆,而是用户唯一uid和加密后的密码)。
代码如下:
EMChatManager.getInstance().login(username, password,
new EMCallBack() {
#####
});
2.这里要说下关于用户昵称和头像,官方给出了两个解决方案,我选择的就是第一个方案。在登陆成功的回掉里面,you有这样的代码
try {
// ** 第一次登录或者之前logout后再登录,加载所有本地群和回话
// 处理好友和群组
processContactsAndGroups();
}
我就是在这里去出来头像和昵称的,用sqlite来保存。在保存的时候,异步通过用户list的username去后台请求昵称和头像,然后保存到sqlite。然后再messageAdapter中设置。
MessageAdapter中
/**
* 显示用户头像
*
* @param message
* @param imageView
*/
private void setUserAvatar(EMMessage message, ImageView imageView) {
if (message.direct == Direct.SEND) {
// // 显示自己头像
UserUtils.setSendAvatar(context, imageView);
} else {
UserUtils.setDirectAvatar(context, message, imageView);
}
}
UserUtils内容
/**
* 设置对方头像
*
* @param username
*/
public static void setDirectAvatar(Context context, EMMessage message,
ImageView imageView) {
AppContext appContext = (AppContext) context.getApplicationContext();
HXIMUserDao hximUserDao = appContext.getHXIMUserDao();
String dbface = hximUserDao.viewItem(HXIMUserDao.COLUMN_NAME_NAME
+ "=?", new String { message.getFrom() });
if (dbface != null&&dbface.length()>0) {
Picasso.with(context).load(dbface)
.placeholder(R.drawable.default_avatar).into(imageView);
} else {
IMUser user = getUserInfo(message.getFrom());
Picasso.with(context).load(user.getAvatar())
.placeholder(R.drawable.default_avatar).into(imageView);
}
}
HXIMUserDao内容
@Override
public String viewItem(String selection, String selectionArgs) {
SQLiteDatabase database = null;
Cursor cursor = null;
Map<String, String> map = new HashMap<String, String>();
try {
database = hximusersqlhelper.getWritableDatabase();
cursor = database.query(false, TABLE_NAME, null, selection,
selectionArgs, null, null, null, null);
int cols_len = cursor.getColumnCount();
while (cursor.moveToNext()) {
for (int i = 0; i < cols_len; i++) {
String cols_name = cursor.getColumnName(i);
String cols_values = cursor.getString(cursor
.getColumnIndex(cols_name));
if (cols_values == null) {
cols_values = "";
}
map.put(cols_name, cols_values);
}
}
} catch (Exception e) {
// TODO: handle exception
} finally {
if (database != null) {
database.close();
}
}
return map.get(COLUMN_NAME_FACE);
}
3.关于注册,由于环信的提倡的是用REST API 注册,不是在app里面,所以注册接口逻辑是,当用户提交账号 和密码我就在自己的数据库中添加一条记录并且返回uid,然后用uid和经过加密后的密码 去向环信提交注册。
//$result为0注册失败
//注册成功后再取hx注册 ,成功则返回result为true
//否则返回false同时删除再app服务器上的注册
if ($result) {
$registcondition['user_id']=$result;
$User=$this->model->table('user');
$registinfo=$User->where($registcondition)->find();
$hxoptions['username']=$registinfo['user_id'];
$hxoptions['password']=$registinfo['user_userpass'];
$mooptions['client_id']=$this->config['CLIENT_ID'];
$mooptions['client_secret']=$this->config['CLIENT_SECRET'];
$mooptions['org_name']=$this->config['ORG_NAME'];
$mooptions['app_name']=$this->config['APP_NAME'];
require_once('Easemob.class.php');
$Easemob = new Easemob($mooptions);
$hxregisterresult= $Easemob->accreditRegister($hxoptions);
if ($hxregisterresult['status']==200) {
$result=true;
}else{
$deleteuser['user_id']=$result;
$deleteresult=$User->where($deleteuser)->delete();
$result=false;
}
}
注意,如果在环信上如果注册失败,则要在本地删除这条记录,以免用处再次cha尝试注册的时候 ,提示已经注册,但其实环信没有注册成功所以无法登陆。修改密码也是这样的 。
建议:在sdk里面,好像没有将该会话置顶的方法。而是统一按照最后消息排序
最后感谢下环信,不管从消息下发,文档,sdk,以及客服各个方面来说都做的不错。最后提交一张我自己集成会话的页面截图。
收起阅读 »
集成环信时遇到的问题
"_ASN1_INTEGER_get",
referenced from:
_get_cert_info in
libEaseMobClientSDKLite.a(ssl_sock_ossl.o)
今天集成环信,可能是因为之前导入过多第三方静态库的原因,导致出现以上75个错去,貌似说编译的时候不能找到.a文件,在真机上可以跑,但在模拟器上就出现此错误,现将我出现错误的原因及解决方法晒出,希望可以帮助将来跟我一样遇到困难的人.
原因如下:在Build Settings下Linking 下的other Linker Flags中添加了-ObjC、-all_load或-force_load从而导致出现上述问题
在Build Settings下Linking 下的other Linker Flags 只保留-ObjC即可
收起阅读 »
关于集成环信即时通讯的文章
iOS 环信 集成 详细步骤图文

没有初始化SDK
去AppDelegate里面初始化
密码
User not exist ??????????
每一个应用都有自己的注册用户 去你的后台管理 去看你的注册的用户数
为什么demo的可以跑起来????????
是在它的应用下注册的
怎么去注册用户 -注册用户
用户名字可以相同 不同的应用
那个打印的loginInfo 是这个字典的也就是用户的登录信息

你会发现打印的loginInfo上面还有一坨恶心的东西,那个是环信SDK自己打印的日志信息
SEND 和 RECV 使用的是XMPP协议 所以 数据的格式是XML 一般的HTTP协议的话他就是JSON格式的
这个是app把客户端登录的信息发给环信服务器后打印出来的日志 是不是很烦
如何去在哪儿隐藏它的控制台的日志信息 ?????????
去初始化的时候把它隐藏 有一个otherConfig的东西 右键-jump to Define
进去瞅瞅--

复制它的key 给他设置为NO

这个时候它的控制台的日志信息就被屏蔽了
跟环信交互的所有类都有这个
[EaseMob sharedInstance].chatManager
注册的时候代码:

如果你把这onQueue改为nil的话他默认也是在主线程的
然后你在去环信的开发中心刷新IM用户 你会发现他多了一个

发送给的数据给服务器的时候还是XML格式,里面的SDK帮你封装了,不用你自己去接触
---自动登录------------------------------------
看到它的主界面只有三个tabbar
进去 mainstoryboard里面 删除原始的viewcontroller 拖入一个 tabbarController 去掉原始的tabbar 的两个子控制器 记得给它的isinitial 那个选项钩上 然后拖入三个navigationcontroller(这个根据你的界面而定)
选中给每个的Item 的title属性可以更改

然后在登录成功中写上加载storyboard的方法

你会发现你已经跳转进去

实现原理:把你的登录信息保存在沙盒中 程序启动时候发送登录请求
只要你在第一次登录成功后发送环信自带登录的网络请求去实现、
环信自己帮你实现上面的东西
具体实现
在登录成功的下面写上
[[EaseMob sharedInstance].chatManager setIsAutoLoginEnabled:YES]

然后去在AppDelegte里面把他的沙盒路径拿到


在AppDelegate方法里面进去 在启动方法里面写


这个时候我们要实现它的 get方法 写在监听登录状态的下面
//如果登录过直接来到主界面

self.window.rootViewController = [UIStoryboard storyboardWithName:@"Main" bundle:nil].instantiateInitialViewController;
}
=------------自动连接--------------------------
网络通不通的时候类似微信那种网络不通的实现
去那个main.storyboard里面把那会话的导航控制器跳转的那个类继承改为你自己创建的那个UItableViewController
1.在会话里面监听的网络的状态 环信的有很多个代理
去那个.m文件里面去实现

- (void)didConnectionStateChanged:(EMConnectionState)connectionState;

还有自动连接的状态的监听 也是加上他的代理方法就行了

--------添加好友请求-----------------------------
1.是用环信的思想
所有的网络请求 [EaseMob sharedInstance].chatManager 由这个管理器发起的
2.所有结果(自动登录 自动连接)通过代理来回调完成
[EaseMob sharedInstance].chatManager的addDelegate来完成
3.EMChatManagerBuddyDelegate 这个代理实现了对用户的基本操作
1.添加好友 2.从本地获取好友 3.从服务器列表获取最新的好友 4.接受好友添加请求
5.删除好友 6.被好友从名单上面删除
在navigation 的第二个控制器上面加上一个UIbarbarItem 选择自定义 然后+
再去创建一个UIviewController 然后再去实现用个+号去拖一个线
可以在上面加上navigationItem 上改为添加好友
再去新建一个控制器AdressBooking 通讯录 这个类是继承于UIviewController


这样你就收到了申请 注意 这是最简单的方式
还有一点 你要在每个控制器里面写上你环信的代理方法
这样就能保证你下面写的环信 的每一个方法会被自动调用了


代理的销毁
好友请求消息反馈写在什么地方 因为你进去那个会话的控制器里面了的话就会被销毁了。我们可以把它的好友请求写在会话控制器 这样他每个控制器都可以收到了 也没有必要写在AppDelegate里面 可以去尝试一下
---------现实好友界面列表------------------
1.新建一个tableViewcontroller 修改那个tableView的继承
2.给他的cell添加一个Identifier

#pragma mark - Tableview datasource
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return self.buddyList.count;
}
-(UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
static NSString *ID = @"BuddyCell";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:ID];
//获取好友列表名称 EMBuddy是环信封装的好友模型
EMBuddy *buddy = self.buddyList[indexPath.row];
//显示头像 和名字
cell.imageView.image = [UIImage imageNamed:@"chatListCellHead"];
cell.textLabel.text = buddy.username;
return cell;
}
注意一个bug在网速很慢的时候话或者用户的手速很快的情况下(遇到单身30年的手速)用户的时候 你会发现好友列表但是没有值的 因为它的好友列表是在你用户登录策划国内恭候才会有值
buddyList是从本地获取的数据 本地有个数据库你可以去里面看看
// buddyList
如果删除了应用或者饿用户第一次登陆的时候 buddyList是没有数据记录的
就要从服务区获取好友列表纪录
在网络登陆之前我们去从服务器获取那个 好友列表并把它写到本地的数据库里面去,注意一下这个方法写在哪个地方 切记切记

当接收到后有的同意后要刷新好友的列表数据 去通讯录控制器监听

我发送了请求 对方接受了 没有刷新好友列表
环信发送的话一定调用了
#pragma mark-好友列表的请求被更新,然而并没什么卵用
-(void)didUpdateBuddyList:(NSArray *)buddyList changedBuddies:(NSArray *)changedBuddies isAdd:(BOOL)isAdd{
// NSLog(@"好友列表被更新%@", buddyList);
NSLog(@"%@",buddyList);
self.buddyList = buddyList;
[self.tableView reloadData];
}
加上这句话就可以解决这个问题
============删除好友==========================
获取移除好友的名字
还有一种删除了是互相删除还是只是将一方的删除
#pragma mark--删除好友的代理方法
-(void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(nonnull NSIndexPath *)indexPath {
if(editingStyle == UITableViewCellEditingStyleDelete) {
//获取要移除的好友的名字
EMBuddy *buddy =self.buddyList[indexPath.row];
NSString *deleteusername = buddy.username;
//删除好友
[[EaseMob sharedInstance].chatManager removeBuddy:deleteusername removeFromRemote:YES error:nil];
}
}
———--------被好友删除的监听-----------------------------
1.在会话里面:
//被好友删除
#pragma mark-监听被好友删除
-(void)didRemovedByBuddy:(NSString *)username {
NSString *message = [username stringByAppendingString:@"把你删除"];
UIAlertView *alert = [[UIAlertView alloc]initWithTitle:@"好友删除提醒" message:message delegate:nil cancelButtonTitle:@"知道了" otherButtonTitles:nil, nil];
[alert show];
}
在同叙录里面:
//监听被删除去会话里面
#pragma mark-被好友删除了刷新一遍列表
-(void)didRemovedByBuddy:(NSString *)username {
[self loadDataFromServer];
}
--------------退出登录--------------------------
选中table View 的然后给他选static cell
2.然后连线去实现它的方法
-重点-------------聊天界面的实现----------------------------
1.拖入一个UIviewController 在上面在取拖入一个navagationBar 去设置他的标题
2.把cell的脱线 show
3.拖入一个View 拖到最下面 你会发现被tabbar 给盖住了 怎么去隐藏他呢?
选中他 有个 Bottom Bar 改为None
4.给这个View设置约束的时候注意 不要给她设置底部距Bottom 选择View
layout后 选择 Hidden Bottom Bar
5.往上面添加button button也要设置约束 要他举例底部的位子不变
否者的话 他会随着聊天的界面文字的的增加 而那个上升
还要加上一个textfield
通过代码监听键盘 如果底部

具体代码:
-(void)viewDidLoad {
[super viewDidLoad];
//1.监听键盘的弹出,把inputToolbar往上移
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(kbWillShow:) name:UIKeyboardWillShowNotification object:nil];
}
#pragma mark-键盘显示的时候会触发方法
-(void)kbWillShow:(NSNotification *)notification {
//获取键盘的高度
//1.获取键盘结束的位子
CGRect kbEndFrame = [notification.userInfo[UIKeyboardFrameEndUserInfoKey] CGRectValue];
CGFloat kbHeight = kbEndFrame.size.height;
//更改inputToolBar的底部约束
self.inputToolBarBottonConstraint.constant = kbHeight;
//添加动画
[UIView animateWithDuration:0.25 animations:^{
[self.view layoutIfNeeded];
}];
}
//移除通知
-(void)dealloc {
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
///////////////////
再加上一个tableView在它的上面 如何实现在拖动tableview 的时候让键盘隐藏
或者去监听tableView滚动的方法
tableView 有一个属性 Keyboard Dismiss on drag
在键盘推出的时候 让inputToolBarView 恢复原位
也要监听他
-----------cell有三种类型的 左边 中间 右边 -------------------------
1.往那个tableView里面去添加cell的时候我们可以 在里面加上图片
思路:在你设置好了头像之后再去设置文本的时候怎么设置
UILabel 设置背景图片的时候 我们可以先去给UILabel设置约束
再去设置那个背景图片,让他去拉伸
设置约束的时候注意头像要写死 但是lable只需要设置他的左边和 上面就行
如何加上测试数据:
1.自定一一个cell 在给他对用的类 和 对应的cell 绑定一个改类名
在。h文件里面给label加上一个属性 , 在给他的cell绑定一cell
最后别忘了在给tableview 右键往那个UIviewController上面加上数据源和代理
也就是自定义cell
------如何去让他显示的换行
1.吧label的Lines设置为0;
2.设置她的最大宽度:选择 Preferred width 这个选项242;
----最后给他设置那个背景图片
吧那个UIimage添加到那个Label上面
如何给那个这个ImageView 和Label 设置左上角对其?????????????
选中他们两个,然后把选择设置约束右下角的正向第2个 然后把4个Edge选择对其
最后别忘了去吧label 设置为clear
怎么去把ImageView的背景图片拉伸???????
选择背景图片 第4个属性倒立的脚 有一个strecting属性 设置为0.5 0.5 0 0
那个时候你会发现它的那个啥的没有那个左边图片的角的属性,什么原因呢?
这是因为你的这个时候的Y值拉升的不够可以把他的Y方向上面改成 0.7
这个时候还缺最后一步,要把它的那个背景ImagView改动一下,让它在上下左右 都往外面
拉伸一下??????//
可以选中它的那个约束,更改它的那个Constant 左边和上面的都是 加10 下边和右边是减10
最后别忘了跟新约束
-----------发送方的cell排布----------------------
先拖入一tableViewCell 然后改他的名字 在去拖入一个Imageview 在去实现它的那个
5 5 35 35
2.拖入一个Label 上面15 右边20 宽和高就不要添加了
3.如何让两个自定义的cell指向同一个的cell 新思想 :两个View公用一个cell
连线的时候又一个选择 选择senderCell 再给SenderCell一个identifier标示符号
在渲染cell的方法中加入一个加载哪一个cell的判断
4.最后设置它的Lines = 0 和 那个Preferred Width的宽度 让它实现自动换行
5.添加一个ImageView然后再去设置它的背景图片和 那个边距的对其
6.更改Image的Stretching
7.最后去把ImageView 的各个边距都改一下 上 10 左 10 下 -10 右边 -15 最后跟新
8 重点----如何去根据label里面的文字的高度去自动计算那个文字的高度?????
1.实现它的给他添加了一个测试的数据源方法

9--重点----还有一个问题 怎么去更改的它的自动计算那个行高呢??????
cell的高度取决于label文字的高度和它的字体的大小决定的、
思想: 去获取那个cell里面的label的高度再去加上一个固定的高度就是cell的高度了
怎么去获取那个label的高度呢???
上面的额头像距上为5 加上那个上面label距离上面的10的高度 再加上label自己 的高度self.label.hight +下面的那个10,再去加上那个固定的高度就是cell的高度了
10.--重点----我们专门搞一个计算高度的属性(他是一个返回cell的方法,然后去返回cell高度的方法里面去给他完成一下赋值的操作,最后去实现)
//还少了一步 ,一定要加上去设置那个label的数据
self.chatCellTool.messageLabel.text = self.dataSources[indexPath.row];
return [self.chatCellTool cellHeight];
/** 计算高度的cell的属性*/
@property (nonatomic,strong)UXZYChatCell *chatCellTool;
//给计算cell高度的对象完成一个赋值
//他返回的是一个cell 这个cell只是在那个返回高度的方法里面去用到了,其他的地方没有用到
记得最好把它的方法static NSString *Indentifier = @"ReceiveCell";写在那个cell的.h文件中
tableview记得再拖一次线 为什么任何一个的Identifier:都可以呢?????
因为你在返回高度的方法里面都是一样的执行的,不获去细分你是哪一个cell的方法 so。。。
self.chatCellTool = [self.tableView dequeueReusableCellWithIdentifier:SenderCell];
--------------发送聊天消息---------------------
1.首先要做的是把那个 textView的发送框改成send属性 第四个选项里面的有一个
Return Key 选项 改成那个Send选项
2.怎么去发送按钮的事件呢?????
1.把那个textView 的代理的线连上 2.然后去那个tableView里面的那个去遵守textView代理
在textView的方法里面去监听他最后的字符有没有换行,如果有换行的字符的话我就代表说他是发送sender的按钮]
3.怎么去发送文字呢???????
#pragma mark-UITextView的代理
-(void)textViewDidChange:(UITextView *)textView {
if([textView.text hasSuffix:@"\n"] ) {
NSLog(@"这是一个发送事件");
//发送文字
[self sendMessage:textView.text];
//清空文字
textView.text = nil;
}
}
-(void)sendMessage:(NSString *)text {
//创建一个消息对象实例
EMMessage *msgObj = [[EMMessage alloc]initWithReceiver:nil bodies:nil];
//发送消息
[[EaseMob sharedInstance].chatManager asyncSendMessage:msgObj progress:nil prepare:^(EMMessage *message, EMError *error) {
NSLog(@"消息准备发送");
} onQueue:nil completion:^(EMMessage *message, EMError *error) {
NSLog(@"消息完成发送");
} onQueue:nil];
}
还缺少一个参数 就是把消息发送给谁? 我们就缺少一个参数传递
initWithReceiver:要去拿到那个参数好友列表的参数 EMBuddy
#warning 每一种消息类型对象不同的消息体
//EMTextMessageBody
//EMVoiceMessageBody
//EMVideoMessageBody
// EMLocationMessageBody
// EMImageMessageBody
//创建一个聊天的文本对象
EMChatText *chatText = [[EMChatText alloc]initWithText:text];
//创建一个文本消息体
EMTextMessageBody *textBody = [[EMTextMessageBody alloc]initWithChatObject:chatText];
//创建一个消息对象实例 bodies是一个数组
EMMessage *msgObj = [[EMMessage alloc]initWithReceiver:self.buddy.username bodies:@[textBody]];
//发送消息
[[EaseMob sharedInstance].chatManager asyncSendMessage:msgObj progress:nil prepare:^(EMMessage *message, EMError *error) {
NSLog(@"消息准备发送");
} onQueue:nil completion:^(EMMessage *message, EMError *error) {
NSLog(@"消息完成发送");
} onQueue:nil];
注意点 : 加上一个return;去测试
把选中的好友列表正向传递进去
最后别忘了把那个return打开;
--------------显示好友的名字---------------------
1.在那个聊天的控制器的viewdidload里面写上
self.titlte = self.buddy.username
——————-------加载本地的聊天数据--------------------
有封装好的东西,先去内存的会话列表中去获取会话 如果没有找到就去数据库中去获取会话
没有找到就会出发现的会话
#pragma mark-加载本地的聊天数据
-(void)loadLocalChatrecords {
//获取本地聊天记录 会话对象
EMConversation *conversation = [[EaseMob sharedInstance].chatManager conversationForChatter:self.buddy.username conversationType: eConversationTypeChat];
//加载当前来哦天用户所有的聊天记录
NSArray *messages = [conversation loadAllMessages];
for(id obj in messages) {
NSLog(@"%@",[obj class]);
}
[self.dataSources addObject:messages];
}
[self.dataSources addObject:messages];加上了就报错了,她的原因是什么呢?
tableView:heightForRowAtIndexPath 错了
跳进设置高度的方法里面去 去吧里面改动一下
//改进后的cell高度的获取
//1.获取消息的模型 这样代码风格不好我们可以去cell的属性里面去创建一个cell的模型
// EMMessage *msg = self.dataSources[indexPath.row];
// id body = msg.messageBodies[0];
// if([body isKindOfClass:[EMTextMessageBody class]]) {//如果是文本消息
// EMTextMessageBody *textBody = body;
// cell.messageLabel.text = textBody;
// }
最后是用一个set方法去代替他们
-(CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
//1.获取消息的模型 这样代码风格不好我们可以去cell的属性里面去创建一个cell的模型
EMMessage *msg = self.dataSources[indexPath.row];
//改进后的cell高度的获取
// id body = msg.messageBodies[0];
//
// if([body isKindOfClass:[EMTextMessageBody class]]) {//如果是文本消息
// EMTextMessageBody *textBody = body;
// cell.messageLabel.text = textBody;
// }
self.chatCellTool.message = msg;
return [self.chatCellTool cellHeight];
//还少了一步 ,一定要加上去设置那个label的数据
// self.chatCellTool.messageLabel.text = self.dataSources[indexPath.row];
// return [self.chatCellTool cellHeight];
}
最后别忘了去那个tableviewcell的模型里面去改,
————为了计算高度我们建立了一个模型——----------
-(CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
//改进后的cell高度的获取
//1.获取消息的模型 这样代码风格不好我们可以去cell的属性里面去创建一个cell的模型
EMMessage *msg = self.dataSources[indexPath.row];
self.chatCellTool.message = msg;
return [self.chatCellTool cellHeight];
}
重写了模型的set方法 ,然后去调用了他的set方法
最后别忘了在显示的时候去调用他渲染那个方法
我的这个msg是一个消息模型
msg.messageBodies[0]
这样才能拿到消息体
msg.messageBodies[0].text才正确
-------------
[Pasted Graphic.tiff]
----------这样还不对-------------------------
你会看到你发出去的消息会在左边能看到
如何让它只能在右边显示呢??????????/
EMmessage有两个属性 from to
from是发送方也就是自己 from是好友的话自己就是接受方
所以我们得在现实渲染的方法里面去先去获取消息模型
然后再去做一个from的判断
UXZYChatCell *cell = nil;
//先去获取消息模型然后去在去判断是发送方还是接收方
EMMessage *msg = self.dataSources[indexPath.row];
if([msg.from isEqualToString:self.buddy.username]) {//自己接收方
cell = [tableView dequeueReusableCellWithIdentifier:Indentifier];
}else {
cell = [tableView dequeueReusableCellWithIdentifier:SenderCell];
}
--------怎么让你的消息立马显示-----------------------
去这个方法里面
-(void)sendMessage:(NSString *)text { }
msgObj是你自己创建一个消息对象实例 bodies是一个数组
EMMessage *msgObj = [[EMMessage alloc]initWithReceiver:self.buddy.username bodies:@[textBody]];
//把消息添加到数据源 再刷新表格
[self.dataSources addObject:msgObj];
[self.tableView reloadData];
------怎么让你的消息自己滚动到最上面的一行----
//4.把消息显示在顶部
[self scrollToBottom];
}
-(void)scrollToBottom {
//获取最后一行
if(self.dataSources.count == 0){
return;
}
NSIndexPath *lastIndex = [NSIndexPath indexPathForRow:self.dataSources.count-1 inSection:0];
[self.tableView scrollToRowAtIndexPath:lastIndex atScrollPosition:UITableViewScrollPositionBottom animated:YES];
}
—————仔细的你到这一步了发没发现你那个你发出去的消息子啊框里面都会多出一行---
原因:是你在点击发送的时候他换了一下行,换行字符只展占用一个字节
如何去清除那个换行的字符,在发消息的放里面去实现
text = [text substringToIndex:text.length-1];
-----------怎么去监听消息的回复--------------------
1.设置代理方法 并遵循它的 代理方法
2.有一个那个mesage的方法去实现一下
------对输入框的完善,即在你输入的时候那个输入框的高速会自动增加-------------------------
1.得定义输入框的一个最小的高度和最大的高度
2.textView是继承于UIscrollView scrollView有一个属性 contensize 属性
这个是计算那个做一个判断 不同情况下面的高度
3.我们再去拿到那个的InputView 的高度的约束 再去连线使它成为一个属性 然后像之前一样去约束它,找到他,然后给他连线 使它成为一个属性
//获取contentsize的高度
CGFloat contentHeight = textView.contentSize.height;
if (contentHeight <minHeight) {
textViewH = minHeight;
}else if (contentHeight >maxHeight) {
textViewH = maxHeight;
}else {
textViewH = contentHeight;
}
self.inputToolBarHeightConstrains.constant = 5 + 8 + textViewH;
4.还是有一个问题,就是在你输入完了一之后发送了 之后textView还是有一个换行的空格
所以还得有一个在发送完了后的判断语句
if([textView.text hasSuffix:@"\n"] ) {
NSLog(@"这是一个发送事件");
//发送文字
[self sendMessage:textView.text];
//清空文字
textView.text = nil;
// 发送时textView的高速为最小的高度
textViewH = minHeight;
}
self.inputToolBarHeightConstrains.constant = 5 + 8 + textViewH;
5.设置textView的背景图片 在SB中去设置 拖入一个IMageView 然后把它拿到那个textView的上面去 然后改透明 设置背景图片 把图片拉伸 streching 别忘了给背景图片加约束 不然的话他会随着那个的高度的增加他不会随着拉伸 最后去加个动画
6.——————---发送完了后发现光标不见了 发大模拟器看在左上角???
原因???scrollView有一个contentOffset.y的值会随着你的那个的光标的contensize的而向上拖动而拖送
NSStringFromCGSize(textView.contentSet)可以打印出来看看
contentOffset一开始为(0,0)之后它的y值为正数了,为什么呢,因为他要显示的下面的文字内容的话它的那个轴必须下移动 所以为正
如何解决 哪次发送完了 让它的contentset恢复到原位
环信遇到的bug1:真机上面不能跑 模拟器可以
[Pasted Graphic_1.tiff]
把那个bitcode 设置为NO bitcode是被编译程序的一种中间形式的代码。包含bitcode配置的程序将会在App store上被编译和链接
默认是yes
storyboard作为控制器的复习
1.拖入一导航控制器 ,更改尺寸 去掉后一个
2.拖入一个viewControler 然后去把它的那个root viewcontroller 设置一下
3.创建一个控制器UIviewcontroller 第三个选项 class里面更改继承关系
4.拖线 连线 1.去改掉main 2。去那个第4个 is initial View Controller 的钩上
登录提供了三种方法
1.同步 2.block的异步 3.代理的回调
注释的方法提示在图表
方式一:/// 点在图标
方式二:
/*!
* @brief 点在图标
*/ 收起阅读 »
java集成环信
项目是一个关于教育的项目,主要的架构是Spring,Struts2,Hibernate,前端页面是Jsp。
手机端:Android,IOS
2.项目需求描述
项目中有一个需求是聊天的功能,主要有好友聊天,陌生人聊天,同班同学群聊三种。
3.集成步骤
1)、服务器集成文档
http://docs.easemob.com/im/100serverintegration/10intro
在页面中点击"示例代码"栏中的“这里”
2)、服务器端代码下载
https://github.com/easemob/emchat-server-examples
在页面中选择对应的语言版本进行下载,本项目需要的是java版本emchat-server-java
4)、将下载的代码引入项目中
将libs下边的jar导入项目。
将代码加入项目并调试不报错,主要注意配置文件中的内容。文件里内容有:
API_PROTOCAL = https //协议
API_HOST = a1.easemob.com //ip
API_ORG = 1193161011115832 //组织信息
API_APP = testapp //应用名称
APP_CLIENT_ID = YXA6Gj8v8KAIEea4-ukHSS2iNQ
APP_CLIENT_SECRET = YXA6pSzVILsRgv9Wu2cRsVhJxoAz924
上述参数要根据具体的环信应用得到
5)、注册环信,登录后创建应用。应用的参数信息主要用于上述的配置文件中。
4、应用步骤
1)、分工
服务器端负责注册/删除环信用户,推送系统消息。手机端负责登录,退出,消息管理。
2)、数据库
1、用户信息
保存项目用户的基本注册信息,一般通过手机号进行注册,环信用户的用户名为手机号,密码和系统保持一致。保证手机端在登录系统后登录环信。保持统一在线。
2、好友
好友表中记录用户之间的关系,关系分为:发送好友请求,同意好友,拒绝好友,黑名单,陌生人。
3)、服务封装
1、登录时先查看用户是否注册环信IM用户,如果没有则先注册然后登录。
2、删除用户时,查看是否注册IM用户,如果有则一起删除。
3、退出时,通过环信的退出提醒在手机端提醒用户退出。
4、添加好友,判断是否是好友,同时对黑名单进行处理。
5、用户的头像,性别,名称等进本信息都是通过系统查询得到。
6、好友列表,陌生人列表通过对好友表查询得到用户的具体信息,然后展示好友/陌生人列表信息
7、同班同学是通过班级表来确认用户的班集体,进而查询得到所有同班同学的信息,展示同班同学列表。
8、聊天时,直接向用户手机号的IM用户发送消息即可。
项目只是用到了环信最基本的聊天功能,环信有更丰富的内容。比如:好友管理,上线下线管理,群聊,批量注册,手机端信息推送,环信客服等功能。
跟多信息请到环信官网查询:http://www.easemob.com/
收起阅读 »
环信SDK3.0时遇到的问题及解决办法汇总
注:官方下载的包里会有两个静态库,一个是包含实时语音的库,另一个则不包含,根据业务需求,记得删除掉一个库,直接move to trash就可以.切记要删除!否则在后边的步骤会报重复引用链接错误.
2.根据官方文档导入依赖库的时候,dylib后缀没有的直接用tbd就可以(Xcode 7之后改为tbd).
加入系统SDK依赖库:
CoreMedia.framework
AudioToolbox.framework
AVFoundation.framework
MobileCoreServices.framework
ImageIO.framework
SystemConfiguration.framework
libc++.a
libresolv.dylib
libz.dylib
libstdc++.6.0.9.dylib
libsqlite3.dylib
libHyphenateSDK.a
如果报错:"_iconv", referenced from: _avcodec_decode_subtitle2 in libHyphenateFullSDK.a(utils.o),则另外要加入libiconv.tbd依赖库.
官方依赖库导入之后编译,此处编译会遇到的坑:
1).报重复引用静态库错误(没删官方两个静态库中的一个)
2).报与-all_load 冲突错误 根据文档改为-ObjC即可 如果改为-ObjC还有错 可按照文档改为 -force_load
注:项目如果用了友盟分享,改为-force_load之后 会与友盟有冲突,具体原因也不清楚,项目直接crash在分享新浪微博,此处求解惑. 改为-ObjC即可
到此步骤直到编译没错的时候就说明导入SDK 配环境成功.
3.环信官方提供了easeUI这个快速集成单聊界面,在集成easeUI之前建议不要用cocoapods来集成,因为会报一些莫名的错误.
集成easeUI:
1).将EaseUI工程下载下来之后,直接拖入EaseUI文件夹,EaseUIResource里面的Resource文件夹,export文件夹里的resource文件夹下的EaseUIResource.bundle 包,到工程中.
2).新建一个pch文件,设置好路径之后,在pch文件中添加引用:EaseUI-Prefix.pch 、ChatDemo-UI3.0-Prefix.pch 这两个pch中的代码.
此时编译会报一个 NSObject + EaseMob类别错误 (该类别是2.0demo中的,根本没有用) 可以选择注释,或者直接删除该类别.将该类别注释掉之后,如果报更多的错误,错误定在NSString或其他系统类上,在你新建的pch文件里,你所包含的头文件开始加上: #ifdef __OBJC__ 结尾处加上: #endif 如下图:450A4D8A-1D3D-463A-8012-D75920068558.png此时编译如果还报错并且错误定在NSLocalizedString, 在你的pch文件里添上如下宏:
#define NSEaseLocalizedString(key, comment) [[NSBundle bundleWithURL:[[NSBundle mainBundle] URLForResource:@"EaseUIResource"withExtension:@"bundle"]] localizedStringForKey:(key) value:@"" table:nil]
3).下载官方提供的ChatDemo-3.0 拖入demo中3rdparty文件夹,因EaseUI本身集成了几个常用三方库,因此会与工程中你所用的重复,此时编译会报错.本身所包含的常用三方如下:MBProgressHUD、VoiceConvert、MJRefresh、SDWebImage. 如果有冲突,切记删除ChatDemo中的三方库,然后改掉相关报错的代码.
此时编译如果还报错,错误为Setting文件夹下的BackUp类,直接删除掉或注释即可,该类用处不大.
至此,编译成功则EaseUI的集成也完毕了.
环信的EaseUI其实只是做到一个展示的作用,包括一些控件的布局,排版都没有处理,因此我们可以直接用ChatDemo3.0中的ChatViewController这个界面,附上集成视频地址:http://v.youku.com/v_show/id_XMTQxOTgyNjU1Mg==.html?from=y1.2-1-87.3.6-2.1-1-1-5-0
好了,到此当你把ChatViewController也集成完毕,一个初步成型的单聊界面就出来了.ps:环信支持非好友之间的聊天,你只要知道对方的环信id即可.
那么你会疑问了,头像和昵称还没有搞定.因为环信服务器不存储用户的头像和昵称,因此需要你与自己app的服务器交互,聊天界面的头像和昵称在如下方法中修改:
- (id<IMessageModel>)messageViewController:(EaseMessageViewController *)viewController
modelForMessage:(EMMessage *)message
如果因业务需求用到会话列表,官方并没有非常完善的文档,可以参考demo中的ConversationListViewController.
转自:http://docs.easemob.com/doku.php?id=im:start 收起阅读 »
集成的单聊界面不能下拉加载更多的问题
祝环信越来越好!
环信receive的本地封装
最近在重构环信的本地封装,思路如下:
- 建立一个新的单例对象(该对象是EaseMob的Receive的进一步抽象)
- 新的单例对象是EaseMob单例的delegate
- 新的单例对象使用block来实现长链消息的本地handle
- 针对每一个接收代理方法建立private一个数组属性,每个数组的成员变量都是block(当delegate被调用,便利数组回执block)
经实测以上方法可行,并避免了在viewwillappear方法中加入冗余代码,影响效率。 收起阅读 »
环信小程序 Demo源码发布,集成视频手把手教你玩转小程序!
为了让大家的小程序都能顺利“聊”起来,环信小程序Demo源码也于今日正式发布,也许在“用完即走”基础上“聊两句再走”也是极好的哦!同时,环信工程师们还贴心的为大家准备好了集成使用教程视频,手把手教你玩转小程序。
环信小程序运行视频在线观看
微信小程序 Demo
环信准备了微信小程序 Demo,该 Demo 基于 Web IM SDK,并在其基础之上进行了修改。如果您想在您的微信小程序中添加即时通讯的功能,可以参考以下方式集成。
小程序运行效果
Demo源码下载
GitHub下载地址:https://github.com/easemob/webim-weixin-xcx.git
或者,执行如下命令:
git clone https://github.com/easemob/webim-weixin-xcx.git安装IDE
进入微信公众平台的官网下载“开发者工具”,并安装。目前支持 windows 64、windows 32、mac 版本。
- 微信开发者文档:https://mp.weixin.qq.com/debug/wxadoc/dev/index.html
- “开发者工具”下载地址:https://mp.weixin.qq.com/debug/wxadoc/dev/devtools/download.html?t=201715
- request合法域名 https://a1.easemob.com
- socket合法域名 wss://im-api.easemob.com
- uploadFile合法域名 https://a1.easemob.com
进入“微信小程序Demo”项目后,可以对项目进行编辑、调试。
收起阅读 »
感谢环信这么几年来的免费支持。
祝愿环信早日上市,拥有美好的未来。
环信动态库sdk上架问题解决方案
但是也会有一些问题,这里讲下关于这个上架的问题。
1.先把Hyphenate.framework放到桌面上;
2.终端位置cd到桌面;
3.运行:lipo Hyphenate.framework/Hyphenate -thin armv7 -output Hyphenate_armv7
4.运行后没有输出提示,直接运行下一个命令:lipo Hyphenate.framework/Hyphenate -thin arm64 -output Hyphenate_arm64
5.运行后一样没有输出提示,直接运行下一个命令:lipo -create Hyphenate_armv7 Hyphenate_arm64 -output Hyphenate
6.运行后一样没有输出提示,直接运行最后一个命令:mv Hyphenate Hyphenate.framework/
得到的Hyphenate.framework就是最后的结果,拖进工程,编译打包上架。
作者:环信ios工程师张磊 收起阅读 »
Web IM V1.4.8已发布,刷新页面保持登陆状态
新功能:
[demo] 增加webrtc视频聊天的声音开关
[demo] 动态创建chatWindow,提高网页性能
[demo] 切换leftbar时会给chatWindow添加遮罩,返回之前的leftbar时会直接跳到之前选中的cate和chatWindow
[demo] 登录成功后,刷新页面不会再回到登录页
Bug修复:
[sdk] 移除sdk中所有log方法
[sdk] 退出muc group room 时,追加发送一条unavailable的presence stanza
webim体验:https://webim.easemob.com/
版本历史:更新日志
SDK下载:点击下载 收起阅读 »
数据驱动下的在线旅游新生态——易观数聚论(旅游专场)
纵观2016年中国的在线度假旅游的市场发展状况;线上线下融合程度提升。2016年中国在线度假旅游市场竞争向全产业链深入,在线度假旅游企业对资源端的渗透能力和用户细分需求的精细化响应是市场竞争核心,行业整合将向产业链上游不断深入;同时线下旅游企业对于互联网平台重视程度提升,线上线下企业通过战略合作、投资入股等方式加强资源端与渠道端协同,出现海航与途牛联姻、众信与携程联姻、万达与同程联姻等案例,说明市场整合不断深入,线上线下一体化加速,互联网正成为串联渠道和资源等产业链多环节的基础平台。
1月14日,易观邀您一起探讨在线旅游市场在大数据的驱动下将如何完成2017年的新突破。
▎活动背景
“易观数聚论”是面向互联网+垂直领域关于大数据应用相关内容的线下研讨会。旨在为企业衡量数字用户资产价值,产品精细化运营,用户转化与变现等问题。
“易观数聚论”将嘉宾架构更新为 “1位易观产品规划师+1位垂直领域知名企业领袖分享+2位该领域有实战案例嘉宾分享+话题互动”的模式,为互联网企业和在职人员提供相关领域的大数据应用干货。
用户运营将成为所有类型企业的核心命题,如何进行数字用户资产管理和经营,是企业未来最重要的竞争力之一。数字用户资产四大价值,市值管理(企业第四张表)、获客、提高忠诚度、提供用户ARPU交相辉映,方法论倾囊相授,企业实践案例分析解读,一站式融会贯通数字用户资产运营方法论,学会经营用户。
数据是新能源,易观是加油站。面对行业的机遇和挑战,易观将助力企业学习用户数字资产管理方法,提高用户转化与精准变现。
▎活动信息
时间:2017.1.14(周六)13:30-16:00
规模:100人(现场座位有限,按照报名顺序安排留座)
地点:上海市静安区愚园东路20号东海广场3号楼(具体地址电话/短信告知)
▎报名须知
为保证参会人员质量与留座到场率,本次活动为收费形式;报名者可在1月10日之前通过本平台购买“早鸟票”(单人:68元;套票双人:88元);1月10日之后即恢复原价100元/人。报名后凭审核短信签到入场,未报名者一概不予进场。以此带来不便,敬请谅解!
注:活动无任何盈利目的,报名参会者可现场领取Linckia现磨咖啡一杯(大杯,种类可选)。
▎联系方式
活动联系人:许宏运 13120751039
▎活动详情
▎活动议程
13:30-13:55——签到
13:55-14:00——主持人开场
14:00-14:30——主题:中国在线旅游行业发展现状与趋势预测
——嘉宾:易观旅游行业中心资深分析师姜昕蔚
14:30-15:00——主题:旅游产品的用户转化与价值变现
——嘉宾:驴妈妈旅游网CMO黄春香
15:00-15:30——主题:个性化定制旅行产品的新风口
——嘉宾:指南猫创始人兼CEO任静
15:30-16:00——圆桌:旅游企业的新风口与新生态
16:00-16:20——交流/提问
(提问者有机会获得价值880元的《互联网产业发展年鉴》一本)
▎合作单位
——感谢以上平台对本次活动的大力支持!
▎往期回顾
我要报名:http://www.huodongxing.com/event/4368713124800 收起阅读 »
我要回归了
美团热更方案ASM实践
这篇文章主要讲美团热更方案中没讲到的部分,包含几个方面:
- 作为云服务提供厂商,需要提供给客户SDK,SDK发布后同样要考虑bug修复问题。这里讲一下作为sdk发布者的热更新方案选型,也就是为什么用美团方案&Instant Run方案。
- 美团方案实现的大致结构
- 最后讲一下asm插桩的过程,字节码导读,以及遇到的各种坑。
方案选择:
我们公司提供及时通讯服务,同时需要提供给用户方便集成的及时通讯的SDK,每次SDK发布的同时也面临SDK发布后紧急bug的修复问题。 现在市面上的热更新方案通常不适用SDK提供方使用。 以阿里的andFix和微信的tinker为例,都是直接修改并合成新的apk。这样做对于普通app没有问题,但是对于sdk提供方是不可以的,SDK发布者不能够直接修改apk,这个事情只能由app开发者来做。
tinker方案如图:
女娲方案,由大众点评Jason Ross实现并开源,他们是在classLoader过程中,將自己的修改后的patch类所在的dex, 插入到dex队列前面,这样在classloader按照类名字加载的时候会优先加载patch类。
女娲方案如图:
女娲方案有一个条件约束,就是每个类都要插桩,插入一个类的引用,并且这个被引用类需要打包到单独的dex文件中,这样保证每个类都没有被打上CLASS_ISPREVERIFIED标志。 具体详细描述在早期的hotpatch方案 安卓App热补丁动态修复技术介绍
作为SDK提供者,只能提供jar包给用户,无法约束用户的dex生成过程,所以女娲方案无法直接应用。 女娲方案是开源的,而且其中提供了asm插桩示例,对于后面应用美团方案有很好参考意义。
美团&&Instant Run方案
美团方案 也就是Instant Run的方案基本思路就是在每个函数都插桩,如果一个类存在bug,需要修复,就将插桩点类的changeRedirect字段从null值变成patch类。 基本原理在美团方案中有讲述,但是美团文中没有讲最重要的一个问题,就是如何在每一个函数前面插桩,下面会详细讲一下。 Patch应用部分,这里忽略,因为是java代码,大家可以反编译Instant Run.jar,看一下大致思路,基本都能写出来。
插桩
插桩的动作就是在每一个函数前面都插入PatchProxy.isSupport...PatchProxy.accessDisPatch这一系列代码(参看美团方案)。插桩工作直接修改class文件,因为这样不影响正常代码逻辑,只有最后打包发布的时候才进行插桩。
插桩最常用的是asm.jar。接下来的部分需要用户先了解asm.jar的大致使用流程。了解这个过程最好是找个实例实践一下,光看介绍文章是看不懂的。
asm有两种方式解析class文件,一种是core API, provides an event based representation of classes,类似解析xml的SAX的事件触发方式,遍历类,以及类的字段,类中的方法,在遍历的过程中会依次触发相应的函数,比如遍历类函数时,触发visitMethod(name, signature...),用户可以在这个方法中修改函数实现。 另外一种 tree API, provides an object based representation,类似解析xml中的DOM树方式。本文中,这里使用了core API方式。asm.jar有对应的manual asm4-guide.pdf,需要仔细研读,了解其用法。
使用asm.jar把java class反编译为字节码
反编译为字节码对应的命令是
java -classpath "asm-all.jar" org.jetbrains.org.objectweb.asm.util.ASMifier State.class这个地方有一个坑,官方版本asm.jar 在执行ASMifier命令的时候总是报错,后来在Android Stuidio的目录下面找一个asm-all.jar替换再执行就不出问题了。但是用asm.jar插桩的过程,仍然使用官方提供的asm.jar。
插入前代码:
class State {ASMifier反编译后字节码如下
long getIndex(int val) {
return 100;
}
}
mv = cw.visitMethod(0, "getIndex", "(I)J", null, null);插桩后代码:
mv.visitCode();
mv.visitLdcInsn(new Long(100L));
mv.visitInsn(LRETURN);
mv.visitMaxs(2, 2);
mv.visitEnd();
long getIndex(int a) {ASMifier反编译后代码如下:
if ($patch != null) {
if (PatchProxy.isSupport(new Object[0], this, $patch, false)) {
return ((Long) com.hyphenate.patch.PatchProxy.accessDispatch(new Object[] {a}, this, $patch, false)).longValue();
}
}
return 100;
}
mv = cw.visitMethod(ACC_PUBLIC, "getIndex", "(I)J", null, null);对于插桩程序来说,需要做的就是把差异部分插桩到代码中
mv.visitCode();
mv.visitFieldInsn(GETSTATIC, "com/hyphenate/State", "$patch", "Lcom/hyphenate/patch/PatchReDirection;");
Label l0 = new Label();
mv.visitJumpInsn(IFNULL, l0);
mv.visitInsn(ICONST_0);
mv.visitTypeInsn(ANEWARRAY, "java/lang/Object");
mv.visitVarInsn(ALOAD, 0);
mv.visitFieldInsn(GETSTATIC, "com/hyphenate/State", "$patch", "Lcom/hyphenate/patch/PatchReDirection;");
mv.visitInsn(ICONST_0);
mv.visitMethodInsn(INVOKESTATIC, "com/hyphenate/patch/PatchProxy", "isSupport", "([Ljava/lang/Object;Ljava/lang/Object;Lcom/hyphenate/patch/PatchReDirection;Z)Z", false);
mv.visitJumpInsn(IFEQ, l0);
mv.visitIntInsn(BIPUSH, 1);
mv.visitTypeInsn(ANEWARRAY, "java/lang/Object");
mv.visitInsn(DUP);
mv.visitIntInsn(BIPUSH, 0);
mv.visitVarInsn(ILOAD, 1);
mv.visitMethodInsn(INVOKESTATIC, "java/lang/Integer", "valueOf", "(I)Ljava/lang/Integer;", false);
mv.visitInsn(AASTORE);
mv.visitVarInsn(ALOAD, 0);
mv.visitFieldInsn(GETSTATIC, "com/hyphenate/State", "$patch", "Lcom/hyphenate/patch/PatchReDirection;");
mv.visitInsn(ICONST_0);
mv.visitMethodInsn(INVOKESTATIC, "com/hyphenate/patch/PatchProxy", "accessDispatch", "([Ljava/lang/Object;Ljava/lang/Object;Lcom/hyphenate/patch/PatchReDirection;Z)Ljava/lang/Object;", false);
mv.visitTypeInsn(CHECKCAST, "java/lang/Long");
mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Long", "longValue", "()J", false);
mv.visitInsn(LRETURN);
mv.visitLabel(l0);
mv.visitFrame(Opcodes.F_SAME, 0, null, 0, null);
mv.visitLdcInsn(new Long(100L));
mv.visitInsn(LRETURN);
mv.visitMaxs(4, 2);
mv.visitEnd();
需要将全部入參传递给patch方法,插入的代码因此会根据入參进行调整,同时也要处理返回值.
可以观察上面代码,上面的例子显示了一个int型入參a,装箱变成Integer,放在一个Object[]数组中,先后调用isSupport和accessDispatch,传递给patch类的对应方法,patch返回类型是Long,然后调用longValue,拆箱变成long类型。
对于普通的java对象,因为均派生自Object,所以对象的引用直接放在数组中;对于primitive类型(包括int, long, float....)的处理,需要先调用Integer, Boolean, Float等java对象的构造函数,将primitive类型装箱后作为object对象放在数组中。
如果原来函数返回结果的是primitive类型,需要插桩代码将其转化为primitive类型。还要处理数组类型,和void类型。 java的primitive类型在 java virtual machine specification中有定义。
这个插入过程有两个关键问题,一个是函数signature的解析,另外一个是适配这个参数变化插入代码。下面详细解释下:
@Override这个函数是asm.jar访问类函数时触发的事件,desc变量对应java jni中的signature,比如这里是'(I)J', 需要解析并转换成primitive类型,类,数组,void。这部分代码参考了android底层的源码libcore/luni/src/main/java/libcore/reflect,和sun java的SignatureParser.java,都有反映了这个遍历过程。
public MethodVisitor visitMethod(int access, String name, String desc,
String signature, String[] exceptions) {
关于java字节码的理解,汇编指令主要是看 Java bytecode instruction listings
理解java字节码,需要理解jvm中的栈的结构。JVM是一个基于栈的架构。方法执行的时候(包括main方法),在栈上会分配一个新的帧,这个栈帧包含一组局部变量。这组局部变量包含了方法运行过程中用到的所有变量,包括this引用,所有的方法参数,以及其它局部定义的变量。对于类方法(也就是static方法)来说,方法参数是从第0个位置开始的,而对于实例方法来说,第0个位置上的变量是this指针。引自: Java字节码浅析
分析中间部分字节码实现,
com.hyphenate.patch.PatchProxy.accessDispatch(new Object[] {a}, this, $patch, false))
对应字节码如下,请对照Java bytecode instruction listings中每条指令观察对应栈帧的变化,下面注释中'[]'中表示栈帧中的内容。mv.visitIntInsn(BIPUSH, 1); # 数字1入栈,对应new Object[1]数组长度1。 栈:[1]熟悉上面的字节码以及对应的栈帧变化,也就掌握了插桩过程。
mv.visitTypeInsn(ANEWARRAY, "java/lang/Object"); # ANEWARRY:count(1) → arrayref, 栈:[arr_ref]
mv.visitInsn(DUP); # 栈:[arr_ref, arr_ref]
mv.visitIntInsn(BIPUSH, 0); # 栈:[arr_ref, arr_ref, 0]
mv.visitVarInsn(ILOAD, 1); # 局部变量位置1的内容入栈, 栈:[arr_ref, arr_ref, 0, a]
mv.visitMethodInsn(INVOKESTATIC, "java/lang/Integer", "valueOf", "(I)Ljava/lang/Integer;", false); # 调用Integer.valueOf, INVOKESTATIC: [arg1, arg2, ...] → result, 栈:[arr_ref, arr_ref, 0, integerObjectOf_a]
mv.visitInsn(AASTORE); # store a reference into array: arrayref, index, value →, 栈:[arr_ref]
mv.visitVarInsn(ALOAD, 0); # this入栈,栈:[arr_ref, this]
mv.visitFieldInsn(GETSTATIC, "com/hyphenate/State", "$patch", "Lcom/hyphenate/patch/PatchReDirection;"); #$patch入栈,栈:[arr_ref, this, $patch]
mv.visitInsn(ICONST_0); #false入栈, # 栈:[arr_ref, this, $patch, false]
mv.visitMethodInsn(INVOKESTATIC, "com/hyphenate/patch/PatchProxy", "accessDispatch", "([Ljava/lang/Object;Ljava/lang/Object;Lcom/hyphenate/patch/PatchReDirection;Z)Ljava/lang/Object;", false); # 调用accessDispatch, 栈包含返回结果,栈:[longObject]
坑:
ClassVisitor.visitMethod()中access如果是ACC_SYNTHETIC或者ACC_BRIDGE,插桩后无法正常运行。ACC_SYNTHETIC表示函数由javac自动生成的,enum类型就会产生这种类型的方法,不需要插桩,直接略过。因为观察到模版类也会产生ACC_SYNTHETIC,所以插桩过程跳过了模版类。
ClassVisitor.visit()函数对应遍历到类触发的事件,access如果是ACC_INTERFACE或者ACC_ENUM,无需插桩。简单说就是接口和enum不涉及方法修改,无需插桩。
静态方法的实现和普通类成员函数略有出入,对于汇编程序来说,本地栈的第一个位置,如果是普通方法,会存储this引用,static方法没有this,这里稍微调整一下就可以实现的。
不定参数由于要求连续输入的参数类型相同,被编译器直接变成了数组,所以对本程序没有造成影响。
大小:
插桩因为对每个函数都插桩,反编译后看实际上增加了大量代码,甚至可以说插入的代码比原有的代码还要多。但是实际上最终生成的jar包增长了大概20%多一点,并没有想的那么多,在可接受范围内。因为class所占的空间不止是代码部分,还包括类描述,字段描述,方法描述,const-pool等,代码段只占其中的不到一半。可以参考[The class File Format](link @http://docs.oracle.com/javase/specs/jvms/se7/html/jvms-4.html)
讨论
前面代码插桩的部分和美团热更文章中保持一致,实际上还有些细节还可以调整。isSupport这个函数的参数可以调整如下if (PatchProxy.isSupport(“getIndex”, "(I)J", false)) {这样能减小插桩部分代码,而且可以区分名字相同的函数。
PatchProxy.isSupport最后一个参数表示是普通类函数还是static函数,这个是方便java应用patch的时候处理。
源码地址
https://github.com/easemob/empatch
作者:李楠
公司:环信
关注领域:Android开发
文章署名: greenmemo 收起阅读 »
17年第一班车,老司机带你撸一个超级表单
麦客表单(http://www.mikecrm.com/)是一款在线表单制作工具,可以帮助我们轻松完成信息收集与整理。
环信移动客服是环信(http://www.easemob.com/)基于环信即时通讯云打造的全媒体智能云客服平台。
先看最终效果http://form.mikecrm.com/xnnc3o
细心的老司机们估计已经发现
在表单的下方多了一个在线咨询按钮,用户填写表单的过程中遇到了问题,可以点击“在线咨询”按钮获得帮助,极大的提升了用户体验与我们的工作效率。
这得益于麦客表单的组件自定义功能与环信移动客服返璞归真的集成方式,可以将两者结合就能让我们在表单中快搭建与用户无障碍交流的桥梁。
原理为:麦客表单组件的超链接里填入环信移动客服访客端入口地址
赶紧分享,仅需3步!
1:注册环信移动客服
http://kefu.easemob.com/
2:获取环信移动客服租户ID
点击右上角切换到管理员模式-->设置-->企业信息
3:将环信移动客服访客端入口地址填入麦客表单的自定义连接。
(PS:环信移动客服访客端入口地址为http://kefu.easemob.com/webim/im.html?tenantId=租户ID)
然后就大功告成!环信移动客服的访客入口地址你能将他放在任何地方,微信公众号、网站,APP,怎么舒服怎么来。 当然,环信移动客服和麦客表单的神奇之处不竟如此,更多功能等待你挖掘。
最后附上客服端的截图,也就是解答你问题的MM工作台
作者:捞鱼-一个0岁的产品经理
收起阅读 »
React Native V0.2.0已发布,android 支持调用相机/本地图片
React Native :v0.2.0 @ 2017-01-03
Feature 新功能:
Android:
Android:BugFix and Update 修复和更新:
• 登录
• 注册
• 好友
◦ 列表及筛选
◦ 好友信息展示
◦ 黑名单
◦ 删除好友
• 好友通知
◦ 添加好友通知展示
◦ 接受好友请求
◦ 拒绝好友请求
◦ 添加好友
• 群组
◦ 群组列表
◦ 群组成员列表
• 聊天
◦ 相机图片消息
◦ 本地图片消息
◦ emoji消息
◦ 普通消息
• 异常状态处理
◦ 断线退出到登录页
◦ 重复登录退出到登录页
• 群成员刷新crash的问题Extra 其他:
• 添加logout图标
• 统一iOS和Android UI
• emoji颜色在android颜色太浅
• 登出的时候清空用户状态
• 完善联系人页面的搜索在切换场景及刷新时的显示逻辑
•[react-native] 升级到最新0.39.2版本历史 :更新日志
• [component/input] 功能完善
• [component/button] 功能完善
• [addContactModal] 统一通过react-native-router-flux管理modal
• [tabBar] 统一通过react-native-router-flux管理tabBar
• [camera] android 支持调用相机/本地图片
• [yarn] 添加yarn lock file,可以通过yarn做包管理
SDK下载:点击下载 收起阅读 »
关于Android Studio如何正确集成环信3.1.5 SDK

2 将build.gradle删除(这一点非常重要,因为没有删除这个文件,后面出现的冲突浪费了我三四天时间)

3 在examples-ChatDemoUI3.0找到libs

4 在你的 (注意是主modules) 主modules-main下创建jniLibs文件夹,把demo中的jar包全部父之过去

5 之后就是导入easeui了(注意:确认build.gradle已经删除)

接下来是添加依赖,选择菜单栏上Project Structure,找到自己的工程,在Dependencies找到绿色的“+”,选择第三个“Module dependency”

6 导入easeui后发现他自动生成的build.gradle,这时才是我们需要的,将easeui的v4版本提升到23

compile'com.android.support:support-v4:23.+'
7 这时应该编译通过了,有些机子运行发现以下问题
java.lang.UnsatisfiedLinkError: com.android.tools.fd.runtime.IncrementalClassLoader$DelegateClassLoader[DexPathList[[dex file "/data/data/com.xxx.xxx/files/instant-run/dex/slice-support-annotations-24.1.0_ce9c5697cabea4565e89d9bb7a81deef74f26296-classes.dex", dex file "/data/data/com.xxx

如果遇到UnsatisfiedLinkError的问题,则在easeUI依赖库-jniLibs创建armeabi-v7a,然后将armeabi的.so文件拷贝一份进去即可。
之后你就可以根据你的需要进行自定义了,可以参考ChatDemoUI3.0,里面的逻辑很齐全了

这是导入easeui的正确方式,是我用几天换来的宝贵经验,希望能帮到大家,多多打赏哈 收起阅读 »
聊天机器人成网络话题焦点,移动客服才是幕后推手
Kik Services的总裁Josh Jacobs在最近的Forrester CXSF大会上尝试对这一问题做出回答。
“我认为答案的关键在于移动端的崛起,首先以及最重要的是,我们正处于一个临界点,此时绝大多数的计算都发生在移动设备上。”
Jacobs说,“聊天成为了杀手级应用,在中国,微信所成就的一切为世界其他国家树立了榜样。我们采取了这样的模式,Facebook也已采取了这种模式。我们认为在世界的其他地方也可以建立这样的模式。”
Jacobs说,在过去的25年里,“我们已经开发出了涵盖各种需求的网络服务,这些服务延展到了我们房间灯泡下面的一切东西;然而没有一种用户界面,能够让所有这些服务以一种简单的方式整合起来。在物联网时代,对话成了一种简便易行的方式;在网络服务驱动的世界里,对话也是一个更好的方案。如今,人们已经习惯了用移动设备来进行对话,越来越多的人也都加入到这个行列之中,且认为体验‘很赞’。”
Jacobs还说,他的公司和其他公司现在所做的工作并不是前无古人的。
“鉴于我的年龄,我的经验告诉我,我们现在所做的一切都并不是前人没有做过的。”他说,“这就是CRM,CRM已经在我们身边出现了很多年。另一个我们看到的趋势是,大公司,尤其是客服领域的大公司都会说‘我们必须得把这个新渠道加上。’”
首先,Jacobs说,聊天“要么扩展了漏斗的顶端,并把很多人吸引进去;要么,就是被成为移动端客户的不适应吓跑。”
其次,Jacobs建议各大品牌关注他们能够提供的休闲体验,以在吸引客户方面“放长线钓大鱼”。
品牌公司们应该随时了解自家客户最舒适的交互方式,并相应地参与到这些方式中去。
“下一代人希望用聊天或者语音,所以你就需要进入到这些环境中去,这些渠道是你接触到这些用户的必由之路。”Jacobs说,“这就是面向Z世代和千禧一代的CRM、再参与、再营销和客户终身价值。”
有的品牌还在对此保持怀疑态度,在往聊天或任何与之相关项目的迈进上踌躇不前。
“我认为我们讨论的一切都是关于如何让品牌发出自己的声音,让你的品牌参与到对话之中。”Jacobs说,“你可以更多的围绕着市场营销的环节去做这件事。”(环信编译自www.loyalty360.org)
关于环信移动客服
全媒体智能云客服倡领者,于2016年荣膺“Gartner 2016 Cool Vendor”。环信支持全媒体接入,包括网页在线客服、社交媒体客服(微博、微信)、APP内置客服和呼叫中心等多种渠道均可一键接入。基于环信业界领先的IM长连接技术保证消息必达,并通过智能客服机器人技术降低人工客服工作量。同时,基于人工智能和大数据挖掘的客户旅程透析产品"环信客户声音"能够帮助企业优化运营,提高跨渠道客服体验。收起阅读 »
截至2016年上半年,环信移动客服共服务了29437家企业用户,现已覆盖包括电商、O2O、互联网金融、在线教育、在线旅游、移动医疗、智能硬件、游戏等领域的Top10客户,典型用户包括国美在线、58到家、楚楚街、海尔、神州专车、新东方、链家、泰康在线、中信证券等众多标杆企业。根据易观发布的《中国SaaS客服市场专题研究报告》显示,环信移动客服在SaaS移动端客服用户覆盖占比高达77.4%,稳居行业第一。
浅论呼入型电话银行中心的风险管理
商业银行是运营风险的企业,与商业银行的其他产品、渠道一样,电话银行的每一运营环节都存在风险。研究电话银行的风险管理。一是更好地服务客户、保障客户权益的需要;二是商业银行风险管理的重要组成部分;三是提高和完善电话银行运营管理水平的需要。
一、电话银行风险管理理念现状
目前利用电话银行的自助交易服务进行犯罪的案件越来越多,尤其在2004年之后,犯罪分子的作案手段不断翻新,甚至出现银行网点员工内部作案的情况。更令人担心的是,随着电话银行开通人工辅助交易,对电话银行中心的风险管理也提出了新的要求。
由于我国银行业对客户服务中心的风险管理并没有一个统一的规范或行业标准,所以各家银行的风险防范理念各不相同,对电话银行的风险认识差异比较大,这就直接导致了在电话银行开通的业务呈现多样化发展趋势,对不同业务防范风险的手段也是多样化的。我国银行对电话银行的风险管理理念主要有以下三种。
(一)业务规避型
由于担心开通的业务可能带来风险,这类银行一般宁可舍弃一些可能给电话银行带来利润的业务,通过减少向客户提供的服务以降低电话银行可能面临的风险。基于这种理念设立的客户服务中心将成为银行的成本中心。
(二)风险规避型
基于“没有风险,就没有利润”的认识,这类银行会选择性地开办一些能够给客户服务中心带来利润且风险可控的业务。
(三)混合型
我国较大规模的银行在各个区域开通的服务不尽相同,同一家银行不同区域的客户服务中心采用的风险管理理念有的是业务规避型、有的是风险规避型,这样的银行由于目前还没有形成统一的理念,所以可以称为“混合型”,但从长远来讲,这种兼而有之的模式并不稳定,最终是会“二者择其一”。
风险防范手段的不足已经极大地影响商业银行电话银行在我国的发展。随着居民对银行推出的金融产品需求日益迫切,银行推出新业务的周期不断缩短,人们更加渴望通过便利的渠道获得这些新业务,但风险防范手段的匮乏已经阻碍了银行在电话银行开拓业务的进程。监管部门多次强调防范风险是商业银行发展的前提,电话银行面临着防范风险与拓展业务双重压力。
二、呼入型电话银行中心业务情况风险分析
(一)一般性呼入业务常见风险
呼入型电话银行中心一般业务包括业务咨询、建议、投诉、交易等,尽管各大电话银行中心出台了许多合规管理条例,也配套客户满意度、抽听录音质检等手段,但特定风险依然存在。主要包括:
1、业务咨询差错风险
业务咨询差错风险的产生因素众多,主要有几个方面。一是知识库更新维护有一定滞后性,甚至可能同一工作日的不同时间段,知识库的更新将导致部分的客户得到的信息不对称,甚至出现矛盾。二是工作人员自身综合能力的差异,在对知识进行解密再加密再解密的过程中对知识理解的偏差、表述的偏差以及客户理解的偏差都可能导致业务咨询出错。三是绩效考核的要求,通话时长、接通率、小休率等指标都会给客服代表造成一定影响,而客户在线等待长也会给客服代表带来无形压力,容易使工作人员在回答过程中凭借主观的知识记忆作答。不仅限于此,业务咨询差错极易导致客户时间和费用的消耗,易引起客户不满,进而引发深层次矛盾,造成声誉风险等。
2、口头挂失处理风险
作为经营风险的企业,商业银行在处理特殊业务时稍有不慎都会导致实实在在的资金风险,而对于电话银行而言最常见的操作风险便是挂失风险,挂错账户、挂失不及时、未提醒客户挂失等都可能给客户带来损失。近年来由于新型电信诈骗层出不穷,客户来电核实账户交易信息,很多情况下应协助其尽快办理挂失,而由于客服代表未能有效识别风险、遗忘提醒客户挂失而导致客户发生的损失的情况也时有发生。另外为最大限度的保障客户资金安全,各大银行在办理电话口头挂失时的核实条件往往会放宽,这种“宽进严出”的做法也时常被不法分子利用,导致蓄意挂失他人账户的情况发生。诸如此类事件,都给我们的日常工作敲响警钟。
3、交易处理风险
随着商业银行电话银行业务受理范围的拓展,目前可通过电话银行办理的交易不断增多,包括转账、缴费、信用卡还款、基金理财、账户金买卖等,该类交易操作一般都有严格的操作流程及风险控制机制,如身份核实要素、验证密码,设置约定账户、主叫号码限制等,但由于人为操作以及新型诈骗手段等因素的影响,仍然存在巨大风险,这要求工作人员在处理交易时既要严格遵照规程办理,也要灵活机警地识别风险,务必集中精力,谨慎操作。
4、身份校验风险
目前电话银行的身份验证既包括IVR密码验证、CSR人工辅助电话银行密码验证、客户账户信息核对等传统手段,也包括声纹识别等新型技术手段。验证身份的过程中若不提高警惕、对储户保密原则保持高度的敏感,将极容易泄露客户个人信息,这将导致隐私风险。在实际操作过程中,限于电话银行的虚拟性及远程性,只能通过客户提供的信息为依据判断是否为户主本人,这种核实的准确度明显不及网点柜台的当面核实。虽然客户有保护个人信息的责任,但银行对进一步加强储户信息安全同样义不容辞。
5、客户问题升级处理风险
电话银行中心由于知识储量、信息传递、处理权限等原因限制,相当一部分问题需升级处理,这包括电话银行中心内部的升级处理,比如投诉受理等,也包括登记工单转交其他业务部门处理的情况。由于工单流转涉及多个部门且不同业务信息处理所需时间不同,甚至由于座席员的疏忽导致工单无法流转,各种不可控因素都会影响回复客户的时间。客户将质疑通话过程中所提供的回复时间,小则为下一手座席及后续处理造成沟通困难,大则影响信誉及客户情感的维系。
6、设施环境风险
电话银行中心主要依赖电信网络等设施提供服务,路由切换、IP地址管理等技术方面的工作显得尤为重要,而这也是电话银行中心不可忽视的风险源。这类风险既包括技术设施老旧等引发故障的风险,也包括新旧设备切换过程中引发的风险,以及新设备投入使用后造成的稳定性风险。该类风险给电话银行中心带来的影响往往是全面性、毁灭性的,具有影响范围广、影响程度深等特点,是电话银行中心的面临的基础性风险之一。
(二)承接海外机构电话银行业务的风险
随着银行国际化经营的发展,目前国内各大银行的电话银行中心承接海外分行电话银行业务的情况越来越多,海外电话银行业务风险管理方面除了面临上述一般性呼入业务的风险外还面临着以下突出的风险源。
1、不符合当地监管规定引发的合规风险
目前我国商业银行的海外拓展一般以子公司、子机构等方式进行,这就意味着在银行的监管体系中如果该银行的国内电话银行中心需要承接海外电话银行业务,需要以第三方公司外包的形式进行承接,这就往往会涉及第三方公司的资质审核等问题,同时在承接的业务类型方面也有更多限制,面临更多的合规性审查等问题。此外,在实际的业务操作中也要以当地的监管规定为准,因此由于国内国外的监管政策不一,极容易出现不符合当地监管规定而引发的合规风险,给海外机构带来损失。
2、服务需求差异引发的声誉风险
随着各大银行海外版图的不断扩大,其电话银行中心的服务范围也不断增大,各国各地区间的文化差异也相对较大,同时各国客户对电话银行的服务需求存在巨大差异,例如亚洲客户和欧美客户,其语言表达、心理诉求等各方面差异都会给海外电话银行服务带来巨大挑战,同时也会产生一定风险。这种风险表面上导致电话银行中心在服务过程中无法达到客户的期望值,而实质上将对海外客户对银行的印象的大打折扣,甚至影响中国银行业在海外的拓展及发展。
(三)运营管理及业务管理风险
1、人员排班风险
受季节、气候、突发事件等外部因素影响,电话银行中心的来电量经常呈不规律变化,来电量预测及排班管理难度较大,人员需求与业务量匹配是否合理是电话银行运营管理的重大风险点之一。排班风险对电话银行的接通率及人员利用率具有重大影响,如果排班人数不足,则可能导致一定时段的接通率低下,严重影响客户体验,甚至引发重大投诉的声誉风险;如某一时段的排班人数过多,又容易造成人力资源浪费,为日后的人员调配埋下隐患,因此人员排班风险也是影响电话银行运营稳定的基础性风险之一。
2、应急处理风险
成熟的电话银行中心均有完善的应急管理体系,尽管如此,应急风险只能有效减小而无法消除,以笔者所在的电话银行中心为例,2013年4月20日四川雅安发生7.0级地震,对当地造成重大影响,当地电话银行中心工作人员紧急撤离,暂停对外服务,电话直接溢入其他电话银行中心。除以上重大突发自然灾害引发来电变化外,在日常运营中还可能遇到诸如电子银行系统、自助设备等系统突发故障引发的短期来电剧增的情况。短期内来电剧增首先会影响接通率,如果处理不当则容易引发重大投诉,因此突发事件处理是影响中心运营的重大风险点之一。
3、集约化处理风险
为提高处理效率,体现集约化经营的优势,目前不少电话银行中心都采取集约化处理的方式,例如针对疑难问题、交易等业务集中处理,或者在话务高峰期采取高峰话务分流等措施,这些集约化处理手段一方面有利于提高处理效率,保障中心整体接通率;另一方面,由于涉及问题登记、流转交接、无法即时处理等,也是引发客户投诉的风险点之一。
4、知识及信息传递风险
知识库是电话银行中心客服代表工作的信息库,是做好电话银行工作的重要基础。一是银行金融服务具有种类齐全、产品丰富、业务规定变化快、产品系统不断推陈出新的特征,且客户数量庞大,若知识库维护不及时,业务资料缺失、出错或者过时等都会在短时间内引发巨大的客户投诉风险,严重的还可能给客户带来损失或者引发声誉风险。二是信息传递容易出现不对称的情况,由于业务规则制定者、知识维护人员、培训人员、前线座席员等岗位的工作分工不同、沟通问题,都可能导致知识信息在传递的过程中出现偏差,从而引发服务风险。
(四)电话银行信息管理风险
1、录像录音凭证管理风险
电话银行中心通过电话语音为客户提供金融服务,无法像柜面渠道一样提供纸质凭证作为交易或服务凭证,因此录音凭证是客户与座席员产生业务往来的最重要凭证。2005年4月1日起施行的《中华人民共和国电子签名法》规定:“数据电文不得仅因为其是以电子、光学、磁或者类似手段生成、发送、接收或者储存的而被拒绝作为证据使用。”客户录音可以作为电子(数码录音)、光学(光盘)、磁(磁带)等形式保存,从法律上讲,可以作为客户与银行要约关系的凭证,录音同签字一样具有不可抵赖性。目前,客户拨打服务95533入线接通人工服务之前,自助语音会提示对服务过程进行录音,客户确认之后才能接通人工座席,此环节可避免因客户不知情而产生侵权风险。
正是由于录音凭证的重要性,所以录音凭证的管理也极为关键。目前录音管理的风险环节包括:一是内部听取录音或使用录音所产生的风险。电话银行中心内部使用录音包括质检抽听录音、培训分享等,这其中涉及人员较多,容易引发泄露风险。二是外部调取录音所产生的风险。包括协助客户、公安机关调查取证等调取录音,由于涉及客户、司法机关、建设银行等多个主体,在录音调取及使用过程容易形成风险。三是由于录音保管期限较短所产生的风险。因前期形成的风险可能在未来的某一个时间段爆发,所以延长录音的保管期限有一定的必要性。另外,一些电话银行中心还对座席员的CSR操作进行录像监屏并在所有的办公区域设置监控探头,系统录像监屏和办公场所实时监控都是为了防范风险,若相关的监测系统失效或者数据丢失,都存在潜在风险。
2、信息保密
电话银行中心涉及到众多数据信息,包括业务知识库、客户数据信息、录音录像监屏等。业务知识库是建行业务的大辞典,所有的信息都只供业务查询使用,需防止员工或外部人员侵入系统或者通过邮件、U盘拷贝等方式外传。另外,电话银行业务需要记录、传输、存储和处理客户手机号、账号、余额、姓名等客户重要信息,在为客户提供人工服务时,客服代表会查询客户的账户信息或按照客户要求协助客户办理相关交易。在操作过程中电话被窃听或客服代表使用的电脑终端存储的客户信息能够通过U 盘或其他手段拷贝等,都会形成客户信息泄露风险。
三、风险防范和对策
(一)业务流程持续改善
业务流程是员工赖以处理问题的行为准则,它是动态的优化过程。流程的优化不仅会带来效率的提高,更加能够防范风险,最大程度上减少漏洞。反之,有漏洞的流程本身便暗藏风险。电话银行中心可通过不断优化的流程来规范员工的日常操作行为,而日常操作行为又推动着流程的优化,二者相辅相成。做好流程优化,一是要从制度上规范流程管理,明确流程制定、试运行、实施、效果评估及再优化等环节的标准;二是调动员工的积极性,全员参与,有利于发现流程漏洞,持续做好各项优化措施的落实。
(二) 优化知识工具和应用短信工具
知识库是座席员的大脑,也是客户了解银行产品和服务的基础。由于知识库有一定的滞后性且涉及面广,在和柜台实际操作中也存在差异,因此及时和相关部门沟通获取第一手资料尤为重要。再者,在进行知识文档优化的过程中,语言表达和排版也要尽量通俗易懂,便于查找,以此提高效率。除此之外还可以开发利用短信工具,避免员工和客户之间口头沟通所产生的误解或遗漏,便于客户直观地获取业务资料和解决方案。
(三) 做好信息传递及培训辅导
信息的传递分由上至下、由下至上。一方面,从银行的产品开发到电话银行中心的服务开展,须保证原则统一、内容统一、落实统一,因此文件的拟定以及解读既要通俗易懂,也要保证传达到位,否则进一步传递到客户的耳朵中可能有大偏差。另一方面,员工所获取到的一手信息不管是高频率咨询业务亦或密集发生型事件都要求能及时上报,进而层层传递再进行确认。培训辅导是信息传递的重要方式之一,通过定期定量的培训不仅有利于业务知识学习、提高服务质量,培训师搜集到的信息反馈,也促进下一次培训辅导的优化,更具针对性。形成回流式的信息传递模式,最终有利于实现大方向的战略部署。
(四) 排班管理科学化
一是要加强来电预测,通过经验数据总结,收集影响因素等建立来电预测模型;二是要加快实现电子化排班,通过引入排班系统来减少人为估算的偏差,实现快速高效排班;三是要做好员工思想动态管理,24小时客服工作性质的特殊性要求必须轮班制,这是对生物钟的挑战,更是对排班管理的挑战,排班人员要深入研究员工的行为状态,结合不同员工的特点进行科学排班。总之,根据经验和科学的预测,不仅能横向覆盖客户的问题,也能纵向提高银行的接通率,而员工也因此得到合理的工作时间安排,与此同时也要做好紧急预案、做好防范工作、安排后备人员,以免来电量异常激增而措手不及。
(五)优化问题处理机制
百密难防一疏,更重要的是问题发生后如何解决,这是客户关注的焦点,也是电话银行时时刻刻面临的挑战。一是要积极地促进多部门合作,同时按照岗位设置的逻辑流转,明确职责,形成多方而高效的处理难题局面;二是要明确原则,分级处理,通过设定不同层级问题的处理时限,及时向客户反馈信息;三是归口统一,在问题解决过程中必须统一由一个落实部门去和客户交流、解决问题;四是要注重问题解决案例的经验总结,通过形成问题解决案例库,引导和提升问题解决人员的处理能力。
(六)规范录音凭证管理
录音是保护客户权益、员工权益和银行权益的手段之一,是具有法律效应的重要凭证。现如今,录音凭证管理更加依赖于技术范畴,因此对设备的维护以及储存空间的容量有了更高要求,定期检查技术设施,及时清理不必要的内存,都能为保存创造更加良好的客观条件。与此同时,凭证管理的人员安排也格外重要,既要与一线工作人员独立开来,保持高度独立性,又要与前台座席员保持紧密联系,且必须做好保密措施。
(七)做好信息保密的核查审计工作
商业银行本身是经营风险的组织,涉及到无数客户个人隐私,因此客户信息的保管异常重要。电话银行中心不仅要严禁工作人员携带纸张、通讯设备等便于记录的工具进入工作区域,也要每天仔细清算工作人员领取及上交的工作稿纸,同时由主管部门牵头定时安排人员清理电脑中含有敏感信息的文件并进行核查。另外还要加大力度培育员工的保密习惯,不携带任何有关客户信息的资料走出工作区域,形成定期检查、互相监督的安全审计局面,做好信息保密的核查审计工作。
(八)加大对服务对象的研究分析
服务对象既包括宏观的服务区域及社会群体,也包括微观的客户组成。随着国内银行的海外拓展,目前不少国内的电话银行中心均有承接海外电话银行服务的职能,而国内的跨区域服务就更为常见,因此研究服务对象尤其重要。一方面要聆听客户声音,发掘客户需求有利于更高效地解决客户问题;另一方面,要进行社会心理分析,针对性给予个性化服务。只有全方位剖析才更有利于信息的反馈,从外部的思考及到内部产品的优化,从源头上防范风险,做好电话银行服务。
(九)建设合规文化以提高风险意识
企业文化是日常工作中形成后被成员广泛认可,认为有效而共享,并共同遵循的基本信念和认知。建设合规文化,提高风险意识,有利于员工在日常工作中下意识地利用流程、知识库等合理地来保护客户、公司、自己的权益。
(十)利用新技术手段提高风险防控能力
一方面,技术的日新月异让不法分子有机可趁,利用改号软件和伪基站冒充各商业银行的客服热线发短信或致电客户,实施诈骗;另一方面,技术的发展也给我们商业银行提供了提高风险防控能力的机会。充分利用新技术手段,加强防盗防骗的防火墙建设,做好预防工作,不让不法分子趁虚而入。同时提高应对能力,面对紧急事件能充分利用新技术手段破解不法行为。这不仅要求有风险防控的成本资金投入,完善设备,也要求有高科技人才的储备。目前可用于电话银行中心的技术手段主要包括更严密的密码校验方式、智能质检系统、智能排班系统、话务监控系统等,通过各种技术系统组合可实现对运营的前瞻预测、事中控制以及事后分析,同时利用大数据分析的方法可充分较少风险发生的可能性,提升风险防控能力。
综上所述,电话银行中心的发展面临着诸多机遇,同时也是巨大挑战,一方面是集约化经营、转型发展的升级良机;另一方面也面临着更加多变的风险挑战及监管要求。只有处理好合规管理与经营发展的关系,才能在不断推进电话银行中心发展的过程中切实抓好风险管理,最大程度维护客户、员工以及银行自身的权益。
本文刊载于《客户世界》2016年11月刊文章;原文作者刘添权,梁雅静,本文作者单位为中国建设银行广州电话银行中心。 收起阅读 »
环信联合阿里云新年送福利
2017“环信”有“你”更精彩!环信祝福小伙伴们,新春快乐,鸡年吉祥!
新的一年,环信联合阿里云给各位环信小伙伴送福利
参与形式:
步骤一:在IMGeek社区发布一篇环信集成笔记
可以是集成教程:遇到的问题,如何解决的,对环信的的建议。
步骤二:在本贴留言区回复文章地址
活动奖品:
奖品一:参与发布文章的童鞋,将获得环信与阿里云共同推出的【阿里云免费套餐半年使用权】,将通过社区私信方式送出激活码,直接激活即可使用。
阿里云免费套餐简介:
• 包含 30 余款云计算产品最长 6 个月免费使用资格
• 产品总价值超 20000 元
套餐里包括云服务器、数据库、存储、网络产品等基础云计算产品,搞个电商、玩个网站等想法都可以轻松实现
奖品二:【.xyz域名免费注册】,在下方回复留言新年祝福,然后自己领取xyz域名带走吧http://click.aliyun.com/m/8900/ ,总量2000个名额,先到先得哦~~
活动时间:1月4日开始,下手要快,先到先得哦~
附:【阿里云免费套餐半年使用权】是以邀请码形式开通
领到码之后,到活动官网(http://click.aliyun.com/m/8901/ ),用阿里云新手帐户登录,通过验证后即可开通。
收起阅读 »
新版群组/聊天室服务 REST API差异说明v2
(最新更新时间: 2017-01-06)
第一版差异说明发出后,我们收到了一些开发者的反馈。根据这些反馈,我们对新版群组/聊天室的异常、状态码进行了调整和优化。相比2017-01-03第一个版本,主要更新内容:
- com.easemob.group.exception.ForbiddenOpException 异常类型的状态码 由400变成403,该异常表示本次调用不符合群组/聊天室操作的正确逻辑,例如调用添加成员接口,添加已经在群组里的用户,或者移除聊天室中不存在的成员等操作。
- 部分操作抛出的com.easemob.group.exception.InvalidParameterException (状态码400) 异常类型替换为 com.easemob.group.exception.ForbiddenOpException (状态码403)
- 如果用户加入群组/聊天室的个数超限,或者appkey下群组/聊天室的个数超限,将抛出异常com.easemob.group.exception.ExceedLimitException, 状态码 403
- 群加人(单加/批量),用户已经在群里
- 群减人(单减/批量减)被减用户(存在但)都不在群
- 转让群主,自己转自己
- 转让群主,转给一个不在群里的用户
- 批量添加黑名单,用户都已经在黑名单
- 单个加入黑名单,用户不是群组成员
- 把群主加入黑名单
- 修改聊天室信息,修改的字段含有不允许修改的字段,例如id
- 聊天室减人,被删除成员不在聊天室
- 限制检查
- 旧群组抛出的异常类型:java.lang.IllegalArgumentException 状态码为400
- 新群组抛出的异常类型: com.easemob.group.exception.ResourceNotFoundException 状态码为404,异常描述为"username %s doesn't exist!"
- 旧群组服务返回200,
- 新群组服务抛出的异常类型:com.easemob.group.exception.ForbiddenOpException 状态码为403 异常描述为 "can not join this group, reason:%s"(注:批量加人时,如果部分用户不在群里,则该部分用户可以添加成功,返回状态码为200)
- 旧群组服务返回200
- 新群组服务抛出的异常类型: com.easemob.group.exception.ForbiddenOpException 状态码为403 异常描述为 "users %s are not members of this group!"(注:批量减人时,如果部分用户在群里,则该部分用户可以移除成功,返回状态码为200)
- 旧版群组服务返回200
- 新版群组服务抛出异常:com.easemob.group.exception.ForbiddenOpException 状态码403 异常描述:"new owner and old owner are the same "
- 旧群组服务可以转成功,原来的群主被从群里移除
- 新群组服务要求新群主必须首先是群成员,因此会抛出异常 com.easemob.group.exception.ForbiddenOpException 状态码403 异常描述为:"user: %s doesn't exist in group: %s"
- 旧群组服务旧群主从群成员中移除
- 新版群组服务,旧群主还留在群里。
- 旧群服务返回200
- 新群服务抛出异常: com.easemob.group.exception.ForbiddenOpException 状态码 403 异常描述为:"users %s are not members of this group!"
- 旧群服务返回200
- 新群服务抛出异常: com.easemob.group.exception.ForbiddenOpException 状态码 403 异常描述为:"forbidden operation on group owner!"
- 旧群组抛出的异常类型:java.lang.IllegalArgumentException 状态码为400
- 新群组抛出的异常类型: com.easemob.group.exception.ResourceNotFoundException 状态码为404 异常描述为"username %s doesn't exist!"
- 旧群组服务返回200
- 新群组服务抛出的异常类型: com.easemob.group.exception.ResourceNotFoundException 状态码为404 异常描述为:"grpID %s does not exist!"
- 旧群组服务返回200
- 新群组服务抛出的异常类型: com.easemob.group.exception.ResourceNotFoundException 状态码为404 异常描述为:"grpID %s does not exist!"
- 旧群组服务返回200,修改了允许修改的内容
- 新群组服务,如果传入的修改字段中,包含不可更改,或者是无效的字段,会抛出异常: com.easemob.group.exception.InvalidParameterException 状态码为400 异常描述为 "some of %s could not be modified" 或者 "some of %s are not valid fields"
- 新群组服务,禁止通过修改群组/聊天室接口来修改owner, 否则会抛出异常:com.easemob.group.exception.ForbiddenOpException,状态码403 异常描述为: "owner cannot be updated through this method!")
- 旧群组服务返回200
- 新群组服务抛出的异常类型: com.easemob.group.exception.ResourceNotFoundException 状态码为404 异常描述为:"grpID %s does not exist!"
- 旧群组服务返回200
- 新群组服务抛出的异常类型: com.easemob.group.exception.ResourceNotFoundException 状态码为404 异常描述:"grpID %s does not exist!"
- 旧群组抛出的异常类型:java.lang.IllegalArgumentException 状态码为400
- 新群组抛出的异常类型: com.easemob.group.exception.ResourceNotFoundException 状态码为404 异常描述: "username %s doesn't exist!"
- 旧群组服务返回200
- 新群组服务抛出的异常类型: com.easemob.group.exception.ForbiddenOpException 状态码为403 异常描述: "users %s are not members of this group!"
- 旧群组服务返回200
- 新群组服务抛出的异常类型: com.easemob.group.exception.ResourceNotFoundException 状态码为404 异常描述:"grpID %s does not exist!"
- 用户加入的群组/聊天室超过上限:"can not join this group, reason:user %s has joined too many groups/chatroom!"
- appkey下创建群组/聊天室超过上限:"this appKey has create too many groups/chatrooms!"
权限检查
所有群组/聊天室的接口,如果调用者不具备相应接口的调用权限, 抛出的异常类型:com.easemob.group.exception.GroupAuthorizationException,状态码为 401 收起阅读 »
2016年度盘点:一家典型互联网公司的必备工具宝箱
2016年刚刚过去,2017年已经来临。在过去的一年,作为互联网从业人员的你肯定和我有同样的感受:无论是产品研发、市场还是运营人员,大家在工作中使用的工具产品越来越多,对工具的依赖程度也越来越重。值此岁末年初之际,现在就盘点一下2016年互联网公司最常用的20款工具产品及服务。正是在这些工具的助力下,众多互联网公司才能以如此矫健的步伐跨入这充满希望的2017年,而没有倒在2016年的资本寒冬里。
● 即时通讯云服务商:环信
微信、陌陌引领了社交媒体的爆发后,移动社交已成使用最多的产品功能之一,社交不仅可以让用户更具黏性, 越来越多的企业倾向在App上添加社交功能。但IM底层开发繁重的问题,也是让很多企业望而却步。
环信作为集成通讯云服务商,通过云端开放的 Rest API 和客户端 SDK 包的方式让App内置聊天功能和以前网页中嵌入分享功能一样简单。不必为基础功能组件耗费自己太多精力,为开发者省钱省力,更是加速了APP的上线速度。
产品官网:http://www.easemob.com/product/im
●产品原型设计工具:墨刀
身为一名PM,画原型写文档是我们的看家必备技能了,在寻觅尝试了众多国内外原型工具后,我们锁定了墨刀这款利器,一用上便爱不释手。
墨刀采用拖拽式的原型制作和交互方式,十分钟即可上手,学习成本非常低。墨刀的功能非常强大:用墨刀可以完成从原型制作-交互动效-团队协同作业-实时分析查看效果-跟踪团队及用户反馈整个产品开发流程中大部分让人挠墙的问题。实时扫一扫预览、打点评论,不仅方便真机演示,还大大降低了沟通协作成本。
产品官网:https://modao.cc/
●企业的云端文档协作平台:WPS云文档
文案校稿时,需要通过QQ/邮件在多人间反复往返发送稿件,异常繁琐低效;团队资料到处都是,需要时常常找不到;团队资料无法全平台同步,无法移动办公。如果你也在被这些问题困扰,不妨试试WPS云文档。
WPS云文档是一个企业文档的存储、共享与协作平台,支持多人同时编辑一个文档、文档内评论和历史版本还原等功能。团队可将文档资料全部存储在云端,方便查找与管理。云文档能满足不同人群的文档协作需求,产品研发用它协作撰写需求文档、管理项目进度和搜集bug;运营团队可以多人在同一个文档里协作撰稿与校稿。WPS云文档既有免费版也有付费版。
产品官网:https://drive.wps.cn/landing.html
●思维导图工具:MindManager
MindManager是一款专业思维导图工具,它可以将你的思想、策略及商务信息转换为行为导图,让你直观感受整个进度。它可以帮助你进行项目管理、头脑风暴、记录笔记、演示演讲,无论你是商务人员、老师还是学生,Mindmanager图文并茂的展示形式都可以为你提供高效清晰的思维方式。MindManager有免费试用版和付费版,试用30日后可付费购买使用。
产品官网:http://www.mindmanager.cc
●表单与联系人管理工具:麦客CRM
作为现代企业营销工作者,我们需要更加有理有据地管理市场活动、品牌建设和线上线下的营销行为,了解这些工作能为企业带来的潜在客户,他们都是谁,他们在哪里,他们的质量高低。
麦客CRM能满足你在营销、获客和客户管理方面的需求。麦客是一款在线表单制作工具,同时也是强大的客户信息处理和关系管理系统,可以帮助你轻松完成信息收集与整理,实现客户挖掘与消息推送,并开展持续营销。
产品官网:www.mikecrm.com
●在线图形设计工具:创客贴
身为一个新媒体运营,老板指望我三头六臂,八项全能:开会要做ppt,得简洁美观;
文案不能干巴巴,要丰富有趣;热点追图、双微首图,必须好看刺激…还好有创客贴。
创客贴是一款极简的网站式平面设计工具,解决了大多数人的设计痛点,让不会使用专业制图软件的运营人员也能快速制作出自己想要的图片:可使用平台提供的大量图片、字体和模板等素材,通过简单的拖拉拽操作,就可以轻松设计出精美的海报、PPT、公号文章首图等图片。另有团队协作版和ipad版会让你的做图工作更加直接高效。
产品官网:https://www.chuangkit.com
●用户行为分析工具:神策分析
近期哪个渠道用户注册量最高?变化趋势如何?新增的注册引导流程是否提升用户粘性……作为产品运营的你如果想知道这些问题的答案,那么可以使用神策分析。
神策分析是一款可以私有化部署的用户行为分析产品,致力帮助用户实现数据驱动。在保障数据安全的前提下,产品实现秒级数据导入与查询,带来灵活的PaaS平台,并针对多业务场景提供专业的数据分析服务,为业务决策提供数据支撑。
产品官网:https://www.sensorsdata.cn/
●商业管理云:数据观
作为企业运营管理部门,需要每天关注各渠道的运营数据(用户、推广、客服、营销)等,整合这些数据孤岛很耗费人力和时间成本,我们需要一个实时的可视化看板以了解企业的运营动态,然后才能针对企业运营过程中的问题及时做出决策。
数据观可以把我们的注册用户、百度推广、在线客服、微信、微博等数据全部关联起来,并基于全量数据进行可视化分析,以便我们实时、准确的了解企业的运营状况。现在公司的销售、市场、运营都在用数据观做日常的业务分析。
产品官网:www.shujuguan.cn
●图文排版工具:135编辑器
内容运营每天都有大量的工作要做,要追热点,写原创,上午想着如何涨粉,下午想着如何促活。在做好内容的同时,如何快速做出既漂亮又有自己独特风格的排版,是很多内容运营的目标。
135编辑器是一款在线图文排版工具,功能比较全面,操作也非常简单。比如一键导入,多平台发布,一键排版和定时群发等大大节约了时间。样式库丰富,自己排列组合自定义为模板,形成自己的风格。企业用户也可以将其嵌入企业内部系统,提升企业排版能力。
产品官网:www.135editor.com
●深度链接企业服务商:LinkedME
当今,每个移动APP都是独立的,内容和服务之间的链接消失,应用搜索断裂,APP的内容被局限在每个APP内。我们能在电脑网页所监测的用户意图、广告曝光和其它盈利指标都不能在移动端很好进行。
LinkedME打破了束缚我们的APP孤岛,它是一个企业级深度链接服务平台,可以帮忙APP解决用户增长和流量变现问题。它旗下的Linkpage提供APP一键直达和推广渠道监测服务,帮助APP企业获取社交媒体和广告曝光用户,提高运营转化,优化渠道投放策略。
产品官网:https://www.linkedme.cc/
●SEM优化工具:九枝兰
搜索竞价推广真不是人干的活儿。单是最基础的调价,一个关键词优化师就需要管理1440种价格!如果以5万关键词的账户为例,每天每个关键词调价1440次,每天竟然需要进行7200万次调价!一个优化师即使有洪荒之力,也极难完成。
于是九枝兰SEM优化工具应运而生,解决SEM优化师力所不能及的事,在需要发挥人的聪明才智的方向投入精力,如:拓词、创意、着陆页优化。
产品官网:http://www.jiuzhilan.com/sem-tool-highlights/
●广告效果监测与分析平台:【友盟+】U-ADplus
U-ADplus广效监测是【友盟+】旗下聚焦营销全链路的第三方数据服务。其中AppTrack是面向App广告主推出的监测与分析平台。它能满足不同类型使用者的推广监测需求。提供基础、电商、游戏应用场景,使用者可根据想监控的指标选择适合的应用场景。支持多样的推广形式,不管是广告平台、信息流广告还是广告联盟 ,它都可以帮使用者监控推广效果。除了能监控推广点击和激活,还能监测用户注册、登陆、行为和付费,让使用者真正了解推广带来用户的转化情况。
产品官网:https://at.umeng.com/fuia0z
●企业消费与报销管理平台:易快报
起初臃肿冗长的报销流程令我们非常头疼,很难进行费用管控。使用易快报后才深切感受到了原来报销流程也可以如此便捷。易快报打通了从提交申请到支付的全部环节,使员工报销的时间大为缩短,还能对企业进行实时全程费用控制,效率提高了不少。
易快报是一个敏捷的企业消费和报销管理平台,面向企业提供专业的订购、费控和报销管理服务。据官网公布的数据,公司目前已经为超过5万家企业客户和220万个人用户提供了这种全新的报销服务。
产品官网:https://www.ekuaibao.com/
●快速建站服务:友好速搭
要做电商,渠道是个问题,入驻各大平台还不够,要有自己的独立的门户网站,但创业初期,资金有限,技术太贵,维护太难,加上服务器配置、域名备案一堆事,一想头就大,有了友好速搭,60 秒轻松搭建门户网站,就算是技术小白,也能做出稳定可靠的官网。
友好速搭可以为企业提供了一站式建站服务,集成域名、DNS、安全证书等基础设施,提供SaaS 建站系统,开放全部API并提供互联网、营销、传媒等资源服务,目前已服务超过2.3万个品牌。
产品官网:youhaosuda.com
●移动应用分发与监控平台:酷传
酷传是一个一站式APP发布及监控平台。开发者不需要添加任何SDK,即可通过酷传把自己的APP同时上架到30家主流安卓应用商店,后续还可以实时跟进各个商店的审核进度,不再需要运营人员一一去各家应用商店进行操作;通过酷传的监控产品,还可以查询一百多万款APP的各项数据表现,目前已经支持安卓和IOS两个平台十多家应用商店。
产品官网:www.coolchuan.com
●企业级云服务商:七牛云
对于有大量数据存储需求的互联网公司,现在不仅仅是把数据托管到云存储供应商就够了,围绕数据展开的一站式的服务,成为当下互联网迫切所需。
七牛云作为企业级云服务商,除了存储、CDN加速服务,和完整的直播云解决方案,围绕数据还有许多玩法。存储在七牛的数据,不需要下载下来,就可以进行批量加水印、裁剪、反垃圾等处理。通过提供稳定、高效、可信赖的底层服务,让客户能集中精力在自身业务的实现和创新上。
产品官网:www.qiniu.com
● 应用性能监测平台:OneAPM
网络访问缓慢?用户无法登录?接口突然失效?你是否也经常由于很多IT系统故障的原因,眼睁睁地看着用户流失?作为一个应用性能监测平台,OneAPM可以帮助你预先发现性能问题。OneAPM目前支持Java、PHP、Ruby、.Net、Python等多种编程语言,同时也支持iOS 和Android操作系统。它可以帮你实时抓取缓慢的程序代码和SQL语句,让你的应用运行更加流畅、稳定。
产品官网:http://www.oneapm.com/
●APP测试服务商:Testin云测
移动互联网的竞争越来越激烈,迫使互联网公司必须要根据用户需求快速对APP进行迭代。每次迭代过程中最担心的是出现Bug,伤害到用户体验可能会是导致用户流失的重要原因。
Testin专注于面向全球范围内的移动互联网应用开发者,如移动APP开发者、移动手机游戏开发商及互联网+相关移动应用企业提供“一站式测试服务”。包括从移动应用内测到功能测试、性能测试、兼容测试及移动应用发布后持续质量监控,解决APP终端在功能、性能、碎片化、兼容性、稳定性等广大移动互联网企业及开发者不易克服的难题。Testin一站式测试服务覆盖开发者从开发完成到版本迭代的全过程。
产品官网:www.testin.cn
●推送技术和大数据服务商:个推
“这么大个红包,再不使用就过期啦!”、“美妆热搜销量王,全场五折起”,收到这样的消息,你肯定也会忍不住打开一探究竟。通过小小的消息,传递用户所需的信息,提高用户活跃度和留存率,这就是推送技术的力量。
个推所做的就是搭建APP与用户沟通的桥梁,确保消息的毫秒级到达,用户即便足不出户,也能第一时间尽知天下事。个推给用户分群组、打标签,通过大数据分析送让合适的消息找到合适的用户。个推提供免费推送和VIP增值服务,并为各垂直领域提供专业大数据解决方案,目前已服务于50万APP,SDK累计接入用户数超过130亿。
产品官网:http://www.getui.com/
希望大家所在的公司都能在上面这20款工具服务的助力下在2017年继续高歌猛进,越跑越快。
收起阅读 »
聊聊即时通讯(IM),基于环信 web im SDK
感觉自从qq、微信这种APP用多了,现在都没啥人发短信了,现在什么APP都想加入IM的功能,曾经有段时间在折腾自己撸一个聊天的东西,也尝试过很多平台,今天这里给大家介绍一下从零开始自己做一个聊天的app功能。因为之前帮朋友做过一个基于环信的聊天功能,这里就以环信的平台为例举个例子说明。这篇文章注意想讲解一下集成这种第三方的一般实现方法。
准备工作
1.注册账号
我们要先去环信官网注册一个账号,然后在后台创建一个应用,因为我们后面在做功能的时候可以用后面发送消息及图片来测试收消息,用户管理在后台也可以看得一清二楚。
创建成功后找到应用标识(AppKey),这个在后期配置中会用到。
2.下载SDK
http://www.easemob.com/download/im
这里我们使用的是Web IM,所以下载的SDK是Web IM版本,下载之后我们会看到一个演示demo,由于这个是pc版本,和我们需求不一致,所以我们只需要关心sdk目录下的文件和sdk集成需要修改的配置文件easemob.im.config.js。
|---README.MD:3.开发文档
|---index.html:demo首页,包含sdk基础功能和浏览器兼容性的解决方案
|---static/:
js/:
easemob.im.config.js:sdk集成需要修改的配置文件
css/:
img/:
sdk/:/*sdk相关文件*/
release.txt:各版本更新细节
quickstart.md:环信WebIM快速入门文档
easemob.im-1.1.js:js sdk
easemob.im-1.1.shim.js:支持老版本sdk api
strophe.js:sdk依赖脚本
Web IM 介绍 http://docs.easemob.com/im/400webimintegration/10webimintro
项目实战
由于这篇重在在于如何使用第三方开发IM,感觉说再多也诶有意义,直接上代码说明。不讲解过多的原理、细节,只讲究开发流程。
1.用户注册功能
首先我们在hbuilder中先新建一个项目easemobIM,然后把环信sdk文件夹和配置文件拷贝到我们的工程中。为了节约时间,下面的功能演示我是根据官方登录模板改的。
html/reg.html
<!DOCTYPE HTML>这是注册页面的代码,我们首先要引入环信的sdk和easemob.im.config.js,并且将easemob.im.config.js中的appkey换成自己的,然后根据用户名/密码/昵称注册环信 Web IM,提交注册的代码为:
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1,minimum-scale=1,maximum-scale=1,user-scalable=no" />
<title></title>
<link href="../css/mui.min.css" rel="stylesheet" />
<link href="../css/style.css" rel="stylesheet" />
<style>
.mui-input-group:first-child {
margin-top: 20px;
}
.mui-input-group label {
width: 22%;
}
.mui-input-row label~input,
.mui-input-row label~select,
.mui-input-row label~textarea {
width: 78%;
}
.mui-checkbox input[type=checkbox],
.mui-radio input[type=radio] {
top: 6px;
}
.mui-content-padded {
margin-top: 25px;
}
.mui-btn {
padding: 10px;
}
</style>
</head>
<body>
<header class="mui-bar mui-bar-nav">
<a class="mui-action-back mui-icon mui-icon-left-nav mui-pull-left"></a>
<h1 class="mui-title">注册</h1>
</header>
<div class="mui-content">
<form class="mui-input-group">
<div class="mui-input-row">
<label>手机</label>
<input id='username' type="text" class="mui-input-clear mui-input" placeholder="请输入手机号码">
</div>
<div class="mui-input-row">
<label>昵称</label>
<input id='nickname' type="text" class="mui-input-clear mui-input" placeholder="请输入昵称">
</div>
<div class="mui-input-row">
<label>密码</label>
<input id='password' type="password" class="mui-input-clear mui-input" placeholder="请输入密码">
</div>
<div class="mui-input-row">
<label>确认</label>
<input id='password_confirm' type="password" class="mui-input-clear mui-input" placeholder="请确认密码">
</div>
</form>
<div class="mui-content-padded">
<button id='reg' class="mui-btn mui-btn-block mui-btn-primary">注册</button>
</div>
</div>
<script src="../js/mui.min.js"></script>
<!--sdk-->
<script src="../sdk/strophe.js"></script>
<script src="../sdk/easemob.im-1.1.js"></script>
<script src="../sdk/easemob.im-1.1.shim.js"></script><!--兼容老版本sdk需引入此文件-->
<!--config-->
<script src="../js/easemob.im.config.js"></script>
<script>
mui.init();
// 输入参数
var regConfig = {
username: mui("#username")[0],
nickname: mui("#nickname")[0],
password: mui("#password")[0],
passwordConfirm: mui("#password_confirm")[0]
};
// 注册事件监听
mui("#reg")[0].addEventListener('tap',function(){
var username = regConfig.username.value;
var nickname = regConfig.nickname.value;
var password = regConfig.password.value;
var passwordConfirm = regConfig.passwordConfirm.value;
// 电话号码校验
if (!isMobile(username)){
mui.toast("电话号码格式不正确");
return;
}
// 昵称非空校验
if (!isEmpty(nickname)){
mui.toast('昵称不能为空');
return;
}
// 密码非空校验
if (!isEmpty(password)){
mui.toast('密码不能为空');
return;
}
// 密码重复校验
if (passwordConfirm != password) {
mui.toast('密码两次输入不一致');
return;
}
// 环信SDK注册
var options = {
username : username,
password : password,
nickname : nickname,
appKey : Easemob.im.config.appkey,
success : function(result) {
//注册成功;
console.log(JSON.stringify(result))
mui.toast('注册成功');
},
error : function(e) {
//注册失败;
console.log(JSON.stringify(e));
mui.toast('注册失败:'+e.error);
}
};
Easemob.im.Helper.registerUser(options);
});
// 是否为电话号码
function isMobile(value) {
var validateReg = /0?(13|14|15|18)[0-9]{9}/;
return validateReg.test(value);
}
// 是否为空
function isEmpty(value){
var validateReg = /^\S+$/;
return validateReg.test(value);
}
</script>
</body>
</html>
var options = {我们注册完了后可以在环信后台【IM用户】查看用户注册信息,我们我们用其他平台,只需要把这块的内容改成相应的内容就OK。
username : username,
password : password,
nickname : nickname,
appKey : Easemob.im.config.appkey,
success : function(result) {
//注册成功;
console.log(JSON.stringify(result))
mui.toast('注册成功');
},
error : function(e) {
//注册失败;
console.log(JSON.stringify(e));
mui.toast('注册失败:'+e.error);
}
};
Easemob.im.Helper.registerUser(options);
2.用户登录功能
有了注册页面的经验,我们写登录页面也很简单,页面布局脚本和其他与登录逻辑无关的代码我这里不贴了,大家在我最后给的地址上下载完整代码,这里只讲解基本基本思路。环信登录优两种方法,一种是通过实例化new Easemob.im.Connection()建立连接,一种是使用工具类Easemob.im.Helper.login2UserGrid(options),我们刚刚注册就是使用了工具类,为了便于大家后面的学习,我们在这里把两种方法都说一下:
实例化new Easemob.im.Connection()建立连接
1.创建连接
var conn = new Easemob.im.Connection();2.初始化连接
conn.init({3.初始化连接
onOpened : function() {
alert("成功登录");
conn.setPresence();
}
});
// 打开连接
conn.open({
user : username,
pwd : password,
appKey : Easemob.im.config.appkey
});
这里我们需要注意的是open()方法中需要配置的属性是user和pwd,这和我们注册时的有区别,要注意哦!
这里需要说明的是init()是环信提供的一个通用的方法,比如后面我们要用到的接收文本消息、图片消息等一系列的回调方法都写在这个里面,onOpened()方法主要是用于当执行conn.open()方法时需要执行的方法,我们一般会把页面需要初始化的逻辑写在onOpened()中,比如查询好友。
完整代码:
// 输入参数工具类Easemob.im.Helper.login2UserGrid(options)建立连接
var loginConfig = {
username: mui("#username")[0],
password: mui("#password")[0]
};
// 创建一个新的连接
var conn = new Easemob.im.Connection();
// 初始化连接
conn.init({
onOpened : function() {
mui.toast("成功登录");
conn.setPresence();
mui.openWindow({
url: 'html/tab-webview-main.html',
extras:{
username:loginConfig.username.value,
password:loginConfig.password.value
}
})
}
});
// 登录事件监听
mui("#login")[0].addEventListener('tap',function(){
var username = loginConfig.username.value;
var password = loginConfig.password.value;
// 电话号码校验
if (!isMobile(username)){
mui.toast("电话号码格式不正确");
return;
}
// 密码非空校验
if (!isEmpty(password)){
mui.toast('密码不能为空');
return;
}
// 打开连接
conn.open({
user : username,
pwd : password,
appKey : Easemob.im.config.appkey
});
});
// 登录
var options = {
user : username,
pwd : password,
appKey : Easemob.im.config.appkey,
success:function(data){
console.log(JSON.stringify(data))
mui.toast("成功登录");
mui.openWindow({
url: 'html/tab-webview-main.html',
extras:{
username:loginConfig.username.value,
password:loginConfig.password.value
}
})
},
error: function(e){
console.log(JSON.stringify(e))
mui.toast("成功失败:"+e);
}
};
Easemob.im.Helper.login2UserGrid(options);
上面我们用了两种方法讲解了登录的方法,各有优劣,第二种只做登录的工作,代码也比较简洁,但是当我们的页面是多个页面时我们的登录状态是不能检测到的,这个时候我们还是需要在每个页面通过创建连接初始化,所以我们在页面跳转过程加入了拓展参数extras传递参数,然后在登陆后的页面接收就可以。
3.页面传参深入探究
为了尽可能简单的演示我们的功能,我这里不使用个性化的设计,就用官方模板组中的【mui底部选项卡(webview模式)】进行展示。新建模板文件如下:
我们去掉第一个选项卡,只保留消息tab-webview-subpage-chat.html、通讯录tab-webview-subpage-contact.html、设置tab-webview-subpage-setting.html三个选项卡。
拓展参数extras传值
上一小节中,我们在登陆页面通过拓展参数extras传值,在主页面接收数据的方法为:
mui.plusReady(function(){在主界面mui.plusReady方法里面拿到值,然后可以在创建子webview时候用拓展参数传值,然后在子页用下面的方法用同样的方法可以拿到值。但是其实我们不需要父页面向子页面发消息,直接在子页面通过这个找到父页面对象就OK了,如下:
var self = plus.webview.currentWebview();
var username = self.username;
var password = self.password;
mui.toast("username:"+username+"<br />"+"password:"+password);
});
子页面代码:
mui.plusReady(function(){预加载时使用mui.fire()传值
var self = plus.webview.currentWebview().parent();
var username = self.username;
var password = self.password;
console.log("username:"+username+"password:"+password);
});
这里需要特别说明一下的是我们有时候想要预加载我们的主页面,这里我们有个地方我需要特别注意的是,我们需要用mui.fire()传递参数:
mui.fire(target,event,data)
特别提醒一下:target是需要接受参数的webview对象,而不是id,在这个地方我出过错误,当时一直没有察觉,如果是id,需要使用plus.webview.getWebviewById(id)进行转换。
比如我们在登陆页面使用preload预加载,代码如下:
...登陆按钮监听事件中的success方法:
var mainPage = null;
mui.plusReady(function(){
mainPage = mui.preload({
"url": 'html/tab-webview-main.html',
"id": 'main'
});
})
...
mui.fire(mainPage,'show',{在主页面中通过自定义show事件获得参数:
username:loginConfig.username.value,
password:loginConfig.password.value
});
setTimeout(function() {
mui.openWindow({
id: 'main',
show: {
aniShow: 'pop-in'
},
waiting: {
autoShow: false
}
});
}, 0);
var username=null,password=null;
// 页面传参数事件监听
window.addEventListener('show',function(event){
// 获得事件参数
username = event.detail.username;
password = event.detail.password;
console.log("username:"+username+"password:"+password);
});
我们需要注意的是我们刚刚在登录页面的账号密码传递到了tab-webview-main.html主页面,但是我们的每个子页面没有拿到账号密码。这里就有个容易犯错的地方,我们可能会直接在创建子webview时候通过拓展参数extras传值。
经过试验发现经过预加载的主界面tab-webview-main.html的mui.plusReady方法比页面的自定义事件监听先执行,这是因为我们通过预加载的时候其实已经就执行了mui.plusReady方法,而自定义事件是在webview打开的时候执行。当主界面被预加载时,子页面的loaded事件也随着完成,创建子页面的时候我们根本就没有拿到数据怎么传,自然在子页得到的是undefined。我们这个时候如果想在主界面生成子页面的时候通过拓展参数extras传递给子页面根本行不通!
当需要接受参数的webview已经完成loaded事件,我们就不能使用拓展参数extras传参数,这个时候我们可以使用webview.evalJS()或者mui.fire();另外我们使用webview.evalJS()或者mui.fire()时,接收参数的页面的loaded事件也必须发生才能使用。
mui传参数只能相互关联的两个webview之间传,比如A页面打开B页面,B页面打开C页面,A页面可以传值给B页面,但是A页面不能传值给C页面,我们可以通过B页面传给C页面。
验证一个webview的loaded事件是否完成的方法:
var ws = plus.webview.getWebviewById(id)验证一个webview的show事件是否完成的方法:
ws.addEventListener( "loaded", function(e){
console.log( "Loaded: "+e.target.getURL() );
}, false );
var ws=plus.webview.currentWebview();说这两个监听事件有啥用处呢,我们在预加载webview的时候,预加载完成的过程,loaded事件也随之完成,但是只有页面被打开时,show事件才完成,我们可以选择合适的时机发送或者接受参数。
ws.addEventListener("show", function(e){
console.log( "Webview Showed" );
}, false );
这里需要说明的是如果你想localstorage、Storage等本地存储传值,完全可以不用extras或者mui.fire(),当然还可以用url传参数。
因为当初就是为了一个想法,预加载试试,然后试着试着各种问题,不过也因此明白了很多规则和调试方法,在这里提出来顺便总结一下页面传参需要注意的问题,免得新手在此花了很多冤枉时间,搞得现在都快忘了前面写了啥。其实这一部分可以独立出来,但是总感觉这种东西不是啥难事,脱离实际去讲总觉得不合适。
4.获取好友列表及添加好友
获取好友列表
我们在登陆页面与环信的服务器建立了联系,但是由于我们执行跳转了,我们依然还需要在需要请求数据时候在当前页面再次建立连接,前面我们讲到可以通过实例化new Easemob.im.Connection()建立连接,我们这里可以在当前页面实例化建立连接,而不是使用登录时的登陆工具类。实例化new Easemob.im.Connection()的三个步骤大家可以查看前面的内容,这里需要说明的是我们获取好友列表是在conn.init方法的onOpened : function(){}; 中添加 getRoster 回调方法,从而获取好友列表。
// 创建连接很显然我们在执行后是空的,因为从开始到现在我们都是自己和自己玩,都没有找朋友,那下面我们就去找朋友,之所以先要把这个先写出来,因为这个我觉得是基本逻辑,你待会儿加了好友,怎么看,就通过这里查询,然后才能说后面的聊天。
var conn = new Easemob.im.Connection();
// 初始化连接
conn.init({
onOpened : function(){
// mui.toast("成功登录");
conn.setPresence(); //设置在线状态
conn.getRoster({
success : function(roster) {
console.log(JSON.stringify(roster))
// 获取当前登录人的好友列表
for ( var i in roster) {
var ros = roster[i]; //好友的对象
//ros.name为好友名称
}
}
});
}
});
mui.plusReady(function(){
var self = plus.webview.currentWebview().parent();
var username = self.username;
var password = self.password;
console.log("username:"+username+"password:"+password);
// 打开连接
conn.open({
user : username,
pwd : password,
appKey : Easemob.im.config.appkey
});
});[/i]
添加好友
首先我们得去邀请对方吧,那么我们得知道对方的号码吧,上面我们用的是手机号码作为用户名,为的就是保证用户ID唯一性。
邀请发起方:
我们通过执行conn.subscribe可以发起邀请,添加发起方,获取要添加好友名称,参数为:
{这里我们在头部右上角叫一个添加好友按钮:
to: user, //对方用户名
message:"加个好友呗" //对方收到的消息
}
<button id="addfriend" class="mui-btn mui-btn-blue mui-btn-link mui-pull-right">添加</button>为了简单演示,我们直接弹出一个输入对话框:
// 添加好友
mui("#addfriend")[0].addEventListener('tap',function(e){
e.detail.gesture.preventDefault();
var btnArray = ['确定','取消'];
mui.prompt('请输入你要添加的好友的用户名:', '手机号', '邀请好友', btnArray, function(e) {
if (e.index == 0) {
var user = e.value;
conn.subscribe({
to : user,
message : "加个好友呗"
});
mui.toast('邀请发送成功!');
} else {
mui.toast('你取消了发送!');
}
});
})
需要说明的是如果添加好友是一个单独的页面,或者说所在页面没有和环信建立连接,依然还有进行前面说的三步连接。
邀请接受方:
被添加方,在 con.init 方法中调用 handlePresence 回调方法。
conn.init({前面登陆注册一直很顺利,没啥问题,但是做这个请求好友的时候就出问题了,我们在发送好友请求的时候,然后切换账号登陆的时候接受不到消息。调了好久才发现一些问题:
//收到联系人订阅请求的回调方法
onPresence : function(message) {
handlePresence(message);
}
});
//easemobwebim-sdk中收到联系人订阅请求的处理方法,具体的type值所对应的值请参考xmpp协议规范
var handlePresence = function(e) {
mui.toast(JSON.stringify(e));
var user = e.from;
//(发送者希望订阅接收者的出席信息)
if (e.type == 'subscribe') {
mui.confirm('有人要添加你为好友', '添加好友', ['确定','取消'], function(e){
if (e.index == 0) {
//同意添加好友操作的实现方法
conn.subscribed({
to : user,
message : "[resp:true]"
});
mui.toast('你同意添加好友请求');
} else {
//拒绝添加好友的方法处理
conn.unsubscribed({
to : user,
message : "rejectAddFriend"
});
mui.toast('你拒绝了添加好友');
}
})
}
};
我们发送好友的消息在主界面,所以我初始化了连接,接受消息的在子页面也初始化了连接,居然有时候会有提示onflict,有两种方法:第一,主界面不做任何请求的事,点击添加好友时候,父页面给子页面发消息,然后子页面执行请求添加好友;第二,所有的初始化请求放在主界面,然后收到消息给对应的子页面发消息,为了减少请求,个人采用第二种方法。
当解决上面的冲突问题,为什么登录后收不到消息?这里有个略坑的是环信文档中查询好友时候把onOpened中的这句conn.setPresence();屏蔽了,然后就收不到消息。查文档 常见问题 中说:
登录之后需要设置在线状态,才能收到消息。请检查登录成功后是否调用过 conn.setPresence();。
加上果然没问题了。。。
剩下的功能我们主要看这个文档 初始化连接,主要是说明了初始化时候的一些回调函数的基本用法,我们这里先来看看onPresence,这个是收到联系人订阅请求的回调方法,基本数据类型如下:
{
"from":"xxxxxxxxxxx",
"to":"yyyyyyyyyyy",
"fromJid":"jszblog#musicbox_xxxxxxxxxxx@easemob.com",
"toJid":"jszblog#musicbox_yyyyyyyyyyy@easemob.com",
"type":"subscribe",
"chatroom":false,
"destroy":false,
"status":"加个好友呗"
}
这里的xxxxxxxxxxx和yyyyyyyyyyy是电话号码,以为我是用电话作为用户名的,出于隐私保护用字母代替。
当我们切换账号会发现查询好友的地方可以查到好友,下面我们就进行好友列表展示,然后就是和好友聊天咯。
5.数据绑定和本地缓存处理机制
当我们重新登录的时候打印roster时会得到下面的json对象:
[{为了考虑如果用户没有联网或者数据不能及时更新也能够正常看到历史记录,这里我们考虑做缓存,由于环信web im不具备缓存功能,所以我们这里采用本地存储作为缓存的方案,本地存储可以使用5+中的storage模块,也可以使用localStorage、sessionStorage,由于storage模块中的数据有效域不同,可在应用内跨域操作,数据存储期是持久化的,并且没有容量限制,这里我们采用这个方案,至于如果想把本案例中的例子用于浏览器端的同志,可以采用localStorage作缓存功能。
"subscription":"from",
"jid":"jszblog#musicbox_xxxxxxxxxxx@easemob.com",
"name":"xxxxxxxxxxx",
"groups":[]
}]
html5+中的storage模块比较简单,文档中介绍了几个基本方法,具体看看文档就可以学会使用,文档见 【storage】。
plus.storage.setItem(key, value);plus.storage.setItem在存储时是以key-value的形式存储,我们可以在查询到好友信息时候,将对象转换成字符串存储在本地,JSON.stringify()将json对象转换成json字符串。
plus.storage.setItem("roster",JSON.stringify(roster));
plus.storage.getItem(key);我们在子页面通过plus.storage.getItem获取存储的字符串,然后通过JSON.parse()将字符串转化成对象获取相关信息。
var roster = plus.storage.getItem("roster");我们现在要做的无非是将信息展示出来,但是这里有用的信息目前只有name,毕竟没有上传文件,所以也不存在头像、昵称、签名这种个性化信息。如何把json信息展示出来前面的文章中我们是使用直接生成dom节点或者拼接html字符串,但是这种过于繁琐,当然也有人使用【js模板引擎】,本来准备早点在文章中给一些新手介绍一下vue.js这种MV-*框架,但是考虑本文中实例的性能,暂且还是用之前用过的一个js模板引擎artTemplate,文档戳这里:https://github.com/aui/artTemplate。
var obj = JSON.parse(roster);
for(var i in obj){
console.log(obj.name);
}
artTemplate有简洁语法版和原生语法版,就是使用语法不一样而已,这里我使用简洁语法版,戳这里下载—— 下载地址
为了简单,我们采用模板中通讯录的html结构,文档中有这样的一个例子:
编写模板:
使用一个type=”text/html”的script标签存放模板:
<script id="test" type="text/html">渲染模板:
<h1>{{title}}</h1>
<ul>
{{each list as value i}}
<li>索引 {{i + 1}} :{{value}}</li>
{{/each}}
</ul>
</script>
var data = {具体语法参考这里:artTemplate 简洁版语法
title: '标签',
list: ['文艺', '博客', '摄影', '电影', '民谣', '旅行', '吉他']
};
var html = template('test', data);
document.getElementById('content').innerHTML = html;
我们可以这样写:
...我们其实可以直接先遍历找到name然后填充就ok,这为了后续
<div class="mui-content">
<!--内容-->
<ul id="roster-cnt" class="mui-table-view mui-table-view-striped mui-table-view-condensed"></ul>
</div>
<!--模板-->
<script id="roster-tpl" type="text/html">
{{each roster as value index}}
<li class="mui-table-view-cell" data-chatname="{{value.name}}">
<div class="mui-slider-cell">
<div class="oa-contact-cell mui-table">
<div class="oa-contact-avatar mui-table-cell">
<img src="http://placehold.it/60x60" />
</div>
<div class="oa-contact-content mui-table-cell">
<div class="mui-clearfix">
<h4 class="oa-contact-name">小青年</h4>
<span class="oa-contact-position mui-h6">湖北</span>
</div>
<p class="oa-contact-email mui-h6">
{{value.name}}
</p>
</div>
</div>
</div>
</li>
{{/each}}
</script>
...
mui.plusReady(function(){
var roster = plus.storage.getItem("roster");
// console.log(roster);
var data = {
roster: JSON.parse(roster)
}
var html = template('roster-tpl', data);
document.getElementById('roster-cnt').innerHTML = html;
})
方便添加昵称、地址、头像等个性化地址,直接使用artTemplate的each方法。
6.聊天消息封装
当我们完成了前面登陆、注册、添加好友等功能,我们就进行最重要的内容了,既然是聊天功能,当然要聊起来,不然就不叫IM,但是很多人一开始就太过于关注聊天这个功能,而忽略了前面的基础过程,导致对api不熟悉,自然些聊天过程也是漏洞百出,代码逻辑混乱,所以也就放弃了。本文为即时通讯第一篇,没有介绍过多原理,也没有介绍聊天过程的高级功能,仅作为新手入门的基础篇介绍,后面会再深入探究更多内容。废话不多说,我们继续看文档写下面的内容。
我们先新建一个single-chat.html,本文不打算基于html mui中的页面去构建聊天页面,打算从零开始写。
首先我们需要在刚刚那个通讯录页面里面点击进入聊天页面,将用户名的值传到聊天页面,我们可以直接在创建的时候用拓展参数传,或者预加载打开时用mui.fire(),不多说,自己参考第三小节。
我们先说说布局的问题,先上图
对应的布局详细代码如下:
<style>我们的消息分为发送和收到两种情况,上面是静态效果,我们下面需要做的事获取数据然后动态展示,现在我们先封装一下页面展示效果的代码。这里我们使用两种方法,一种是直接用js生成dom节点,这种使用于结构固定后面不需要改动的,直接用一个js function封装,每次调用一行代码就可以直接显示内容,这样想想都觉得很棒。
.chat-history-date{
display: block;
padding-top: 5px;
text-align: center;
font-size: 12px;
}
.chat-receiver,.chat-sender{
margin: 5px;
clear:both;
}
.chat-avatar img{
width: 40px;
height: 40px;
border-radius: 50%;
}
.chat-receiver .chat-avatar{
float: left;
}
.chat-sender .chat-avatar{
float: right;
}
.chat-content{
position: relative;
max-width: 60%;
min-height: 20px;
margin: 0 10px 10px 10px;
padding: 10px;
font-size:15px;
border-radius:7px;
}
.chat-content img{
width: 100%;
}
.chat-receiver .chat-content{
float: left;
color: #383838;
background-color: #f5f5f5;
}
.chat-sender .chat-content{
float:right;
color: #ffffff;
background-color: #15b5e9;
}
.chat-triangle{
position: absolute;
top:6px;
width:0px;
height:0px;
border-width:8px;
border-style:solid;
}
.chat-receiver .chat-triangle{
left:-16px;
border-color:transparent #f5f5f5 transparent transparent;
}
.chat-sender .chat-triangle{
right:-16px;
border-color:transparent transparent transparent #15b5e9;
}
</style>
<!--消息最后历史时间-->
<p class="chat-history-date">01:59</p>
<!--接收文本消息-->
<div class="chat-receiver">
<div class="chat-avatar">
<img src="../img/chat-1.png">
</div>
<div class="chat-content">
<div class="chat-triangle"></div>
<span>如果是接受消息,请使用.chat-receiver类,如果是发送消息,请使用.chat-sender,头像是.chat-avatar类,内容是.chat-content类。.chat-content下如果是span标签则为文本消息,若为img标签则为图片消息。</span>
</div>
</div>
<!--发送文本消息-->
<div class="chat-sender">
<div class="chat-avatar">
<img src="../img/chat-2.png">
</div>
<div class="chat-content">
<div class="chat-triangle"></div>
<span>如果你要修改聊天气泡的背景颜色,请修改.chat-content的background-color和.chat-triangle的border-color</span>
</div>
</div>
<!--发送图片消息-->
<div class="chat-sender">
<div class="chat-avatar">
<img src="../img/chat-2.png">
</div>
<div class="chat-content">
<div class="chat-triangle"></div>
<img src="../img/test.jpg"/>
</div>
</div>
老司机,别说话,快看代码!
/**其实后面我们拓展也很容易的,只需要不断加type类型就ok,这些都是dom操作的基本方法,如果对一些方法不熟悉,建议看看相关的内容。这里遵照JSDoc+规范还加上了使用参数提示,在hbuilder使用可以查看参数含义,再也不用担心写代码时忘记了参数含义。
* @description 显示消息
* @param {String} who 消息来源,可选参数: {params} 'sender','receiver'
* @param {Object} type 消息类型,可选参数: {params} 'text','url','img'
* @param {JSON} data 消息数据,可选参数: {params} {{el:'消息容器选择器'},{senderAvatar:'发送者头像地址'},{receiverAvatar:'接收者头像地址'},{msg:'消息内容'}}
* ('text'和'url'类型的msg是文字,img类型的msg是img地址)
*/
var appendMsg = function(who,type,data) {
// 生成节点
var domCreat = function(node){
return document.createElement(node)
};
// 基本节点
var msgItem = domCreat("div"),
avatarBox = domCreat("div"),
contentBox = domCreat("div"),
avatar = domCreat("img"),
triangle = domCreat("div");
// 头像节点
avatarBox.className="chat-avatar";
avatar.src = (who=="sender")?data.senderAvatar:data.receiverAvatar;
avatarBox.appendChild(avatar);
// 内容节点
contentBox.className="chat-content";
triangle.className="chat-triangle";
contentBox.appendChild(triangle);
// 消息类型
switch (type){
case "text":
var msgTextNode = domCreat("span");
var textnode=document.createTextNode(data.msg);
msgTextNode.appendChild(textnode);
contentBox.appendChild(msgTextNode);
break;
case "url":
var msgUrlNode = domCreat("a");
var textnode=document.createTextNode(data.msg);
if(data.indexOf('http://') < 0){
data.msg = "http://" + data.msg;
}
msgUrlNode.setAttribute("href",data.msg);
msgUrlNode.appendChild(textnode);
contentBox.appendChild(msgUrlNode);
break;
case "img":
var msgImgNode = domCreat("img");
msgImgNode.src = data.msg;
contentBox.appendChild(msgImgNode);
break;
default:
break;
}
// 节点连接
msgItem.className="chat-"+who;
msgItem.appendChild(avatarBox);
msgItem.appendChild(contentBox);
document.querySelector(data.el).appendChild(msgItem);
}
这里我们也可以用模板引擎的办法去封装,代码如下:
模板内容:
<script id="msg-tpl" type="text/html">模板渲染:
<div class="chat-{{who}}">
<div class="chat-avatar">
<img src="{{avatar}}">
</div>
<div class="chat-content">
<div class="chat-triangle"></div>
{{if type=="text"}}
<span>{{msg}}</span>
{{else if type=="url"}}
<a href="{{msg}}">{{msg}}</a>
{{else if type=="img"}}
<img src="{{msg}}"/>
{{/if}}
</div>
</div>
</script>
/**大家使用也很简单,调用方法如下:
* @description 显示消息
* @param {String} who 消息来源,可选参数: {params} 'sender','receiver'
* @param {Object} type 消息类型,可选参数: {params} 'text','url','img'
* @param {JSON} data 消息数据,可选参数: {params} {{el:'消息容器选择器'},{senderAvatar:'发送者头像地址'},{receiverAvatar:'接收者头像地址'},{msg:'消息内容'}}
* ('text'和'url'类型的msg是文字,img类型的msg是img地址)
*/
var appendMsg = function(who,type,data){
var html = template('msg-tpl', {
who: who,
type: type,
avatar: who=='sender'?data.senderAvatar:data.receiverAvatar,
msg: data.msg
});
document.querySelector(data.el).innerHTML += html;
}
appendMsg('sender','text',{如果大家觉得每次调用还要填写容器id,头像地址这种基本固定的内容很麻烦,大家也可以继续封装:
el: '#msg-list', //消息容器
senderAvatar: '../img/chat-1.png', //发送者头像
receiverAvatar: '../img/chat-2.png', //接收者头像
msg: '你好' //消息内容
})
/**调用方法很简单:
* 消息初始化
*/
var msgInit = {
el: '#msg-list', //消息容器
senderAvatar: '../img/chat-1.png', //发送者头像
receiverAvatar: '../img/chat-2.png', //接收者头像
}
/**
* @description 展示消息精简版
* @param {String} who 消息来源,可选参数: {params} 'sender','receiver'
* @param {Object} type 消息类型,可选参数: {params} 'text','url','img'
* @param {Object} msg ('text'和'url'类型的msg是文字,img类型的msg是img地址)
*/
var msgShow = function(who,type,msg){
appendMsg(who,type,{
el: msgInit.el,
senderAvatar: msgInit.senderAvatar,
receiverAvatar: msgInit.receiverAvatar,
msg: msg
});
}
msgShow('sender','text','你好');两种方法实现封装的函数一样,这里只是给大家演示一下对于这种动态结构的html的一些方法,当然只要你愿意,你可以直接用字符串拼接,或者用<template></template>标签自己做一个这样的模板引擎,或者使用使用更加方便的mvc或mvvm框架。
之所以要花大篇幅内容将这些基础内容,是因为看到很多人代码写得那叫一个混乱,如果接口啥的一改,我相信这些人会疯掉,因为代码缺乏一定的通用性,没有把变与不变的内容分别拿出来。当然我们上面其实有些东西没有封装进去,比如用户名或者昵称,这在群聊中是有必要的,这里只是以最简单的例子来说明,大家可以根据自己的业务需求自由发挥。
7.单聊之文本消息
基本思路
其实写到这里本篇基本也算告一段落,但是考虑到很多新手对于收发消息很多还是有一些问题,我们这里就还是把文本消息发送接收写完了再收篇。
上面我们我们讲了怎么把消息展示出来,但是毕竟聊起来数据是动态的,那么发送接收数据是很重要的一步,先来写发送消息。我们先定义一个底部的输入框加按钮,代码如下:
<style type="text/css">为了代码整洁规范,方便后期封装,参考hello mui中im-chat.html的写法,我们先定义一下ui控件对象:
footer {
position: fixed;
width: 100%;
height: 50px;
min-height: 50px;
border-top: solid 1px #bbb;
left: 0px;
bottom: 0px;
overflow: hidden;
padding: 0px 50px;
background-color: #fafafa;
}
.footer-left {
position: absolute;
width: 50px;
height: 50px;
left: 0px;
bottom: 0px;
text-align: center;
vertical-align: middle;
line-height: 100%;
padding: 12px 4px;
}
.footer-right {
position: absolute;
width: 50px;
height: 50px;
right: 0px;
bottom: 0px;
text-align: center;
vertical-align: middle;
line-height: 100%;
padding: 12px 5px;
display: inline-block;
}
.footer-center {
height: 100%;
padding: 5px 0px;
}
.footer-center [class*=input] {
width: 100%;
height: 100%;
border-radius: 5px;
}
.footer-center .input-text {
background: #fff;
border: solid 1px #ddd;
padding: 10px !important;
font-size: 16px !important;
line-height: 18px !important;
font-family: verdana !important;
overflow: hidden;
}
footer .mui-icon {
color: #000;
}
footer .mui-icon:active {
color: #007AFF !important;
}
.footer-right span{
color: #0062CC;
line-height: 30px;
}
</style>
<div class="mui-content">
<div id="msg-list"></div>
</div>
<footer>
<div class="footer-left">
<i id='msg-choose-img' class="mui-icon mui-icon-camera" style="font-size: 28px;"></i>
</div>
<div class="footer-center">
<textarea id='msg-text' type="text" class='input-text'></textarea>
</div>
<div class="footer-right">
<span id='msg-send-text'>发送</span>
</div>
</footer>
// UI控件对象发送文本消息很简单:
var ui = {
content: mui('.mui-content'[0]),
msgList: mui('#msg-list')[0],
footer: mui('footer')[0],
msgChooseImg: mui("#msg-choose-img")[0],
msgText: mui('#msg-text')[0],
msgSendText: mui('#msg-send-text')[0]
}
// 发送文本消息这里的msgTextFocus();和msgScrollTop();是封装的两个方法,具体的且看下文。
ui.msgSendText.addEventListener('tap',function(){
sendText();
})
// 发送文本
var sendText = function(){
var msg = ui.msgText.value.replace(new RegExp('\n', 'gm'), '<br/>');
var validateReg = /^\S+$/;
// 获得键盘焦点
msgTextFocus();
if(validateReg.test(msg)){
// 消息展示出来
msgShow('sender','text',msg);
// 发送文本消息到环信服务器
conn.sendTextMessage({
to: chatName, //用户登录名,SDK根据AppKey和domain组织jid,如easemob-demo#chatdemoui_**TEST**@easemob.com,中"to:TEST",下同
msg: msg, //文本消息
type: "chat"
//ext :{"extmsg":"extends messages"}//用户自扩展的消息内容(群聊用法相同)
});
// 清空文本框
ui.msgText.value = '';
// 恢复输入框高度(因为我们这里是50px,你可以写一个全局变量)
ui.footer.style.height = '50px';
// 保持输入状态
mui.trigger(ui.msgText, 'input', null);
// 这一句让内容滚动起来
msgScrollTop();
}else{
mui.toast("文本消息不能为空");
}
}
再来说说收消息,我们需要在conn.init()配置设置收到消息的回调函数onTextMessage:
// 初始化连接至此我们完成了基本的文本消息收发功能,但是有几个细节是需要处理的,比如我们上面说的两个函数啥意思,我们没有解释。
conn.init({
onOpened : function(){
//mui.toast("成功登录");
conn.setPresence();
},
// 收到文本消息时的回调函数
onTextMessage : function(message) {
// console.log(JSON.stringify(message));
var from = message.from;//消息的发送者
var msg = message.data;//文本消息体
//mui.toast(msg);
// 收到文本消息在页面展示
msgShow('receiver','text',msg);
msgScrollTop();
},
// 收到图片消息时的回调函数
onPictureMessage : function(message) {
handlePictureMessage(message);
}
});
获得输入框焦点事件和强制弹出软键盘
我们如果不做处理,在输入框失去焦点时软键盘会自动收回软键盘,这样很影响聊天时候的用户体验。这个时候我们可以在输入完内容,准备发送时,保持输入状态mui.trigger(ui.msgText, 'input', null);。
让输入框获得焦点的方法:
// 获得输入框键盘焦点强制弹出软键盘的方法:
var msgTextFocus = function(){
ui.msgText.focus();
setTimeout(function() {
ui.msgText.focus();
}, 150);
}
// 强制弹出软键盘聊天消息高度调整
var showKeyboard = function() {
if (mui.os.ios) {
var webView = plus.webview.currentWebview().nativeInstanceObject();
webView.plusCallMethod({
"setKeyboardDisplayRequiresUserAction": false
});
} else if(mui.os.android) {
var Context = plus.android.importClass("android.content.Context");
var InputMethodManager = plus.android.importClass("android.view.inputmethod.InputMethodManager");
var main = plus.android.runtimeMainActivity();
var imm = main.getSystemService(Context.INPUT_METHOD_SERVICE);
imm.toggleSoftInput(0,InputMethodManager.SHOW_FORCED);
}
};
聊天消息如何发送或者收到一条自己往上滚动呢?我们看qq消息就是最后一条消息就会自动出现在输入框之上,调整方法是使用scrollTop方法,通过计算scrollHeight和`offsetHeight的高度,实现调整。对这些高度不理解?看这里:
HTML 获取屏幕、浏览器、页面的高度宽度
深入理解高度。获取屏幕、webview、软键盘高度
其实这个地方有很多技术细节,比如消息高度虽然可以获取,但是要实现局部滚动,那么必须禁止浏览器默认的滚动模式,具体可以看看这篇文章的实现原理浅议内滚动布局
具体css样式设置方法:
html,调用的函数封装如下:
body {
height: 100%;
margin: 0px;
padding: 0px;
overflow: hidden;
-webkit-touch-callout: none;
-webkit-user-select: none;
}
.mui-content{
height: 100%;
padding: 44px 0px 50px 0px;
overflow: auto;
background-color: #eaeaea;
}
#msg-list {
height: 100%;
overflow: auto;
-webkit-overflow-scrolling: touch;
}
// 消息滚动输入框高度如何自适应
var msgScrollTop = function(){
ui.msgList.scrollTop = ui.msgList.scrollHeight + ui.msgList.offsetHeight;
}
不多说直接上代码:
// 输入框监听事件解决长按导致致键盘关闭的问题
ui.msgText.addEventListener('input', function(event) {
msgTextFocus();
ui.footer.style.height = this.scrollHeight + 'px';
});
// 解决长按“发送”按钮,导致键盘关闭的问题;
ui.msgSendText.addEventListener('touchstart', function(event) {
msgTextFocus();
event.preventDefault();
});
ui.msgSendText.addEventListener('touchmove', function(event) {
msgTextFocus();
event.preventDefault();
});
当做到这里我们基本要讲解的够新手去理解了,但是对于项目功能实现来说,远远不够,毕竟只是文字发送接收,那么图片、语音、地址等等高级功能呢,我们这篇文章限于篇幅不可能一一道来,只能后面再做补充。这里希望更多人参与到其中进行贡献。这里可以放出地址了,详情代码请关注这里:https://github.com/zhaomenghuan/mui-demo/tree/master/easemobIM。后期功能拓展和bug修复都贵提交到这里,欢迎大家贡献。
写在后面
由于这段时间确实有点忙,这篇文章也花了很多时间去码字,去修改,改了很多次,才有这篇文章,希望能够给新手一些启示和帮助吧!本文不是着重讲环信sdk怎么用,而是讲解这个过程中可能会遇到的一些问题和实现思路,所以不建议新手直接拿最后的代码改之类的,还是看懂了思路再说,所以至于这个IM更多的功能后期会不会继续开发,暂时是未知数,所以大家不要等待,欢迎大神多多贡献分享相关代码,这样方便更多人学习使用。
这里想和大家简单说下,不要所有的问题直接私聊我,我时间精力有限,个人觉得不回复不好,所以我不会看到消息装着没看到,但是也不可能一一去回复,毕竟时间精力上也有限,我也需要不断学习,所以不希望过多的被打扰。大家有问题建议去论坛先搜搜答案,看看官方文档自己解决,大家确实有解决不了的问题,可以在群里寻求帮助或者给我发消息。提问前建议把问题描述清楚,想要实现什么,然后现在的实现思路,最好附上代码说明或者发测试文件,这样也方便解决问题。很多人说直接是直接是根据官方demo改的,说啥bug
,很多时候聊到最后发现是他自己的原因,这种情况真的很浪费彼此时间。在此声明以后一上来直接要帮忙写代码的,或者让我发一遍文档地址的等等这种可以自己可以解决的问题,请原谅我直接果断拒绝,我理解新手理解小白初入门的盲目,但是建议不要去依赖,要自己去尝试,不懂再问,不要还没有去了解去查资料,直接一上来求带这种要求。
如果有项目需求,欢迎私聊。承接各种前端项目,同时如果有功能定制,代码优化等需求也可以商量,算发个小广告吧,毕竟我也要生活,要挣钱娶媳妇养家糊口。
写文章不容易,也许写这些代码就几十分钟的事,写一篇大家好接受的文章或许需要几天的酝酿,然后加上几天的码字,累并快乐着。如果文章对您有帮助请我喝杯咖啡吧!进行赞助的同学私信留下你们的联系方式,后期发文章会单独邮件通知,有开发的问题也可以私聊,有相关功能需求,可以考虑优先写文分享。在此特别感谢之前给予赞助的同学,名单有保存,后期在博客会有公示。
如果觉得觉得本文不错在github上给我点个赞呗!
https://github.com/zhaomenghuan/mui-demo/tree/master/example/easemobIM
作者:环信开发者小青年 收起阅读 »
pika新特性支持codis slot迁移
已经有越来越多的公司在线上使用pika. pika兼容redis绝大数接口,所以之前版本已经支持作为codis的server,替换redis,但是由于不支持codis的slot迁移命令,所以不能在线动态扩容。
现在pika的codis分支已经支持codis的slot迁移,目前slot 迁移已经在环信线上稳定运行, 每天承载数亿的访问量, 这个大feature 由环信云乾同学开发测试完成.
环信即时通讯云使用案例
背景介绍
pika在环信的即时通讯云平台系统中使用,用来存储消息内容和日志,目前在部分集群中数据量已经达到数TB,QPS在数十万级别。这些数据最早存储在mysql里面,但mysql性能有限,而且扩容不方便。我们评估了下codis,能解决我们在mysql里面面临的性能及扩容问题,在一段时间用codis存储这些数据。但随着数据量级增长,TB级别的数据存储在内存里面,成本过于昂贵。后来了解到pika数据存储在磁盘里面,而且兼容redis协议及命令,可以挂载在codis proxy后面做server,这样既保留codis proxy的高性能,又能解决TB级数据量的存储成本问题。但pika还不支持codis的slot迁移,当压力上涨时,扩容是个问题。通过研究codis及pika的实现,在pika上开发实现了codis的slot迁移。目前已在生产环境中使用,运行良好,考虑到该功能能帮助更多的人解决一些实际的问题,我们经过再三测试验证后向官方提交了PR
线上数据
支持slot迁移版本的pika已经在环信的生产环境中使用,获得了一些线上环境的数据:
- 当pika容量在500G时,全量同步一次的时间大概在7个小时左右(千M网卡)
- 运行slotsreload命令每秒钟处理key的数据在万级,所以上面针对过期数据使用支持slot迁移的方式是可行的,已经过线上环境验证。
- 在进行slot迁移时,每秒钟迁移的key数量在200~300之间,当数据量大时,迁移需要花费很长时间,但迁移期间不影响线上业务正常使用。
- codis多连接版本可以提升性能2~3倍,完全释放pika的性能
# slotmigrate [yes | no]slotmigrate : yes
部署成功后即可进行使用,和官方的codis没有任何不同,一个大的可以扩容的redis。当codis-server(pika)压力过大需要扩容的使用,按照官方的迁移方法进行slot迁移即可。带来好处 - codis-server集群可以在不停机的情况下进行水平扩容
- dbsize命令可以实时获取当前数据库(pika)中key的数量
$ redis-cli -h 127.0.0.1 -p 9221 config set slotmigrate yes$ redis-cli -h 127.0.0.1 -p 9221 config set slotmigrate no当关闭开关时,pika不支持slot迁移,和之前版本的pika没有区别(dbsize也不能实时获取);当打开开关时,支持slot迁移(dbsize能实时获取),但需要有以下几点注意: [list=1]
127.0.0.1:9221> slotsreload
$ 127.0.0.1:9221> slotsdel 1013 990额外开销如上面所说,pika支持slot迁移会使用更多一些的磁盘,性能上会有一些下降。如果对磁盘使用或者性能有很高的要求,则可以按照上面3针对过期数据的使用方式进行使用。同名不同类型的keypika支持同名的key有kv,hash,list,set, zset等5种类型,但如果要使用pika支持slot迁移,不要使用同名但不同类型的key,如不要:set test1 100后再lpush test1 a b c,产生同名不同类型的key。在支持slot迁移的pika中使用同名但不同类型的key,迁移的时候会丢失那些同名的key。codis多连接codis 2.0及之前版本proxy和后端server是使用的单连接,当后端server是redis时,这个性能还是很不错的,但当后面是pika、ssdb等磁盘数据库时,单连接严重限制了后端server的性能,需要让codis支持多连接,针对多连接这个问题的讨论见:https://github.com/CodisLabs/codis/pull/1007该讨论提供了两种解决方法: [list=1]
# Proxy connections number model with backend server: server/slot, server means only one connection between proxy and backend server, # slot means every slot has one connection between proxy and backend server, default is serverbackend_connection_model=server
总结
pika支持codis slot迁移版本带来了一些好处,如动态水平扩容,dbsize实时获取,也带来了一些开销,如磁盘和性能。但它提供了一个开关,一个供你在这之间可以进行权衡的开关;它同时为pika的水平扩展提供了一个选择,一个当你的数据量快速增长带来问题时无痛解决问题的选择。
有任何问题可以在pika讨论群(294254078)交流,也可以在官方github提issue,欢迎试用
本文作者:环信性能工程师张云乾 收起阅读 »
Android ios V3.2.3 SDK 已发布,SDK十余项更新,更加简洁易用,新增广告红包
Android V3.2.3 2016-12-29
新功能/优化:
- sdk提供aar及gradle方式集成,具体方法查看gradle方式导入aar
- 增加离线推送设置的相关接口,具体方法可查看EMPushManager API文档
- 为了使sdk更简洁易用,修改以及过时了一些api,具体修改查看3.2.3api修改,另外过时的api后续3-5个版本会进行删除
- 优化loadAllConversationsFromDB()方法,从联表查询改为从两个表分别查询,解决在个别乐视手机上执行很慢的问题
- 优化登录模块,减少登录失败的概率
- 鉴于市面上的手机基本都是armeabi-v7a及以上的架构,从这版本开始不再提供普通的armeabi架构的so,减少打包时app的体积
- 小额随机红包
- 增加广告红包(需要使用请单独联系商务)
- 商户后台增加广告红包配置、统计功能
- 商户后台增加修改密码功能
- 绑卡后的用户验证四要素改为验证二要素
- 发红包等页面增加点击空白区域收回键盘的功能
- 群成员列表索引增加常用姓氏以及汉字的支持
- 红包详情页领取人列表展示不全
- 华为P8手机密码框无法获取焦点
- 部分银行卡号输入正确,提示银行卡号不正确
- 红包祝福语有换行符显示不正确
- 修复Emoji表情显示乱码
- 修复商户自主配置红包最低限额错误
- 修复零钱明细显示顺序错误问题
- 新增:实时1v1音视频,设置了对方不在线发送离线推送的前提下,当对方不在线时返回回调,以便于用户自定义离线消息推送
- 更新:SDK支持bitcode
- 更新:SDK使用动态库
- 为了使SDK更简洁易用,过时的API会在后续3~5个版本进行删除
- 小额随机红包
- 商户后台增加修改密码功能
- 绑卡后的用户验证四要素改为验证二要素
- iOS和Android两端UI展示一致性
- 支付流程的优化
- SDK注册流程
- 去掉XIB
- 集成过程的参数检查
- 风险策略
- SDKToken注册失败的问题
- 发红包缺少参数的问题
- 修复Emoji表情显示乱码
- 修复支付密码可能误报出错
- 修复商户自主配置红包最低限额错误
- 修复零钱明细显示顺序错误问题
- 修改抢红包流程为依赖后端数据
- 修复支行信息返回为空时的文案
版本历史:Android SDK更新日志 ios SDK更新日志
下载地址:SDK下载 收起阅读 »
个推开发者说第一期:论前端技术实践之道
个推技术开放日第一期,以「前端」为主题,围绕前端开发实践,力邀3位技术大牛就前端的设计、开发、测试,以及跨平台等进行多维度分享和探讨。我们拒绝华而不实的背景和光环,让技术分享回归实践,只为开发者们带来纯粹的技术分享会。
「开发者说」是由个推主办,联合业内开发大牛致力于为开发者打造高品质分享会的系列技术沙龙。
活动议程
13:30-14:00 签到
14:00-14:30 主题分享《前端开发背后的力量—前端构建之路》
14:30-14:40 Q&A
14:40-15:10 主题分享《移动互联网产品中如何用好HTML5 》
15:10-15:20 Q&A
15:20-15:50 主题分享《JavaScript之面向对象那些事儿》
15:50-16:00 Q&A
16:00-16:30 自由交流
活动群二维码
合作和报名,随时联系:美玉,18600532085;Jack,17710330365
嘉宾介绍
姜季廷 个推前端首席架构师
资深前端工程师,开源项目活跃贡献者。曾做过全端开发,发起并完成“人才储备池”O2O平台,“同心网”等项目。拥有多年ASP,PHP,Angular JS,Node.js开发经验。2013年开始全面投入到 Angular JS 的研究中,目前全面负责个推前端云组件设计及Node JS中间衔接模式的研究与实践以及前端技术团队管理。
李德兴 APICloud技术负责人
APICloud终端引擎及相关核心模块的技术实现者。一直从事浏览器、JavaScript引擎及相关中间件技术,参与过多个基于浏览器的移动中间件引擎的开发工作。有丰富的App架构及开发经验。热爱新技术,熟悉HTML5及W3C规范,对HTML5及浏览器技术的实际应用有深入研究并付诸大量实践。
刘溟川码易CBO
北京航空航天大学硕士,精通java、c、c#、c++、html语言及平台开发语言,曾牵头完成奔驰、戴尔、搜狐等软件开发项目,后因业务需求转型商务合作,主要负责为企业提供权威、优质、高效的售前、项目管理及售后服务。
主办方
个推
个推是基于大数据的推送技术服务商,为App开发者提供推送技术服务,帮助有效提升产品活跃度,增加用户留存率。截止2016年11月,个推SDK累计接入安装量达128亿,覆盖独立终端16亿。以推送技术为基础,个推同时发展了大数据和移动营销业务,目前已成为基于大数据的移动互联网综合服务提供商。
联合主办
太库
太库科技创业发展有限公司作为一家专注于孵化器运营管理和科技创业企业培育的专业机构,致力于成为全球领先的创业生态系统的生力军。目前,太库在全球主要创新城市北京、上海、深圳、河北、硅谷、首尔、特拉维夫、德国等地建立孵化培育体系,帮助全球太库会员快速成长。太库将与全球的产、学、政、金、研 等领域的创新要素紧密合作,全程陪伴企业从创业第一步到每一步,真正帮助企业在创业者国度快速成长。
合作伙伴
APICloud
APICloud现有APP开发平台、APP定制平台两大业务满足企业的APP开发需求。APP开发平台简化移动应用开发技术,大幅降低APP开发周期和成本,帮助开发者快速实现APP的开发、测试、发布、管理和运营,目前开发平台上已累计创建应用50多万个。APP定制平台旨在为企业提供快速高效、高性价比的App定制服务。官方签约、保上线、便宜、快,四大亮点给企业客户带来可靠的承诺。
码易
码易是为开发者提供个人任务众包、微猎头、技术分享交流的平台。同时码易也为需求客户提供高质量的软件产品研发、软件技术孵化服务。码易现已为世界500强、政府、企事业机构和优秀创业公司在内的数百家客户,完成了千余个项目的高质量交付。
环信
环信成立于2013年4月,是一家企业级服务软件提供商,并于2016年荣膺“Gartner 2016 Cool Vendor”。产品包括国内上线最早规模最大的即时通讯云平台——环信即时通讯云,以及移动端最佳实践的全媒体智能云客服平台——环信移动客服。
特别支持
报名链接:http://www.huodongxing.com/event/7367567171200 收起阅读 »
环信获“2016最佳企业服务商”大奖,SaaS客服将迎来春天
环信荣获猎云网“2016年度最佳企业服务商”大奖
2015年作为中国企业级服务的元年,受益于互联网+国家战略的推动,人口红利消失以及消费升级的大市场背景下,包括CRM、SaaS客服、HR、协同办公等企业级服务赛道百花齐放百家争鸣,其中诞生了包括阿里钉钉、纷享销客、销售易、北森、环信等一批明星公司,融资金额屡创新高。2016年企业级服务在所谓“资本寒冬”的情况下,仍凸显为创投热点,资本的青睐从侧面印证了行业高速发展机会的到来。2016年全球范围内,根据网络公开数据显示,共计619起企业级服务获得融资,其中402起总融资额为1469亿元。数据显示,美国投资机构有40%的钱投在ToB的公司上,中国目前只有10%左右,因此国内企业级服务领域的冬天远未到来。
在企业级服务市场,SaaS是目前最火的一个领域,显然,资本对于这个市场的预估远远超过现在的空间,随着企业级SaaS服务市场的火爆,包括腾讯、阿里、网易等互联网巨头都已经进入,但相对于细分垂直领域BAT的流量和技术优势并不明显,对于创业公司而言,机会空间还将非常大,SaaS客服领域亟待爆发!
从行业细分来看:“客服”是CRM四大细分市场之一。而且,从2015年的数据显示,客服是CRM细分领域中最大的一个,占市场总额的37%。在北美,2015年客服软件市场采购总额高达96亿美元。这个市场中已经出现了数家“独角兽”SaaS公司包括Salesforce、Zendesk和Freshdesk。
从政策来看:SaaS业务需要IDC托管服务,对这一块国家有明确的政策监管,国外巨头企业进入中国市场有高政策门槛。同时,他们很难支撑在中国的落地,实事上Zendesk的中国客户整体使用体验因为访问缓慢问题而变得糟糕。所以中国企业有足够的时间和空间野蛮生长。
从用户体验来看:随着移动互联时代带来,社交媒体的渗透率不断上升,用户随时、随地、跨平台接触商家咨询获取服务不再有软硬件障碍,客服咨询也不再局限于售后支持,售前售中的咨询比例在稳步上升。随着消费升级,用户从价格敏感上升到品牌服务和用户体验敏感,有数据显示:1、71%的顾客因糟糕的客户服务而停止合作。2、坏的服务体验后,48%的客户会劝说他人不要购买。3、高收入人士和85-90后更容易对坏的体验耿耿于怀。4、吸引新顾客的成本是维系老客户的5倍。良好的客户体验越来越重要,“客户是上帝”正在被重新定义和加强。
从技术趋势来看:1、移动端的特性支持任何人、任何时间、任何地点都可以瞬间连接在线客服。环信移动客服在移动端领域的大中型客户以及超高的市场占有率就完美证明了市场的刚需。2、客户咨询在包括企业官网、APP、社交媒体、400电话等渠道蜂拥而至,环信全媒体客服接入技术可以支持商家一个工作台一键回复来自所有渠道咨询。3、由新技术、新行业和新消费习惯引爆的海量客服咨询需要新的技术手段解决,环信智能客服机器人将能够帮助解答80%的常见问题,极大节省成本提高效率。4、环信大数据技术全景用户画像深度挖掘用户需求,轻松玩转反向营销。5,国内首款基于人工智能和大数据挖掘的客户旅程透析产品"环信客户声音"能够帮助企业优化运营,提高跨渠道客服体验。
综上所述,SaaS客服的春天即将到来,你已经准备好了么? 收起阅读 »
软件测试员工作经验分享
1、手机app的测试:当前主要为android和ios两大阵营了。针对不同平台的功能、自动化框架、兼容性等都有很大的差别。而当今移动互联网的快速发展让app的测试人员需求大大的增加,同时应运而生的是类似于TestBird这样的第三方测试机构。
2、web测试:不仅仅是网站,包括web服务器的测试都属于web的测试;在移动互联网兴起前,这块是主流,不过现在该领域的需求还是很大的。
3、游戏测试:这里包括手游和网游。只要有游戏的地方就有游戏测试,这块的需求就不用怀疑了,对于喜欢游戏的朋友们来说,是一个不错的选择。
4、传统软件测试:这块在在移动互联网兴起前也属于主流测试,现在当然也占据这不小的需求,毕竟还是有很大一部分人是用电脑的。
5、网络设备测试:这块的测试应该主要是面向企业客户的,如路由器,防火墙等等。以华为、中兴为代表的企业部分业务都是在这块。
6、云产品的测试:这块主要说指云存储和云计算,典型的如现在的阿里云。应该来说,这方面的测试大部分还属于初级阶段,对于测试的需求还是比较大的。
小编再强调下,选择一个领域还是很重要的,建议自己能够选择一个打算长期发展的领域,这个对自己的业务积累以及职业发展都是很有帮助的(当然,这里并没有说哪个行业不好,还是看个人的兴趣)。
确定行业后,大家会看到涉及到的测试类型都差不多(比如:功能测试、性能测试、自动化测试等等)。一般情况下会先安排功能测试相关的任务,这个对熟悉业务很有帮助的(而且大部分的人员还一直在做这方面的测试),接着会再根据个人的擅长和意愿去安排性能测试或自动化测试。然后自己在某个领域去持续发展,成为该行业的测试专家或者走向管理岗位。
根据上图我们将测试领域的技术岗位归纳下吧!
app测试专家,自动化测试专家,测试开发工程师,性能测试专家,web测试专家,web性能测试专家,安全测试专家,游戏测试专家,网络软件业务测试专家,网络设备业务测试专家,云产品的测试专家(云计算、大数据分析、网络存储、服务器等等)。 收起阅读 »
IOS 2.2.9设置https only遇到无法登录的解决方案
SDK的老版本(版本范围为SDK2.1.5-2.2.3)中存在默认不使用https的设置。部分使用了范围内SDK版本的用户在升级到最新的SDK2.2.9时,设置https only的选项后会出现用户无法正常登录的问题。
这个问题可以升级到SDK2.2.9及以上版本,在SDK初始化时添加otherConfig:@{kSDKConfigUseHttps:@YES}的设置,具体的代码实现如下:
[[EaseMob sharedInstance] registerSDKWithAppKey:@"easemob-demo#chatdemoui" apnsCertName:@"chatdemoui" otherConfig:@{kSDKConfigUseHttps:@YES}];
上记代码仅未示例,具体在使用时需要将appkey等信息替换为自己的对应信息就可以了。
集成过程中遇到问题欢迎在IMGeek社区发帖咨询。 收起阅读 »
SaaS颠覆软件世界已成全球化现象
文章摘要:SaaS 初创公司已经遍及越来越多的国家,其全球化达到了怎样的水平呢?
澳大利亚、加拿大、以色列、中国、印度,SaaS 初创公司已经遍及越来越多的国家。下一代软件公司将来自于世界不同地区,其中一些或许价值数十亿美元,比如加拿大的 Shopify 和 Hootsuite、澳大利亚的 Atlassian、新西兰的 Xer。随着这些成功的初创公司的迅速发展,其早期筹资市场又是如何演变的呢?
根据 Crunchbase 数据,2010 年以来,美国的 SaaS 投资从每年 15 亿美元迅速增长到 70 亿美元,2016 年略有下降,环境或许是一个原因,此外这个结果也并非涵盖了所有数据。同一时期,非美国 SaaS 投资则增长了 6.3 倍,这对于 SaaS 创始人来说是一个非常振奋人心的数据。
分别来看不同国家的数据,可以发现在美国以外存在一定不稳定性,但是长期趋势是,在以上八个国家:巴西、加拿大、德国、西班牙、法国、应该、印度、以色列,SAAS 投资都呈增长态势。
A 轮投资数额中值随地域变化。在巴西,这个数值是 100 万美元,同时,以色列超过六美元,法国和德国情况也都类似。加拿大的 SaaS 环境从稳定性和投资周期上来看与美国最相似。产生波动的原因有很多,不同地区筹资环境不同,术语也有天壤之别。但是一定程度上,我们可以预期,数值会聚集。无论这家 SaaS 初创公司的名字是什么,在什么地区。
通过投资数额来观察这些数据可以发现,几乎每个地区的长期增长都有着更加振奋人心的趋势。再次声明,2016 年的数据并未全部包含。毫无疑问,SaaS 是一个全球趋势。
关于国际 SaaS 初创公司,令人激动的部分应该是它们能够识别到对自己地区来说最独特的机会,而目前还没有软件公司抓住了这些机会。有时这些独特的方法和解决方案可以发展到全球规模。
对于软件公司来说,这只是一个起步,2016 对于创始人来说是伟大的一年,融资环境越来越能满足他们的需求。美国依然是软件公司最大的筹资市场。
但是,我们可以看到越来越多的国际 SaaS 公司在自己国家内部进行首轮融资,之后在到美国进行融资。建立一个以美国为目标的进入市场团队,就能接触到更广阔的融资生态系统。
所以,SaaS 的确是一个全球现象,在很长一段时间里会继续颠覆软件世界。
原文作者:Tomasz Tunguz
翻译:徐婧欣 收起阅读 »
数人云1月Meetup上海|容器之Mesos/K8S/Swarm三国演义
Mesos/K8S/Swarm集群管理工具在容器生态圈里帮助企业客户排兵布阵,驰骋疆场,
呈现出三国鼎立的局面;
16年10月份北京场活动中IBM、新浪微博、Acttao、数人云的大牛们各抒己见(文末推荐4篇活动后期整理的文章),
在即将到来的17年,数人云Meetup重新起航啦,
1月7日上海、深圳"三国演义" 活动两城联动,
相信不一样的技术实践,
有着同样的精彩分享~
—— 文臣武将 ——
罗勇,携程云平台开发经理
主要负责携程云平台建设和维护,熟悉OpenStack,Docker ,Linux/Windows Container 等技术领域。
《Windows Container在携程的应用》
主要分享Linux/Windows Container在携程的应用场景,
Windows Container技术介绍包括:
传统应用的容器化迁移,容器网络模型选择、
与OpenStack/Mesos集成的取舍、监控方案等,
Windows Container在落地过程中我们遇到和解决的问题,
对未来的展望等。
王成昌,唯品会PaaS平台高级开发工程师
主要工作内容包括:平台DevOps方案流程优化,持续部署,平台日志收集,Docker以及Kubernetes研究。
《唯品会PaaS基于kubernetes的实践》
PaaS构建部署流程、架构;
基于kubernetes的网络方案定制;
PaaS日志收集及监控方案。
梁晓聪,哔哩哔哩弹幕网运维开发经理
曾就职猎豹移动任运维开发负责人。目前主要负责B站运维与运维技术栈建设,对于服务端微服务docker化,持续交付,集中式配置管理领域具有丰富经验。
《B站基于Mesos的弹性计算资源探索》
主要分享B站在2016年基于Mesos和Docker实现弹性计算资源的探索之路
我们的场景
解决什么问题
弹性计算资源探索的6个细节
未来的展望
谢乐冰,数人云COO
在德国工作十年,回国后加盟惠普电信运营商部门。 拥有多年项目经验和创业公司工作经验。在数人科技负责互联网数据搜集和处理,擅长技术应用领域,为电商、招聘、电信、互联网金融等行业提供服务。
《一款基于Mesos的分布式系统应用开发手记》
gPRC使用手记;
将Raft集成到Mesos调度器Swan中;
Actor模式和事件驱动。
—— 排兵布阵 ——
13:30 - 14:00 签到/微软嘉宾发言
14:00 - 14:40 《Windows Container在携程的应用》@携程
14:40 - 15:20 《一款基于Mesos的分布式系统应用开发手记》谢乐冰@数人云
15:20 - 16:00 《B站基于Mesos的弹性计算资源探索》梁晓聪@哔哩哔哩
16:00 - 16:40 《唯品会PaaS基于kubernetes的实践》王成昌@唯品会
16:40 - 17:00 自由交流
—— 安营扎寨 ——
时间:1月7日 14:00 - 17:00
地点:上海市虹桥路3号港汇中心2座10层
微软(中国)上海分公司
主办方/数人云
联合主办/微软
报名链接:http://www.huodongxing.com/event/8365715940700 收起阅读 »
数人云1月深圳Meetup|容器之Mesos/K8S/Swarm三国演义
容器正在成为企业级应用的新一代交付标准,
Mesos/K8S/Swarm集群管理工具在容器生态圈里帮助企业客户排兵布阵,驰骋疆场,
呈现出三国鼎立的局面;
16年10月份北京场活动中IBM、新浪微博、Acttao、数人云的大牛们各抒己见(文末推荐4篇活动后期整理的文章),
在即将到来的17年,数人云Meetup重新起航,
1月7日上海、深圳"三国演义" 活动两城联动,
相信不一样的技术实践,
有着同样的精彩分享
数人云1月Meetup上海站|容器之Mesos/K8S/Swarm三国演义
—— 文臣武将 ——
黄惠波,腾讯互娱高级工程师
目前主要负责游戏计算资源容器化平台的研发工作,包括kubernetes/docker研究以及定制化开发,主导腾讯游戏万级容器资源调度平台的建设工作。
《kubernetes在腾讯游戏的应用实践》
主要分享海量在线游戏场景下,基于kubernetes的容器资源调度的探索和应用实践。包括:
腾讯在线游戏的容器化应用场景;
基于kubernetes的调度方案和网络方案定制优化;
海量应用过程中遇到的问题与解决方案。
黄浩松,Apache Mesos PMC
目前就职于Shopee(东南亚社交电商平台),负责公司自动化运维平台建设,业余时间长期在Apache Hadoop / Apache HBase / Tensorflow 打酱油。
《Mesos Unified Containerizer及对Pod的支持》
Mesos Unified Containerizer的最新特性解读
王璞,数人云创始人&CEO
美国 George Mason 大学计算机博士。曾先后供职于 Google、Groupon 和 StumbleUpon 等硅谷互联网公司。擅长分布式计算、大规模机器学习、海量数据处理。曾担任 Google 广告部门数据平台构架师,负责管理每秒访问量全球最高的架构平台。
《SRE-分布式系统运维的DevOps实践》
来自Google的SRE理念
SRE与DevOps
SRE落地实践-开源Mesos调度器
神秘嘉宾,敬请期待~~
——排兵布阵——
13:30 - 14:00 签到
14:00 - 14:40 《SRE-分布式系统运维的DevOps实践》@王璞
14:40 - 15:20 《Mesos Unified Containerizer及对Pod的支持》@黄浩松
15:20 - 16:00 《kubernetes在腾讯游戏的应用实践》@黄惠波
16:00 - 16:40 神秘嘉宾
16:40 - 17:00 自由交流
——安营扎寨——
时间:1月7日 14:00 - 17:00
地点:深圳市南山区科园路18号北科大厦4002室
报名链接:http://www.huodongxing.com/event/4366729880600 收起阅读 »
IMGeek社区赞赏功能临时下线通知,请于12月31日前完成提现
根据中国人民银行制定的《非金融机构支付服务管理办法》,规定未经中国人民银行批准,任何非金融机构和个人不得从事或变相从事支付业务。
IMGeek作为移动开发者技术社区,接到了上级有关部门整改,根据第三方支付政策法规,不具备支付牌照,IMGeek配合停掉赞助功能使用,下次上线时间待通知。请各位小伙伴们于本周内(2016年12月31号前)将收到的赞赏提现到自己账户,逾期赞赏功能关闭,做清零处理。
社区赞赏提现教程提现教程
Thx! 收起阅读 »
环信React Native Demo发布,支持 JavaScript 和 React Native 开发
React Native Demo 介绍
React Native 可以让开发者使用 JavaScript 和 React Native 开发原生 iOS 和 Android 应用,提高开发效率(Learn once, write anywhere)。
React Native Demo 已集成环信 Web IM SDK,并提供即时通讯基本功能,开发者可以直接将该 Demo 集成到您的应用中,立即获得即时通讯的能力。后期将引入热加载功能,无需审核,直接发布。
GitHub 下载地址:https://github.com/easemob/webim-react-native
版本支持
React Native Demo 支持 iOS 9.0 以上版本,以及 Android 4.1 (API 16)。
注:所有开发调试环境均基于Mac。
功能
React Native Demo 分为 iOS Demo 和 Android Demo 两部分,已完成的功能如下。
iOS
iOS Demo 已完成功能:Android
• 登录
• 注册
• 好友
◦ 列表及筛选
◦ 好友信息展示
◦ 黑名单
◦ 删除好友
◦ 好友通知
◾ 添加好友通知展示
◾ 接受好友请求
◾ 拒绝好友请求
◾ 添加好友
• 群组
◦ 群组列表
◦ 群组成员列表
• 聊天
◦ 相机图片消息
◦ 本地图片消息
◦ emoji消息
◦ 普通消息
• 异常状态处理
◦ 断线退出到登录页
◦ 重复登录退出到登录页
Android Demo 已完成功能:目录结构
• 登录
• 注册
App 的目录结构如下:Redux State
• Containers: 容器 | 页面 | 路由
◦ App.js 总入口
◾ Redux/ 初始化
◾ I18n/ 初始化
◾ Config/index.js 系统初始配置
◦ RootContainer.js 根容器
◾ Navigation/NavigationRouter.js 初始化路由
◾ /Config/ReduxPersist 持久化初始化
• Components 常用组件
• I18n 多语言支持
• Images 图片资源
• Lib WebIM初始化
• Navigation: 路由相关
• Redux: actions / reducers
• Sdk: webim-easemobo SDK
{版本历史 :更新日志
// ui相关
ui: [
// ui通用:比如loading
common: {
fetching:false
},
login: {
username: '',
password: '',
isSigned: false,
},
register: { },
contactInfo: { },
],
im: ,
// 数据实体
entities: {
roster: {
byName: {
{
jid, name, subscription, groups?
}
},
names: ['lwz2'...],
// 好友列表在此,因为好友列表来源于roster,息息相关
friends: ,
},
// 订阅通知
subscribe: {
byFrom: {}
},
room: {},
group: {
byId: {},
names:
},
members: {
byName: ,
byGroupId:
}
blacklist: {},
message: {
byId: {}
chat: {
[chatId]: [messageId1, messageId2]
},
groupChat: {
[chatId]: {}
},
}
}
}
SDK下载:点击下载 收起阅读 »
Web IM V1.4.7已发布,优化手机浏览器后台重连
新功能:
[demo] 在demo.html中新增视频聊天及发送视频文件的功能
Bug修复:
[sdk] 解决在手机浏览器在后台运行时无法断线重连的问题
[demo] WebIM建群,等待后台建群成功后再拉取群信息并更新UI中的群列表
[demo] WebIM群加人,群主和被添加的群成员均可以收到通知
[demo] WebIM群主将群成员从黑名单移除后,不再回到群成员列表中,而直接被删除
webim体验:https://webim.easemob.com/
版本历史:更新日志
SDK下载:点击下载 收起阅读 »
李理:自动梯度求解——使用自动求导实现多层神经网络
作者:李理
目前就职于环信,即时通讯云平台和全媒体智能客服平台,在环信从事智能客服和智能机器人相关工作,致力于用深度学习来提高智能机器人的性能。
相关文章:
李理:从Image Caption Generation理解深度学习(part I)
李理:从Image Caption Generation理解深度学习(part II)
李理:从Image Caption Generation理解深度学习(part III)
李理:自动梯度求解 反向传播算法的另外一种视角
李理:自动梯度求解——cs231n的notes
接下来介绍一种非常重要的神经网络——卷积神经网络。这种神经网络在计算机视觉领域取得了重大的成功,而且在自然语言处理等其它领域也有很好的应用。深度学习受到大家的关注很大一个原因就是Alex等人实现的AlexNet(一种深度卷积神经网络)在LSVRC-2010 ImageNet这个比赛中取得了非常好的成绩。此后,卷积神经网络及其变种被广泛应用于各种图像相关任务。
这里主要参考了Neural Networks and Deep Learning和cs231n的课程来介绍CNN,两部分都会有理论和代码。前者会用theano来实现,而后者会使用我们前一部分介绍的自动梯度来实现。下面首先介绍Michael Nielsen的部分(其实主要是翻译,然后加一些我自己的理解)。
前面的话
如果读者自己尝试了上一部分的代码,调过3层和5层全连接的神经网络的参数,我们会发现神经网络的层数越多,参数(超参数)就越难调。但是如果参数调得好,深的网络的效果确实比较浅的好(这也是为什么我们要搞深度学习的原因)。所以深度学习有这样的说法:“三个 bound 不如一个 heuristic,三个 heuristic 不如一个trick”。以前搞机器学习就是feature engineering加调参,现在就剩下调参了。网络的结构,参数的初始化,learning_rate,迭代次数等等都会影响最终的结果。有兴趣的同学可以看看Michael Nielsen这个电子书的相应章节,cs231n的Github资源也有介绍,另外《Neural Networks: Tricks of the Trade》这本书,看名字就知道讲啥的了吧。
不过我们还是回到正题“卷积神经网络”吧。
CNN简介
在之前的章节我们使用了神经网络来解决手写数字识别(MNIST)的问题。我们使用了全连接的神经网络,也就是前一层的每一个神经元都会连接到后一层的每一个神经元,如果前一层有m个节点,后一层有n个,那么总共有m*n条边(连接)。连接方式如下图所示:
具体来讲,对于输入图片的每一个像素,我们把它的灰度值作为对应神经元的输入。对于28×28的图像来说,我们的网络有784个输入神经元。然后我们训练这个网络的weights和biases来使得它可以正确的预测对应的数字。
我们之前设计的神经网络工作的很好:在MNIST手写识别数据集上我们得到了超过98%的准确率。但是仔细想一想的话,使用全连接的网络来识别图像有一些奇怪。因为这样的网络结构没有考虑图像的空间结构。比如,它对于空间上很近或者很远的像素一样的对待。这些空间的概念【比如7字会出现某些像素在某个水平方向同时灰度值差不多,也就是上面的那一横】必须靠网络从训练数据中推测出来【但是如果训练数据不够而且图像没有做居中等归一化的话,如果训练数据的7的一横都出现在图像靠左的地方,而测试数据把7写到右下角,那么网络很可能学不到这样的特征】。那为什么我们不能设计一直网络结构考虑这些空间结构呢?这样的想法就是下面我们要讨论的CNN的思想。
这种神经网络利用了空间结构,因此非常适合用来做图片分类。这种结构训练也非常的快,因此也可以训练更“深”的网络。目前,图像识别大都使用深层的卷积神经网络及其变种。
卷积神经网络有3个基本的idea:局部感知域(Local Recpetive Field),权值共享和池化(Pooling)。下面我们来一个一个的介绍它们。
局部感知域
在前面图示的全连接的层里,输入是被描述成一列神经元。而在卷积网络里,我们把输入看成28×28方格的二维神经元,它的每一个神经元对应于图片在这个像素点的强度(灰度值),如下图所示:
和往常一样,我们把输入像素连接到隐藏层的神经元。但是我们这里不再把输入的每一个像素都连接到隐藏层的每一个神经元。与之不同,我们把很小的相临近的区域内的输入连接在一起。
更加具体的来讲,隐藏层的每一个神经元都会与输入层一个很小的区域(比如一个5×5的区域,也就是25个像素点)相连接。隐藏对于隐藏层的某一个神经元,连接如下图所示:
输入图像的这个区域叫做那个隐藏层神经元的局部感知域。这是输入像素的一个小窗口。每个连接都有一个可以学习的权重,此外还有一个bias。你可以把那个神经元想象成用来分析这个局部感知域的。
我们然后在整个输入图像上滑动这个局部感知域。对于每一个局部感知域,都有一个隐藏层的神经元与之对应。为了具体一点的展示,我们首先从最左上角的局部感知域开始:
然后我们向右滑动这个局部感知域:
以此类推,我们可以构建出第一个隐藏层。注意,如果我们的输入是28×28,并且使用5×5的局部关注域,那么隐藏层是24×24。因为我们只能向右和向下移动23个像素,再往下移动就会移出图像的边界了。【说明,后面我们会介绍padding和striding,从而让图像在经过这样一次卷积处理后尺寸可以不变小】
这里我们展示了一次向右/下移动一个像素。事实上,我们也可以使用一次移动不止一个像素【这个移动的值叫stride】。比如,我们可以一次向右/下移动两个像素。在这篇文章里,我们只使用stride为1来实验,但是请读者知道其他人可能会用不同的stride值。
共享权值
之前提到过每一个隐藏层的神经元有一个5×5的权值。这24×24个隐藏层对应的权值是相同的。也就是说,对于隐藏层的第j,k个神经元,输出如下:
σ(b+∑l=04∑m=04wl,maj+l,k+m)这里,σ是激活函数,可以是我们之前提到的sigmoid函数。b是共享的bias,Wl,m 是5×5的共享权值。ax,y 是输入在x,y的激活。
【从这个公式可以看出,权值是5×5的矩阵,不同的局部感知域使用这一个参数矩阵和bias】
这意味着这一个隐藏层的所有神经元都是检测同一个特征,只不过它们位于图片的不同位置而已。比如这组weights和bias是某个局部感知域学到的用来识别一个垂直的边。那么预测的时候不管这条边在哪个位置,它都会被某个对于的局部感知域检测到。更抽象一点,卷积网络能很好的适应图片的位置变化:把图片中的猫稍微移动一下位置,它仍然知道这是一只猫。
因为这个原因,我们有时把输入层到隐藏层的映射叫做特征映射(feature map)。我们把定义特征映射的权重叫做共享的权重(shared weights),bias叫做共享的bias(shared bais)。这组weights和bias定义了一个kernel或者filter。
上面描述的网络结构只能检测一种局部的特征。为了识别图片,我们需要更多的特征映射。隐藏一个完整的卷积神经网络会有很多不同的特征映射:
在上面的例子里,我们有3个特征映射。每个映射由一个5×5的weights和一个biase确定。因此这个网络能检测3种特征,不管这3个特征出现在图像的那个局部感知域里。
为了简化,上面之展示了3个特征映射。在实际使用的卷积神经网络中我们会使用非常多的特征映射。早期的一个卷积神经网络——LeNet-5,使用了6个特征映射,每一个都是5×5的局部感知域,来识别MNIST数字。因此上面的例子和LeNet-5很接近。后面我们开发的卷积层将使用20和40个特征映射。下面我们先看看模型学习到的一些特征:
这20个图片对应了20个不同的特征映射。每个映射是一个5×5的图像,对应于局部感知域的5×5个权重。颜色越白(浅)说明权值越小(一般都是负的),因此对应像素对于识别这个特征越不重要。颜色越深(黑)说明权值越大,对应的像素越重要。
那么我们可以从这些特征映射里得出什么结论呢?很显然这里包含了非随机的空间结构。这说明我们的网络学到了一些空间结构。但是,也很难说它具体学到了哪些特征。我们学到的不是一个 Gabor滤波器 的。事实上有很多研究工作尝试理解机器到底学到了什么样的特征。如果你感兴趣,可以参考Matthew Zeiler 和 Rob Fergus在2013年的论文 Visualizing and Understanding Convolutional Networks。
共享权重和bias的一大好处是它极大的减少了网络的参数数量。对于每一个特征映射,我们只需要 25=5×5 个权重,再加一个bias。因此一个特征映射只有26个参数。如果我们有20个特征映射,那么只有20×26=520个参数。如果我们使用全连接的神经网络结构,假设隐藏层有30个神经元(这并不算很多),那么就有784*30个权重参数,再加上30个bias,总共有23,550个参数。换句话说,全连接的网络比卷积网络的参数多了40倍。
当然,我们不能直接比较两种网络的参数,因为这两种模型有本质的区别。但是,凭直觉,由于卷积网络有平移不变的特性,为了达到相同的效果,它也可能使用更少的参数。由于参数变少,卷积网络的训练速度也更快,从而相同的计算资源我们可以训练更深的网络。
“卷积”神经网络是因为公式(1)里的运算叫做“卷积运算”。更加具体一点,我们可以把公式(1)里的求和写成卷积:$a^1 = \sigma(b + w * a^0)$。*在这里不是乘法,而是卷积运算。这里不会讨论卷积的细节,所以读者如果不懂也不要担心,这里只不过是为了解释卷积神经网络这个名字的由来。【建议感兴趣的读者参考colah的博客文章 《Understanding Convolutions》】
池化(Pooling)
除了上面的卷积层,卷积神经网络也包括池化层(pooling layers)。池化层一般都直接放在卷积层后面池化层的目的是简化从卷积层输出的信息。
更具体一点,一个池化层把卷积层的输出作为其输入并且输出一个更紧凑(condensed)的特征映射。比如,池化层的每一个神经元都提取了之前那个卷积层的一个2×2区域的信息。更为具体的一个例子,一种非常常见的池化操作叫做Max-pooling。在Max-Pooling中,这个神经元选择2×2区域里激活值最大的值,如下图所示:
注意卷积层的输出是24×24的,而池化后是12×12的。
就像上面提到的,卷积层通常会有多个特征映射。我们会对每一个特征映射进行max-pooling操作。因此,如果一个卷积层有3个特征映射,那么卷积加max-pooling后就如下图所示:
我们可以把max-pooling看成神经网络关心某个特征在这个区域里是否出现。它忽略了这个特征出现的具体位置。直觉上看,如果某个特征出现了,那么这个特征相对于其它特征的精确位置是不重要的【精确位置不重要,但是大致的位置是重要的,比如识别一个猫,两只眼睛和鼻子有一个大致的相对位置关系,但是在一个2×2的小区域里稍微移动一下眼睛,应该不太影响我们识别一只猫,而且它还能解决图像拍摄角度变化,扭曲等问题】。而且一个很大的好处是池化可以减少特征的个数【2×2的max-pooling让特征的大小变为原来的1/4】,因此减少了之后层的参数个数。
Max-pooling不是唯一的池化方法。另外一种常见的是L2 Pooling。这种方法不是取2×2区域的最大值,而是2×2区域的每个值平方然后求和然后取平方根。虽然细节有所不同,但思路和max-pooling是类似的:L2 Pooling也是从卷积层压缩信息的一种方法。在实践中,两种方法都被广泛使用。有时人们也使用其它的池化方法。如果你真的想尝试不同的方法来提供性能,那么你可以使用validation数据来尝试不同池化方法然后选择最合适的方法。但是这里我们不在讨论这些细节。【Max-Pooling是用的最多的,甚至也有人认为Pooling并没有什么卵用。深度学习一个问题就是很多经验的tricks由于没有太多理论依据,只是因为最早的人用了,而且看起来效果不错(但可能换一个数据集就不一定了),所以后面的人也跟着用。但是过了没多久又被认为这个trick其实没啥用】
放到一起
现在我们可以把这3个idea放到一起来构建一个完整的卷积神经网络了。它和之前我们看到的结构类似,不过增加了一个有10个神经元的输出层,这个层的每个神经元对应于0-9直接的一个数字:
这个网络的输入的大小是28×28,每一个输入对于MNIST图像的一个像素。然后使用了3个特征映射,局部感知域的大小是5×5。这样得到3×24×24的输出。然后使用对每一个特征映射的输出应用2×2的max-pooling,得到3×12×12的输出。
最后一层是全连接的网络,3×12×12个神经元会连接到输出10个神经元中的每一个。这和之前介绍的全连接神经网络是一样的。
卷积结构和之前的全连接结构有很大的差别。但是整体的图景是类似的:一个神经网络有很多神经元,它们的行为有weights和biase确定。并且整体的目标也是类似的:使用训练数据来训练网络的weights和biases使得网络能够尽量好的识别图片。
和之前介绍的一样,这里我们仍然使用随机梯度下降来训练。不过反向传播算法有所不同。原因是之前bp算法的推导是基于全连接的神经网络。不过幸运的是求卷积和max-pooling的导数是非常简单的。如果你想了解细节,请自己推导。【这篇文章不会介绍CNN的梯度求解,后面实现使用的是theano,后面介绍CS231N的CNN是会介绍怎么自己来基于自动求导来求这个梯度,而且还会介绍高效的算法,感兴趣的读者请持续关注】
CNN实战
前面我们介绍了CNN的基本理论,但是没有讲怎么求梯度。这里的代码是用theano来自动求梯度的。我们可以暂时把cnn看出一个黑盒,试试用它来识别MNIST的数字。后面的文章会介绍theano以及怎么用theano实现CNN。
代码
首先得到代码: git clone
安装theano
参考这里 ;如果是ubuntu的系统,可以参考这里 ;如果您的机器有gpu,请安装好cuda以及让theano支持gpu。
默认的network3.py的第52行是 GPU = True,如果您的机器没有gpu,请把这一行改成GPU = False
baseline
首先我们实现一个baseline的系统,我们构建一个只有一个隐藏层的3层全连接网络,隐藏层100个神经元。我们训练时60个epoch,使用learning rate $\eta = 0.1$,batch大小是10,没有正则化:
$cd src得到的分类准确率是97.8%。这是在test_data上的准确率,这个模型使用训练数据训练,并根据validation_data来选择当前最好的模型。使用validation数据来可以避免过拟合。读者运行时可能结果会有一些差异,因为模型的参数是随机初始化的。
$ipython
>>> import network3
>>> from network3 import Network
>>> from network3 import ConvPoolLayer, FullyConnectedLayer, SoftmaxLayer
>>> training_data, validation_data, test_data = network3.load_data_shared()
>>> mini_batch_size = 10
>>> net = Network([
FullyConnectedLayer(n_in=784, n_out=100),
SoftmaxLayer(n_in=100, n_out=10)], mini_batch_size)
>>> net.SGD(training_data, 60, mini_batch_size, 0.1,
validation_data, test_data)
改进版本1
我们首先在输入的后面增加一个卷积层。我们使用5 5的局部感知域,stride等于1,20个特征映射。然后接一个2 2的max-pooling层。之后接一个全连接的层,最后是softmax(仿射变换加softmax):
在这种网络结构中,我们可以认为卷积和池化层可以学会输入图片的局部的空间特征,而全连接的层整合全局的信息,学习出更抽象的特征。这是卷积神经网络的常见结构。
下面是代码:
>>> net = Network([【注意图片的大小,开始是(mini_batch_size, 1, 28 ,28),经过一个20个5 5的卷积池层后变成了(mini_batch_size, 20, 24,24),然后在经过2 2的max-pooling后变成了(mini_batch_size, 20, 12, 12),然后接全连接层的时候可以理解成把所以的特征映射展开,也就是20 12 12,所以FullyConnectedLayer的n_in是20 12 12】
ConvPoolLayer(image_shape=(mini_batch_size, 1, 28, 28),
filter_shape=(20, 1, 5, 5),
poolsize=(2, 2)),
FullyConnectedLayer(n_in=20*12*12, n_out=100),
SoftmaxLayer(n_in=100, n_out=10)], mini_batch_size)
>>> net.SGD(training_data, 60, mini_batch_size, 0.1,
validation_data, test_data)
这个模型得到98.78%的准确率,这相对之前的97.8%是一个很大的提高。事实上我们的错误率减少了1/3,这是一个很大的提高。【准确率很高的时候就看错误率的减少,这样比较有成就感,哈哈】
如果要用gpu,可以把上面的命令保存到一个文件test.py,然后:
$THEANO_FLAGS=mode=FAST_RUN,device=gpu,floatX=float32 python test.py在这个网络结构中,我们吧卷积和池化层看出一个整体。这只是一种习惯。network3.py会把它们当成一个整体,每个卷积层后面都会跟一个池化层。但实际的一些卷积神经网络并不都要接池化层。
改进版本2
我们再加入第二个卷积-池化层。这个卷积层插入在第一个卷积层和全连接层中间。我们使用同样的5×5的局部感知域和2×2的max-pooling。代码如下:
>>> net = Network([【注意图片的大小,开始是(mini_batch_size, 1, 28 ,28),经过一个20个5 5的卷积池层后变成了(mini_batch_size, 20, 24,24),然后在经过2 2的max-pooling后变成了(mini_batch_size, 20, 12, 12)。然后是40个5*5的卷积层,变成了(mini_batch_size, 40, 8, 8),然后是max-pooling得到(mini_batch_size, 40, 4, 4)。然后是全连接的层】
ConvPoolLayer(image_shape=(mini_batch_size, 1, 28, 28),
filter_shape=(20, 1, 5, 5),
poolsize=(2, 2)),
ConvPoolLayer(image_shape=(mini_batch_size, 20, 12, 12),
filter_shape=(40, 20, 5, 5),
poolsize=(2, 2)),
FullyConnectedLayer(n_in=40*4*4, n_out=100),
SoftmaxLayer(n_in=100, n_out=10)], mini_batch_size)
>>> net.SGD(training_data, 60, mini_batch_size, 0.1,
validation_data, test_data)
这个模型得到99.6%的准确率!
这里有两个很自然的问题。第一个是:加第二个卷积-池化层有什么意义呢?事实上,你可以认为第二个卷积层的输入是12*12的”图片“,它的”像素“代表某个局部特征。【比如你可以认为第一个卷积层识别眼睛鼻子,而第二个卷积层识别脸,不同生物的脸上面鼻子和眼睛的相对位置是有意义的】
这是个看起来不错的解释,那么第二个问题来了:第一个卷积层的输出是不同的20个不同的局部特征,因此第二个卷积层的输入是20 12 12。这就像我们输入了20个不同的”图片“,而不是一个”图片“。那第二个卷积层的神经元学到的是什么呢?【如果第一层的卷积网络能识别”眼睛“,”鼻子“,”耳朵“。那么第二层的”脸“就是2个眼睛,2个耳朵,1个鼻子,并且它们满足一定的空间约束。所以第二层的每一个神经元需要连接第一层的每一个输出,如果第二层只连接”眼睛“这个特征映射,那么只能学习出2个眼睛,3个眼睛这样的特征,那就没有什么用处了】
改进版本3
使用ReLU激活函数。ReLU的定义是:
ReLU(x)=max(0,x)
>>> from network3 import ReLU使用ReLU后准确率从99.06%提高到99.23%。从作者的经验来看,ReLU总是要比sigmoid激活函数要好。
>>> net = Network([
ConvPoolLayer(image_shape=(mini_batch_size, 1, 28, 28),
filter_shape=(20, 1, 5, 5),
poolsize=(2, 2),
activation_fn=ReLU),
ConvPoolLayer(image_shape=(mini_batch_size, 20, 12, 12),
filter_shape=(40, 20, 5, 5),
poolsize=(2, 2),
activation_fn=ReLU),
FullyConnectedLayer(n_in=40*4*4, n_out=100, activation_fn=ReLU),
SoftmaxLayer(n_in=100, n_out=10)], mini_batch_size)
>>> net.SGD(training_data, 60, mini_batch_size, 0.03,
validation_data, test_data, lmbda=0.1)
但为什么ReLU就比sigmoid或者tanh要好呢?目前并没有很好的理论介绍。ReLU只是在最近几年开始流行起来的。为什么流行的原因是经验:有一些人尝试了ReLU,然后在他们的任务里取得了比sigmoid好的结果,然后其他人也就跟风。理论上没有人证明ReLU是更好的激活函数。【所以说深度学习有很多tricks,可能某几年就流行起来了,但过几年又有人认为这些tricks没有意义。比如最早的pretraining,现在几乎没人用了。】
改进版本4
扩展数据。
深度学习非常依赖于数据。我们可以根据任务的特点”构造“新的数据。一种简单的方法是把训练数据里的数字进行一下平移,旋转等变换。虽然理论上卷积神经网络能学到与位置无关的特征,但如果训练数据里数字总是出现在固定的位置,实际的模型也不一定能学到。所以我们构造一些这样的数据效果会更好。
$ python expand_mnist.pyexpand_mnist.py这个脚本就会扩展数据。它只是简单的把图片向上下左右各移动了一个像素。扩展后训练数据从50000个变成了250000个。
接下来我们用扩展后的数据来训练模型:
>>> expanded_training_data, _, _ = network3.load_data_shared(这个模型的准确率是99.37%。扩展数据看起来非常trival,但是却极大的提高了识别准确率。
"../data/mnist_expanded.pkl.gz")
>>> net = Network([
ConvPoolLayer(image_shape=(mini_batch_size, 1, 28, 28),
filter_shape=(20, 1, 5, 5),
poolsize=(2, 2),
activation_fn=ReLU),
ConvPoolLayer(image_shape=(mini_batch_size, 20, 12, 12),
filter_shape=(40, 20, 5, 5),
poolsize=(2, 2),
activation_fn=ReLU),
FullyConnectedLayer(n_in=40*4*4, n_out=100, activation_fn=ReLU),
SoftmaxLayer(n_in=100, n_out=10)], mini_batch_size)
>>> net.SGD(expanded_training_data, 60, mini_batch_size, 0.03,
validation_data, test_data, lmbda=0.1)
改进版本5
接下来还有改进的办法吗?我们的全连接层只有100个神经元,增加神经元有帮助吗? 作者尝试了300和1000个神经元的全连接层,得到了99.46%和99.43%的准确率。相对于99.37%并没有本质的提高。
那再加一个全连接的层有帮助吗?我们了尝试一下:
>>> net = Network([在第一个全连接的层之后有加了一个100个神经元的全连接层。得到的准确率是99.43%,把这一层的神经元个数从100增加到300个和1000个得到的准确率是99.48 %和99.47%。有一些提高但是也不明显。
ConvPoolLayer(image_shape=(mini_batch_size, 1, 28, 28),
filter_shape=(20, 1, 5, 5),
poolsize=(2, 2),
activation_fn=ReLU),
ConvPoolLayer(image_shape=(mini_batch_size, 20, 12, 12),
filter_shape=(40, 20, 5, 5),
poolsize=(2, 2),
activation_fn=ReLU),
FullyConnectedLayer(n_in=40*4*4, n_out=100, activation_fn=ReLU),
FullyConnectedLayer(n_in=100, n_out=100, activation_fn=ReLU),
SoftmaxLayer(n_in=100, n_out=10)], mini_batch_size)
>>> net.SGD(expanded_training_data, 60, mini_batch_size, 0.03,
validation_data, test_data, lmbda=0.1)
为什么增加更多层提高不多呢,按说它的表达能力变强了,可能的原因是过拟合。那怎么解决过拟合呢?一种方法就是dropout。drop的详细解释请参考这里。简单来说,dropout就是在训练的时候随机的让一些神经元的激活“丢失”,这样网络就能学到更加鲁棒的特征,因为它要求某些神经元”失效“的情况下网络仍然能工作,因此就不会那么依赖某一些神经元,而是每个神经元都有贡献。
下面是在两个全连接层都加入50%的dropout:
>>> net = Network([使用dropout后,我们得到了99.60%的一个模型。
ConvPoolLayer(image_shape=(mini_batch_size, 1, 28, 28),
filter_shape=(20, 1, 5, 5),
poolsize=(2, 2),
activation_fn=ReLU),
ConvPoolLayer(image_shape=(mini_batch_size, 20, 12, 12),
filter_shape=(40, 20, 5, 5),
poolsize=(2, 2),
activation_fn=ReLU),
FullyConnectedLayer(
n_in=40*4*4, n_out=1000, activation_fn=ReLU, p_dropout=0.5),
FullyConnectedLayer(
n_in=1000, n_out=1000, activation_fn=ReLU, p_dropout=0.5),
SoftmaxLayer(n_in=1000, n_out=10, p_dropout=0.5)],
mini_batch_size)
>>> net.SGD(expanded_training_data, 40, mini_batch_size, 0.03,
validation_data, test_data)
这里有两点值得注意:
- 训练的epoch变成了40.因为dropout减少了过拟合,所以我们不需要60个epoch。
- 全连接层使用了1000个神经元。因为dropout会丢弃50%的神经元,所以从直觉来看1000个神经元也相当于只有500个。如果过用100个神经元感觉太少了点。作者经过验证发现有了dropout用1000个比300个的效果好。
改进版本6
ensemble多个神经网络。作者分别训练了5个神经网络,每一个都达到了99.6%的准确率,然后用它们来投票,得到了99.67%准确率的模型。
这是一个非常不错的模型了,10000个测试数据只有33个是错误的,我们把错误的图片都列举了出来:
片的右上角是正确的分类,右下角是模型的分类。可以发现有些错误可能人也会犯,因为有些数字人也很难分清楚。
【为什么只对全连接的层使用dropout?】
如果读者仔细的阅读代码,你会发现我们只对全连接层进行了dropout,而卷积层没有。当然我们也可以对卷积层进行dropout。但是没有必要。因为卷积层本身就有防止过拟合的能力。原因是权值共享强制网络学到的特征是能够应用到任何位置的特征。这让它不太容易学习到特别局部的特征。因此也就没有必要对它进行的dropout了。
更进一步
感兴趣的读者可以参考这里,列举了MNIST数据集的最好结果以及对应的论文。目前最好的结果是99.79%
What’s Next?
接下来的文章会介绍theano,一个非常流行的深度学习框架,然后会讲解network3.py,也就是怎么用theano实现CNN。敬请关注。 收起阅读 »
从被动客服到主动营销 环信荣获“杰出新零售客服服务商”大奖
环信荣获“杰出新零售客服服务商”大奖
过去,以阿里、京东等巨头为代表的电商企业在包括消费习惯的形成、商业配套(支付、物流等)的完善、零售平台的规范等一系列做出了巨大贡献。然而巨大的光环下,也隐藏了危机:流量越来越贵、营销成本越来越高,小企业的生存空间被打压,整个电商好像只是巨头的游戏。中国零售行业亟需出现新物种、新规则、新电商,而环信移动客服作为SaaS客服领域的破局者,凭借其在大中小型电商领域的深耕细作以及对新一代SaaS客服技术的推动,一举夺得“杰出新零售客服服务商”大奖。
同时,在新服务·思路服务分论坛上环信VP程旭文发表了《从被动客服到主动营销》的主题演讲,他分享了包括国美在线、楚楚街、金融界等很多环信客户在营销领域的应用。如何根据用户的购买前后的数据做营销投放,客服产品怎样帮助企业使得这个营销环节做到闭环。环信在过去的实践当中会基于一个核心的点,就是讲“长连接”技术,指你时时刻刻和你的潜在客户、和你的目标客户、付费客户保持一条长连接通道,使得你随时可以找到你的客户,可以随时触达他们。
环信VP程旭文主题演讲《从被动客服到主动营销》
以下是演讲实录:
程旭文:我也是接到一个命题作文,其实环信在很长时间内我们并没有单独去做营销领域,更多的在做SaaS软件,我们主要做两个产品:
第一,即时通讯云。第二,全媒体智能客服。
以上两个都是IT领域做在线软件交付。
我们一不小心进入了营销行业,我们可以基于环信的基础,包括服务10万多家APP企业,服务了超过5万家的有客服场景的APP和企业。
我们怎么进入到营销服务领域?在这个过程当中做了什么样的实践?
(PPT图示)这张图是我们每一个企业要面对的,你覆盖的客户群体人数、客单价多少,可以算出整个生意有多大。这个沉淀用户是指已经触达到用户,或者注册了网站,或者订阅了你的微信公众号等等这些用户。还有一部分是你的付费用户,对于电商企业来讲很重要的两个环节是怎样降低你的获取成本,怎样降低用户转化成本,我们做了很多的实践,我们接触了很多客户,我们发现在这个环节可以给用户带来很多价值。
(PPT图示)如果拿一张图把整个营销获取用户到转化客户变成付费用户的过程当中,上面部分是获取成本,下面两个图是关乎转化率。用户的角度看上去接触的点是营销环节和客服环节,进而往后走就是一些落地点,现在大部分电商说落地点放在淘系,很多电商说有自己的网站、APP,这些都是企业商品的落地点,再往后走就是企业客户关系系统和物流、企业资源管理、ERP等等。
今天王詠谈了关于企业大数据、关于电商大数据方面的话题,其实我感触特别深刻,用户很多行为,包括购买前的数据、购买后的数据,或多或少在企业内都有管理系统。通过这一套系统怎样做营销投放,再回到针对用户环节的营销、客服怎样使得这个环节可以做到闭环。
环信在过去的实践当中我们会基于一个核心的点,就是讲“长连接”技术,指你时时刻刻和你的潜在客户、和你的目标客户、付费客户保持一条长连接通道,使得你随时可以找到你的客户,可以随时触达他们。
从三个维度讲:
第一,长连接相对传统客服可以随时触达你的客户,改变被动的接受用户询问的现状。第二,相对于传统的消息推送,长连接使得你与客户的互动是双向的、及时的。第三,结合自己的数据管理平台、环信的管理平台,可以使得你精准的挑选客户。
举几个例子:
第一,国美在线,国美在线有上亿的客户,他们客服系统用的是环信的,营销环节有什么机制可以随时找到他们、随时联系他们,随时和他们互动,他们的反馈可以让我们知道。
比如说用户,国美APP放在后台或者锁屏状态,我们可以做到哪怕这个后台或者锁屏状态客服的坐席依然根据一定的规则发放消息给国美所有客户和部分客户,使得我们消息可以及时的触达,用户一点击基本上上做到客服状态了,后台可以监测哪些用户点了这个消息,有多少点了消息,有多少回复消息,可以做精准统计,用户行为洞察可以比单向消息推送更加细致。
第二,楚楚街,他们号称“小淘宝”定向推动2000套儿童座椅给3-4岁儿童的妈妈,我们给楚楚街做了客户管理系统,通过这些找到目标群体,通过基于长连接可以时时刻刻和用户保持长连接的客服系统,将一些非常具有价值的营销消息给APP用户,只要APP没有被卸载,用户就可以看到我们消息,及时产生互动。
第三,在金融界,互联网金融在大的范畴类也属于在线卖东西,只不过交付形式略有不同。
一是为电话销售配置一个APP的主动营销的平台,当用户上线及时的推送,用户一上APP及时的推送用户行为,之前的历史行为、购买习惯给客户经理。
二是结合用户分析推荐相关产品和用户密切互动。一些新的客户可以进入销售公海池,让新的销售抢单,立刻和客户产生互动。
再举一个例子,楚楚街利用“回呼”功能降低退换货率,我们有的时候发现这个客户下单了,但是有点问题,比如说地址不对等等,我们审核的时候立刻发现这个单子有问题,通过“回呼”找到客户,减少因为小事情碰到一些退货情况。
综合来讲,我们之前并没有主动做这方面,但是我们发现通过两个产品结合已经做了很多精准化营销方面的东西,我们与客户保持了长连接,这个长连接是即时通讯云的产品。在淘系、在腾讯,我们都离不开他们,因为他们与客户保持了密切联系,你们依然可以构建自己的APP,通过环信的即时通讯与用户保持长连接,业界当中除了旺旺、除了微信、除了陌陌第四大和用户保持长连接的平台就是环信。
我的演讲到此结束,如果大家需要交流可以来我们展台继续沟通。谢谢大家! 收起阅读 »
李理:自动梯度求解——cs231n的notes
作者:李理
目前就职于环信,即时通讯云平台和全媒体智能客服平台,在环信从事智能客服和智能机器人相关工作,致力于用深度学习来提高智能机器人的性能。
相关文章:
环信李理:从Image Caption Generation了解深度学习
李理:从Image Caption Generation理解深度学习(part II)
李理:从Image Caption Generation理解深度学习(part III)
李理:自动梯度求解 反向传播算法的另外一种视角
Optimization
这一部分内容来自:CS231n Convolutional Neural Networks for Visual Recognition
简介
我们的目标:x是一个向量,f(x)是一个函数,它的输入是一个向量(或者认为是多变量的函数,这个输入向量就是自变量),输出是一个实数值。我们需要计算的是f对每一个自变量的导数,然后把它们排成一个向量,也就是梯度。
为什么要求这个呢?前面我们也讲了,我们的神经网络的损失函数最终可以看成是权重weights和bias的函数,我们的目标就是调整这些参数,使得损失函数最小。
简单的表达式和梯度的解释
首先我们看一个很简单的函数 f(x,y)=xy,求f对x和y的偏导数很简单:
首先来看导数的定义:
函数在某个点的导数就是函数曲线在这个点的斜率,也就是f(x)随x的变化率。
比如上面的例子,当x=4,y=−3时 f(x,y)=−12,f对x的偏导数
也就是说,如果我们固定y=4,然后给x一个很小的变化h,那么f(x,y)的变化大约是-3*h。
因此乘法的梯度就是
同样,加法的梯度更简单:
最后一个简单函数是max函数:
这个导数是ReLU(x)=max(x,0)的导数,其实也简单,如果 x>=y,那么 max(x,y)=x,则导数是1,否则 max(x,y)=0,那么对x求导就是0。
复杂表达式的链式法则
接下来看一个稍微复杂一点的函数 f(x,y,z)=(x+y)z。我们引入一个中间变量q,f=qz,q=x+y,我们可以使用链式法则求f对x和y的导数。
对y的求导也是类似的。
下面是用python代码来求f对x和y的导数在某一个点的值。
# 设置自变量的值我们也可以用计算图来表示和计算:
x = -2; y = 5; z = -4
# “前向”计算f
q = x + y # q becomes 3
f = q * z # f becomes -12
# 从“后”往前“反向”计算
# 首先是 f = q * z
dfdz = q # 因为df/dz = q, 所以f对z的梯度是 3
dfdq = z # 因为df/dq = z, 所以f对q的梯度是 -4
# 然后 q = x + y
dfdx = 1.0 * dfdq # 因为dq/dx = 1,所以使用链式法则计算dfdx=-4
dfdy = 1.0 * dfdq # 因为dq/dy = 1,所以使用链式法则计算dfdy=-4
绿色的值是feed forward的结果,而红色的值是backprop的结果。
不过我觉得cs231n课程的这个图没有上面blog的清晰,原因是虽然它标示出来了最终的梯度,但是没有标示出local gradient,我在下面会画出完整的计算过程。
反向传播算法的直觉解释
我们如果把计算图的每一个点看成一个“门”(或者一个模块),或者说一个函数。它有一个输入(向量),也有一个输出(标量)。对于一个门来说有两个计算,首先是根据输入,计算输出,这个一般很容易。还有一种计算就是求输出对每一个输入的偏导数,或者说输出对输入向量的”局部“梯度(local gradient)。一个复杂计算图(神经网络)的计算首先就是前向计算,然后反向计算,反向计算公式可能看起来很复杂,但是如果在计算图上其实就是简单的用local gradient乘以从后面传过来的gradient,然后加起来。
Sigmoid模块的例子
接下来我们看一个更复杂的例子:
这个函数是一个比较复杂的复合函数,但是构成它的基本函数是如下4个简单函数:
下面是用计算图画出这个计算过程:
这个图有4种gate,加法,乘法,指数和倒数。加法有加一个常数和两个变量相加,乘法也是一样。
上图绿色的值是前向计算的结果,而红色的值是反向计算的结果,local graident并没有标示出来,所以看起来可能有些跳跃,下面我在纸上详细的分解了其中的步骤,请读者跟着下图自己动手计算一遍。
上图就是前向计算的过程,比较简单。
第二个图是计算local gradient,对于两个输入的乘法和加法,local gradient也是两个值,local gradient的值我是放到图的节点上了。
第三个图是具体计算一个乘法的local gradient的过程,因为上图可能看不清,所以单独放大了这一步。
最后计算真正的梯度,是把local gradient乘以来自上一步的gradient。不过这个例子一个节点只有一个输出,如果有多个的话,梯度是加起来的,可以参考1.4的
上面我们看到把
分解成最基本的加法,乘法,导数和指数函数,但是我们也可以不分解这么细。之前我们也学习过了sigmoid函数,那么我们可以这样分解:
σ(x)σ(x) 的导数我们之前已经推导过一次了,这里再列一下:
因此我们可以把后面一长串的gate”压缩“成一个gate:
我们来比较一下,之前前向计算 σ(x)σ(x) 需要一次乘法,一次exp,一次加法导数;而反向计算需要分别计算这4个gate的导数。
而压缩后前向计算是一样的,但是反向计算可以”利用“前向计算的结果
这只需要一次减法和一次乘法!当然如果不能利用前向的结果,我们如果需要重新计算 σ(x)σ(x) ,那么压缩其实没有什么用处。能压缩的原因在于σ函数导数的特殊形式。而神经网络的关键问题是在训练,训练性能就取决于这些细节。如果是我们自己来实现反向传播算法,我们就需要利用这样的特性。而如果是使用工具,那么就依赖于工具的优化水平了。
下面我们用代码来实现一下:
w = [2,-3,-3] # assume some random weights and data上面的例子用了一个小技巧,就是所谓的staged backpropagation,说白了就是给中间的计算节点起一个名字。比如dot。为了让大家熟悉这种技巧,下面有一个例子。
x = [-1, -2]
# forward pass
dot = w[0]*x[0] + w[1]*x[1] + w[2]
f = 1.0 / (1 + math.exp(-dot)) # sigmoid function
# backward pass through the neuron (backpropagation)
ddot = (1 - f) * f # gradient on dot variable, using the sigmoid gradient derivation
dx = [w[0] * ddot, w[1] * ddot] # backprop into x
dw = [x[0] * ddot, x[1] * ddot, 1.0 * ddot] # backprop into w
# we're done! we have the gradients on the inputs to the circuit
Staged computation练习
我们用代码来计算这个函数对x和y的梯度在某一点的值
前向计算
x = 3 # example values反向计算
y = -4
# forward pass
sigy = 1.0 / (1 + math.exp(-y)) # 分子上的sigmoid #(1)
num = x + sigy # 分子 #(2)
sigx = 1.0 / (1 + math.exp(-x)) # 分母上的sigmoid #(3)
xpy = x + y #(4)
xpysqr = xpy**2 #(5)
den = sigx + xpysqr # 分母 #(6)
invden = 1.0 / den #(7)
f = num * invden # done! #(8)
# backprop f = num * invden需要注意的两点:1. 前向的结果都要保存下来,反向的时候要用的。2. 如果某个变量有多个出去的边,第一次是等于,第二次就是+=,因为我们要把不同出去点的梯度加起来。
dnum = invden # gradient on numerator #(8)
dinvden = num #(8)
# backprop invden = 1.0 / den
dden = (-1.0 / (den**2)) * dinvden #(7)
# backprop den = sigx + xpysqr
dsigx = (1) * dden #(6)
dxpysqr = (1) * dden #(6)
# backprop xpysqr = xpy**2
dxpy = (2 * xpy) * dxpysqr #(5)
# backprop xpy = x + y
dx = (1) * dxpy #(4)
dy = (1) * dxpy #(4)
# backprop sigx = 1.0 / (1 + math.exp(-x))
dx += ((1 - sigx) * sigx) * dsigx # Notice += !! See notes below #(3)
# backprop num = x + sigy
dx += (1) * dnum #(2)
dsigy = (1) * dnum #(2)
# backprop sigy = 1.0 / (1 + math.exp(-y))
dy += ((1 - sigy) * sigy) * dsigy #(1)
# done! phew
下面我们来逐行分析反向计算:
(8) f = num * invden
local gradient
而上面传过来的梯度是1,所以 dnum=1∗invden。注意变量的命名规则, df/dnum就命名为dnum【省略了df,因为默认我们是求f对所有变量的偏导数】
同理: dinvden=num
(7) invden = 1.0 / den
local gradient是 (−1.0/(den∗∗2)) ,然后乘以上面来的dinvden
(6) den = sigx + xpysqr
这个函数有两个变量sigx和xpysqr,所以需要计算两个local梯度,然后乘以dden
加法的local梯度是1,所以就是(1)*dden
(5) xpysqr = xpy**2
local gradient是2*xpy,再乘以dxpysqr
(4) xpy = x + y
还是一个加法,local gradient是1,所以dx和dy都是dxpy乘1
(3) sigx = 1.0 / (1 + math.exp(-x))
这是sigmoid函数,local gradient是 (1-sigx)*sigx,再乘以dsigx。
不过需要注意的是这是dx的第二次出现,所以是+=,表示来自不同路径反向传播过来给x的梯度值
(2) num = x + sigy
还是个很简单的加法,local gradient是1。需要注意的是dx是+=,理由同上。
(1) sigy = 1.0 / (1 + math.exp(-y))
最后是sigmoid(y)和前面(3)一样的。
请仔细阅读上面反向计算的每一步代码,确保自己理解了之后再往下阅读。
梯度的矩阵运算
前面都是对一个标量的计算,在实际实现时用矩阵运算一次计算一层的所有梯度会更加高效。因为矩阵乘以向量和向量乘以向量都可以看出矩阵乘以矩阵的特殊形式,所以下面我们介绍矩阵乘法怎么求梯度。
首先我们得定义什么叫矩阵对矩阵的梯度!
我查阅了很多资料,也没找到哪里有矩阵对矩阵的梯度的定义,如果哪位读者知道,请告诉我,谢谢!唯一比较接近的是Andrew Ng的课程cs294的背景知识介绍的slides linalg的4.1节定义了gradient of Matrix,关于矩阵对矩阵的梯度我会有一个猜测性的解释,可能会有问题。
首先介绍graident of matrix
假设 f:Rm×n→R是一个函数,输入是一个m×n的实数值矩阵,输出是一个实数。那么f对A的梯度是如下定义的:
看起来定义很复杂?其实很简单,我们把f看成一个mn个自变量的函数,因此我们可以求f对这mn个自变量的偏导数,然后把它们排列成m*n的矩阵就行了。为什么要多此一举把变量拍成矩阵把他们的偏导数也排成矩阵?想想我们之前的神经网络的weights矩阵,这是很自然的定义,同时我们需要计算loss对weights矩阵的每一个变量的偏导数,写出这样的形式计算起来比较方便。
那么什么是矩阵对矩阵的梯度呢?我们先看实际神经网络的一个计算情况。对于全连接的神经网络,我们有一个矩阵乘以向量 D=WxD=Wx 【我们这里把向量x看成矩阵】。现在我们需要计算loss对某一个 WijWij 的偏导数,根据我们之前的计算图, WijWij 有多少条出边,那么就有多少个要累加的梯度乘以local梯度。
假设W是m×n的矩阵,x是n×p的矩阵,则D是m×p的矩阵
根据矩阵乘法的定义
我们可以计算:
请仔细理解上面这一步,如果 k≠i,则不论s是什么,Wks跟Wij不是同一个变量,所以导数就是0;如果k=i,∑sWisxsl=xjl,也就求和的下标s取j的时候有WijWij。
因此
上面计算了loss对一个Wij的偏导数,如果把它写成矩阵形式就是:
前面我们推导出了对Wij的偏导数的计算公式,下面我们把它写成矩阵乘法的形式并验证【证明】它。
为什么可以写成这样的形式呢?
上面的推导似乎很复杂,但是我们只要能记住就行,记法也很简单——把矩阵都变成最特殊的1 1的矩阵(也就是标量,一个实数)。D=w x,这个导数很容易吧,对w求导就是local gradient x,然后乘以得到dW=dD x;同理dx=dD W。
但是等等,刚才那个公式里还有矩阵的转置,这个怎么记?这里有一个小技巧,就是矩阵乘法的条件,两个矩阵能相乘他们的大小必须匹配,比如D=Wx,W是m n,x是n p,也就是第二个矩阵的行数等于第一个的列数。
现在我们已经知道dW是dD”乘以“x了,dW的大小和W一样是m n,而dD和D一样是m p,而x是n p,那么为了得到一个m n的矩阵,唯一的办法就是 dD∗xT
同理dx是n p,dD是m p,W是m*n,唯一的乘法就是 WT∗dD
下面是用python代码来演示,numpy的dot就是矩阵乘法,可以用numpy.dot(A,B),也可以直接调用ndarray的dot函数——A.dot(B):
# forward pass至此,本系列文章的第5部分告一段落。在接下来的文章中,作者将为大家详细讲述关于常见的深度学习框架/工具的使用方法、使用自动求导来实现多层神经网络等内容,敬请期待。 收起阅读 »
W = np.random.randn(5, 10)
X = np.random.randn(10, 3)
D = W.dot(X)
# now suppose we had the gradient on D from above in the circuit
dD = np.random.randn(*D.shape) # same shape as D
dW = dD.dot(X.T) #.T gives the transpose of the matrix
dX = W.T.dot(dD)
环信VP程旭文:从被动客服到主动营销
[思路网注]程旭文分享了包括国美在线、楚楚街、金融界等很多环信客户在营销领域的应用。怎样做营销投放,再回到针对用户环节的营销、客服怎样使得这个环节可以做到闭环。
【亿邦动力网讯】12月19日消息,在2016亿邦未来零售大会新服务·思路服务分论坛上环信VP程旭文发表了《从被动客服到主动营销》演讲,他表示:“其实环信在很长时间内我们并没有做营销类的领域,更多的在做SaaS软件,主要做两个软件: 第一,即时通讯云。 第二,全媒体智能客服。”
图为环信VP副总裁程旭文
2016亿邦未来零售大会由亿邦动力网主办,思路网协办,于12月19日-21日在广州白云万达希尔顿酒店举行。国内外电商领域知名企业高管、专家学者、媒体代表共计2000余人出席。
本届大会以“新物种、新规则、新电商”为主题,包括两天的主论坛、五场分论坛、电商经理人之夜以及马蹄社和亿邦疯人会等系列活动。值得关注的是,在本届大会上,电商产业所熟知的如阿里巴巴、京东、唯品会、当当、亚马逊等面孔都没有出现,取而代之的全部是新生代的零售平台和品牌商阵营,反映了电商领域正寻求破局、寻找新增长的行业心态。
(温馨提示:本文为速记初审稿,保证现场嘉宾原意,未经删节,或存纰漏,敬请谅解。)
以下是演讲实录:
程旭文:我也是接到一个命题作文,其实环信在很长时间内我们并没有单独去做营销领域,更多的在做SaaS软件,我们主要做两个软件:
第一,即时通讯云。
第二,全媒体智能客服。
以上两个都是IT领域做在线软件交付。
我们一不小心进入了营销行业,我们可以基于环信的基础,包括服务10万多家APP企业,服务了超过5万家的有客服场景的APP和企业。
我们怎么进入到营销服务领域?在这个过程当中做了什么样的实践?
这张图是我们每一个企业要面对的,你覆盖的客户群体人数、客单价多少,可以算出整个生意有多大。这个沉淀用户是指已经触达到用户,或者注册了网站,或者订阅了你的微信公众号等等这些用户。还有一部分是你的付费用户,对于电商企业来讲很重要的两个环节是怎样降低你的获取成本,怎样降低用户转化成本,我们做了很多的实践,我们接触了很多客户,我们发现在这个环节可以给用户带来很多价值。
如果拿一张图把整个营销获取用户到转化客户变成付费用户的过程当中,上面部分是获取成本,下面两个图是关乎转化率。用户的角度看上去接触的点是营销环节和客服环节,进而往后走就是一些落地点,现在大部分电商说落地点放在淘系,很多电商说有自己的网站、APP,这些都是企业商品的落地点,再往后走就是企业客户关系系统和物流、企业资源管理、ERP等等。
今天王詠谈了关于企业大数据、关于电商大数据方面的话题,其实我感触特别深刻,用户很多行为,包括购买前的数据、购买后的数据,或多或少在企业内都有管理系统。通过这一套系统怎样做营销投放,再回到针对用户环节的营销、客服怎样使得这个环节可以做到闭环。
环信在过去的实践当中我们会基于一个核心的点,就是讲“长连接”技术,指你时时刻刻和你的潜在客户、和你的目标客户、付费客户保持一条长连接通道,使得你随时可以找到你的客户,可以随时触达他们。
从三个维度讲:
第一,长连接相对传统客服可以随时触达你的客户,改变被动的接受用户询问的现状。
第二,相对于传统的消息推送,长连接使得你与客户的互动是双向的、及时的。
第三,结合自己的数据管理平台、环信的管理平台,可以使得你精准的挑选客户。
举几个例子:
第一,国美,国美有上亿的客户,他们客服系统用的是我们的,营销环节有什么机制可以随时找到他们、随时联系他们,随时和他们互动,他们的反馈可以让我们知道。
比如说用户,国美APP放在后台或者锁屏状态,我们可以做到哪怕这个后台或者锁屏状态客服的坐席依然根据一定的规则发放消息给国美所有客户和部分客户,使得我们消息可以及时的触达,用户一点击基本上上做到客服状态了,后台可以监测哪些用户点了这个消息,有多少点了消息,有多少回复消息,可以做精准统计,用户行为洞察可以比单向消息推送更加细致。
第二,楚楚街,他们号称“小淘宝”定向推动2000套儿童座椅给3-4岁儿童的妈妈,我们给楚楚街做了客户管理系统,通过这些找到目标群体,通过基于长连接可以时时刻刻和用户保持长连接的客服系统,将一些非常具有价值的营销消息给APP用户,只要APP没有被卸载,用户就可以看到我们消息,及时产生互动。
第三,在金融界,互联网金融在大的范畴类也属于在线卖东西,只不过交付形式略有不同。
一是为电话销售配置一个APP的主动营销的平台,当用户上线及时的推送,用户一上APP及时的推送用户行为,之前的历史行为、购买习惯给客户经理。
二是结合用户分析推荐相关产品和用户密切互动。一些新的客户可以进入销售公海池,让新的销售抢单,立刻和客户产生互动。
再举一个例子,楚楚街利用“回呼”功能降低退换货率,我们有的时候发现这个客户下单了,但是有点问题,比如说地址不对等等,我们审核的时候立刻发现这个单子有问题,通过“回呼”找到客户,减少因为小事情碰到一些退货情况。
综合来讲,我们之前并没有主动做这方面,但是我们发现通过两个产品结合已经做了很多精准化营销方面的东西,我们与客户保持了长连接,这个长连接是即时通讯云的产品。在淘系、在腾讯,我们都离不开他们,因为他们与客户保持了密切联系,你们依然可以构建自己的APP,通过环信的即时通讯与用户保持长连接,业界当中除了旺旺、除了微信、除了陌陌第四大和用户保持长连接的平台就是环信。
我的演讲到此结束,如果大家需要交流可以来我们展台继续沟通。谢谢大家! 收起阅读 »
游戏测试与软件测试的区别!
游戏本质也是软件的一种,所以从测试工程的角度来讲,游戏测试与软件测试的本质是完全相同的。2者的不同更多的是在表象层面或者流程方面,我们可以把游戏测试看作软件测试的子类,它继承了软件测试这个父类的特性,又有自己的一些新特性。
笔者通过归纳总结,把游戏测试相对软件测试的不同归纳为以下几点:
1. UI&&UE
2. 数值
3. 活动
4. 进度
5. 工具
6. 性能
7. 安全
8. 合服(针对网游)
9. 交互
10. 网络
下面我们就每一点来详细探讨下。
1、UI&&UE。相对来讲UI&&UE在游戏和软件测试中,重要性并非很高,但它们确是用户和测试人员最直观感受的部分,也最受“非专业人士”的关注,游戏行业尤甚。对大部分软件来说,UI&&UE的重要性没有游戏那么高,毕竟软件使用过程愉悦感和趣味性并非是重要的事情,我们日常使用各种各样的软件时肯定深有体会,大部分情况是用软件来完成一项任务,能完成就好了,在使用过程中很难体会到上面说的愉悦感和趣味性。而游戏则不然,在玩游戏的过程中,愉悦感和趣味性是至关重要的,如果缺失了这些要素,用户可能瞬间就流失了,也就意味着这款游戏失败了。这好比高层小户型和海景别墅,虽然都能满足居住需求,但给人的感觉是完全不同的。
2、数值。数值对游戏而言是至关重要的,无论是单机游戏还是网络游戏,玩家非常重视自己角色的数值增长,任何差错都可能导致用户的抱怨甚至流失。另一个层面是游戏的功能之间的耦合度非常高,数值之间有着千丝万缕的关联。所以测试的过程中需要关注每个数值变化带来的各种影响。而软件功能之间的耦合度则没有这么高,很多情况下功能之间的数值是相对独立的。而且软件的用户很多时候并不关注内部的数值,能完成所需即可,细微的差错甚至都没人关心。举个例子,比如很多显示开机速度的软件,在用户打开电脑时会提示用户开机速度击败了百分之多少的其它用户,至于是20%还是25%,可能对用户而言没什么太大的差别。而游戏则不然,比如一个角色的战斗力是1000,下次登陆变成999,仅仅是1的差距,玩家可能就会愤怒的打客服电话质问了。
3、活动。很多软件也经常搞活动,笔者经常遇到某邮箱或某论坛搞活动送积分之类的,但是在游戏中,活动则是频度更高的一种玩法。所以测试过程中可能受到的关注度更高一些,尤其是网络游戏。游戏活动的测试更关注时间与资源产出,如开启时间,关闭时间,资源产出概率等。因为一个活动的开启和关闭及产出都已经提前公告给玩家,如果出了任何差错,都会导致玩家不满。而且一个活动完毕后可能紧接另一个活动,任何差错都可能导致更大的损失。而软件上的活动则没这么严格的概念。
4、进度。在软件开发和测试过程中,延期是非常普遍的情况。很多软件测试人员的时间观念也没那么强。游戏则是非常不同的,由于游戏的**倾向,所以其产业链涉及很多前期的市场推广,各种广告和推广活动都是真金白银砸下去的,任何延期可能都会导致前期的推广功亏一篑及商业上的信誉,这些损失都是不可接受的。所以游戏测试作为产品发布前的最后一环,必须严格控制版本进度,确保能够按期交付。
5、工具。游戏测试依赖更多的测试工具,因为用户的数值和角色状态千差万别,为了尽量模拟用户状态,测试过程中总需要造出各色各样的测试数据,而制造这些数据,则需要测试工具的帮助。另一个层面是游戏测试还需要对测试工具本身的正确性进行测试,确保工具本身是正确的。这点在传统软件测试行业则是不多见的。
6、性能。性能测试对游戏而言也是至关重要的一点,无论在台式机还是移动设备上,任何游戏的卡顿都会让玩家产生厌恶感。游戏测试过程中比较重视的是客户端的内存和cpu的使用率,确保游戏能够流畅的运行。对网络游戏而言,服务端的性能也十分重要,一款良好的网游,需要服务器能够稳定持久的运行。而且我们也希望大部分用户都能玩我们的游戏,而用户的设备则差异性很大,尤其是移动设备。所以我们必须确保客户端的性能符合我们的预期标准,以使更多的玩家能够玩我们的游戏。软件则没太多这方面的需求。
7、安全。安全对软件和游戏而言都十分重要。但是对游戏而言,则是关乎身家性命的事情,很多游戏都死于外挂横行。而且游戏的客户端与服务端的交互非常频繁,数据安全更加凸显。所以测试的时候更加关注安全方面的测试。有资源产出的地方则有安全测试的地方。防刷防外挂,是游戏测试人员始终要保持谨慎认真的对待的地方。
8、合服。这个可能是游戏的独有特色。有时候服务器中用户便少,为了带给玩家更好的游戏体验,需要合并几组服务器为1组。在合服的过程中需要保证原有服务器和目标服务器中所有用户的数据信息不发生错乱。涉及到用户方方面面的数据信息,复杂度也比较高,所以也许要测试人员认真的测试。确保测试无误后,才能正式开始合服操作。
9、交互。更多的时候是相对网络游戏而言,网游中很大程度的乐趣都来源于玩家与玩家之间的交互。这一特性在传统软件(此处请忽略各种社交软件)中并不多见。玩家交互的越频繁,则意味着数据之间交互的程度越高,数据之间的复杂变换及相互影响需要我们时刻关注。
10、网络。网络对于网络游戏是必不可少的,游戏的实时交互性比较高,游戏过程中突然断网的痛苦是难以忍受的。所以对网络的测试要求也比较高,因为不同用户用的网络运营商可能不同,不同地区的网络信号也不同,甚至移动过程中会出现不同网络之间的切换,这些都是需要我们去认真测试的。这样才能尽量保证不同网络条件下用户的体验达到最佳。
想要高效的完成app功能测试,就需要选择一款合适的功能测试工具。尽管现阶段存在少数不采用任何功能测试工具,从事功能测试外包项目的软件服务企业。短期来看,这类企业盈利状况尚可,但长久来看,它们极有可能被自动化程度较高的软件服务企业取代。
TestBird - 手游和App自动化测试平台 收起阅读 »
环信移动客服v5.5.1更新:新增客户资料自定义
支持查看客服同事的真实姓名
客服与同事在移动客服系统交流时,可以查看对方的真实姓名,更利于同事间沟通。支持以下两种场景:
- 与同事聊天时,将鼠标放在“客服同事”列表中同事的昵称上,可以查看该同事的真实姓名;
- 转接会话时,将鼠标放在“转接会话”对话框中同事的昵称上,可以查看该同事的真实姓名。
注意:客服可以在客服模式下“客服信息”页面设置自己的名字(真实姓名);管理员可以在管理员模式下“成员管理 > 客服”页面设置其他客服的真实姓名。
支持查看待接入会话详情
在待接入页面,点击任意一条会话,可以查看该会话的消息详情。
前提条件:管理员进入“管理员模式 > 设置 > 系统开关”页面,打开“客服查看待接入详情”开关。
租户下待接入会话数上限
新增待接入会话数上限,每个租户允许的最大待接入会话数为1000,如果某个租户下坐席数超过5个,则该租户的最大待接入会话数为坐席数x200。
待接入会话数超过上限后,不允许访客创建新的会话,当访客试图接入时提示,系统繁忙无法接入。
为避免访客无法接入的情况,当租户的待接入会话数即将达到上限时,系统向消息中心发送通知提醒管理员;当租户的待接入会话数已达到上限时,系统会再次向消息中心发送通知提醒管理员及时处理。
支持客服主动发起会话
在待接入页面,客服可以查看正在访问网站的访客列表,并主动发起会话。发起会话后,会话进入客服的进行中会话列表,客服可以主动与访客聊天。
该功能为增值服务,如需开通,请联系环信商务经理。开通后,在网页访客端进行配置eventCollector为true即可使用。关于详细配置方法,请参考网页渠道集成。
呼叫中心支持电话转接和呼叫保持
呼叫中心支持电话转接和呼叫等待功能。在通话过程中,如果电话需要转接,可以点击转接按钮 [转接] ,将电话转接给呼叫中心客服同事;如果有其他操作处理,需要暂停通话,可以点击保持按钮 [保持] ,将通话置为“保持中”状态,完成操作后,可以手动恢复通话。
呼叫中心功能为增值服务,如需开通,请联系环信商务经理。
管理员模式
优化机器人开关设置
优化机器人开关的“工作时间设置”,支持为机器人设置不同的工作场景:
- 全天接会话:访客发起会话时,由机器人接待。
- 上班时间客服全忙以及下班时间接会话:在上班时间,访客发起会话时,如果客服全忙,会话由机器人接待;在下班时间,访客发起会话时,由机器人接待。
- 仅下班时间接会话:在下班时间,访客发起会话时,由机器人接待。
机器人回答不了时,访客可以选择转人工客服。转人工后,如果有空闲客服则自动调度,如果没有空闲客服,则会话进入待接入,客服可以手动接入会话。
注意:该版本更新前的“仅下班时间接会话”与更新后的“上班时间客服全忙以及下班时间接会话”功能一致。如果您之前选择了“仅下班时间接会话”,更新后默认选择的是“上班时间客服全忙以及下班时间接会话”,您可以根据您的需要调整机器人开关。
优化上下班时间设置
“设置 > 系统开关”页面的“上下班时间”设置更名为“工作时间设置”,“会话结束语”和“下班提示语”上移至“工作时间设置”之前,原有数据保持不变。“工作时间设置”支持为周一至周日设置单独的上下班时间,以适应不同的工作时间场景:
- 为工作日(周一至周五)和周末(周六、周日)设置不同的上下班时间段(如下图)
- 为工作日的上午、下午设置不同的上下班时间段
- 以星期为周期,自定义每天的上下班时间
进入“设置 > 系统开关”页面,在“工作时间设置”区域,点击“添加新的工作时间”,设置新的工作时间段。
历史会话支持分配功能
管理员可以将历史会话重新分配给客服或技能组,分配后,生成一个新的会话。
- 分配给客服时,新会话直接进入客服的进行中会话列表。
- 分配给技能组时,如果技能组内有空闲客服,新会话进入空闲客服的进行中会话列表;如果技能组内客服全忙,新会话进入该技能组的待接入会话列表。
进入管理员模式,选择“历史会话”,点击会话右侧的转接按钮可以对该会话进行分配。
问候语中增加访客昵称
新增在问候语中增加访客昵称的功能,提升亲密度。问候语包含企业问候语、客服问候语、技能组问候语,在这三种问候语中均可以设置。设置方式为,在问候语中添加特殊字符和默认称呼(##亲##)。当访客昵称有效时,显示访客昵称;当访客昵称无效时,显示默认称呼(亲)。默认称呼可以自定义。
例如,设置企业问候语为“##亲##,您好,很高兴为您服务!”
- 当访客昵称有效时(访客昵称和ID不一致),假设访客昵称为Jon,该访客收到的问候语为“Jon,您好,很高兴为您服务!”
- 当访客昵称无效时(访客昵称和ID一致),访客收到的问候语为“亲,您好,很高兴为您服务!”
客户资料自定义
新增“客户资料自定义”功能,允许管理员设置在系统中显示哪些客户资料,包括系统字段和自定义字段,并对这些字段进行排序。设置后,新的字段列表和顺序将显示在客服模式下“会话”、“历史会话”和“客户中心”等页面的“资料”页签,以及管理员模式下“客户中心”、“历史会话”、“当前会话”等页面的“资料”页签。
进入“设置 > 客户资料自定义”页面对客户资料进行自定义,步骤如下:
1. 添加自定义字段。点击“添加自定义字段”按钮,在对话框中输入字段名称,选择字段格式,并进行相应设置,点击“保存”按钮。重复该步骤,可添加多个自定义字段。
自定义字段默认对坐席可见,当关闭“坐席可见”开关时,在客服模式下不显示该字段。
2. 设置字段是否显示,以及在“资料”页签的排列顺序。在“字段开关”一列,勾选 [勾选] 需要显示的字段,取消勾选 [取消勾选] 不需要显示的字段。点击字段后面的排序按钮 [排序] ,可将该字段的顺序上移一位。
示例,根据下图的设置,“资料”页签将只显示:昵称、名字、ID、微信号、微博账号、描述。
允许客服查看待接入会话详情
新增“客服查看待接入详情”功能,在客服模式的待接入页面,点击任意一条会话,可以查看该会话的历史消息。进入“设置 > 系统开关”页面,打开“客服查看待接入详情”开关。该开关默认关闭。
待接入超时提醒
新增“待接入超时提醒”功能,当访客进入待接入排队超过一定时间后,系统将自动发送消息提示访客。进入“设置 > 系统开关”页面,打开“待接入超时提醒”开关,并设置超时提示语、排队超时提醒时间及提醒次数。该开关默认关闭。
- 当提醒次数设置为1次时,访客在待接入排队时长达到“排队超时提醒时间”时,系统发送“超时提示语”给访客;
- 当提醒次数设置为多次时(例如3次),访客在待接入排队时长达到“排队超时提醒时间”时,系统发送“超时提示语”给访客,之后,当每次达到“排队提醒间隔”设定的时长,系统再次发送“超时提示语”给访客,直到会话被客服接起,或达到“提醒次数”。
待接入超时结束会话
新增“待接入超时结束会话”功能,当访客排队时长达到设定数值时,仍然没有被客服接入,会话将被自动结束。进入“设置 > 系统开关”页面,打开“待接入超时结束会话”开关,并设置超时提示语、超时时间及会话标签和备注。该开关默认关闭。
支持转人工指定技能组
支持为机器人设置转人工指定技能组。为了不影响现有会话路由规则,默认情况下不指定。
如果没有开通多机器人功能,分两种场景:
- 场景一:使用默认配置(不指定)。会话经过默认机器人转接人工客服时,按照原有会话路由规则分配给对应的技能组或客服。
- 场景二:设置转人工指定某个技能组。会话经过默认机器人转接人工客服时,都转给指定的技能组。
如果开通了多机器人功能,建议配置如下:
- 为每个新创建的机器人指定不同的技能组。这样,会话经过机器人转接人工客服时,将分配给指定的技能组,从而实现机器人绑定技能组功能。
进入“智能机器人 > 机器人设置”页面,选择一个机器人,再次选择“自动回复 > 转人工设置”页签,为该机器人选择“转人工指定技能组”。
机器人问答优化功能
新增机器人问答优化功能,系统自动收集机器人未能匹配的重复出现的访客消息,并以列表的形式显示。您可以将这些访客消息添加到知识规则中,并设置对应的答案,提高机器人回答的匹配率和准确性。
进入“智能机器人 > 机器人设置 > 问答优化”页面,点击任意一条未匹配问句后的加号(+),可以将该问句添加到知识规则,支持三种方式:
- 添加到推荐知识规则:如果存在相似度高的知识规则,系统会将其展示为“推荐知识规则”,选择该知识规则,并点击“添加”按钮即可;
- 将问句添加到现有知识规则:选择“将问句添加到现有知识规则”,系统展示现有知识规则列表,您可以选择并添加该问句到任一知识规则;
- 创建新知识规则并添加此问句:选择“创建新知识规则并添加此问句”,系统自动为该问句创建一条知识规则,请您手动为该问句添加对应答案(如果知识规则中没有答案,机器人会回复空消息)。
机器人问答优化功能为增值服务,如需开通,请联系环信商务经理。
新增“删除坐席”事件
自定义事件推送功能新增“删除坐席”事件,当坐席被删除时,可将相关信息以回调方式自动推送到其他系统。进入“设置 > 自定义事件推送”页面,点击“创建事件推送”,填写自定义事件名称、接收事件的服务器地址,勾选需要推送的事件,并保存。
自定义事件推送功能为增值服务,如需开通,请联系环信商务经理。
客户之声支持手动添加关注的关键词
优化客户之声的“热门关键词”,支持查看一段时间内的热门关键词,并且支持手动添加您关注的业务相关的关键词。
进入客户之声页面,在“热门关键词”区域的右上角可以选择时间范围;点击“关注词设置”,可以添加您关注的关键词,更新的关注词将于第二天生效,并在词云中高亮显示。
客户之声功能为增值服务,如需开通,请联系环信商务经理。
环信移动客服更新日志http://docs.easemob.com/cs/releasenote/5.6
环信移动客服登陆地址http://kefu.easemob.com/ 收起阅读 »
没想到你是这样的环信!!!
一封来自环信小伙伴的来信,没想到你是这样任劳任怨,兢兢业业,恪尽职守的环信!
致环信:
在自如客服项目的支持过程中,贵公司的孔令莹兢兢业业、不辞劳苦的工作态度令人由衷赞佩,必须提笔赞扬一下,为工作上能有这样值得信赖的合作伙伴感到欣慰和放心。
我们客服系统是早9:00-22:00在线提供支持,自如客是O2O的业务应用模式,我们的客服需要及时解决来自租客和业主的各种问题,涉及多个渠道接入:4个微信号,1个APP,1个门户。每个渠道的接入情况都不一样,关联系统复杂,在这样的业务背景下,很考验环信客服系统无停歇稳定运行能力,以及项目支持人员的应急处理能力。我们要特别感谢孔令莹,从支持自如客服系统以来,不论是工作日还是节假日,我们任何时候提出的问题,她都会快速响应帮我们排查协调。令人印象深刻的是,有很多次都是由于我们内部系统原因造成,但孔令莹依旧会和我们一起帮助客服同学定位原因,直到客服系统正常恢复使用。还记得在国庆节和双十一期间,我问到环信是否会安排轮班值守人员时,孔令莹的答复是遇到问题随时联系她,会及时为我们解决问题。这样的态度让我们倍感欣慰,她用行动诠释了她的承诺,这样的工作精神令我们感动。
为她以客户为先的工作精神而点赞,希望贵司能对这样优秀的员工给予表扬,特发此邮件表达诚挚的谢意。
自如服务产品部
2016-11-30
收起阅读 »