2
评论

在微信小程序里实现聊天室 聊天室 小程序

Tolazy 发表了文章 • 154 次浏览 • 2019-04-19 17:49 • 来自相关话题

第一次搞小程序,老板让我实现一个聊天室功能,压力山大啊。
花了几天时间研究比较了一下方案,最后基于环信的小程序SDK 开发了一个聊天室。
 
准备工作
下载环信 小程序demo+sdkgit clone https://github.com/easemob/webim-weixin-xcx创建一个文件夹,将 demo 中的文件 comps、images、sdk、utils 拷贝到新的文件,文件目录说明



集成
登录环信没什么可说的,这里选择的是使用 username/password 登录,和demo中的一样,文件没有进行任何更改


在app.js 中注册的 WebIM.conn.listen, 然后在 登陆成功的回调 onOpened 设置的跳转页面,并将登陆的 username 赋给 myName,传到新的页面中使用


修改 roomlist.js 获取聊天室列表,是分页获取的,这里先偷个懒,获取了第一页 20 个聊天室


然后将listChatrooms() 分别在onLoad、onShow 内,更改下,将原有的 listGroups() 替换掉然后在roomlist.wxml 修改对应的 变量绑定名称





demo中的group.js 中,获取到的是当前登陆账号已加入的群组,咱们做的是聊天室功能,所以需要有一个加入的操作,找roomlist.js 中找到 into_room: function (event),然后填写加入聊天室的方法, 我是直接在当前这个里面加的跳转到聊天页面,并将当前登陆的IDmyName,聊天室IDgroupID,聊天室名称your 传给新页面


Ex:监听是否加入聊天室成功的回调是在 onPresence 中,type:memberJoinChatRoomSuccess,正常是监听这个回调跳转页面,有点麻烦就直接这样吧到会话页面后,需要修改一下对应的消息格式,在comps/chat/suit 目录下,将里面的文件对应的 js 文件根据文档给聊天室发送消息 格式进行修改,聊天室消息和群组消息不同,所以我目前是直接将getSendToParam()、isGroupChat() 注释,改成下面这样,demo 中下面还有代码的,这里就用 …… 代替了





就这样了,简单集成聊天室功能,demo中的UI 是开源的,可以根据自己的需求更改~下面是具体实现过程。代码也放在github 上了,有需要的兄弟自取。demo下载地址:https://github.com/lizgDonkey/room-xcx 查看全部
第一次搞小程序,老板让我实现一个聊天室功能,压力山大啊。
花了几天时间研究比较了一下方案,最后基于环信的小程序SDK 开发了一个聊天室。
 
准备工作
  1. 下载环信 小程序demo+sdk
    git clone https://github.com/easemob/webim-weixin-xcx
  2. 创建一个文件夹,将 demo 中的文件 comps、images、sdk、utils 拷贝到新的文件,文件目录说明
    ml.png

集成
  1. 登录环信没什么可说的,这里选择的是使用 username/password 登录,和demo中的一样,文件没有进行任何更改
    login.png
  2. 在app.js 中注册的 WebIM.conn.listen, 然后在 登陆成功的回调 onOpened 设置的跳转页面,并将登陆的 username 赋给 myName,传到新的页面中使用
    tz.png
  3. 修改 roomlist.js 获取聊天室列表,是分页获取的,这里先偷个懒,获取了第一页 20 个聊天室
    getroom.png
    然后将listChatrooms() 分别在onLoad、onShow 内,更改下,将原有的 listGroups() 替换掉
  4. 然后在roomlist.wxml 修改对应的 变量绑定名称
    listui.png
    list.png
  5. demo中的group.js 中,获取到的是当前登陆账号已加入的群组,咱们做的是聊天室功能,所以需要有一个加入的操作,找roomlist.js 中找到 into_room: function (event),然后填写加入聊天室的方法, 我是直接在当前这个里面加的跳转到聊天页面,并将当前登陆的IDmyName,聊天室IDgroupID,聊天室名称your 传给新页面
    joinrom.png
    Ex:监听是否加入聊天室成功的回调是在 onPresence 中,type:memberJoinChatRoomSuccess,正常是监听这个回调跳转页面,有点麻烦就直接这样吧
  6. 到会话页面后,需要修改一下对应的消息格式,在comps/chat/suit 目录下,将里面的文件对应的 js 文件根据文档给聊天室发送消息 格式进行修改,聊天室消息和群组消息不同,所以我目前是直接将getSendToParam()、isGroupChat() 注释,改成下面这样,demo 中下面还有代码的,这里就用 …… 代替了
    send.png
    chat.png
    就这样了,简单集成聊天室功能,demo中的UI 是开源的,可以根据自己的需求更改~下面是具体实现过程。代码也放在github 上了,有需要的兄弟自取。demo下载地址:https://github.com/lizgDonkey/room-xcx

4
评论

【开源项目】全国首个开源直播小程序源码

beyond 发表了文章 • 20756 次浏览 • 2018-07-20 17:30 • 来自相关话题

今天你看直播了吗?拥有10亿微信生态用户的小程序已经成为了继移动互联后的又一个现象级风口,随着微信小程序对外开放实时音视频录制及播放等更多连接能力,小程序与直播强强联合,在各行各业找到了非常多的玩法,小程序直播相比微信直播和APP直播更加简洁、流畅、低延时、多入口等众多优势迅速向商业直播领域及泛娱乐直播领域蔓延。从小游戏、内容付费、工具、大数据、社交电商创业者到传统品牌商们,都在努力搭上小程序直播这辆快车,以免错过微信生态里新的流量洼地。
 





作为一名环信生态圈资深开发者,本着对技术的热衷,对环信的眷恋和对党的忠诚,基于环信即时通讯云写了“直播购物小程序”,目前项目源码已全部免费开放,希望对有需求的企业和开发者提供一个思路和参考。
直播购物小程序源码github地址:https://github.com/YuTongNetworkTechnology/wechat_live/tree/master 
git打不开可直接点下面链接下载


小程序直播demo_2018-06-21.zip







直播购物小程序运行预览图 
 
小程序体验指南(仅需两步):
 
1、下载微信小程序开发工具,下载地址:https://developers.weixin.qq.c ... .html 
 




2、导入源码:将附件的源码解压直接导入 







环信小程序直播技术文档
一、 使用的技术
1、 环信IM直播室。
2、 微信小程序实时音视频播放组件live-player。
3、 推流软件(obs、易推流)等推流。
4、 视频流服务器(UCLOUD、七牛、腾讯)等视频流服务器。
二、 系统使用流程。
1、 视频推流软件将视频流推到流服务器。
2、 打开视频直播demo小程序注册环信账号。
3、 进入软件直播室进行测试。
三、 技术流程及使用的SDk
1、 注册环信账号
打开https://www.easemob.com/ 环信官网,点击右上角注册按钮,选择[注册即时通讯云]




填写对相关信息进行注册





注册成功后进行登录




注:新注册用户需进行账号的认证。
2、 直播应用创建
登录成功点击应用列表选择创建应用




输入应用名称等信息
 





创建成功后点击应用进入





需要注意的是应用的OrgName 和AppName这两个是以后都需要用到的两个参数变量




3、 直播创建
1)在创建直播之前需要对应用进行设置首先需要设置应用的直播流地址
第一步获取应用管理员的Tokencurl -X POST "https://a1.easemob.com/[应用OrgName]/[应用AppName]/token" -d '{"grant_type":"client_credentials","client_id":"[应用client_id]","client_secret":"[应用] client_secret"}'返回格式{
"access_token":"YWMtWY779DgJEeS2h9OR7fw4QgAAAUmO4Qukwd9cfJSpkWHiOa7MCSk0MrkVIco",
"expires_in":5184000,
"application":"c03b3e30-046a-11e4-8ed1-5701cdaaa0e4"












第二步设置直播流地址curl -X POST -H "Authorization: Bearer [管理员Token]" " https://a1.easemob.com/[应用OrgName]/[应用AppName]/liverooms/stream_url -d '{"pc_pull":"[pc拉流地址]","pc_push":"[pc推流地址]","mobile_pull":"[手机拉流地址]","mobile_push":"[手机推流地址]"}'"成功返回格式:{
"action": "post",
"application": "e1a09de0-0e03-11e7-ad8e-a1d913615409",
"uri": "http://127.0.0.1:8080/easemob- ... ot%3B,
"entities": [ ],
"data": {
"pc_pull": true,
"mobile_push": true,
"mobile_pull": true,
"pc_push": true
},
"timestamp": 1494084474885,
"duration": 1,
"organization": "easemob-demo",
"applicationName": "chatdemoui"
}












2)创建主播
点击IM用户





点击注册IM用户





填写用户信息





创建用户的过程同样也可以通过REST API形式进行curl -X POST -i " https://a1.easemob.com/[应用OrgName]/[应用AppName]/users" -d '{"username":"[用户名]","password":"[密码]"}'
注:应用必须为开放注册





将注册的用户添加为主播curl -X POST -H "Authorization: [管理员Token]" https://a1.easemob.com/[应用OrgName]/[应用AppName]/super_admin -d'{"superadmin":"[IM用户名]"}'返回结果示例:{
"action": "post",
"application": "4d7e4ba0-dc4a-11e3-90d5-e1ffbaacdaf5",
"uri": "http://127.0.0.1:8080/easemob- ... ot%3B,
"entities": [ ],
"data": {
"result": "success"
},
"timestamp": 1496236798886,
"duration": 0,
"organization": "easemob-demo",
"applicationName": "chatdemoui"
}












3)创建直播
点击直播





点击新建房间





填写房间信息




创建房间同时也可以使用REST API形式进行详情可以查看http://docs.easemob.com/im/live/server-integration环信官方文档。
4、 小程序demo集成使用
小程序直播购物demo集成官方WebIM SDK详情请查看https://github.com/easemob/webim-weixin-xcx
Demo具体配置如下
打开demo 下sdk配置文件





修改appkey为自己应用的appkey





打开pages/live/index.js修改房间默认拉流地址及直播间房间号





四、 扩展说明
Demo中房间为固定测试房间,实际使用中应获取环信直播的房间信息及房间列表。具体如下:
获取直播间列表:curl -X GET -H "Authorization: Bearer [用户Token]" https://a1.easemob.com/[应用OrgName]/[应用AppName]/liverooms?ongoing=true&limit=[获取数量]&cursor=[游标地址(不填写为充开始查询)]
响应:{
"action": "get",
"application": "4d7e4ba0-dc4a-11e3-90d5-e1ffbaacdaf5",
"params": {
"cursor": [
"ZGNiMjRmNGY1YjczYjlhYTNkYjk1MDY2YmEyNzFmODQ6aW06Y2hhdHJvb206ZWFzZW1vYi1kZW1vI2NoYXRkZW1vdWk6MzE"
],
"ongoing": [
"true"
],
"limit": [
"2"
]
},
"uri": "http://127.0.0.1:8080/easemob- ... ot%3B,
"entities": [ ],
"data": [
{
"id": "1924",
"chatroom_id": "17177265635330",
"title": "具体了",
"desc": "就咯",
"startTime": 1495779917352,
"endTime": 1495779917352,
"anchor": "wuls",
"gift_count": 0,
"praise_count": 0,
"current_user_count": 8,
"max_user_count": 9,
"status": "ongoing",
"cover_picture_url": "",
"pc_pull_url": "rtmp://vlive3.rtmp.cdn.ucloud.com.cn/ucloud/easemob-demo_chatdemoui_1924_1",
"pc_push_url": "rtmp://publish3.cdn.ucloud.com.cn/ucloud/easemob-demo_chatdemoui_1924_1",
"mobile_pull_url": "rtmp://vlive3.rtmp.cdn.ucloud.com.cn/ucloud/easemob-demo_chatdemoui_1924_1",
"mobile_push_url": "rtmp://publish3.cdn.ucloud.com.cn/ucloud/easemob-demo_chatdemoui_1924_1"
},
{
"id": "1922",
"chatroom_id": "17175003856897",
"title": "香山",
"desc": "随便",
"startTime": 1495777760957,
"endTime": 1495777760957,
"anchor": "sx001",
"gift_count": 0,
"praise_count": 8,
"current_user_count": 1,
"max_user_count": 3,
"status": "ongoing",
"cover_picture_url": "http://127.0.0.1:8080/easemob- ... ot%3B,
"pc_pull_url": "rtmp://vlive3.rtmp.cdn.ucloud.com.cn/ucloud/easemob-demo_chatdemoui_1922_1",
"pc_push_url": "rtmp://publish3.cdn.ucloud.com.cn/ucloud/easemob-demo_chatdemoui_1922_1",
"mobile_pull_url": "rtmp://vlive3.rtmp.cdn.ucloud.com.cn/ucloud/easemob-demo_chatdemoui_1922_1",
"mobile_push_url": "rtmp://publish3.cdn.ucloud.com.cn/ucloud/easemob-demo_chatdemoui_1922_1"
}
],
"timestamp": 1496303336669,
"duration": 0,
"organization": "easemob-demo",
"applicationName": "chatdemoui",
"cursor": "ZGNiMjRmNGY1YjczYjlhYTNkYjk1MDY2YmEyNzFmODQ6aW06Y2hhdHJvb206ZWFzZW1vYi1kZW1vI2NoYXRkZW1vdWk6NDk",
"count": 2
}












获取直播间详情:curl -X GET -H "Authorization: Bearer [用户Token]" " https://a1.easemob.com/[应用OrgName]/[应用AppName]/[房间id]/status"响应:{
"action": "get",
"application": "4d7e4ba0-dc4a-11e3-90d5-e1ffbaacdaf5",
"uri": "http://127.0.0.1:8080/easemob- ... ot%3B,
"entities": [ ],
"data": {
"liveRoomID": "1946",
"status": "ongoing"
},
"timestamp": 1496234759930,
"duration": 0,
"organization": "easemob-demo",
"applicationName": "chatdemoui",
"count": 0
}














 
使用环信直播购物小程序遇到任何问题欢迎跟帖讨论。 查看全部
今天你看直播了吗?
拥有10亿微信生态用户的小程序已经成为了继移动互联后的又一个现象级风口,随着微信小程序对外开放实时音视频录制及播放等更多连接能力,小程序与直播强强联合,在各行各业找到了非常多的玩法,小程序直播相比微信直播和APP直播更加简洁、流畅、低延时、多入口等众多优势迅速向商业直播领域及泛娱乐直播领域蔓延。从小游戏、内容付费、工具、大数据、社交电商创业者到传统品牌商们,都在努力搭上小程序直播这辆快车,以免错过微信生态里新的流量洼地。
 
微信图片_20180725162426.jpg


作为一名环信生态圈资深开发者,本着对技术的热衷,对环信的眷恋和对党的忠诚,基于环信即时通讯云写了“直播购物小程序”,目前项目源码已全部免费开放,希望对有需求的企业和开发者提供一个思路和参考。
直播购物小程序源码github地址:https://github.com/YuTongNetworkTechnology/wechat_live/tree/master 
git打不开可直接点下面链接下载



预览图.jpg

直播购物小程序运行预览图 
 
小程序体验指南(仅需两步):
 
1、下载微信小程序开发工具,下载地址:https://developers.weixin.qq.c ... .html 
 
Catch9A07(07-20-17-38-30).jpg

2、导入源码:将附件的源码解压直接导入 


Catch1C69(07-20-17-38-30).jpg


环信小程序直播技术文档
一、 使用的技术
1、 环信IM直播室。
2、 微信小程序实时音视频播放组件live-player。
3、 推流软件(obs、易推流)等推流。
4、 视频流服务器(UCLOUD、七牛、腾讯)等视频流服务器。
二、 系统使用流程。
1、 视频推流软件将视频流推到流服务器。
2、 打开视频直播demo小程序注册环信账号。
3、 进入软件直播室进行测试。
三、 技术流程及使用的SDk
1、 注册环信账号
打开https://www.easemob.com/ 环信官网,点击右上角注册按钮,选择[注册即时通讯云]
1.png

填写对相关信息进行注册

2.png

注册成功后进行登录
3.png

注:新注册用户需进行账号的认证。
2、 直播应用创建
登录成功点击应用列表选择创建应用
4.png

输入应用名称等信息
 

5.png

创建成功后点击应用进入

6.png

需要注意的是应用的OrgName 和AppName这两个是以后都需要用到的两个参数变量
7.png

3、 直播创建
1)在创建直播之前需要对应用进行设置首先需要设置应用的直播流地址
第一步获取应用管理员的Token
curl -X POST "https://a1.easemob.com/[应用OrgName]/[应用AppName]/token" -d '{"grant_type":"client_credentials","client_id":"[应用client_id]","client_secret":"[应用] client_secret"}'
返回格式
{
"access_token":"YWMtWY779DgJEeS2h9OR7fw4QgAAAUmO4Qukwd9cfJSpkWHiOa7MCSk0MrkVIco",
"expires_in":5184000,
"application":"c03b3e30-046a-11e4-8ed1-5701cdaaa0e4"












第二步设置直播流地址
curl -X POST -H "Authorization: Bearer [管理员Token]"  " https://a1.easemob.com/[应用OrgName]/[应用AppName]/liverooms/stream_url -d '{"pc_pull":"[pc拉流地址]","pc_push":"[pc推流地址]","mobile_pull":"[手机拉流地址]","mobile_push":"[手机推流地址]"}'"
成功返回格式:
{
"action": "post",
"application": "e1a09de0-0e03-11e7-ad8e-a1d913615409",
"uri": "http://127.0.0.1:8080/easemob- ... ot%3B,
"entities": [ ],
"data": {
"pc_pull": true,
"mobile_push": true,
"mobile_pull": true,
"pc_push": true
},
"timestamp": 1494084474885,
"duration": 1,
"organization": "easemob-demo",
"applicationName": "chatdemoui"
}












2)创建主播
点击IM用户

8.png

点击注册IM用户

9.png

填写用户信息

10.png

创建用户的过程同样也可以通过REST API形式进行
curl -X POST -i " https://a1.easemob.com/[应用OrgName]/[应用AppName]/users" -d '{"username":"[用户名]","password":"[密码]"}'

注:应用必须为开放注册

11.png

将注册的用户添加为主播
curl -X POST -H "Authorization: [管理员Token]"  https://a1.easemob.com/[应用OrgName]/[应用AppName]/super_admin -d'{"superadmin":"[IM用户名]"}'
返回结果示例:
{
"action": "post",
"application": "4d7e4ba0-dc4a-11e3-90d5-e1ffbaacdaf5",
"uri": "http://127.0.0.1:8080/easemob- ... ot%3B,
"entities": [ ],
"data": {
"result": "success"
},
"timestamp": 1496236798886,
"duration": 0,
"organization": "easemob-demo",
"applicationName": "chatdemoui"
}












3)创建直播
点击直播

12.png

点击新建房间

13.png

填写房间信息
14.png

创建房间同时也可以使用REST API形式进行详情可以查看http://docs.easemob.com/im/live/server-integration环信官方文档。
4、 小程序demo集成使用
小程序直播购物demo集成官方WebIM SDK详情请查看https://github.com/easemob/webim-weixin-xcx
Demo具体配置如下
打开demo 下sdk配置文件

15.png

修改appkey为自己应用的appkey

16.png

打开pages/live/index.js修改房间默认拉流地址及直播间房间号

17.png

四、 扩展说明
Demo中房间为固定测试房间,实际使用中应获取环信直播的房间信息及房间列表。具体如下:
获取直播间列表:
curl -X GET -H "Authorization: Bearer  [用户Token]"  https://a1.easemob.com/[应用OrgName]/[应用AppName]/liverooms?ongoing=true&limit=[获取数量]&cursor=[游标地址(不填写为充开始查询)]

响应:
{
"action": "get",
"application": "4d7e4ba0-dc4a-11e3-90d5-e1ffbaacdaf5",
"params": {
"cursor": [
"ZGNiMjRmNGY1YjczYjlhYTNkYjk1MDY2YmEyNzFmODQ6aW06Y2hhdHJvb206ZWFzZW1vYi1kZW1vI2NoYXRkZW1vdWk6MzE"
],
"ongoing": [
"true"
],
"limit": [
"2"
]
},
"uri": "http://127.0.0.1:8080/easemob- ... ot%3B,
"entities": [ ],
"data": [
{
"id": "1924",
"chatroom_id": "17177265635330",
"title": "具体了",
"desc": "就咯",
"startTime": 1495779917352,
"endTime": 1495779917352,
"anchor": "wuls",
"gift_count": 0,
"praise_count": 0,
"current_user_count": 8,
"max_user_count": 9,
"status": "ongoing",
"cover_picture_url": "",
"pc_pull_url": "rtmp://vlive3.rtmp.cdn.ucloud.com.cn/ucloud/easemob-demo_chatdemoui_1924_1",
"pc_push_url": "rtmp://publish3.cdn.ucloud.com.cn/ucloud/easemob-demo_chatdemoui_1924_1",
"mobile_pull_url": "rtmp://vlive3.rtmp.cdn.ucloud.com.cn/ucloud/easemob-demo_chatdemoui_1924_1",
"mobile_push_url": "rtmp://publish3.cdn.ucloud.com.cn/ucloud/easemob-demo_chatdemoui_1924_1"
},
{
"id": "1922",
"chatroom_id": "17175003856897",
"title": "香山",
"desc": "随便",
"startTime": 1495777760957,
"endTime": 1495777760957,
"anchor": "sx001",
"gift_count": 0,
"praise_count": 8,
"current_user_count": 1,
"max_user_count": 3,
"status": "ongoing",
"cover_picture_url": "http://127.0.0.1:8080/easemob- ... ot%3B,
"pc_pull_url": "rtmp://vlive3.rtmp.cdn.ucloud.com.cn/ucloud/easemob-demo_chatdemoui_1922_1",
"pc_push_url": "rtmp://publish3.cdn.ucloud.com.cn/ucloud/easemob-demo_chatdemoui_1922_1",
"mobile_pull_url": "rtmp://vlive3.rtmp.cdn.ucloud.com.cn/ucloud/easemob-demo_chatdemoui_1922_1",
"mobile_push_url": "rtmp://publish3.cdn.ucloud.com.cn/ucloud/easemob-demo_chatdemoui_1922_1"
}
],
"timestamp": 1496303336669,
"duration": 0,
"organization": "easemob-demo",
"applicationName": "chatdemoui",
"cursor": "ZGNiMjRmNGY1YjczYjlhYTNkYjk1MDY2YmEyNzFmODQ6aW06Y2hhdHJvb206ZWFzZW1vYi1kZW1vI2NoYXRkZW1vdWk6NDk",
"count": 2
}












获取直播间详情:
curl -X GET -H "Authorization: Bearer [用户Token]" " https://a1.easemob.com/[应用OrgName]/[应用AppName]/[房间id]/status"
响应:
{
"action": "get",
"application": "4d7e4ba0-dc4a-11e3-90d5-e1ffbaacdaf5",
"uri": "http://127.0.0.1:8080/easemob- ... ot%3B,
"entities": [ ],
"data": {
"liveRoomID": "1946",
"status": "ongoing"
},
"timestamp": 1496234759930,
"duration": 0,
"organization": "easemob-demo",
"applicationName": "chatdemoui",
"count": 0
}














 
使用环信直播购物小程序遇到任何问题欢迎跟帖讨论。
19
评论

【新手快速入门】集成环信常见问题+解决方案汇总

dujiepeng 发表了文章 • 21279 次浏览 • 2017-05-22 15:51 • 来自相关话题

   这里整理了集成环信的常见问题和一些功能的实现思路,希望能帮助到大家。感谢热心的开发者贡献,大家在观看过程中有不明白的地方欢迎直接跟帖咨询。
 
ios篇
APNs证书创建和上传到环信后台头像昵称的简述和处理方案音视频离线推送Demo实现环信服务器聊天记录保存多久?离线收不到好友请求IOS中环信聊天窗口如何实现文件发送和预览的功能ios集成常见问题环信推送的一些常见问题实现名片|红包|话题聊天室等自定义cell
 
Android篇
Android sdk 的两种导入方式环信3.0SDK集成小米推送教程EaseUI库中V4、v7包冲突解决方案Android EaseUI里的百度地图替换为高德地图android扩展消息(名片集成)关于会话列表的置顶聊天java.lang.UnsatisfiedLinkError: 的问题android 端 app 后台被杀死收不到消息的解决方案
昵称头像篇
android中如何显示开发者服务器上的昵称和头像 Android中显示头像(接上一篇文章看)环信(Android)设置头像和昵称的方法(最简单暴力的基于环信demo的集成)IOS中如何显示开发者服务器上的昵称和头像【环信公开课第12期视频回放】-所有关于环信IM昵称头像的问题听这课就够了
 
直播篇
一言不合你就搞个直播APP
 
客服集成
IM-SDK和客服SDK并存开发指南—Android篇IM-SDK和客服SDK并存开发指南—iOS篇
 
开源项目
Android简版demoios简版demo凡信2.0:超仿微信的开源项目 凡信3.0:携直播和红包而来高仿微信:Github 3,515 Star方圆十里:环信编程大赛冠军项目泛聊:定一个小目标写一个QQSlack聊天机器人:一天时间做一个聊天机器人TV视频通话:在电视上视频通话视频通话:Android手机视频通话酷信:ios高仿微信公众号助手:与订阅用户聊天沟通
 
持续更新ing...小伙伴们还有什么想知道欢迎跟帖提出。
  查看全部
   这里整理了集成环信的常见问题和一些功能的实现思路,希望能帮助到大家。感谢热心的开发者贡献,大家在观看过程中有不明白的地方欢迎直接跟帖咨询。
 
ios篇

 
Android篇

昵称头像篇

 
直播篇
  1. 一言不合你就搞个直播APP

 
客服集成
  1. IM-SDK和客服SDK并存开发指南—Android篇
  2. IM-SDK和客服SDK并存开发指南—iOS篇

 
开源项目

 
持续更新ing...小伙伴们还有什么想知道欢迎跟帖提出。
 
13
回复

收集基于环信SDK开发的开源项目 开源项目

xiaoyan2015 回复了问题 • 13 人关注 • 14845 次浏览 • 2019-03-14 13:52 • 来自相关话题

0
回复

ease ui 怎么将读取数据方式改为从本地数据库? 消息保存 Android 环信_Android

回复

Carthach 发起了问题 • 1 人关注 • 13 次浏览 • 2019-04-24 16:37 • 来自相关话题

0
评论

在线直播源码打造互动直播平台需要开发的功能 直播平台开发 直播 直播系统源码

q2466131704 发表了文章 • 36 次浏览 • 2019-04-23 16:39 • 来自相关话题

通常情况下,我们在购买一套在线直播源码之前,需要事先了解一下它都包含哪些功能。在直播应用场景下,其自身具备的互动性和实时性是非常显著的特点。要想满足直播的实时性和互动性,并且能够给用户带来优质的产品体验,就需要在功能层面上多下功夫。那么,要想打造一款互动直播平台需要开发哪些功能呢?
1.弹幕功能
直播中最基础且必不可少的就是弹幕功能了。不仅可以带动直播间内的活跃程度,还可以增进用户与主播之间的交流沟通,特别是游戏直播场景下,满屏的弹幕和发送弹幕已经成为直播平台靓丽的“风景线”。当然,在开发弹幕功能时还需要注意弹幕的自动审核,对于弹幕消息进行一定的过滤,从而保证平台的整体语言风气。





2.礼物功能
刷礼物,是直播中最直接也最实现的变现方式。用户以送礼物的形式表达自己对主播的喜爱,而主播也会为了获得更多的礼物努力输出令人感兴趣的直播内容,从而增加直播平台的用户黏性和活跃程度。当然,礼物的配置也需要进行一定的优化,比如支持PC、APP、H5的道具播放效果。
3.活动功能
大多数运营级直播平台都会利用各种各样的节日策划各种活动,以奖励为“噱头”吸引用户参与互动,从而增加平台的用户黏性。
4.任务功能
一些直播平台为了增强用户黏性,提高用户日活量,会在新用户注册之后提供任务指引,比如“观看3位主播的直播,并尝试送礼物1次”,以完成任务的形式发送奖励,打消用户的试玩心理。






5.靓号功能
这一功能主要是给喜欢个性并且有意义号码需求的提供的,就像有的人喜欢换简单好记的手机号,有的人并不介意手机号的排列顺序。
6.会员功能
为用户提供相应的特权服务,将会员分为多个不同的层次,刺激用户进行消费充值,并设定有效期,这也是直播平台能够变现的方式之一。
7.守护功能
大部分直播平台都会具备守护功能,给主播礼物打赏数量最高的会获得守护头像框,并排名在显眼的位置,还可以享受一定的特权服务。
8.游戏功能
小游戏对于互动性强的直播平台来说是必不可少的,主播可以在直播过程中与用户玩游戏进行互动,以游戏的方式增强用户黏性,从而给主播增加一定的观看人数,并且使用户能够感受到强烈的参与感。
所以说,要想打购买在线直播源码并打造一款互动直播平台的话,不仅需要对其质量进行考察,还需要了解其中所包含的功能是否能够调动用户的活跃性和积极性。只有这样,才能增加平台的用户流量,最终实现变现目的。文章结尾给大家提个建议,购买源码还是要找专业的源码服务商才行。
本文声明原创,转载请注明出处及作者。 查看全部
通常情况下,我们在购买一套在线直播源码之前,需要事先了解一下它都包含哪些功能。在直播应用场景下,其自身具备的互动性和实时性是非常显著的特点。要想满足直播的实时性和互动性,并且能够给用户带来优质的产品体验,就需要在功能层面上多下功夫。那么,要想打造一款互动直播平台需要开发哪些功能呢?
1.弹幕功能
直播中最基础且必不可少的就是弹幕功能了。不仅可以带动直播间内的活跃程度,还可以增进用户与主播之间的交流沟通,特别是游戏直播场景下,满屏的弹幕和发送弹幕已经成为直播平台靓丽的“风景线”。当然,在开发弹幕功能时还需要注意弹幕的自动审核,对于弹幕消息进行一定的过滤,从而保证平台的整体语言风气。

47291482820971.jpg

2.礼物功能
刷礼物,是直播中最直接也最实现的变现方式。用户以送礼物的形式表达自己对主播的喜爱,而主播也会为了获得更多的礼物努力输出令人感兴趣的直播内容,从而增加直播平台的用户黏性和活跃程度。当然,礼物的配置也需要进行一定的优化,比如支持PC、APP、H5的道具播放效果。
3.活动功能
大多数运营级直播平台都会利用各种各样的节日策划各种活动,以奖励为“噱头”吸引用户参与互动,从而增加平台的用户黏性。
4.任务功能
一些直播平台为了增强用户黏性,提高用户日活量,会在新用户注册之后提供任务指引,比如“观看3位主播的直播,并尝试送礼物1次”,以完成任务的形式发送奖励,打消用户的试玩心理。

14816215168966_副本.png


5.靓号功能
这一功能主要是给喜欢个性并且有意义号码需求的提供的,就像有的人喜欢换简单好记的手机号,有的人并不介意手机号的排列顺序。
6.会员功能
为用户提供相应的特权服务,将会员分为多个不同的层次,刺激用户进行消费充值,并设定有效期,这也是直播平台能够变现的方式之一。
7.守护功能
大部分直播平台都会具备守护功能,给主播礼物打赏数量最高的会获得守护头像框,并排名在显眼的位置,还可以享受一定的特权服务。
8.游戏功能
小游戏对于互动性强的直播平台来说是必不可少的,主播可以在直播过程中与用户玩游戏进行互动,以游戏的方式增强用户黏性,从而给主播增加一定的观看人数,并且使用户能够感受到强烈的参与感。
所以说,要想打购买在线直播源码并打造一款互动直播平台的话,不仅需要对其质量进行考察,还需要了解其中所包含的功能是否能够调动用户的活跃性和积极性。只有这样,才能增加平台的用户流量,最终实现变现目的。文章结尾给大家提个建议,购买源码还是要找专业的源码服务商才行。
本文声明原创,转载请注明出处及作者。
0
评论

直播平台开发搭建常见问题汇总 直播平台开发

q3557873521 发表了文章 • 27 次浏览 • 2019-04-23 16:00 • 来自相关话题

直播平台开发的重点在于功能设计,以及如何优化提升后期直播平台的用户直播体验,从整个直播平台开发到搭建部署、封装上架,有很多常见的问题是需要我们关注的。我们就来汇总整理下。
1.直播平台CDN是如何计费的?
三方CDN服务为直播平台内容的分发提供了可行性方案。但CDN费用如何计算经常困扰着运营方,尤其是刚刚接触直播平台开发项目的朋友。目前两种主流的CDN计费方式,一种是按照峰值计费,另一种则是按照流量计费。在后期的直播平台运营过程中,当在线用户较多时,建议采用峰值计费方式会更加经济实惠一些。
2.直播平台搭建部署需要准备哪些内容?
针对手机直播APP的搭建部署需要准备的内容主要有:APP名字,APP应用进场图,观看页图等,开通CDN服务,三方接口信息资料,合理的服务器配置,苹果App Store上架和安卓相关应用市场上架需要的内容资料等。
3.苹果开发者账号相关问题
申请苹果开发者账号,主要的目的是用于将应用上架到App Store供用户安装下载。目前苹果开发者账号主要有以下三种:
(1)企业开发者账号:企业账号开发的应用不能上架App Store,通常用于企业内部分发测试应用。申请费用299美元,现阶段来讲企业账号很难申请。需要提供公司的邓白氏编码。
(2)公司开发者账号:申请时需要填写公司的邓白氏编码,可以用来进行应用上架App Store,有条件的话尽量申请。
(3)个人开发者账号:个人开发者账号也可以进行上架,如果用作签名分发时会有设备数量限制,从字面意思看更适用于个人开发者,如果做APP运营推广,还是要申请公司账号。
4.打开直播APP源码用什么开发工具?
安卓端的开发工具:Android studio 
iOS端开发工具:xcode
5.服务器的系统该如何选择?
建议使用Linux 服务系统, CentOS 7.2 64位操作系统。通常直播平台搭建部署环境是支持LNMP或LAMP配置环境的。
6.什么是苹果UDID?
UDID是由子母和数字组成的40个字符串的序号,主要用来区分每一个唯一的iOS设备的标识,当使用个人签名进行应用分发下载时,需要提供设备的UDID给开发者。如果是企业签名分发下载则不需要UDID,而且没有设备数量限制。
7.直播平台开发广泛使用的流媒体协议有哪些? 
比较常见的如UDP私有协议,使用最为广泛的是RTMP协议,在手机网页端观看视频直播则更多地采用HLS传输协议。这些协议各有优劣势,适用的场景也各不相同。
8.直播平台中的 定位功能,跟附近的人功能有何区别?
绝大多数直播平台都具有“定位”功能和“附近的人”功能,二者是不同的。定位功能主要用于直播的个人信息中,显示主播的位置;附近的人功能,则是在主播列表中,用来显示主播和用户之间的距离。
以上这些问题都是大家在直播平台开发搭建运营时经常遇到的,搞清楚这些问题后,会使得整个开发过程更加顺畅。 查看全部
直播平台开发的重点在于功能设计,以及如何优化提升后期直播平台的用户直播体验,从整个直播平台开发到搭建部署、封装上架,有很多常见的问题是需要我们关注的。我们就来汇总整理下。
1.直播平台CDN是如何计费的?
三方CDN服务为直播平台内容的分发提供了可行性方案。但CDN费用如何计算经常困扰着运营方,尤其是刚刚接触直播平台开发项目的朋友。目前两种主流的CDN计费方式,一种是按照峰值计费,另一种则是按照流量计费。在后期的直播平台运营过程中,当在线用户较多时,建议采用峰值计费方式会更加经济实惠一些。
2.直播平台搭建部署需要准备哪些内容?
针对手机直播APP的搭建部署需要准备的内容主要有:APP名字,APP应用进场图,观看页图等,开通CDN服务,三方接口信息资料,合理的服务器配置,苹果App Store上架和安卓相关应用市场上架需要的内容资料等。
3.苹果开发者账号相关问题
申请苹果开发者账号,主要的目的是用于将应用上架到App Store供用户安装下载。目前苹果开发者账号主要有以下三种:
(1)企业开发者账号:企业账号开发的应用不能上架App Store,通常用于企业内部分发测试应用。申请费用299美元,现阶段来讲企业账号很难申请。需要提供公司的邓白氏编码。
(2)公司开发者账号:申请时需要填写公司的邓白氏编码,可以用来进行应用上架App Store,有条件的话尽量申请。
(3)个人开发者账号:个人开发者账号也可以进行上架,如果用作签名分发时会有设备数量限制,从字面意思看更适用于个人开发者,如果做APP运营推广,还是要申请公司账号。
4.打开直播APP源码用什么开发工具?
安卓端的开发工具:Android studio 
iOS端开发工具:xcode
5.服务器的系统该如何选择?
建议使用Linux 服务系统, CentOS 7.2 64位操作系统。通常直播平台搭建部署环境是支持LNMP或LAMP配置环境的。
6.什么是苹果UDID?
UDID是由子母和数字组成的40个字符串的序号,主要用来区分每一个唯一的iOS设备的标识,当使用个人签名进行应用分发下载时,需要提供设备的UDID给开发者。如果是企业签名分发下载则不需要UDID,而且没有设备数量限制。
7.直播平台开发广泛使用的流媒体协议有哪些? 
比较常见的如UDP私有协议,使用最为广泛的是RTMP协议,在手机网页端观看视频直播则更多地采用HLS传输协议。这些协议各有优劣势,适用的场景也各不相同。
8.直播平台中的 定位功能,跟附近的人功能有何区别?
绝大多数直播平台都具有“定位”功能和“附近的人”功能,二者是不同的。定位功能主要用于直播的个人信息中,显示主播的位置;附近的人功能,则是在主播列表中,用来显示主播和用户之间的距离。
以上这些问题都是大家在直播平台开发搭建运营时经常遇到的,搞清楚这些问题后,会使得整个开发过程更加顺畅。
0
评论

同一个网站,手机端跟电脑端显示不同是怎么实现的? 网站建设

jiyulin 发表了文章 • 30 次浏览 • 2019-04-23 12:49 • 来自相关话题

同一个网站,手机端跟电脑端不同是怎么实现的?

常见的方式有三种:

1,自适应网站

同一套代码,自动实现手机端和电脑端的布局自动调整。例如:openGPS.cn 网站现在大部分页面已经支持自适应展示,手机端电脑端都可以访问本站内容,正常阅读。自适应站点,往往是对CSS布局的重点考虑,本站使用的是BootStrap这个前端样式组件实现的自适应布局。

2,网站二级目录

这种是早期网站比较喜欢的做法,因为其实这是一个网站。早期网站往往是使用虚拟主机(也叫空间)发布,一个空间只能放一个网站,所以这种做法在早期特别流行。这种结构本质还是一个网站,但是针对手机电脑客户端单独做了往往对应的一套目录,例如:

电脑站点地址一般是:www.domain.com/xxxxxxx

手机站点地址往往是:www.domain.com/m/xxxxxxx

3,手机站点使用二级域名,电脑手机各一套2套站点代码


这种做法,工作量跟二级目录基本相似,严格来说代码量稍微多点。由于是2套代码,所以发布时候也得配备2套域名,不过要求两套站点连接同一个数据库来实现数据统一。例如:

电脑端网站域名是:www.domain.com

手机端网站域名是:m.domain.com




原文地址: https://www.opengps.cn/Blog/View.aspx?id=302 文章的更新编辑依此链接为准。欢迎关注源站原创文章!
  查看全部
同一个网站,手机端跟电脑端不同是怎么实现的?

常见的方式有三种:

1,自适应网站

同一套代码,自动实现手机端和电脑端的布局自动调整。例如:openGPS.cn 网站现在大部分页面已经支持自适应展示,手机端电脑端都可以访问本站内容,正常阅读。自适应站点,往往是对CSS布局的重点考虑,本站使用的是BootStrap这个前端样式组件实现的自适应布局。

2,网站二级目录

这种是早期网站比较喜欢的做法,因为其实这是一个网站。早期网站往往是使用虚拟主机(也叫空间)发布,一个空间只能放一个网站,所以这种做法在早期特别流行。这种结构本质还是一个网站,但是针对手机电脑客户端单独做了往往对应的一套目录,例如:

电脑站点地址一般是:www.domain.com/xxxxxxx

手机站点地址往往是:www.domain.com/m/xxxxxxx

3,手机站点使用二级域名,电脑手机各一套2套站点代码


这种做法,工作量跟二级目录基本相似,严格来说代码量稍微多点。由于是2套代码,所以发布时候也得配备2套域名,不过要求两套站点连接同一个数据库来实现数据统一。例如:

电脑端网站域名是:www.domain.com

手机端网站域名是:m.domain.com




原文地址: https://www.opengps.cn/Blog/View.aspx?id=302 文章的更新编辑依此链接为准。欢迎关注源站原创文章!
 
0
评论

(客服云)iOS访客端集成常见问题(非报错) 客服云

kijieoeew 发表了文章 • 174 次浏览 • 2019-04-08 10:27 • 来自相关话题

1、UI上很多地方显示英文,比如聊天页面的工具栏




把客服demo中配置的国际化文件添加到您自己的工程中。拖之前要打开国际化文件,全部选中这三个,再进行拖入。




 
2、进入聊天页面没有加载聊天记录
这种情况一般出现在只使用了 HDMessageViewController 没有使用 HDChatViewController 的时候
在HDMessageViewController 的 viewDidLoad 方法中, 将 [self tableViewDidTriggerHeaderRefresh]; 的注释打开,再在这之前
加上 self.showRefreshHeader = YES; 这句代码
 
3、发送表情却显示字符串




把下面这段代码添加到appdelegate中就可以了
[[HDEmotionEscape sharedInstance] setEaseEmotionEscapePattern:@"\\[[^\\[\\]]{1,3}\\]"];
[[HDEmotionEscape sharedInstance] setEaseEmotionEscapeDictionary:[HDConvertToCommonEmoticonsHelper emotionsDictionary]];
 
4、客服能收到访客的消息,访客收不到客服的消息
(1)客服和im同时使用的话,初始化sdk、登录、登出用的是im的api会出现这种情况。必须使用客服的api。
(2)IM sdk升级为客服sdk,不兼容导致的,这种情况可以线上发起会话咨询。
     
5、发送的消息,出现在聊天页面的左侧
一般是由于当前访客没有登录或者登录失败,断点仔细检查下。 查看全部
1、UI上很多地方显示英文,比如聊天页面的工具栏
显示英文1.png

把客服demo中配置的国际化文件添加到您自己的工程中。拖之前要打开国际化文件,全部选中这三个,再进行拖入。
显示英文2.png

 
2、进入聊天页面没有加载聊天记录
这种情况一般出现在只使用了 HDMessageViewController 没有使用 HDChatViewController 的时候
在HDMessageViewController 的 viewDidLoad 方法中, 将 [self tableViewDidTriggerHeaderRefresh]; 的注释打开,再在这之前
加上 self.showRefreshHeader = YES; 这句代码
 
3、发送表情却显示字符串
访客端表情符号.png

把下面这段代码添加到appdelegate中就可以了
[[HDEmotionEscape sharedInstance] setEaseEmotionEscapePattern:@"\\[[^\\[\\]]{1,3}\\]"];
[[HDEmotionEscape sharedInstance] setEaseEmotionEscapeDictionary:[HDConvertToCommonEmoticonsHelper emotionsDictionary]];
 
4、客服能收到访客的消息,访客收不到客服的消息
(1)客服和im同时使用的话,初始化sdk、登录、登出用的是im的api会出现这种情况。必须使用客服的api。
(2)IM sdk升级为客服sdk,不兼容导致的,这种情况可以线上发起会话咨询。
     
5、发送的消息,出现在聊天页面的左侧
一般是由于当前访客没有登录或者登录失败,断点仔细检查下。
0
评论

(客服云)iOS访客端集成常见报错(总有一款适合你) 客服云 报错集锦 iOS访客端

kijieoeew 发表了文章 • 280 次浏览 • 2019-04-02 17:02 • 来自相关话题

注意:向自己工程中添加环信SDK和UI文件的时候,不要直接向xcode中拖拽添加,先把SDK和UI文件粘贴到自己工程的finder目录中,再从finder中向xcode中拖拽添加,避免出现找不到SDK或者UI文件的情况。
 
1、如果工程中引入的是不带音视频版本的sdk:HyphenateLite.framework和HelpDeskLite.framework,#import <HelpDeskLite/HelpDeskLite.h> 头文件报错的话,那么把HDMessage.h中的#import <Hyphenate/Hyphenate.h>改成#import <HyphenateLite/HyphenateLite.h>即可(HDMessage.h 用xcode全局搜索不到,需要到sdk里面去找)。
 
2、很多同学在首次“导入SDK”或“更新SDK重新导入SDK”后,Xcode运行报以下的error:
dyld: Library not loaded: @rpath/Hyphenate.framework/Hyphenate
  Referenced from: /Users/shenchong/Library/Developer/CoreSimulator/Devices/C768FE68-6E79-40C8-8AD1-FFFC434D51A9/data/Containers/Bundle/Application/41EA9A48-4DD5-4AA4-AB3F-139CFE036532/CallBackTest.app/CallBackTest
  Reason: image not found
       这个原因是工程未加载到 framework,正确的处理方式是在TARGETS → General → Embedded Binaries 中添加HelpDesk.framework和Hyphenate.framework依赖库,且 Linked Frameworks and Libraries中依赖库的Status必须是Required。




 
3、运行之后,自变量为nil,这就有可能是因为上面所说的依赖库的status设置为了Optional,需要改成Required。




 
4、打包后上传到appstore报错
(1)ERROR ITMS-90535: "Unexpected CFBundleExecutable Key. The bundle at 'Payload/toy.app/HelpDeskUIResource.bundle' does not contain a bundle executable. If this bundle intentionally does not contain an executable, consider removing the CFBundleExecutable key from its Info.plist and using a CFBundlePackageType of BNDL. If this bundle is part of a third-party framework, consider contacting the developer of the framework for an update to address this issue."
方法:把HelpDeskUIResource.bundle里的Info.plist删掉就即可。




(2)This bundle is invalid. The value for key CFBundleShortVersionString ‘1.2.2.1’in the Info.plist must be a period-separated list of at most three non-negative integers. 




把sdk里的plist文件的版本号改成3位数即可




(3)还有很多同学打包失败,看不出什么原因,那么可以先看看有没有按照文档剔除x86_64 i386两个平台
文档链接:http://docs.easemob.com/cs/300visitoraccess/iossdk#%E4%B8%8A%E4%BC%A0appstore%E4%BB%A5%E5%8F%8A%E6%89%93%E5%8C%85ipa%E6%B3%A8%E6%84%8F%E4%BA%8B%E9%A1%B9
 
5、那么剔除x86_64 i386时会遇到can't open input file的错误,这是因为cd的路径错误,把“/HelpDesk.framework”删掉。是cd到framework所在的路径,不是cd到framework




 
6、下图中的报错,需要在pch文件添加如下判断,环信的和自己的头文件都引入到#ifdef内部
   #ifdef __OBJC__
   #endif
   



   



 
7、集成环信HelpDeskUI的时候,由于HelpDeskUI内部使用了第三方库,如果与开发者第三方库产生冲突,可将HelpDeskUI中冲突的第三方库删除,如果第三方库中的接口有升级的部分,请酌情进行升级。




 
8、集成1.2.2版本demo中的HelpDeskUI,Masonry报错:Passing ‘CGFloat’(aka ‘double’) to parameter of incompatible type ‘__strong id’
需要在pch中添加#define MAS_SHORTHAND_GLOBALS




  查看全部
注意:向自己工程中添加环信SDK和UI文件的时候,不要直接向xcode中拖拽添加,先把SDK和UI文件粘贴到自己工程的finder目录中,再从finder中向xcode中拖拽添加,避免出现找不到SDK或者UI文件的情况。
 
1、如果工程中引入的是不带音视频版本的sdk:HyphenateLite.framework和HelpDeskLite.framework,#import <HelpDeskLite/HelpDeskLite.h> 头文件报错的话,那么把HDMessage.h中的#import <Hyphenate/Hyphenate.h>改成#import <HyphenateLite/HyphenateLite.h>即可(HDMessage.h 用xcode全局搜索不到,需要到sdk里面去找)。
 
2、很多同学在首次“导入SDK”或“更新SDK重新导入SDK”后,Xcode运行报以下的error:
dyld: Library not loaded: @rpath/Hyphenate.framework/Hyphenate
  Referenced from: /Users/shenchong/Library/Developer/CoreSimulator/Devices/C768FE68-6E79-40C8-8AD1-FFFC434D51A9/data/Containers/Bundle/Application/41EA9A48-4DD5-4AA4-AB3F-139CFE036532/CallBackTest.app/CallBackTest
  Reason: image not found
       这个原因是工程未加载到 framework,正确的处理方式是在TARGETS → General → Embedded Binaries 中添加HelpDesk.framework和Hyphenate.framework依赖库,且 Linked Frameworks and Libraries中依赖库的Status必须是Required。
访客端_image_not_found.png

 
3、运行之后,自变量为nil,这就有可能是因为上面所说的依赖库的status设置为了Optional,需要改成Required。
访客端自变量为nil.png

 
4、打包后上传到appstore报错
(1)ERROR ITMS-90535: "Unexpected CFBundleExecutable Key. The bundle at 'Payload/toy.app/HelpDeskUIResource.bundle' does not contain a bundle executable. If this bundle intentionally does not contain an executable, consider removing the CFBundleExecutable key from its Info.plist and using a CFBundlePackageType of BNDL. If this bundle is part of a third-party framework, consider contacting the developer of the framework for an update to address this issue."
方法:把HelpDeskUIResource.bundle里的Info.plist删掉就即可。
访客端打包90535.png

(2)This bundle is invalid. The value for key CFBundleShortVersionString ‘1.2.2.1’in the Info.plist must be a period-separated list of at most three non-negative integers. 
访客端打包90060.png

把sdk里的plist文件的版本号改成3位数即可
访客端打包1.2_.2_.1位置_.png

(3)还有很多同学打包失败,看不出什么原因,那么可以先看看有没有按照文档剔除x86_64 i386两个平台
文档链接:http://docs.easemob.com/cs/300visitoraccess/iossdk#%E4%B8%8A%E4%BC%A0appstore%E4%BB%A5%E5%8F%8A%E6%89%93%E5%8C%85ipa%E6%B3%A8%E6%84%8F%E4%BA%8B%E9%A1%B9
 
5、那么剔除x86_64 i386时会遇到can't open input file的错误,这是因为cd的路径错误,把“/HelpDesk.framework”删掉。是cd到framework所在的路径,不是cd到framework
访客端剔除cd错误.png

 
6、下图中的报错,需要在pch文件添加如下判断,环信的和自己的头文件都引入到#ifdef内部
   #ifdef __OBJC__
   #endif
   
pch加判断1.png

   
pch加判断2.png

 
7、集成环信HelpDeskUI的时候,由于HelpDeskUI内部使用了第三方库,如果与开发者第三方库产生冲突,可将HelpDeskUI中冲突的第三方库删除,如果第三方库中的接口有升级的部分,请酌情进行升级。
第三方库冲突.png

 
8、集成1.2.2版本demo中的HelpDeskUI,Masonry报错:Passing ‘CGFloat’(aka ‘double’) to parameter of incompatible type ‘__strong id’
需要在pch中添加#define MAS_SHORTHAND_GLOBALS
访客端Masonry报错.png

 
0
评论

(客服云)iOS访客端怎么判断会话是否结束 客服云 会话是否结束

kijieoeew 发表了文章 • 177 次浏览 • 2019-04-02 15:47 • 来自相关话题

1、联系商务开通【会话创建、接起、结束】功能
2、在 cmdMessagesDidReceive 方法中做如下判断,返回ServiceSessionClosedEvent,则是会话已结束,如图:




代码:
if ([message.body isKindOfClass:[EMCmdMessageBody class]]) {
EMCmdMessageBody *_bb = (EMCmdMessageBody *)message.body;
if ([_bb.action isEqualToString:@"ServiceSessionCreatedEvent"]) {
NSLog(@"hhhhh--Creat");
} else if ([_bb.action isEqualToString:@"ServiceSessionOpenedEvent"]) {
NSLog(@"hhhhh--Open");
} else if ([_bb.action isEqualToString:@"ServiceSessionClosedEvent"]) {
NSLog(@"hhhhh--Close");
}
}
  查看全部
1、联系商务开通【会话创建、接起、结束】功能
2、在 cmdMessagesDidReceive 方法中做如下判断,返回ServiceSessionClosedEvent,则是会话已结束,如图:
判断会话是否结束.png

代码:
if ([message.body isKindOfClass:[EMCmdMessageBody class]]) {
EMCmdMessageBody *_bb = (EMCmdMessageBody *)message.body;
if ([_bb.action isEqualToString:@"ServiceSessionCreatedEvent"]) {
NSLog(@"hhhhh--Creat");
} else if ([_bb.action isEqualToString:@"ServiceSessionOpenedEvent"]) {
NSLog(@"hhhhh--Open");
} else if ([_bb.action isEqualToString:@"ServiceSessionClosedEvent"]) {
NSLog(@"hhhhh--Close");
}
}
 
0
评论

(客服云)iOS访客端点击订单消息 客服云 访客订单消息

kijieoeew 发表了文章 • 170 次浏览 • 2019-03-29 18:01 • 来自相关话题

1、在HDMessageCell.m 的
- (void)_setupSubviewsWithType:(EMMessageBodyType)messageType
                      isSender:(BOOL)isSender
                         model:(id<HDIMessageModel>)model
方法中给orderBgView 添加手势




代码:
UITapGestureRecognizer *tapRecognizer3 = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(orderImageViewTapAction:)];
[_bubbleView.orderBgView addGestureRecognizer:tapRecognizer3];
 
2、在HDMessageCell.m 中添加手势点击事件




代码:
- (void)orderImageViewTapAction:(UITapGestureRecognizer *)tapRecognizer
{
    if ([_delegate respondsToSelector:@selector(messageCellSelected:)]) {
        [_delegate messageCellSelected:_model];
    }
}
 
3、在HDMessageViewController的 
   - (void)messageCellSelected:(id<HDIMessageModel>)model 方法中添加订单消息的判断




代码:
if ([HDMessageHelper getMessageExtType:model.message] == HDExtOrderMsg) {
                // 订单消息携带的扩展
                NSDictionary *dic = model.message.ext;
                NSLog(@"点击了订单消息");
}
 
  查看全部
1、在HDMessageCell.m 的
- (void)_setupSubviewsWithType:(EMMessageBodyType)messageType
                      isSender:(BOOL)isSender
                         model:(id<HDIMessageModel>)model
方法中给orderBgView 添加手势
点击订单消息1.png

代码:
UITapGestureRecognizer *tapRecognizer3 = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(orderImageViewTapAction:)];
[_bubbleView.orderBgView addGestureRecognizer:tapRecognizer3];
 
2、在HDMessageCell.m 中添加手势点击事件
点击订单消息2.png

代码:
- (void)orderImageViewTapAction:(UITapGestureRecognizer *)tapRecognizer
{
    if ([_delegate respondsToSelector:@selector(messageCellSelected:)]) {
        [_delegate messageCellSelected:_model];
    }
}
 
3、在HDMessageViewController的 
   - (void)messageCellSelected:(id<HDIMessageModel>)model 方法中添加订单消息的判断
点击订单消息3.png

代码:
if ([HDMessageHelper getMessageExtType:model.message] == HDExtOrderMsg) {
                // 订单消息携带的扩展
                NSDictionary *dic = model.message.ext;
                NSLog(@"点击了订单消息");
}
 
 
0
评论

(客服云)IOS访客端设置访客昵称头像 客服云 昵称头像 访客昵称头像

kijieoeew 发表了文章 • 180 次浏览 • 2019-03-29 18:01 • 来自相关话题

1.在HDMessageViewController.h 中添加访客昵称、头像的属性
// 访客昵称
@property (nonatomic, strong) NSString *sendName;
// 访客头像(url)
@property (nonatomic, strong) NSString *sendAvatarUrl;
// 访客头像(本地图片)
@property (nonatomic, strong) UIImage *sendAvatarImage;






 
2.在HDMessageViewController.m   - (NSArray *)formatMessages:(NSArray *)messages 方法中添加判断






if (isSender) {
            if (self.sendName) {
                model.nickname = self.sendName;
            }
            // 加载网络头像
            if (self.sendAvatarUrl) {
                model.avatarURLPath = self.sendAvatarUrl;
            }
            // 加载本地头像
            if (self.sendAvatarImage) {
                model.avatarImage = self.sendAvatarImage;
                model.avatarURLPath = nil;
            }
        }
 
3.在初始化聊天页面的时候,传入访客的昵称、头像即可。
(可选择url或者本地头像图片)






ctrl.sendName = @"访客昵称";
ctrl.sendAvatarImage = [UIImage imageNamed:@"测试图片"];
//    chat.sendAvatarUrl = @""; 查看全部
1.在HDMessageViewController.h 中添加访客昵称、头像的属性
// 访客昵称
@property (nonatomic, strong) NSString *sendName;
// 访客头像(url)
@property (nonatomic, strong) NSString *sendAvatarUrl;
// 访客头像(本地图片)
@property (nonatomic, strong) UIImage *sendAvatarImage;

1.png


 
2.在HDMessageViewController.m   - (NSArray *)formatMessages:(NSArray *)messages 方法中添加判断

2.png


if (isSender) {
            if (self.sendName) {
                model.nickname = self.sendName;
            }
            // 加载网络头像
            if (self.sendAvatarUrl) {
                model.avatarURLPath = self.sendAvatarUrl;
            }
            // 加载本地头像
            if (self.sendAvatarImage) {
                model.avatarImage = self.sendAvatarImage;
                model.avatarURLPath = nil;
            }
        }
 
3.在初始化聊天页面的时候,传入访客的昵称、头像即可。
(可选择url或者本地头像图片)

3.png


ctrl.sendName = @"访客昵称";
ctrl.sendAvatarImage = [UIImage imageNamed:@"测试图片"];
//    chat.sendAvatarUrl = @"";
0
评论

(客服云)iOS访客端设置客服系统头像 客服云 客服系统头像 昵称头像

kijieoeew 发表了文章 • 167 次浏览 • 2019-03-29 18:01 • 来自相关话题

0、在客服系统内 管理员模式--设置--企业基本信息 处上传企业logo
   在 管理员模式--设置--系统开关--系统开关--访客端显示客服昵称 处打开开关








1、在HDIMessageModel.h 中添加客服系统头像url属性
@property (strong, nonatomic) NSString *officialAccountURL;




 
2、在HDMessageModel.h 中添加客服系统头像url属性
@property (strong, nonatomic) NSString *officialAccountURL;




 
3、在HDMessageModel.m类  - (instancetype)initWitMessage:(HDMessage *)message方法中添加  

            NSDictionary *officialAccount = [NSDictionary dictionary];
                if ([weichat objectForKey:@"official_account"]) {
                    officialAccount = [weichat valueForKey:@"official_account"];
                    if ([officialAccount objectForKey:@"img"]) {
                        self.officialAccountURL = [[@"https:" stringByAppendingString:[officialAccount objectForKey:@"img"]] stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
                    }
                }




 
4、在HDBaseMessageCell.m类  - (void)setModel:(id<HDIMessageModel>)model方法中 修改代码 
(“系统消息”改成您自己客服系统中设置的调度员昵称)

    if (model.avatarURLPath) {
        if (model.nickname) {
            if ([model.nickname isEqualToString:@"系统消息"]) {
                if (model.officialAccountURL) {
                    [self.avatarView sd_setImageWithURL:[NSURL URLWithString:model.officialAccountURL] placeholderImage:model.avatarImage];
                }
            } else {
                    [self.avatarView sd_setImageWithURL:[NSURL URLWithString:model.avatarURLPath] placeholderImage:model.avatarImage];
            }
        }
    
    } else {
        self.avatarView.image = model.avatarImage;
    } 查看全部
0、在客服系统内 管理员模式--设置--企业基本信息 处上传企业logo
   在 管理员模式--设置--系统开关--系统开关--访客端显示客服昵称 处打开开关
客服系统头像1.png

客服系统头像2.png

1、在HDIMessageModel.h 中添加客服系统头像url属性
@property (strong, nonatomic) NSString *officialAccountURL;
客服系统头像3.png

 
2、在HDMessageModel.h 中添加客服系统头像url属性
@property (strong, nonatomic) NSString *officialAccountURL;
客服系统头像4.png

 
3、在HDMessageModel.m类  - (instancetype)initWitMessage:(HDMessage *)message方法中添加  

            NSDictionary *officialAccount = [NSDictionary dictionary];
                if ([weichat objectForKey:@"official_account"]) {
                    officialAccount = [weichat valueForKey:@"official_account"];
                    if ([officialAccount objectForKey:@"img"]) {
                        self.officialAccountURL = [[@"https:" stringByAppendingString:[officialAccount objectForKey:@"img"]] stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
                    }
                }
客服系统头像5.png

 
4、在HDBaseMessageCell.m类  - (void)setModel:(id<HDIMessageModel>)model方法中 修改代码 
(“系统消息”改成您自己客服系统中设置的调度员昵称)

    if (model.avatarURLPath) {
        if (model.nickname) {
            if ([model.nickname isEqualToString:@"系统消息"]) {
                if (model.officialAccountURL) {
                    [self.avatarView sd_setImageWithURL:[NSURL URLWithString:model.officialAccountURL] placeholderImage:model.avatarImage];
                }
            } else {
                    [self.avatarView sd_setImageWithURL:[NSURL URLWithString:model.avatarURLPath] placeholderImage:model.avatarImage];
            }
        }
    
    } else {
        self.avatarView.image = model.avatarImage;
    }
客服系统头像6.png
0
评论

(客服云)iOS访客端获取机器人欢迎语 客服云 机器人欢迎语

kijieoeew 发表了文章 • 176 次浏览 • 2019-03-29 18:01 • 来自相关话题

注意:
1、此文档只支持获取单机器人的欢迎语,多机器人会获取第一个机器人的欢迎语。
2、此文档暂不支持获取图文消息类型的机器人欢迎语。
老版机器人与新版机器人集成方法不同,集成前需区分好。









一、先到客服系统配置机器人欢迎语
1、老版机器人设置方案:
管理员模式--智能机器人--机器人设置--自动回复--欢迎语,开启开关并添加欢迎语




2、新版(企业版)机器人设置方案:
(1)管理员模式--智能机器人--机器人设置--基础设置,点击“机器人管理”跳转到机器人管理页面
(2)机器人设置--自动回复--欢迎语,开启开关并添加欢迎语




 
二、iOS端获取、解析机器人欢迎语
在HDMessageViewController.m类的 -(void)viewDidLoad 方法最后调用robotWelcome或者newRobotWelcome 方法即可。

//获取老版机器人欢迎语
- (void)robotWelcome
{
    // kDefaultTenantId:租户id
    // kDefaultCustomerName:IM服务号 stringWithFormat:@"https://kefu.easemob.com/v1/Te ... ot%3B, kDefaultTenantId];
    NSString *newStr = [urlStr stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
    NSURL *url = [NSURL URLWithString:newStr];
    NSURLRequest *requst = [NSURLRequest requestWithURL:url cachePolicy:NSURLRequestReloadIgnoringLocalCacheData timeoutInterval:10];
    //异步链接(形式1,较少用)
    [NSURLConnection sendAsynchronousRequest:requst queue:[NSOperationQueue mainQueue] completionHandler:^(NSURLResponse *response, NSData *data, NSError *connectionError) {
        
        // 解析
        NSString *result  =[[ NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
        //同样的可以替换字符
        NSLog(@"result-----%@", result);
        NSString *str = [result stringByReplacingOccurrencesOfString:@"&quot;" withString:@"\""];
        NSString *str1 = [str stringByReplacingOccurrencesOfString:@"\"{" withString:@"{"];
        NSString *str2 = [str1 stringByReplacingOccurrencesOfString:@"}\"" withString:@"}"];
        // JSON字符串转字典
        NSDictionary *dic = [self dictionaryWithJsonString:str2];
        // 取消息的ext
        NSLog(@"dic---%@",dic);
        
        NSString *robotText = nil;
        NSDictionary *dicExt = [NSDictionary dictionary];
        if ([[dic objectForKey:@"greetingText"] isKindOfClass:[NSString class]]) {
           
            robotText = [dic objectForKey:@"greetingText"];
        } else {
             dicExt = [[dic objectForKey:@"greetingText"] objectForKey:@"ext"];
        }
        
                //构建消息
                EMTextMessageBody *bdy = [[EMTextMessageBody alloc] initWithText:robotText];
                NSString *from = [[HDClient sharedClient] currentUsername];

                    HDMessage *message = [[HDMessage alloc] initWithConversationID:kDefaultCustomerName from:from to:kDefaultCustomerName body:bdy];
                   message.ext = dicExt;
                    message.direction = 1;                
               message.status = HDMessageStatusSuccessed;
                // 消息添加到UI
                [self addMessageToDataSource:message progress:nil];
                // 消息插入到会话
                HDError *pError;
                [self.conversation addMessage:message error:&pError];
        
    }];
    
}

//获取新版(企业版)机器人欢迎语
- (void)newRobotWelcome
{
    [[HDClient sharedClient] accessToken];
    // 以下信息换成自己的
    // kDefaultTenantId:租户id
    // kDefaultOrgName:appkey中#的前半部分
    // kDefaultAppName:appkey中#的后半部分
    // kDefaultCustomerName:IM服务号
    NSString *imToken = [HDClient sharedClient].accessToken;
    NSString *urlStr = [NSString stringWithFormat:@"https://kefu.easemob.com/v1/we ... ot%3B,kDefaultTenantId, kDefaultOrgName, kDefaultAppName, kDefaultCustomerName, imToken];
    NSString *newStr = [urlStr stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
    NSLog(@"newStr---%@", newStr);
    NSURL *url = [NSURL URLWithString:newStr];
    NSURLRequest *requst = [NSURLRequest requestWithURL:url cachePolicy:NSURLRequestReloadIgnoringLocalCacheData timeoutInterval:10];
    //异步链接(形式1,较少用)
    [NSURLConnection sendAsynchronousRequest:requst queue:[NSOperationQueue mainQueue] completionHandler:^(NSURLResponse *response, NSData *data, NSError *connectionError) {
        // 解析
        NSString *result = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
        //同样的可以替换字符
        NSString *str = [result stringByReplacingOccurrencesOfString:@"&amp;quot;" withString:@"\""];
        NSString *str1 = [str stringByReplacingOccurrencesOfString:@"\"{" withString:@"{"];
        NSString *str2 = [str1 stringByReplacingOccurrencesOfString:@"}\"" withString:@"}"];
        // JSON字符串转字典
        NSDictionary *dic = [self dictionaryWithJsonString:str2];
        // 取消息的ext
        NSString *robotText = nil;
        NSDictionary *dicExt = [NSDictionary dictionary];
        if ([[[dic objectForKey:@"entity"] objectForKey:@"greetingText"] isKindOfClass:[NSString class]]) {
            robotText = [[dic objectForKey:@"entity"] objectForKey:@"greetingText"];
        } else {
            dicExt = [[[dic objectForKey:@"entity"] objectForKey:@"greetingText"] objectForKey:@"ext"];
        }  
        //构建消息
        NSLog(@"dicExt---%@",dicExt);
        EMTextMessageBody *bdy = [[EMTextMessageBody alloc] initWithText:robotText];
        NSString *from = [[HDClient sharedClient] currentUsername];
        
        HDMessage *message = [[HDMessage alloc] initWithConversationID:kDefaultCustomerName from:from to:kDefaultCustomerName body:bdy];
        message.ext = dicExt;
        message.direction = 1;
        message.status = HDMessageStatusSuccessed;
        // 消息添加到UI
        [self addMessageToDataSource:message progress:nil];
        // 消息插入到会话
        HDError *pError;
        [self.conversation addMessage:message error:&pError];
    }];    
}

// JSON字符串转化为字典
- (NSDictionary *)dictionaryWithJsonString:(NSString *)jsonString
{
    if (jsonString == nil) {
        return nil;
    }
    NSData *jsonData = [jsonString dataUsingEncoding:NSUTF8StringEncoding];
    NSError *err;
    NSDictionary *dic = [NSJSONSerialization JSONObjectWithData:jsonData
                                                        options:NSJSONReadingMutableContainers
                                                          error:&err];
    if(err)
    {
        NSLog(@"json解析失败:%@",err);
        return nil;
    }
return dic;
}
  查看全部
注意:
1、此文档只支持获取单机器人的欢迎语,多机器人会获取第一个机器人的欢迎语。
2、此文档暂不支持获取图文消息类型的机器人欢迎语。
老版机器人与新版机器人集成方法不同,集成前需区分好。
机器人欢迎语1.png

机器人欢迎语2.png


一、先到客服系统配置机器人欢迎语
1、老版机器人设置方案:
管理员模式--智能机器人--机器人设置--自动回复--欢迎语,开启开关并添加欢迎语
机器人欢迎语3.png

2、新版(企业版)机器人设置方案:
(1)管理员模式--智能机器人--机器人设置--基础设置,点击“机器人管理”跳转到机器人管理页面
(2)机器人设置--自动回复--欢迎语,开启开关并添加欢迎语
机器人欢迎语4.png

 
二、iOS端获取、解析机器人欢迎语
在HDMessageViewController.m类的 -(void)viewDidLoad 方法最后调用robotWelcome或者newRobotWelcome 方法即可。

//获取老版机器人欢迎语
- (void)robotWelcome
{
    // kDefaultTenantId:租户id
    // kDefaultCustomerName:IM服务号 stringWithFormat:@"https://kefu.easemob.com/v1/Te ... ot%3B, kDefaultTenantId];
    NSString *newStr = [urlStr stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
    NSURL *url = [NSURL URLWithString:newStr];
    NSURLRequest *requst = [NSURLRequest requestWithURL:url cachePolicy:NSURLRequestReloadIgnoringLocalCacheData timeoutInterval:10];
    //异步链接(形式1,较少用)
    [NSURLConnection sendAsynchronousRequest:requst queue:[NSOperationQueue mainQueue] completionHandler:^(NSURLResponse *response, NSData *data, NSError *connectionError) {
        
        // 解析
        NSString *result  =[[ NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
        //同样的可以替换字符
        NSLog(@"result-----%@", result);
        NSString *str = [result stringByReplacingOccurrencesOfString:@"&quot;" withString:@"\""];
        NSString *str1 = [str stringByReplacingOccurrencesOfString:@"\"{" withString:@"{"];
        NSString *str2 = [str1 stringByReplacingOccurrencesOfString:@"}\"" withString:@"}"];
        // JSON字符串转字典
        NSDictionary *dic = [self dictionaryWithJsonString:str2];
        // 取消息的ext
        NSLog(@"dic---%@",dic);
        
        NSString *robotText = nil;
        NSDictionary *dicExt = [NSDictionary dictionary];
        if ([[dic objectForKey:@"greetingText"] isKindOfClass:[NSString class]]) {
           
            robotText = [dic objectForKey:@"greetingText"];
        } else {
             dicExt = [[dic objectForKey:@"greetingText"] objectForKey:@"ext"];
        }
        
                //构建消息
                EMTextMessageBody *bdy = [[EMTextMessageBody alloc] initWithText:robotText];
                NSString *from = [[HDClient sharedClient] currentUsername];

                    HDMessage *message = [[HDMessage alloc] initWithConversationID:kDefaultCustomerName from:from to:kDefaultCustomerName body:bdy];
                   message.ext = dicExt;
                    message.direction = 1;                
               message.status = HDMessageStatusSuccessed;
                // 消息添加到UI
                [self addMessageToDataSource:message progress:nil];
                // 消息插入到会话
                HDError *pError;
                [self.conversation addMessage:message error:&pError];
        
    }];
    
}

//获取新版(企业版)机器人欢迎语
- (void)newRobotWelcome
{
    [[HDClient sharedClient] accessToken];
    // 以下信息换成自己的
    // kDefaultTenantId:租户id
    // kDefaultOrgName:appkey中#的前半部分
    // kDefaultAppName:appkey中#的后半部分
    // kDefaultCustomerName:IM服务号
    NSString *imToken = [HDClient sharedClient].accessToken;
    NSString *urlStr = [NSString stringWithFormat:@"https://kefu.easemob.com/v1/we ... ot%3B,kDefaultTenantId, kDefaultOrgName, kDefaultAppName, kDefaultCustomerName, imToken];
    NSString *newStr = [urlStr stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
    NSLog(@"newStr---%@", newStr);
    NSURL *url = [NSURL URLWithString:newStr];
    NSURLRequest *requst = [NSURLRequest requestWithURL:url cachePolicy:NSURLRequestReloadIgnoringLocalCacheData timeoutInterval:10];
    //异步链接(形式1,较少用)
    [NSURLConnection sendAsynchronousRequest:requst queue:[NSOperationQueue mainQueue] completionHandler:^(NSURLResponse *response, NSData *data, NSError *connectionError) {
        // 解析
        NSString *result = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
        //同样的可以替换字符
        NSString *str = [result stringByReplacingOccurrencesOfString:@"&amp;quot;" withString:@"\""];
        NSString *str1 = [str stringByReplacingOccurrencesOfString:@"\"{" withString:@"{"];
        NSString *str2 = [str1 stringByReplacingOccurrencesOfString:@"}\"" withString:@"}"];
        // JSON字符串转字典
        NSDictionary *dic = [self dictionaryWithJsonString:str2];
        // 取消息的ext
        NSString *robotText = nil;
        NSDictionary *dicExt = [NSDictionary dictionary];
        if ([[[dic objectForKey:@"entity"] objectForKey:@"greetingText"] isKindOfClass:[NSString class]]) {
            robotText = [[dic objectForKey:@"entity"] objectForKey:@"greetingText"];
        } else {
            dicExt = [[[dic objectForKey:@"entity"] objectForKey:@"greetingText"] objectForKey:@"ext"];
        }  
        //构建消息
        NSLog(@"dicExt---%@",dicExt);
        EMTextMessageBody *bdy = [[EMTextMessageBody alloc] initWithText:robotText];
        NSString *from = [[HDClient sharedClient] currentUsername];
        
        HDMessage *message = [[HDMessage alloc] initWithConversationID:kDefaultCustomerName from:from to:kDefaultCustomerName body:bdy];
        message.ext = dicExt;
        message.direction = 1;
        message.status = HDMessageStatusSuccessed;
        // 消息添加到UI
        [self addMessageToDataSource:message progress:nil];
        // 消息插入到会话
        HDError *pError;
        [self.conversation addMessage:message error:&pError];
    }];    
}

// JSON字符串转化为字典
- (NSDictionary *)dictionaryWithJsonString:(NSString *)jsonString
{
    if (jsonString == nil) {
        return nil;
    }
    NSData *jsonData = [jsonString dataUsingEncoding:NSUTF8StringEncoding];
    NSError *err;
    NSDictionary *dic = [NSJSONSerialization JSONObjectWithData:jsonData
                                                        options:NSJSONReadingMutableContainers
                                                          error:&err];
    if(err)
    {
        NSLog(@"json解析失败:%@",err);
        return nil;
    }
return dic;
}
 
0
评论

客服满意度评价设置 客服满意度评价设置

lizg 发表了文章 • 178 次浏览 • 2019-03-29 17:29 • 来自相关话题

1、怎么设置会话结束时自动发送评价邀请
    管理员模式­----­­设置­­­----满意度评价邀请设置 第一项:会话结束自动发送满意度评价邀请,勾选需要设置的访客渠道

2、访客是否能多次评价
   访客的评价次数是不限制的,但是可以设置客服后台统计的评为为第一次还是最后一次,可以在管理员模式­----­­设置­­­----  满意度评价邀请设置 第三项:多次满意度评价只取第一次
3、系统结束的会话是否自动发送满意度评价
  管理员模式­----­­设置­­­----满意度评价邀请设置  开关打开时,因访客超时未回复、不活跃超时而自动结束的会话,系统不会自动推送满意度评价

4、如何设置满意度邀请提示语
管理员模式­----­­设置­­­----满意度评价邀请设置  第6项

  查看全部

1、怎么设置会话结束时自动发送评价邀请
    管理员模式­----­­设置­­­----满意度评价邀请设置 第一项:会话结束自动发送满意度评价邀请,勾选需要设置的访客渠道

2、访客是否能多次评价
   访客的评价次数是不限制的,但是可以设置客服后台统计的评为为第一次还是最后一次,可以在管理员模式­----­­设置­­­----  满意度评价邀请设置 第三项:多次满意度评价只取第一次
3、系统结束的会话是否自动发送满意度评价
  管理员模式­----­­设置­­­----满意度评价邀请设置  开关打开时,因访客超时未回复、不活跃超时而自动结束的会话,系统不会自动推送满意度评价

4、如何设置满意度邀请提示语
管理员模式­----­­设置­­­----满意度评价邀请设置  第6项

 
0
评论

android客服云如何获取机器人欢迎语 环信机器人欢迎语

lizg 发表了文章 • 169 次浏览 • 2019-03-29 15:50 • 来自相关话题

1.APP端要想获取到机器人菜单欢迎语,首先在移动客服管理员模式下,智能机器人,机器人设置——自动回复——欢迎语配置菜单欢迎语或者是欢迎语消息。
2、在会话分配规则中,渠道指定一定要指定机器人,APP端才能获取到机器人欢迎语,否则获取不到
具体的代码如下附件
图1中的
MyApplication.tenantId记得替换为自己的tenantId
new Thread(new Runnable() {
                    @Override
                    public void run() {
                        HttpClient httpClient = new DefaultHttpClient();
                        HttpGet httpGet = new HttpGet("http://kefu.easemob.com/v1/Ten ... 6quot;);
                        try {
                            HttpResponse response = httpClient.execute(httpGet);
                            int code = response.getStatusLine().getStatusCode();
                            if (code == 200) {
                                final String rev  = EntityUtils.toString(response.getEntity());

                                JSONObject obj = new JSONObject(rev);
                                int type = obj.getInt("greetingTextType");
                                final String rob_welcome = obj.getString("greetingText");
                                //type为0代表是文字消息的机器人欢迎语
                                //type为1代表是菜单消息的机器人欢迎语
                                if(type == 0){
                                    //把解析拿到的string保存在本地
                                    shareUtil.saveRobot(rob_welcome);
                                    runOnUiThread(new Runnable() {
                                        @Override
                                        public void run() {
                                            Toast.makeText(getApplicationContext(),"rob_welcome="+rob_welcome,Toast.LENGTH_SHORT).show();
                                        }
                                    });
                                }else if(type == 1){
                                    final String str = rob_welcome.replaceAll("&quot;","\"");
                                    JSONObject json = new JSONObject(str);
                                    JSONObject ext = json.getJSONObject("ext");
                                    final JSONObject msgtype = ext.getJSONObject("msgtype");
                                    //把解析拿到的string保存在本地
                                    shareUtil.saveRobot(msgtype.toString());
                                    runOnUiThread(new Runnable() {
                                        @Override
                                        public void run() {
                                            Toast.makeText(getApplicationContext(),"rob_welcome="+msgtype,Toast.LENGTH_SHORT).show();
                                        }
                                    });
                                }

                            }
                        }catch(final Exception e){
                            runOnUiThread(new Runnable() {
                                @Override
                                public void run() {
                                    Toast.makeText(getApplicationContext(),"exception="+e.getMessage(),Toast.LENGTH_SHORT).show();
                                }
                            });
                        }
                    }
                }).start();


图2中的:
//创建消息插入本地
            Message message = Message.createReceiveMessage(Message.Type.TXT);
            //从本地获取保存的string
            String str = shareUtil.getRobot();
            EMTextMessageBody body = null;
            //判断是否是菜单消息的string,这是自己实现的一个方法
            if(!isRobotMenu(str)){
                //文字消息直接去设置给消息
            body = new EMTextMessageBody(str);
            }else{
                //菜单消息需要设置给消息扩展
                try{
                    body = new EMTextMessageBody("");
                    JSONObject msgtype = new JSONObject(str);
                    message.setAttribute("msgtype",msgtype);
                }catch (Exception e){
                    Toast.makeText(this,"exception="+e.getMessage(),Toast.LENGTH_SHORT).show();
                }
}

            message.setFrom(MyApplication.imService);
            message.addBody(body);
            message.setMsgTime(System.currentTimeMillis());
            message.setStatus(Message.Status.SUCCESS);
            message.setMsgId(UUID.randomUUID().toString());
ChatClient.getInstance().chatManager().saveMessage(message);



图3中的:
private boolean isRobotMenu(String str){
        try {
            JSONObject json = new JSONObject(str);
            JSONObject obj = json.getJSONObject("choice");
        }catch (Exception e){
            return false;
        }
        return true;
}
 
  查看全部
1.APP端要想获取到机器人菜单欢迎语,首先在移动客服管理员模式下,智能机器人,机器人设置——自动回复——欢迎语配置菜单欢迎语或者是欢迎语消息。
2、在会话分配规则中,渠道指定一定要指定机器人,APP端才能获取到机器人欢迎语,否则获取不到
具体的代码如下附件
图1中的
MyApplication.tenantId记得替换为自己的tenantId
new Thread(new Runnable() {
                    @Override
                    public void run() {
                        HttpClient httpClient = new DefaultHttpClient();
                        HttpGet httpGet = new HttpGet("http://kefu.easemob.com/v1/Ten ... 6quot;);
                        try {
                            HttpResponse response = httpClient.execute(httpGet);
                            int code = response.getStatusLine().getStatusCode();
                            if (code == 200) {
                                final String rev  = EntityUtils.toString(response.getEntity());

                                JSONObject obj = new JSONObject(rev);
                                int type = obj.getInt("greetingTextType");
                                final String rob_welcome = obj.getString("greetingText");
                                //type为0代表是文字消息的机器人欢迎语
                                //type为1代表是菜单消息的机器人欢迎语
                                if(type == 0){
                                    //把解析拿到的string保存在本地
                                    shareUtil.saveRobot(rob_welcome);
                                    runOnUiThread(new Runnable() {
                                        @Override
                                        public void run() {
                                            Toast.makeText(getApplicationContext(),"rob_welcome="+rob_welcome,Toast.LENGTH_SHORT).show();
                                        }
                                    });
                                }else if(type == 1){
                                    final String str = rob_welcome.replaceAll("&quot;","\"");
                                    JSONObject json = new JSONObject(str);
                                    JSONObject ext = json.getJSONObject("ext");
                                    final JSONObject msgtype = ext.getJSONObject("msgtype");
                                    //把解析拿到的string保存在本地
                                    shareUtil.saveRobot(msgtype.toString());
                                    runOnUiThread(new Runnable() {
                                        @Override
                                        public void run() {
                                            Toast.makeText(getApplicationContext(),"rob_welcome="+msgtype,Toast.LENGTH_SHORT).show();
                                        }
                                    });
                                }

                            }
                        }catch(final Exception e){
                            runOnUiThread(new Runnable() {
                                @Override
                                public void run() {
                                    Toast.makeText(getApplicationContext(),"exception="+e.getMessage(),Toast.LENGTH_SHORT).show();
                                }
                            });
                        }
                    }
                }).start();


图2中的:
//创建消息插入本地
            Message message = Message.createReceiveMessage(Message.Type.TXT);
            //从本地获取保存的string
            String str = shareUtil.getRobot();
            EMTextMessageBody body = null;
            //判断是否是菜单消息的string,这是自己实现的一个方法
            if(!isRobotMenu(str)){
                //文字消息直接去设置给消息
            body = new EMTextMessageBody(str);
            }else{
                //菜单消息需要设置给消息扩展
                try{
                    body = new EMTextMessageBody("");
                    JSONObject msgtype = new JSONObject(str);
                    message.setAttribute("msgtype",msgtype);
                }catch (Exception e){
                    Toast.makeText(this,"exception="+e.getMessage(),Toast.LENGTH_SHORT).show();
                }
}

            message.setFrom(MyApplication.imService);
            message.addBody(body);
            message.setMsgTime(System.currentTimeMillis());
            message.setStatus(Message.Status.SUCCESS);
            message.setMsgId(UUID.randomUUID().toString());
ChatClient.getInstance().chatManager().saveMessage(message);



图3中的:
private boolean isRobotMenu(String str){
        try {
            JSONObject json = new JSONObject(str);
            JSONObject obj = json.getJSONObject("choice");
        }catch (Exception e){
            return false;
        }
        return true;
}
 
 
0
评论

(客服云)知识库添加了附件,怎么发送的时候附件没有发送出去? 知识库附件不显示 知识库附件

kijieoeew 发表了文章 • 143 次浏览 • 2019-03-29 15:36 • 来自相关话题

附件需要单独发送给访客。发送知识时,图文消息仅包含知识标题和知识内容,不包含知识的附件。若发送知识中的附件,需打开知识详情,并点击附件右上角的“发送”按钮。
 
附件需要单独发送给访客。发送知识时,图文消息仅包含知识标题和知识内容,不包含知识的附件。若发送知识中的附件,需打开知识详情,并点击附件右上角的“发送”按钮。
 
0
评论

(客服云)iOS访客端插入企业欢迎语 企业欢迎语

kijieoeew 发表了文章 • 157 次浏览 • 2019-03-29 15:15 • 来自相关话题

0、前提配置
(1)在客服系统内 管理员模式--设置--系统开关--企业欢迎语 处设置欢迎语,并开启开关




(2)联系商务开通会话创建、接起、结束的透传事件功能

1、在HDMessageViewController.m中添加企业欢迎语属性
// 企业欢迎语
@property (nonatomic, strong) NSString *companyWelcome;




 
2、在HDMessageViewController.m中的 viewdidload 方法的最后调用 [self judge]; 
// 这个方法是属于逻辑判断,用UD去记录参数,只有第一次进入聊天页面和会话结束之后再进入到聊天页面才插入消息。
- (void)judge
{
    // 判断会话中是否有消息
    if (self.conversation.latestMessage == nil) {
        // 如果会话中没有消息,那么UD的值变成NO
        [[NSUserDefaults standardUserDefaults] setBool:NO forKey:@"isGetWelcome"];
    }
    // 根据以前的判断,如果UD值为NO则插入消息欢迎语
    if (![[NSUserDefaults standardUserDefaults] boolForKey:@"isGetWelcome"]) {
        // 插入企业欢迎语
        [self welcome];
    }
    
    [[NSUserDefaults standardUserDefaults] setBool:YES forKey:@"isGetWelcome"];
}
 
// 这个方法是构建消息以及向UI页面以及本地数据库插入消息
- (void)welcome
{
    // 此方法是SDK获取企业欢迎语的
    __weak typeof(self) weakself = self;
    [[HDClient sharedClient].chatManager getEnterpriseWelcomeWithCompletion:^(NSString *welcome, HDError *error) {
        // 判断客服系统中是否设置了企业欢迎语并且开关有没有打开,兼容误操作客服关闭了企业欢迎语开关,会插入一条空消息,那么自己选择是否插入一条默认的欢迎语,自行修改  ‘您好,欢迎光临!’ 的内容
        if (![welcome isEqualToString:@""]) {
            weakself.companyWelcome = welcome;
        } else {
            weakself.companyWelcome = @"您好,欢迎光临!";
        }
        // 构建消息
        EMTextMessageBody *bdy = [[EMTextMessageBody alloc] initWithText:weakself.companyWelcome];
        NSString *from = [[HDClient sharedClient] currentUsername];
        // _converID就是IM服务号,可以自己替换下
        HDMessage *message = [[HDMessage alloc] initWithConversationID:weakself.conversation.conversationId from:from to:weakself.conversation.conversationId body:bdy];
        // 构建的消息用ext来标记此条消息是插入的欢迎语,然后在cell里面根据消息的ext来修改插入消息的昵称和头像
        NSDictionary *welcomeExt = @{@"insertWelcome":@"插入的欢迎语"};
        [message addAttributeDictionary:welcomeExt];   
        message.direction = 1;
        message.status = HDMessageStatusSuccessed;
        // 更新UI
        dispatch_async(dispatch_get_main_queue(), ^{
            // 消息添加到数据源,刷新UI
            [weakself addMessageToDataSource:message progress:nil];
        });
        // 消息插入到会话
        HDError *pError;
        [weakself.conversation addMessage:message error:&pError];
        
    }];
}








 
3、在HDMessageViewController.m中的cmdMessagesDidReceive方法中调用 messageExtwithEventName




这个方法是判断接收到客服消息的ext里面有没有ServiceSessionClosedEvent,如果有就把UD记录改变参数,下次进入聊天页面的时候插入欢迎语
如果是ServiceSessionOpenedEvent,客服系统接入了会话,那么就不插入欢迎语

这个方法是接收透传消息里面调用,客服系统会话被接入会给app端发送透传消息通知,如果没有接收到透传消息,那么找对接的环信商务开通这个功能
[self messageExtwithEventName:message];

- (void)messageExtwithEventName:(HDMessage *)message
{
    if (![[[message.ext objectForKey:@"weichat"] objectForKey:@"event"] isKindOfClass:[NSNull class]]) {
        NSDictionary *dict = [[message.ext objectForKey:@"weichat"] objectForKey:@"event"];
        if ([[dict objectForKey:@"eventName"] isEqualToString:@"ServiceSessionClosedEvent"]) {
            //如果接收到客服的消息ext中有 ServiceSessionClosedEvent,表示会话已经结束,那么UD的值变成NO
            [[NSUserDefaults standardUserDefaults] setBool:NO forKey:@"isGetWelcome"];
        }
        if ([[dict objectForKey:@"eventName"] isEqualToString:@"ServiceSessionOpenedEvent"]) {
            //如果接收到客服的消息ext中有 ServiceSessionClosedEvent,表示会话已经结束,那么UD的值变成NO
            [[NSUserDefaults standardUserDefaults] setBool:YES forKey:@"isGetWelcome"];
        }
        
    }
}
 
4、在cell中设置插入消息的昵称头像,根据插入消息ext中的key去判断。




if (!self.model.isSender) {
        if (![model.nickname isKindOfClass:[NSNull class]]) {
            // 判断此条消息是否为插入的企业欢迎语,是的话赋值昵称和头像
            if ([self.model.message.ext objectForKey:@"insertWelcome"]) {
                _nameLabel.text = @"系统消息";
                self.avatarView.image = [UIImage imageNamed:@"测试图片"];
            }
        }
}
 
效果图




  查看全部
0、前提配置
(1)在客服系统内 管理员模式--设置--系统开关--企业欢迎语 处设置欢迎语,并开启开关
企业欢迎语0.png

(2)联系商务开通会话创建、接起、结束的透传事件功能

1、在HDMessageViewController.m中添加企业欢迎语属性
// 企业欢迎语
@property (nonatomic, strong) NSString *companyWelcome;
企业欢迎语1.png

 
2、在HDMessageViewController.m中的 viewdidload 方法的最后调用 [self judge]; 
// 这个方法是属于逻辑判断,用UD去记录参数,只有第一次进入聊天页面和会话结束之后再进入到聊天页面才插入消息。
- (void)judge
{
    // 判断会话中是否有消息
    if (self.conversation.latestMessage == nil) {
        // 如果会话中没有消息,那么UD的值变成NO
        [[NSUserDefaults standardUserDefaults] setBool:NO forKey:@"isGetWelcome"];
    }
    // 根据以前的判断,如果UD值为NO则插入消息欢迎语
    if (![[NSUserDefaults standardUserDefaults] boolForKey:@"isGetWelcome"]) {
        // 插入企业欢迎语
        [self welcome];
    }
    
    [[NSUserDefaults standardUserDefaults] setBool:YES forKey:@"isGetWelcome"];
}
 
// 这个方法是构建消息以及向UI页面以及本地数据库插入消息
- (void)welcome
{
    // 此方法是SDK获取企业欢迎语的
    __weak typeof(self) weakself = self;
    [[HDClient sharedClient].chatManager getEnterpriseWelcomeWithCompletion:^(NSString *welcome, HDError *error) {
        // 判断客服系统中是否设置了企业欢迎语并且开关有没有打开,兼容误操作客服关闭了企业欢迎语开关,会插入一条空消息,那么自己选择是否插入一条默认的欢迎语,自行修改  ‘您好,欢迎光临!’ 的内容
        if (![welcome isEqualToString:@""]) {
            weakself.companyWelcome = welcome;
        } else {
            weakself.companyWelcome = @"您好,欢迎光临!";
        }
        // 构建消息
        EMTextMessageBody *bdy = [[EMTextMessageBody alloc] initWithText:weakself.companyWelcome];
        NSString *from = [[HDClient sharedClient] currentUsername];
        // _converID就是IM服务号,可以自己替换下
        HDMessage *message = [[HDMessage alloc] initWithConversationID:weakself.conversation.conversationId from:from to:weakself.conversation.conversationId body:bdy];
        // 构建的消息用ext来标记此条消息是插入的欢迎语,然后在cell里面根据消息的ext来修改插入消息的昵称和头像
        NSDictionary *welcomeExt = @{@"insertWelcome":@"插入的欢迎语"};
        [message addAttributeDictionary:welcomeExt];   
        message.direction = 1;
        message.status = HDMessageStatusSuccessed;
        // 更新UI
        dispatch_async(dispatch_get_main_queue(), ^{
            // 消息添加到数据源,刷新UI
            [weakself addMessageToDataSource:message progress:nil];
        });
        // 消息插入到会话
        HDError *pError;
        [weakself.conversation addMessage:message error:&pError];
        
    }];
}
企业欢迎语2.png

企业欢迎语3.png

 
3、在HDMessageViewController.m中的cmdMessagesDidReceive方法中调用 messageExtwithEventName
企业欢迎语4.png

这个方法是判断接收到客服消息的ext里面有没有ServiceSessionClosedEvent,如果有就把UD记录改变参数,下次进入聊天页面的时候插入欢迎语
如果是ServiceSessionOpenedEvent,客服系统接入了会话,那么就不插入欢迎语

这个方法是接收透传消息里面调用,客服系统会话被接入会给app端发送透传消息通知,如果没有接收到透传消息,那么找对接的环信商务开通这个功能
[self messageExtwithEventName:message];

- (void)messageExtwithEventName:(HDMessage *)message
{
    if (![[[message.ext objectForKey:@"weichat"] objectForKey:@"event"] isKindOfClass:[NSNull class]]) {
        NSDictionary *dict = [[message.ext objectForKey:@"weichat"] objectForKey:@"event"];
        if ([[dict objectForKey:@"eventName"] isEqualToString:@"ServiceSessionClosedEvent"]) {
            //如果接收到客服的消息ext中有 ServiceSessionClosedEvent,表示会话已经结束,那么UD的值变成NO
            [[NSUserDefaults standardUserDefaults] setBool:NO forKey:@"isGetWelcome"];
        }
        if ([[dict objectForKey:@"eventName"] isEqualToString:@"ServiceSessionOpenedEvent"]) {
            //如果接收到客服的消息ext中有 ServiceSessionClosedEvent,表示会话已经结束,那么UD的值变成NO
            [[NSUserDefaults standardUserDefaults] setBool:YES forKey:@"isGetWelcome"];
        }
        
    }
}
 
4、在cell中设置插入消息的昵称头像,根据插入消息ext中的key去判断。
企业欢迎语5.png

if (!self.model.isSender) {
        if (![model.nickname isKindOfClass:[NSNull class]]) {
            // 判断此条消息是否为插入的企业欢迎语,是的话赋值昵称和头像
            if ([self.model.message.ext objectForKey:@"insertWelcome"]) {
                _nameLabel.text = @"系统消息";
                self.avatarView.image = [UIImage imageNamed:@"测试图片"];
            }
        }
}
 
效果图
企业欢迎语效果图.png

 
0
评论

Android MVP架构从入门到精通-真枪实弹 Android MVP MVP Android IT大前端

serge 发表了文章 • 225 次浏览 • 2019-03-22 16:42 • 来自相关话题

一. 前言

你是否遇到过Activity/Fragment中成百上千行代码,完全无法维护,看着头疼?

你是否遇到过因后台接口还未写而你不能先写代码逻辑的情况?

你是否遇到过用MVC架构写的项目进行单元测试时的深深无奈?

如果你现在还是用MVC架构模式在写项目,请先转到MVP模式!

二. MVC架构

MVC架构模式最初生根于服务器端的Web开发,后来渐渐能够胜任客户端Web开发,再后来因Android项目由XML和Activity/Fragment组成,慢慢的Android开发者开始使用类似MVC的架构模式开发应用.





 
M层:模型层(model),主要是实体类,数据库,网络等存在的层面,model将新的数据发送到view层,用户得到数据响应.

V层:视图层(view),一般指XML为代表的视图界面.显示来源于model层的数据.用户的点击操作等事件从view层传递到controller层.

C层:控制层(controller),一般以Activity/Fragment为代表.C层主要是连接V层和M层的,C层收到V层发送过来的事件请求,从M层获取数据,展示给V层.

从上图可以看出M层和V层有连接关系,而Activity有时候既充当了控制层又充当了视图层,导致项目维护比较麻烦.
 
1. MVC架构优缺点
A. 缺点

M层和V层有连接关系,没有解耦,导致维护困难.

Activity/Fragment中的代码过多,难以维护.

Activity中有很多关于视图UI的显示代码,因此View视图和Activity控制器并不是完全分离的,当Activity类业务过多的时候,会变得难以管理和维护.尤其是当UI的状态数据,跟持久化的数据混杂在一起,变得极为混乱.

B. 优点

控制层和View层都在Activity中进行操作,数据操作方便.

模块职责划分明确.主要划分层M,V,C三个模块.

三. MVP架构





 
MVP,即是Model,View,Presenter架构模式.看起来类似MVC,其实不然.从上图能看到Model层和View层没有相连接,完全解耦.

用户触碰界面触发事件,View层把事件通知Presenter层,Presenter层通知Model层处理这个事件,Model层处理后把结果发送到Presenter层,Presenter层再通知View层,最后View层做出改变.这是一整套流程.

M层:模型层(Model),此层和MVC中的M层作用类似.

V层:视图层(View),在MVC中V层只包含XML文件,而MVP中V层包含XML,Activity和Fragment三者.理论上V层不涉及任何逻辑,只负责界面的改变,尽量把逻辑处理放到M层.

P层:通知层(Presenter),P层的主要作用就是连接V层和M层,起到一个通知传递数据的作用.

1. MVP架构优缺点
A. 缺点

MVP中接口过多.

每一个功能,相比于MVC要多写好几个文件.

如果某一个界面中需要请求多个服务器接口,这个界面文件中会实现很多的回调接口,导致代码繁杂.

如果更改了数据源和请求中参数,会导致更多的代码修改.

额外的代码复杂度及学习成本.

B. 优点

模块职责划分明显,层次清晰,接口功能清晰.

Model层和View层分离,解耦.修改View而不影响Model.

功能复用度高,方便.一个Presenter可以复用于多个View,而不用更改Presenter的逻辑.

有利于测试驱动开发,以前的Android开发是难以进行单元测试.

如果后台接口还未写好,但已知返回数据类型的情况下,完全可以写出此接口完整的功能.

四. MVP架构实战(真枪实弹)
1. MVP三层代码简单书写

接下来笔者从简到繁,一点一点的堆砌MVP的整个架构.先看一下XML布局,布局中一个Button按钮和一个TextView控件,用户点击按钮后,Presenter层通知Model层请求处理网络数据,处理后Model层把结果数据发送给Presenter层,Presenter层再通知View层,然后View层改变TextView显示的内容.
 





 
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="center"
android:gravity="center"
android:orientation="vertical"
tools:context=".view.SingleInterfaceActivity">

<Button
android:id="@+id/button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="点击" />

<TextView
android:id="@+id/textView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="100px"
android:text="请点击上方按钮获取数据" />
</LinearLayout>
接下来是Activity代码,里面就是获取Button和TextView控件,然后对Button做监听,先简单的这样写,一会慢慢的增加代码.
 
public class SingleInterfaceActivity extends AppCompatActivity {

private Button button;
private TextView textView;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_single_interface);
button = findViewById(R.id.button);
textView = findViewById(R.id.textView);

button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {

}
});

}
}
下面是Model层代码.本次网络请求用的是wanandroid网站的开放api,其中的文章首页列表接口.SingleInterfaceModel文件里面有一个方法getData,第一个参数curPage意思是获取第几页的数据,第二个参数callback是Model层通知Presenter层的回调.
 
public class SingleInterfaceModel {

public void getData(int curPage, final Callback callback) {
NetUtils.getRetrofit()
.create(Api.class)
.getData(curPage)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(new Subscriber<ArticleListBean>() {
@Override
public void onCompleted() {
LP.w("completed");
}

@Override
public void onError(Throwable e) {
callback.onFail("出现错误");
}

@Override
public void onNext(ArticleListBean bean) {
if (null == bean) {
callback.onFail("出现错误");
} else if (bean.errorCode != 0) {
callback.onFail(bean.errorMsg);
} else {
callback.onSuccess(bean);
}
}
});
}
}Callback文件内容如下.里面一个成功一个失败的回调接口,参数全是泛型,为啥使用泛型笔者就不用说了吧.
 
public interface Callback<K, V> {
void onSuccess(K data);

void onFail(V data);
}再接下来是Presenter层的代码.SingleInterfacePresenter类构造函数中直接new了一个Model层对象,用于Presenter层对Model层的调用.然后SingleInterfacePresenter类的方法getData用于与Model的互相连接.
 
public class SingleInterfacePresenter {
private final SingleInterfaceModel singleInterfaceModel;

public SingleInterfacePresenter() {
this.singleInterfaceModel = new SingleInterfaceModel();
}

public void getData(int curPage) {
singleInterfaceModel.getData(curPage, new Callback<ArticleListBean, String>() {
@Override
public void onSuccess(ArticleListBean loginResultBean) {
//如果Model层请求数据成功,则此处应执行通知View层的代码

}

@Override
public void onFail(String errorMsg) {
//如果Model层请求数据失败,则此处应执行通知View层的代码

}
});
}
}至此,MVP三层简单的部分代码算是完成.那么怎样进行整个流程的相互调用呢.我们把刚开始的SingleInterfaceActivity代码改一下,让SingleInterfaceActivity持有Presenter层的对象,这样View层就可以调用Presenter层了.修改后代码如下.
 
public class SingleInterfaceActivity extends AppCompatActivity {

private Button button;
private TextView textView;
private SingleInterfacePresenter singleInterfacePresenter;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_single_interface);
button = findViewById(R.id.button);
textView = findViewById(R.id.textView);

singleInterfacePresenter = new SingleInterfacePresenter();
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
singleInterfacePresenter.getData(0);
}
});

}
}从以上所有代码可以看出,当用户点击按钮后,View层按钮的监听事件执行调用了Presenter层对象的getData方法,此时,Presenter层对象的getData方法调用了Model层对象的getData方法,Model层对象的getData方法中执行了网络请求和逻辑处理,把成功或失败的结果通过Callback接口回调给了Presenter层,然后Presenter层再通知View层改变界面.但此时SingleInterfacePresenter类中收到Model层的结果后无法通知View层,因为SingleInterfacePresenter未持有View层的对象.如下代码的注释中有说明.(如果此时点击按钮,下方代码LP.w()处会打印出网络请求成功的log)
 
public class SingleInterfacePresenter {
private final SingleInterfaceModel singleInterfaceModel;

public SingleInterfacePresenter() {
this.singleInterfaceModel = new SingleInterfaceModel();
}

public void getData(int curPage) {
singleInterfaceModel.getData(curPage, new Callback<ArticleListBean, String>() {
@Override
public void onSuccess(ArticleListBean loginResultBean) {
//如果Model层请求数据成功,则此处应执行通知View层的代码
//LP.w()是一个简单的log打印
LP.w(loginResultBean.toString());
}

@Override
public void onFail(String errorMsg) {
//如果Model层请求数据失败,则此处应执行通知View层的代码

}
});
}
}代码写到这里,笔者先把这些代码提交到github(https://github.com/serge66/MVPDemo),github上会有一次提交记录,如果想看此时的代码,可以根据提交记录"第一次修改"克隆此时的代码.

2. P层V层沟通桥梁

现在P层未持有V层对象,不能通知V层改变界面,那么就继续演变MVP架构.
在MVP架构中,我们要为每个Activity/Fragment写一个接口,这个接口需要让Presenter层持有,P层通过这个接口去通知V层更改界面.接口中包含了成功和失败的回调,这个接口Activity/Fragment要去实现,最终P层才能通知V层.
 
public interface SingleInterfaceIView {
void showArticleSuccess(ArticleListBean bean);

void showArticleFail(String errorMsg);
}一个完整的项目以后肯定会有许多功能界面,那么我们应该抽出一个IView公共接口,让所有的Activity/Fragment都间接实现它.IVew公共接口是用于给View层的接口继承的,注意,不是View本身继承.因为它定义的是接口的规范, 而其他接口才是定义的类的规范(这句话请仔细理解).
public interface IView {
}这个接口中可以写一些所有Activigy/Fragment共用的方法,我们把SingleInterfaceIView继承IView接口.
public interface SingleInterfaceIView extends IView {
void showArticleSuccess(ArticleListBean bean);

void showArticleFail(String errorMsg);
}同理Model层和Presenter层也是如此.
public interface IModel {
}
public interface IPresenter {
}现在项目中Model层是一个SingleInterfaceModel类,这个类对象被P层持有,对于面向对象设计来讲,利用接口达到解耦目的已经人尽皆知,那我们就要对SingleInterfaceModel类再写一个可继承的接口.代码如下.
public interface ISingleInterfaceModel extends IModel {
void getData(int curPage, final Callback callback);
}如此,SingleInterfaceModel类的修改如下.
 
public class SingleInterfaceModel implements ISingleInterfaceModel {

@Override
public void getData(int curPage, final Callback callback) {
NetUtils.getRetrofit()
.create(Api.class)
.getData(curPage)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(new Subscriber<ArticleListBean>() {
@Override
public void onCompleted() {
LP.w("completed");
}

@Override
public void onError(Throwable e) {
callback.onFail("出现错误");
}

@Override
public void onNext(ArticleListBean bean) {
if (null == bean) {
callback.onFail("出现错误");
} else if (bean.errorCode != 0) {
callback.onFail(bean.errorMsg);
} else {
callback.onSuccess(bean);
}
}
});
}
}同理,View层持有P层对象,我们也需要对P层进行改造.但是下面的代码却没有像ISingleInterfaceModel接口继承IModel一样继承IPresenter,这点需要注意,笔者把IPresenter的继承放在了其他处,后面会讲解.
 
public interface ISingleInterfacePresenter {
void getData(int curPage);
}
然后SingleInterfacePresenter类的修改如下:
 
public class SingleInterfacePresenter implements ISingleInterfacePresenter {
private final ISingleInterfaceModel singleInterfaceModel;

public SingleInterfacePresenter() {
this.singleInterfaceModel = new SingleInterfaceModel();
}

@Override
public void getData(int curPage) {
singleInterfaceModel.getData(curPage, new Callback<ArticleListBean, String>() {
@Override
public void onSuccess(ArticleListBean loginResultBean) {
//如果Model层请求数据成功,则此处应执行通知View层的代码
//LP.w()是一个简单的log打印
LP.w(loginResultBean.toString());
}

@Override
public void onFail(String errorMsg) {
//如果Model层请求数据失败,则此处应执行通知View层的代码
LP.w(errorMsg);
}
});
}
}3. 生命周期适配

至此,MVP三层每层的接口都写好了.但是P层连接V层的桥梁还没有搭建好,这个慢慢来,一个好的高楼大厦都是一步一步建造的.上面IPresenter接口我们没有让其他类继承,接下来就讲下这个.P层和V层相连接,V层的生命周期也要适配到P层,P层的每个功能都要适配生命周期,这里可以把生命周期的适配放在IPresenter接口中.P层持有V层对象,这里把它放到泛型中.代码如下.
 
public interface IPresenter<T extends IView> {

    /**
     * 依附生命view
     *
     * @param view
     */
    void attachView(T view);

    /**
     * 分离View
     */
    void detachView();

    /**
     * 判断View是否已经销毁
     *
     * @return
     */
    boolean isViewAttached();

}
 
这个IPresenter接口需要所有的P层实现类继承,对于生命周期这部分功能都是通用的,那么就可以抽出来一个抽象基类BasePresenter,去实现IPresenter的接口.
public abstract class BasePresenter<T extends IView> implements IPresenter<T> {
protected T mView;

@Override
public void attachView(T view) {
mView = view;
}

@Override
public void detachView() {
mView = null;
}

@Override
public boolean isViewAttached() {
return mView != null;
}
}此时,SingleInterfacePresenter类的代码修改如下.泛型中的SingleInterfaceIView可以理解成对应的Activity,P层此时完成了对V层的通信.
public class SingleInterfacePresenter extends BasePresenter<SingleInterfaceIView> implements ISingleInterfacePresenter {
private final ISingleInterfaceModel singleInterfaceModel;

public SingleInterfacePresenter() {
this.singleInterfaceModel = new SingleInterfaceModel();
}

@Override
public void getData(int curPage) {
singleInterfaceModel.getData(curPage, new Callback<ArticleListBean, String>() {
@Override
public void onSuccess(ArticleListBean loginResultBean) {
//如果Model层请求数据成功,则此处应执行通知View层的代码
//LP.w()是一个简单的log打印
LP.w(loginResultBean.toString());
if (isViewAttached()) {
mView.showArticleSuccess(loginResultBean);
}
}

@Override
public void onFail(String errorMsg) {
//如果Model层请求数据失败,则此处应执行通知View层的代码
LP.w(errorMsg);
if (isViewAttached()) {
mView.showArticleFail(errorMsg);
}
}
});
}
}此时,P层和V层的连接桥梁已经搭建,但还未搭建完成,我们需要写个BaseMVPActvity让所有的Activity继承,统一处理Activity相同逻辑.在BaseMVPActvity中使用IPresenter的泛型,因为每个Activity中需要持有P层对象,这里把P层对象抽出来也放在BaseMVPActvity中.同时BaseMVPActvity中也需要继承IView,用于P层对V层的生命周期中.代码如下.
 
public abstract class BaseMVPActivity<T extends IPresenter> extends AppCompatActivity implements IView {

protected T mPresenter;

@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
initPresenter();
init();
}

protected void initPresenter() {
mPresenter = createPresenter();
//绑定生命周期
if (mPresenter != null) {
mPresenter.attachView(this);
}
}

@Override
protected void onDestroy() {
if (mPresenter != null) {
mPresenter.detachView();
}
super.onDestroy();
}

/**
* 创建一个Presenter
*
* @return
*/
protected abstract T createPresenter();

protected abstract void init();

}接下来让SingleInterfaceActivity实现这个BaseMVPActivity.
public class SingleInterfaceActivity extends BaseMVPActivity<SingleInterfacePresenter> implements SingleInterfaceIView {

private Button button;
private TextView textView;

@Override
protected void init() {
setContentView(R.layout.activity_single_interface);
button = findViewById(R.id.button);
textView = findViewById(R.id.textView);

button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
mPresenter.getData(0);
}
});
}

@Override
protected SingleInterfacePresenter createPresenter() {
return new SingleInterfacePresenter();
}


@Override
public void showArticleSuccess(ArticleListBean bean) {
textView.setText(bean.data.datas.get(0).title);
}

@Override
public void showArticleFail(String errorMsg) {
Toast.makeText(this, errorMsg, Toast.LENGTH_SHORT).show();
}
}到此,MVP架构的整个简易流程完成.

代码写到这里,笔者先把这些代码提交到github(https://github.com/serge66/MVPDemo),github上会有一次提交记录,如果想看此时的代码,可以根据提交记录"第二次修改"克隆此时的代码.

4. 优化MVP架构





 
 
上面是MVP的目录,从目录中我们可以看到一个功能点(网络请求)MVP三层各有两个文件需要写,相对于MVC来说写起来确实麻烦,这也是一些人不愿意写MVP,宁愿用MVC的原因.

这里我们可以对此优化一下.MVP架构中有个Contract的概念,Contract有统一管理接口的作用,目的是为了统一管理一个页面的View和Presenter接口,用Contract可以减少部分文件的创建,比如P层和V层的接口文件.

那我们就把P层的ISingleInterfacePresenter接口和V层的SingleInterfaceIView接口文件删除掉,放入SingleInterfaceContract文件中.代码如下.
 
public interface SingleInterfaceContract {


interface View extends IView {
void showArticleSuccess(ArticleListBean bean);

void showArticleFail(String errorMsg);
}

interface Presenter {
void getData(int curPage);
}


}此时,SingleInterfacePresenter和SingleInterfaceActivity的代码修改如下.
 
public class SingleInterfacePresenter extends BasePresenter<SingleInterfaceContract.View>
implements SingleInterfaceContract.Presenter {

private final ISingleInterfaceModel singleInterfaceModel;

public SingleInterfacePresenter() {
this.singleInterfaceModel = new SingleInterfaceModel();
}

@Override
public void getData(int curPage) {
singleInterfaceModel.getData(curPage, new Callback<ArticleListBean, String>() {
@Override
public void onSuccess(ArticleListBean loginResultBean) {
//如果Model层请求数据成功,则此处应执行通知View层的代码
//LP.w()是一个简单的log打印
LP.w(loginResultBean.toString());
if (isViewAttached()) {
mView.showArticleSuccess(loginResultBean);
}
}

@Override
public void onFail(String errorMsg) {
//如果Model层请求数据失败,则此处应执行通知View层的代码
LP.w(errorMsg);
if (isViewAttached()) {
mView.showArticleFail(errorMsg);
}
}
});
}
}
public class SingleInterfaceActivity extends BaseMVPActivity<SingleInterfacePresenter>
implements SingleInterfaceContract.View {

private Button button;
private TextView textView;

@Override
protected void init() {
setContentView(R.layout.activity_single_interface);
button = findViewById(R.id.button);
textView = findViewById(R.id.textView);

button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
mPresenter.getData(0);
}
});
}

@Override
protected SingleInterfacePresenter createPresenter() {
return new SingleInterfacePresenter();
}


@Override
public void showArticleSuccess(ArticleListBean bean) {
textView.setText(bean.data.datas.get(0).title);
}

@Override
public void showArticleFail(String errorMsg) {
Toast.makeText(this, errorMsg, Toast.LENGTH_SHORT).show();
}
}
代码写到这里,笔者先把这些代码提交到github(https://github.com/serge66/MVPDemo),github上会有一次提交记录,如果想看此时的代码,可以根据提交记录"第三次修改"克隆此时的代码.

5. 单页面多网络请求以及P层复用

上面的MVP封装只适用于单页面一个网络请求的情况,当一个界面有两个网络请求时,此封装已不适合.以及考虑到P层的复用.为此,我们再次新建一个MultipleInterfaceActivity来进行说明.XML中布局是两个按钮两个Textview,点击则可以进行网络请求.





 
 
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="center"
android:gravity="center"
android:orientation="vertical"
tools:context=".view.MultipleInterfaceActivity">

<Button
android:id="@+id/button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="点击" />

<TextView
android:id="@+id/textView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="50px"
android:text="请点击上方按钮获取数据" />

<Button
android:id="@+id/btn"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="100px"
android:text="点击" />

<TextView
android:id="@+id/tv"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="50px"
android:text="请点击上方按钮获取数据" />
</LinearLayout>MultipleInterfaceActivity类代码暂时如下.
 
public class MultipleInterfaceActivity extends BaseMVPActivity {

private Button button;
private TextView textView;
private Button btn;
private TextView tv;


@Override
protected void init() {
setContentView(R.layout.activity_multiple_interface);

button = findViewById(R.id.button);
textView = findViewById(R.id.textView);

button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {

}
});


btn = findViewById(R.id.btn);
tv = findViewById(R.id.tv);

btn.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {

}
});
}

@Override
protected IPresenter createPresenter() {
return null;
}

}此时我们可以想下,当一个页面中有多个网络请求时,Activity所继承的BaseMVPActivity的泛型中要写多个参数,那有没有上面代码的框架不变的情况下实现这个需求呢?答案必须有的.我们可以把多个网络请求的功能当做一个网络请求来看待,封装成一个MultiplePresenter,其继承至BasePresenter实现生命周期的适配.此MultiplePresenter类的作用就是容纳多个Presenter,连接同一个View.代码如下.
 
public class MultiplePresenter<T extends IView> extends BasePresenter<T> {
private T mView;

private List<IPresenter> presenters = new ArrayList<>();

@SafeVarargs
public final <K extends IPresenter<T>> void addPresenter(K... addPresenter) {
for (K ap : addPresenter) {
ap.attachView(mView);
presenters.add(ap);
}
}

public MultiplePresenter(T mView) {
this.mView = mView;
}

@Override
public void detachView() {
for (IPresenter presenter : presenters) {
presenter.detachView();
}
}

}因MultiplePresenter类中需要有多个网络请求,现在举例说明时,暂时用两个网络请求接口.MultipleInterfaceActivity类中代码改造如下.
 
public class MultipleInterfaceActivity extends BaseMVPActivity<MultiplePresenter>
implements SingleInterfaceContract.View, MultipleInterfaceContract.View {

private Button button;
private TextView textView;
private Button btn;
private TextView tv;
private SingleInterfacePresenter singleInterfacePresenter;
private MultipleInterfacePresenter multipleInterfacePresenter;


@Override
protected void init() {
setContentView(R.layout.activity_multiple_interface);

button = findViewById(R.id.button);
textView = findViewById(R.id.textView);

button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
singleInterfacePresenter.getData(0);
}
});


btn = findViewById(R.id.btn);
tv = findViewById(R.id.tv);

btn.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
multipleInterfacePresenter.getBanner();
}
});
}

@Override
protected MultiplePresenter createPresenter() {
MultiplePresenter multiplePresenter = new MultiplePresenter(this);

singleInterfacePresenter = new SingleInterfacePresenter();
multipleInterfacePresenter = new MultipleInterfacePresenter();

multiplePresenter.addPresenter(singleInterfacePresenter);
multiplePresenter.addPresenter(multipleInterfacePresenter);
return multiplePresenter;
}

@Override
public void showArticleSuccess(ArticleListBean bean) {
textView.setText(bean.data.datas.get(0).title);
}

@Override
public void showArticleFail(String errorMsg) {
Toast.makeText(this, errorMsg, Toast.LENGTH_SHORT).show();
}

@Override
public void showMultipleSuccess(BannerBean bean) {
tv.setText(bean.data.get(0).title);
}

@Override
public void showMultipleFail(String errorMsg) {
Toast.makeText(this, errorMsg, Toast.LENGTH_SHORT).show();
}
}写到这里,MVP框架基本算是完成.如果想再次优化,其实还是有可优化的地方,比如当View销毁时,现在只是让P层中的View对象置为null,并没有继续对M层通知.如果View销毁时,M层还在请求网络中呢,可以为此再加入一个取消网络请求的通用功能.这里只是举一个例子,每个人对MVP的理解不一样,而MVP架构也并不是一成不变,适合自己项目的才是最好的.

6. 完整项目地址

完整项目已提交到github(https://github.com/serge66/MVPDemo).点击下方阅读原文即可访问.

五. 参考资料

[一步步带你精通MVP](https://mp.weixin.qq.com/s/DuNbl3V4gZY-ZCETbhZGug)

[从0到1搭建MVP框架](https://mp.weixin.qq.com/s/QFpHhC-5JkAb4IlMP0nKug)

[Presenter层如何高度的复用](https://juejin.im/post/599ce8016fb9a0247e4255f4)
 
六. 后续

MVVM架构从入门到精通-真枪实弹 敬请期待~~~
 





微信公众号:IT大前端
关注可了解更多的大前端领域技术
  查看全部

0.jpeg

一. 前言

你是否遇到过Activity/Fragment中成百上千行代码,完全无法维护,看着头疼?

你是否遇到过因后台接口还未写而你不能先写代码逻辑的情况?

你是否遇到过用MVC架构写的项目进行单元测试时的深深无奈?

如果你现在还是用MVC架构模式在写项目,请先转到MVP模式!

二. MVC架构

MVC架构模式最初生根于服务器端的Web开发,后来渐渐能够胜任客户端Web开发,再后来因Android项目由XML和Activity/Fragment组成,慢慢的Android开发者开始使用类似MVC的架构模式开发应用.

0_(1).jpeg

 
M层:模型层(model),主要是实体类,数据库,网络等存在的层面,model将新的数据发送到view层,用户得到数据响应.

V层:视图层(view),一般指XML为代表的视图界面.显示来源于model层的数据.用户的点击操作等事件从view层传递到controller层.

C层:控制层(controller),一般以Activity/Fragment为代表.C层主要是连接V层和M层的,C层收到V层发送过来的事件请求,从M层获取数据,展示给V层.

从上图可以看出M层和V层有连接关系,而Activity有时候既充当了控制层又充当了视图层,导致项目维护比较麻烦.
 
1. MVC架构优缺点
A. 缺点

M层和V层有连接关系,没有解耦,导致维护困难.

Activity/Fragment中的代码过多,难以维护.

Activity中有很多关于视图UI的显示代码,因此View视图和Activity控制器并不是完全分离的,当Activity类业务过多的时候,会变得难以管理和维护.尤其是当UI的状态数据,跟持久化的数据混杂在一起,变得极为混乱.

B. 优点

控制层和View层都在Activity中进行操作,数据操作方便.

模块职责划分明确.主要划分层M,V,C三个模块.

三. MVP架构

0_(2).jpeg

 
MVP,即是Model,View,Presenter架构模式.看起来类似MVC,其实不然.从上图能看到Model层和View层没有相连接,完全解耦.

用户触碰界面触发事件,View层把事件通知Presenter层,Presenter层通知Model层处理这个事件,Model层处理后把结果发送到Presenter层,Presenter层再通知View层,最后View层做出改变.这是一整套流程.

M层:模型层(Model),此层和MVC中的M层作用类似.

V层:视图层(View),在MVC中V层只包含XML文件,而MVP中V层包含XML,Activity和Fragment三者.理论上V层不涉及任何逻辑,只负责界面的改变,尽量把逻辑处理放到M层.

P层:通知层(Presenter),P层的主要作用就是连接V层和M层,起到一个通知传递数据的作用.

1. MVP架构优缺点
A. 缺点

MVP中接口过多.

每一个功能,相比于MVC要多写好几个文件.

如果某一个界面中需要请求多个服务器接口,这个界面文件中会实现很多的回调接口,导致代码繁杂.

如果更改了数据源和请求中参数,会导致更多的代码修改.

额外的代码复杂度及学习成本.

B. 优点

模块职责划分明显,层次清晰,接口功能清晰.

Model层和View层分离,解耦.修改View而不影响Model.

功能复用度高,方便.一个Presenter可以复用于多个View,而不用更改Presenter的逻辑.

有利于测试驱动开发,以前的Android开发是难以进行单元测试.

如果后台接口还未写好,但已知返回数据类型的情况下,完全可以写出此接口完整的功能.

四. MVP架构实战(真枪实弹)
1. MVP三层代码简单书写

接下来笔者从简到繁,一点一点的堆砌MVP的整个架构.先看一下XML布局,布局中一个Button按钮和一个TextView控件,用户点击按钮后,Presenter层通知Model层请求处理网络数据,处理后Model层把结果数据发送给Presenter层,Presenter层再通知View层,然后View层改变TextView显示的内容.
 

0.gif

 
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="center"
android:gravity="center"
android:orientation="vertical"
tools:context=".view.SingleInterfaceActivity">

<Button
android:id="@+id/button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="点击" />

<TextView
android:id="@+id/textView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="100px"
android:text="请点击上方按钮获取数据" />
</LinearLayout>

接下来是Activity代码,里面就是获取Button和TextView控件,然后对Button做监听,先简单的这样写,一会慢慢的增加代码.
 
public class SingleInterfaceActivity extends AppCompatActivity {

private Button button;
private TextView textView;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_single_interface);
button = findViewById(R.id.button);
textView = findViewById(R.id.textView);

button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {

}
});

}
}

下面是Model层代码.本次网络请求用的是wanandroid网站的开放api,其中的文章首页列表接口.SingleInterfaceModel文件里面有一个方法getData,第一个参数curPage意思是获取第几页的数据,第二个参数callback是Model层通知Presenter层的回调.
 
public class SingleInterfaceModel {

public void getData(int curPage, final Callback callback) {
NetUtils.getRetrofit()
.create(Api.class)
.getData(curPage)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(new Subscriber<ArticleListBean>() {
@Override
public void onCompleted() {
LP.w("completed");
}

@Override
public void onError(Throwable e) {
callback.onFail("出现错误");
}

@Override
public void onNext(ArticleListBean bean) {
if (null == bean) {
callback.onFail("出现错误");
} else if (bean.errorCode != 0) {
callback.onFail(bean.errorMsg);
} else {
callback.onSuccess(bean);
}
}
});
}
}
Callback文件内容如下.里面一个成功一个失败的回调接口,参数全是泛型,为啥使用泛型笔者就不用说了吧.
 
public interface Callback<K, V> {
void onSuccess(K data);

void onFail(V data);
}
再接下来是Presenter层的代码.SingleInterfacePresenter类构造函数中直接new了一个Model层对象,用于Presenter层对Model层的调用.然后SingleInterfacePresenter类的方法getData用于与Model的互相连接.
 
public class SingleInterfacePresenter {
private final SingleInterfaceModel singleInterfaceModel;

public SingleInterfacePresenter() {
this.singleInterfaceModel = new SingleInterfaceModel();
}

public void getData(int curPage) {
singleInterfaceModel.getData(curPage, new Callback<ArticleListBean, String>() {
@Override
public void onSuccess(ArticleListBean loginResultBean) {
//如果Model层请求数据成功,则此处应执行通知View层的代码

}

@Override
public void onFail(String errorMsg) {
//如果Model层请求数据失败,则此处应执行通知View层的代码

}
});
}
}
至此,MVP三层简单的部分代码算是完成.那么怎样进行整个流程的相互调用呢.我们把刚开始的SingleInterfaceActivity代码改一下,让SingleInterfaceActivity持有Presenter层的对象,这样View层就可以调用Presenter层了.修改后代码如下.
 
public class SingleInterfaceActivity extends AppCompatActivity {

private Button button;
private TextView textView;
private SingleInterfacePresenter singleInterfacePresenter;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_single_interface);
button = findViewById(R.id.button);
textView = findViewById(R.id.textView);

singleInterfacePresenter = new SingleInterfacePresenter();
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
singleInterfacePresenter.getData(0);
}
});

}
}
从以上所有代码可以看出,当用户点击按钮后,View层按钮的监听事件执行调用了Presenter层对象的getData方法,此时,Presenter层对象的getData方法调用了Model层对象的getData方法,Model层对象的getData方法中执行了网络请求和逻辑处理,把成功或失败的结果通过Callback接口回调给了Presenter层,然后Presenter层再通知View层改变界面.但此时SingleInterfacePresenter类中收到Model层的结果后无法通知View层,因为SingleInterfacePresenter未持有View层的对象.如下代码的注释中有说明.(如果此时点击按钮,下方代码LP.w()处会打印出网络请求成功的log)
 
public class SingleInterfacePresenter {
private final SingleInterfaceModel singleInterfaceModel;

public SingleInterfacePresenter() {
this.singleInterfaceModel = new SingleInterfaceModel();
}

public void getData(int curPage) {
singleInterfaceModel.getData(curPage, new Callback<ArticleListBean, String>() {
@Override
public void onSuccess(ArticleListBean loginResultBean) {
//如果Model层请求数据成功,则此处应执行通知View层的代码
//LP.w()是一个简单的log打印
LP.w(loginResultBean.toString());
}

@Override
public void onFail(String errorMsg) {
//如果Model层请求数据失败,则此处应执行通知View层的代码

}
});
}
}
代码写到这里,笔者先把这些代码提交到github(https://github.com/serge66/MVPDemo),github上会有一次提交记录,如果想看此时的代码,可以根据提交记录"第一次修改"克隆此时的代码.

2. P层V层沟通桥梁

现在P层未持有V层对象,不能通知V层改变界面,那么就继续演变MVP架构.
在MVP架构中,我们要为每个Activity/Fragment写一个接口,这个接口需要让Presenter层持有,P层通过这个接口去通知V层更改界面.接口中包含了成功和失败的回调,这个接口Activity/Fragment要去实现,最终P层才能通知V层.
 
public interface SingleInterfaceIView {
void showArticleSuccess(ArticleListBean bean);

void showArticleFail(String errorMsg);
}
一个完整的项目以后肯定会有许多功能界面,那么我们应该抽出一个IView公共接口,让所有的Activity/Fragment都间接实现它.IVew公共接口是用于给View层的接口继承的,注意,不是View本身继承.因为它定义的是接口的规范, 而其他接口才是定义的类的规范(这句话请仔细理解).
public interface IView {
}
这个接口中可以写一些所有Activigy/Fragment共用的方法,我们把SingleInterfaceIView继承IView接口.
public interface SingleInterfaceIView extends IView {
void showArticleSuccess(ArticleListBean bean);

void showArticleFail(String errorMsg);
}
同理Model层和Presenter层也是如此.
public interface IModel {
}
public interface IPresenter {
}
现在项目中Model层是一个SingleInterfaceModel类,这个类对象被P层持有,对于面向对象设计来讲,利用接口达到解耦目的已经人尽皆知,那我们就要对SingleInterfaceModel类再写一个可继承的接口.代码如下.
public interface ISingleInterfaceModel extends IModel {
void getData(int curPage, final Callback callback);
}
如此,SingleInterfaceModel类的修改如下.
 
public class SingleInterfaceModel implements ISingleInterfaceModel {

@Override
public void getData(int curPage, final Callback callback) {
NetUtils.getRetrofit()
.create(Api.class)
.getData(curPage)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(new Subscriber<ArticleListBean>() {
@Override
public void onCompleted() {
LP.w("completed");
}

@Override
public void onError(Throwable e) {
callback.onFail("出现错误");
}

@Override
public void onNext(ArticleListBean bean) {
if (null == bean) {
callback.onFail("出现错误");
} else if (bean.errorCode != 0) {
callback.onFail(bean.errorMsg);
} else {
callback.onSuccess(bean);
}
}
});
}
}
同理,View层持有P层对象,我们也需要对P层进行改造.但是下面的代码却没有像ISingleInterfaceModel接口继承IModel一样继承IPresenter,这点需要注意,笔者把IPresenter的继承放在了其他处,后面会讲解.
 
public interface ISingleInterfacePresenter {
void getData(int curPage);
}

然后SingleInterfacePresenter类的修改如下:
 
public class SingleInterfacePresenter implements ISingleInterfacePresenter {
private final ISingleInterfaceModel singleInterfaceModel;

public SingleInterfacePresenter() {
this.singleInterfaceModel = new SingleInterfaceModel();
}

@Override
public void getData(int curPage) {
singleInterfaceModel.getData(curPage, new Callback<ArticleListBean, String>() {
@Override
public void onSuccess(ArticleListBean loginResultBean) {
//如果Model层请求数据成功,则此处应执行通知View层的代码
//LP.w()是一个简单的log打印
LP.w(loginResultBean.toString());
}

@Override
public void onFail(String errorMsg) {
//如果Model层请求数据失败,则此处应执行通知View层的代码
LP.w(errorMsg);
}
});
}
}
3. 生命周期适配

至此,MVP三层每层的接口都写好了.但是P层连接V层的桥梁还没有搭建好,这个慢慢来,一个好的高楼大厦都是一步一步建造的.上面IPresenter接口我们没有让其他类继承,接下来就讲下这个.P层和V层相连接,V层的生命周期也要适配到P层,P层的每个功能都要适配生命周期,这里可以把生命周期的适配放在IPresenter接口中.P层持有V层对象,这里把它放到泛型中.代码如下.
 
public interface IPresenter<T extends IView> {

    /**
     * 依附生命view
     *
     * @param view
     */
    void attachView(T view);

    /**
     * 分离View
     */
    void detachView();

    /**
     * 判断View是否已经销毁
     *
     * @return
     */
    boolean isViewAttached();

}
 
这个IPresenter接口需要所有的P层实现类继承,对于生命周期这部分功能都是通用的,那么就可以抽出来一个抽象基类BasePresenter,去实现IPresenter的接口.
public abstract class BasePresenter<T extends IView> implements IPresenter<T> {
protected T mView;

@Override
public void attachView(T view) {
mView = view;
}

@Override
public void detachView() {
mView = null;
}

@Override
public boolean isViewAttached() {
return mView != null;
}
}
此时,SingleInterfacePresenter类的代码修改如下.泛型中的SingleInterfaceIView可以理解成对应的Activity,P层此时完成了对V层的通信.
public class SingleInterfacePresenter extends BasePresenter<SingleInterfaceIView> implements ISingleInterfacePresenter {
private final ISingleInterfaceModel singleInterfaceModel;

public SingleInterfacePresenter() {
this.singleInterfaceModel = new SingleInterfaceModel();
}

@Override
public void getData(int curPage) {
singleInterfaceModel.getData(curPage, new Callback<ArticleListBean, String>() {
@Override
public void onSuccess(ArticleListBean loginResultBean) {
//如果Model层请求数据成功,则此处应执行通知View层的代码
//LP.w()是一个简单的log打印
LP.w(loginResultBean.toString());
if (isViewAttached()) {
mView.showArticleSuccess(loginResultBean);
}
}

@Override
public void onFail(String errorMsg) {
//如果Model层请求数据失败,则此处应执行通知View层的代码
LP.w(errorMsg);
if (isViewAttached()) {
mView.showArticleFail(errorMsg);
}
}
});
}
}
此时,P层和V层的连接桥梁已经搭建,但还未搭建完成,我们需要写个BaseMVPActvity让所有的Activity继承,统一处理Activity相同逻辑.在BaseMVPActvity中使用IPresenter的泛型,因为每个Activity中需要持有P层对象,这里把P层对象抽出来也放在BaseMVPActvity中.同时BaseMVPActvity中也需要继承IView,用于P层对V层的生命周期中.代码如下.
 
public abstract class BaseMVPActivity<T extends IPresenter> extends AppCompatActivity implements IView {

protected T mPresenter;

@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
initPresenter();
init();
}

protected void initPresenter() {
mPresenter = createPresenter();
//绑定生命周期
if (mPresenter != null) {
mPresenter.attachView(this);
}
}

@Override
protected void onDestroy() {
if (mPresenter != null) {
mPresenter.detachView();
}
super.onDestroy();
}

/**
* 创建一个Presenter
*
* @return
*/
protected abstract T createPresenter();

protected abstract void init();

}
接下来让SingleInterfaceActivity实现这个BaseMVPActivity.
public class SingleInterfaceActivity extends BaseMVPActivity<SingleInterfacePresenter> implements SingleInterfaceIView {

private Button button;
private TextView textView;

@Override
protected void init() {
setContentView(R.layout.activity_single_interface);
button = findViewById(R.id.button);
textView = findViewById(R.id.textView);

button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
mPresenter.getData(0);
}
});
}

@Override
protected SingleInterfacePresenter createPresenter() {
return new SingleInterfacePresenter();
}


@Override
public void showArticleSuccess(ArticleListBean bean) {
textView.setText(bean.data.datas.get(0).title);
}

@Override
public void showArticleFail(String errorMsg) {
Toast.makeText(this, errorMsg, Toast.LENGTH_SHORT).show();
}
}
到此,MVP架构的整个简易流程完成.

代码写到这里,笔者先把这些代码提交到github(https://github.com/serge66/MVPDemo),github上会有一次提交记录,如果想看此时的代码,可以根据提交记录"第二次修改"克隆此时的代码.

4. 优化MVP架构

0_(3).jpeg

 
 
上面是MVP的目录,从目录中我们可以看到一个功能点(网络请求)MVP三层各有两个文件需要写,相对于MVC来说写起来确实麻烦,这也是一些人不愿意写MVP,宁愿用MVC的原因.

这里我们可以对此优化一下.MVP架构中有个Contract的概念,Contract有统一管理接口的作用,目的是为了统一管理一个页面的View和Presenter接口,用Contract可以减少部分文件的创建,比如P层和V层的接口文件.

那我们就把P层的ISingleInterfacePresenter接口和V层的SingleInterfaceIView接口文件删除掉,放入SingleInterfaceContract文件中.代码如下.
 
public interface SingleInterfaceContract {


interface View extends IView {
void showArticleSuccess(ArticleListBean bean);

void showArticleFail(String errorMsg);
}

interface Presenter {
void getData(int curPage);
}


}
此时,SingleInterfacePresenter和SingleInterfaceActivity的代码修改如下.
 
public class SingleInterfacePresenter extends BasePresenter<SingleInterfaceContract.View>
implements SingleInterfaceContract.Presenter {

private final ISingleInterfaceModel singleInterfaceModel;

public SingleInterfacePresenter() {
this.singleInterfaceModel = new SingleInterfaceModel();
}

@Override
public void getData(int curPage) {
singleInterfaceModel.getData(curPage, new Callback<ArticleListBean, String>() {
@Override
public void onSuccess(ArticleListBean loginResultBean) {
//如果Model层请求数据成功,则此处应执行通知View层的代码
//LP.w()是一个简单的log打印
LP.w(loginResultBean.toString());
if (isViewAttached()) {
mView.showArticleSuccess(loginResultBean);
}
}

@Override
public void onFail(String errorMsg) {
//如果Model层请求数据失败,则此处应执行通知View层的代码
LP.w(errorMsg);
if (isViewAttached()) {
mView.showArticleFail(errorMsg);
}
}
});
}
}

public class SingleInterfaceActivity extends BaseMVPActivity<SingleInterfacePresenter>
implements SingleInterfaceContract.View {

private Button button;
private TextView textView;

@Override
protected void init() {
setContentView(R.layout.activity_single_interface);
button = findViewById(R.id.button);
textView = findViewById(R.id.textView);

button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
mPresenter.getData(0);
}
});
}

@Override
protected SingleInterfacePresenter createPresenter() {
return new SingleInterfacePresenter();
}


@Override
public void showArticleSuccess(ArticleListBean bean) {
textView.setText(bean.data.datas.get(0).title);
}

@Override
public void showArticleFail(String errorMsg) {
Toast.makeText(this, errorMsg, Toast.LENGTH_SHORT).show();
}
}

代码写到这里,笔者先把这些代码提交到github(https://github.com/serge66/MVPDemo),github上会有一次提交记录,如果想看此时的代码,可以根据提交记录"第三次修改"克隆此时的代码.

5. 单页面多网络请求以及P层复用

上面的MVP封装只适用于单页面一个网络请求的情况,当一个界面有两个网络请求时,此封装已不适合.以及考虑到P层的复用.为此,我们再次新建一个MultipleInterfaceActivity来进行说明.XML中布局是两个按钮两个Textview,点击则可以进行网络请求.

0_(1).gif

 
 
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="center"
android:gravity="center"
android:orientation="vertical"
tools:context=".view.MultipleInterfaceActivity">

<Button
android:id="@+id/button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="点击" />

<TextView
android:id="@+id/textView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="50px"
android:text="请点击上方按钮获取数据" />

<Button
android:id="@+id/btn"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="100px"
android:text="点击" />

<TextView
android:id="@+id/tv"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="50px"
android:text="请点击上方按钮获取数据" />
</LinearLayout>
MultipleInterfaceActivity类代码暂时如下.
 
public class MultipleInterfaceActivity extends BaseMVPActivity {

private Button button;
private TextView textView;
private Button btn;
private TextView tv;


@Override
protected void init() {
setContentView(R.layout.activity_multiple_interface);

button = findViewById(R.id.button);
textView = findViewById(R.id.textView);

button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {

}
});


btn = findViewById(R.id.btn);
tv = findViewById(R.id.tv);

btn.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {

}
});
}

@Override
protected IPresenter createPresenter() {
return null;
}

}
此时我们可以想下,当一个页面中有多个网络请求时,Activity所继承的BaseMVPActivity的泛型中要写多个参数,那有没有上面代码的框架不变的情况下实现这个需求呢?答案必须有的.我们可以把多个网络请求的功能当做一个网络请求来看待,封装成一个MultiplePresenter,其继承至BasePresenter实现生命周期的适配.此MultiplePresenter类的作用就是容纳多个Presenter,连接同一个View.代码如下.
 
public class MultiplePresenter<T extends IView> extends BasePresenter<T> {
private T mView;

private List<IPresenter> presenters = new ArrayList<>();

@SafeVarargs
public final <K extends IPresenter<T>> void addPresenter(K... addPresenter) {
for (K ap : addPresenter) {
ap.attachView(mView);
presenters.add(ap);
}
}

public MultiplePresenter(T mView) {
this.mView = mView;
}

@Override
public void detachView() {
for (IPresenter presenter : presenters) {
presenter.detachView();
}
}

}
因MultiplePresenter类中需要有多个网络请求,现在举例说明时,暂时用两个网络请求接口.MultipleInterfaceActivity类中代码改造如下.
 
public class MultipleInterfaceActivity extends BaseMVPActivity<MultiplePresenter>
implements SingleInterfaceContract.View, MultipleInterfaceContract.View {

private Button button;
private TextView textView;
private Button btn;
private TextView tv;
private SingleInterfacePresenter singleInterfacePresenter;
private MultipleInterfacePresenter multipleInterfacePresenter;


@Override
protected void init() {
setContentView(R.layout.activity_multiple_interface);

button = findViewById(R.id.button);
textView = findViewById(R.id.textView);

button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
singleInterfacePresenter.getData(0);
}
});


btn = findViewById(R.id.btn);
tv = findViewById(R.id.tv);

btn.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
multipleInterfacePresenter.getBanner();
}
});
}

@Override
protected MultiplePresenter createPresenter() {
MultiplePresenter multiplePresenter = new MultiplePresenter(this);

singleInterfacePresenter = new SingleInterfacePresenter();
multipleInterfacePresenter = new MultipleInterfacePresenter();

multiplePresenter.addPresenter(singleInterfacePresenter);
multiplePresenter.addPresenter(multipleInterfacePresenter);
return multiplePresenter;
}

@Override
public void showArticleSuccess(ArticleListBean bean) {
textView.setText(bean.data.datas.get(0).title);
}

@Override
public void showArticleFail(String errorMsg) {
Toast.makeText(this, errorMsg, Toast.LENGTH_SHORT).show();
}

@Override
public void showMultipleSuccess(BannerBean bean) {
tv.setText(bean.data.get(0).title);
}

@Override
public void showMultipleFail(String errorMsg) {
Toast.makeText(this, errorMsg, Toast.LENGTH_SHORT).show();
}
}
写到这里,MVP框架基本算是完成.如果想再次优化,其实还是有可优化的地方,比如当View销毁时,现在只是让P层中的View对象置为null,并没有继续对M层通知.如果View销毁时,M层还在请求网络中呢,可以为此再加入一个取消网络请求的通用功能.这里只是举一个例子,每个人对MVP的理解不一样,而MVP架构也并不是一成不变,适合自己项目的才是最好的.

6. 完整项目地址

完整项目已提交到github(https://github.com/serge66/MVPDemo).点击下方阅读原文即可访问.

五. 参考资料

[一步步带你精通MVP](https://mp.weixin.qq.com/s/DuNbl3V4gZY-ZCETbhZGug)

[从0到1搭建MVP框架](https://mp.weixin.qq.com/s/QFpHhC-5JkAb4IlMP0nKug)

[Presenter层如何高度的复用](https://juejin.im/post/599ce8016fb9a0247e4255f4)
 
六. 后续

MVVM架构从入门到精通-真枪实弹 敬请期待~~~
 

qrcode_for_gh_08bfa7313fb2_258.jpg

微信公众号:IT大前端
关注可了解更多的大前端领域技术
 
0
评论

在线直播源码实现直播技术曾遇到的那些小问题 源码 互联网+ 直播

q2466131704 发表了文章 • 237 次浏览 • 2019-03-19 17:12 • 来自相关话题

文章主要内容:在直播过程经常会遇到哪些问题?在线直播源码是怎样实现相应的直播技术的?这些问题的产生是由怎样的原因导致的?
以下这些问题,我相信都是直播中十分常见,并且具有一定参考性的问题。大家可以通过以下内容寻找对应的问题和原因,希望能给大家产生一定的帮助。
1.播放失败:服务器连接失败、域名解析失败、只有音频没有视频、只有视频没有音频。
2.直播出现卡顿:(1)主播端网络不好,导致推流上行不稳定。(2)服务端线路质量差,造成分发不稳定。(3)用户端网络质量差,从而拉流下行不稳定。
3.延时高:网络传输延时、协议延时、业务代码中的缓冲区。
4.音画不同步:(应从视频直播的生产端进行排查)采集设备内部出现问题、时间戳没有在采集时被获取、采集源距离太远、时间戳出现回退或紊乱现象、播放端的性能问题。
5.马赛克:图像尺寸原因、视频编码参数配置原因、关键帧丢失。
6.播放黑屏:主播端编码失效、视频编码失效、码流前半段只有音频没有视频。
7.播放花屏:播放器没有从关键帧开始解码、码流中的视频尺寸发生变化、丢失参考帧、硬编解兼容性问题、推流端的图像尺寸格式。
8.播放闪屏:推流端原因、播放器缓冲机制原因。
9.播放杂音(回声):网络波动、回声消除、参数配置、混音越界。
10.拖动不准:直播过程中丢帧、关键帧间隔太大。
11.CPU/GPU占用率高:数据量大、格式转换、软编解格式。
12.在直播过程中,决定视频预加载效果的好坏主要由:视频的码率、缓冲文件大小和网速决定。
原因:网速快且码率低的情况下,不需要使用预加载。(码率中等且网速一般的情况适用)需要注意的是:缓冲文件不能设置过大,会影响正常播放。
12.为什么播放视频时,会停留在第一帧画面。
原因:(1)解码器出现错误,只接出了第一帧图像。(2)没有接收到视频帧。(3)时间戳的计算有误。
   以上内容简单总结了直播中经常出现的问题及原因,那么在文章的结尾,想给大家举个简单的例子,比如盖楼需要混凝土和砖;种树需要土壤和水;养鱼需要水和饲料,开发一个直播平台就需要在线直播源码。源码就是开发的基础,没有源码就无法完成。所以,选择优质的源码也是开发过程中十分重要的一步。
本文声明原创,转载请注明出处。 查看全部
文章主要内容:在直播过程经常会遇到哪些问题?在线直播源码是怎样实现相应的直播技术的?这些问题的产生是由怎样的原因导致的?
以下这些问题,我相信都是直播中十分常见,并且具有一定参考性的问题。大家可以通过以下内容寻找对应的问题和原因,希望能给大家产生一定的帮助。
1.播放失败:服务器连接失败、域名解析失败、只有音频没有视频、只有视频没有音频。
2.直播出现卡顿:(1)主播端网络不好,导致推流上行不稳定。(2)服务端线路质量差,造成分发不稳定。(3)用户端网络质量差,从而拉流下行不稳定。
3.延时高:网络传输延时、协议延时、业务代码中的缓冲区。
4.音画不同步:(应从视频直播的生产端进行排查)采集设备内部出现问题、时间戳没有在采集时被获取、采集源距离太远、时间戳出现回退或紊乱现象、播放端的性能问题。
5.马赛克:图像尺寸原因、视频编码参数配置原因、关键帧丢失。
6.播放黑屏:主播端编码失效、视频编码失效、码流前半段只有音频没有视频。
7.播放花屏:播放器没有从关键帧开始解码、码流中的视频尺寸发生变化、丢失参考帧、硬编解兼容性问题、推流端的图像尺寸格式。
8.播放闪屏:推流端原因、播放器缓冲机制原因。
9.播放杂音(回声):网络波动、回声消除、参数配置、混音越界。
10.拖动不准:直播过程中丢帧、关键帧间隔太大。
11.CPU/GPU占用率高:数据量大、格式转换、软编解格式。
12.在直播过程中,决定视频预加载效果的好坏主要由:视频的码率、缓冲文件大小和网速决定。
原因:网速快且码率低的情况下,不需要使用预加载。(码率中等且网速一般的情况适用)需要注意的是:缓冲文件不能设置过大,会影响正常播放。
12.为什么播放视频时,会停留在第一帧画面。
原因:(1)解码器出现错误,只接出了第一帧图像。(2)没有接收到视频帧。(3)时间戳的计算有误。
   以上内容简单总结了直播中经常出现的问题及原因,那么在文章的结尾,想给大家举个简单的例子,比如盖楼需要混凝土和砖;种树需要土壤和水;养鱼需要水和饲料,开发一个直播平台就需要在线直播源码。源码就是开发的基础,没有源码就无法完成。所以,选择优质的源码也是开发过程中十分重要的一步。
本文声明原创,转载请注明出处。
0
回复

如何进行二维码的扫码渠道来源监测? 二维码扫码统计

回复

whb2010 发起了问题 • 1 人关注 • 491 次浏览 • 2019-03-19 16:19 • 来自相关话题

0
回复

微信公众号关注来源统计查询谁会搞? 公众号关注来源

回复

whb2010 发起了问题 • 1 人关注 • 525 次浏览 • 2019-03-18 17:26 • 来自相关话题

0
回复

iOSapp上架有哪些方法 审核 马甲包 ios上架

回复

THX 发起了问题 • 1 人关注 • 433 次浏览 • 2019-03-15 12:36 • 来自相关话题

0
回复

识别二维码群发海报兑换功能怎么实现? 生成二维码海报

回复

whb2010 发起了问题 • 1 人关注 • 405 次浏览 • 2019-03-14 14:29 • 来自相关话题

0
回复

公众号的二维码怎么生成? 公众号的二维码怎么生成

回复

whb2010 发起了问题 • 1 人关注 • 498 次浏览 • 2019-03-13 11:23 • 来自相关话题

0
评论

React Native调用原生Android/iOS代码方案并实现拨号功能 iOS andorid react native IT大前端

serge 发表了文章 • 259 次浏览 • 2019-03-09 23:11 • 来自相关话题

一 前言

由于前几个月公司2.0项目开发技术选型为React Native,技术部相关人员开始学习React Native相关的技术,笔者是一名Android开发者,下文所描述的React Native调用Android/iOS模块中关于iOS的部分如有误的地方,请指出。为了让从Android或iOS学习React Native的同志更加清楚的了解另一移动端,笔者尽可能写的详细点。

二 效果

下面两张图分别为iOS和Android上效果图,其中iOS效果图中点击电话号码会打印log,并不会调起iOS拨号界面,因为iOS模拟器不支持此功能,所以要想看效果只能用真机查看。这里打印log是为了证明React Native成功调起了原生iOS模块功能。





 





 
 
三 实现方案

关于调用拨号功能以及调用浏览器、短信、邮箱等功能,可有两种实现方案。

一种是按照React Native提供的调用原生的过程方案来调用,这种适合大部分React Native调用原生功能的需求,掌握这种后,基本以后再有调用原生需求即可按照此过程方案解决,此文也会选用这种方案进行描述。

另一种是React Native帮我们封装的Linking模块可以实现这类的需求,这种相比上一种来说相对简单,主要适用于调用原生的电话、短信、邮箱、浏览器等功能。

四 实现原生Android模块

1.在自己新建的Reacat Native项目中android/app/src/main/java/xxx(项目包名)/ 目录下(为了和其他文件分离,笔者又在此目录下新建一个native文件夹),需要新建一个java类文件,例如文件名为CallPhoneModule.java,这个java类一定要继承RN提供的ReactContextBaseJavaModule抽象类,然后实现其构造函数,其中的参数要为ReactApplicationContext reactContext。
 
public class CallPhoneModule extends ReactContextBaseJavaModule {
public CallPhoneModule(ReactApplicationContext reactContext) {
super(reactContext);
}
}
 
2.然后实现NativeModule中定义的getName()方法,返回一个String类型字符串,这个返回结果将要在JavaScript中使用,例如返回“CallPhoneModule”,则可以在JavaScript中通过React.NativeModules.CallPhoneModule调用。
 
注意,如果返回的字符串中有RCT前缀,则会自动移除RCT前缀。例如返回“RCTCallPhoneModule”,则在JavaScript中依然可以通过React.NativeModules.CallPhoneModule调用。CallPhoneModule继承ReactContextBaseJavaModule,ReactContextBaseJavaModule继承BaseJavaModule,BaseJavaModule实现了NativeModule接口,这是CallPhoneModule与NativeModule的关系。 
@Override
public String getName() {
return "CallPhoneModule";
}
 
3.然后在CallPhoneModule类中写一个方法,这个方法提供给JavaScript调用,例如方法名为callPhone,里面传递String类型参数(非必须),特别的这个方法要使用@ReactMethod注解,以及返回类型必须为void。然后在callPhone方法中写入要实现的功能,这里写入了拨号功能的实现。
 
Intent intent = new Intent(Intent.ACTION_DIAL, Uri.parse("tel:" + phoneString));
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
this.reactContext.startActivity(intent);
CallPhoneModule.java文件的全部代码如下:
package com.zhuku02;

import android.content.Intent;
import android.net.Uri;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.bridge.ReactMethod;
import java.lang.String;

public class CallPhoneModule extends ReactContextBaseJavaModule {

public ReactApplicationContext reactContext;

public CallPhoneModule(ReactApplicationContext reactContext) {
super(reactContext);
this.reactContext = reactContext;
}

@ReactMethod
public void callPhone(String phoneString) {
Intent intent = new Intent(Intent.ACTION_DIAL, Uri.parse("tel:" + phoneString));
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
this.reactContext.startActivity(intent);
}

@Override
public String getName() {
return "CallPhoneModule";
}
}
4.新建一个java类文件,例如文件名为CallPhoneReactPackage.java,这个类必须实现ReactPackage接口,然后实现createViewManagers、createNativeModules两个方法,特别的要在createNativeModules方法的返回值中add进刚才新建的CallPhoneModule类。

CallPhoneReactPackage.java全部代码如下:
package com.zhuku02;

import com.facebook.react.ReactPackage;
import com.facebook.react.bridge.NativeModule;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.uimanager.ViewManager;
import com.zhuku02.CallPhoneModule;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

public class CallPhoneReactPackage implements ReactPackage {

@Override
public List<ViewManager> createViewManagers(ReactApplicationContext reactContext) {
return Collections.emptyList();
}

@Override
public List<NativeModule> createNativeModules(ReactApplicationContext reactContext) {
List<NativeModule> modules = new ArrayList<>();

modules.add(new CallPhoneModule(reactContext));

return modules;
}
}
5.最后在MainApplication.java文件中的getPackages方法中加上刚才新建的CallPhoneReactPackage类。至此,原生Android模块书写完毕。关于JavaScript调用原生Android模块代码会在文末和调用原生iOS一起写出。

修改后的MainApplication.java文件代码如下:
package com.zhuku02;

import android.app.Application;
import com.facebook.react.ReactApplication;
import com.facebook.react.ReactNativeHost;
import com.facebook.react.ReactPackage;
import com.facebook.react.shell.MainReactPackage;
import com.facebook.soloader.SoLoader;

import com.zhuku02.CallPhoneReactPackage;

import java.util.Arrays;
import java.util.List;


public class MainApplication extends Application implements ReactApplication {

private final ReactNativeHost mReactNativeHost = new ReactNativeHost(this) {
@Override
public boolean getUseDeveloperSupport() {
return BuildConfig.DEBUG;
}

@Override
protected List<ReactPackage> getPackages() {
return Arrays.<ReactPackage>asList(
new MainReactPackage(),
new CallPhoneReactPackage()
);
}
};

@Override
public ReactNativeHost getReactNativeHost() {
return mReactNativeHost;
}

@Override
public void onCreate() {
super.onCreate();
SoLoader.init(this, /* native exopackage */ false);
}
}
 
五 实现原生iOS模块

1.在自己新建的Reacat Native项目中ios/xxx(项目包名)/ 目录下,需要新建一个后缀为m和一个后缀为h的文件。为了和其他文件分离以及和Android保持一致,笔者又在此目录下新建一个native文件夹。
 
在Xcode的此文件夹上右键New File,然后在弹出的页面中Cocoa Touch Class选项输入文件名,这样会建立出相同文件名的m和h文件。如果New File时,分别选择Objective-C File和Header File,则这两个文件名要相同。例如文件名称为CallPhoneModuleIos。

2.在CallPhoneModuleIos.h文件中,类要实现RN提供的RCTBridgeModule协议。RCT是ReaCT的缩写,React Native中Object-C相关的命名均以RCT开头。
 
RCTBridgeModule是定义好的protocol,实现该协议的类,会自动注册到Object-C对应的Bridge中。Object-C Bridge上层负责与Object-C通信,下层负责和JavaScript Bridge通信,而JavaScript Bridge负责和JavaScript通信。这样,通过Object-C Bridge和JavaScript Bridge就可以实现JavaScript和Object-C的相互调用。

CallPhoneModuleIos.h文件如下:
#import <UIKit/UIKit.h>
#import <React/RCTBridgeModule.h>
#import <React/RCTLog.h>

@interface CallPhoneModuleIos : NSObject <RCTBridgeModule>

@end
3.CallPhoneModuleIos.m文件中,类需要包含RCT_EXPORT_MODULE()宏,作用是自动注册一个module。这个宏可以添加一个参数,用来指定在JavaScript调用这个模块的名字,类似于上文中说的getName()方法。如果不添加这个参数,则默认就是这个类的名字。

4.然后需要在此类中写一个方法,提供给RN调用,方法通过RCT_EXPORT_METHOD()宏来实现。
RCT_EXPORT_METHOD(callPhone: (NSString *)phone){
NSLog(@"======%@",phone);
}
CallPhoneModuleIos.m完整代码:
#import "CallPhoneModuleIos.h"
#import <Foundation/Foundation.h>

@implementation CallPhoneModuleIos

RCT_EXPORT_MODULE(CallPhoneModuleIos);

RCT_EXPORT_METHOD(callPhone: (NSString *)phone){

NSLog(@"======%@",phone);
//去掉注释,下面代码就是实现拨号功能代码,但还未真机测试
// NSMutableString * str = [[NSMutableString alloc] initWithFormat:@"telprompt://%@",phone];
// [[UIApplication sharedApplication] openURL:[NSURL URLWithString:str]];

}

// -(dispath_queue_t)methodQueue{
// return dispath_get_main_queue();
// }

@end
至此,则原生iOS代码书写完成,现在即将开始调用。

六 React Native调用Android、iOS原生模块

为了在JavaScript端同时访问Android、iOS原生模块更加方便,笔者把原生模块的调用封装在一个JavaScript文件中,例如文件名为CallPhone.js,这样在需要调用的地方直接调用此JavaScript文件既可,同时在此文件中,处理好Android、iOS、Web(若有)的分别调用。
import {Platform, NativeModules} from 'react-native';

var module = null;
if (Platform.OS == 'ios') {
module = NativeModules.CallPhoneModuleIos;
} else if (Platform.OS == 'android') {
module = NativeModules.CallPhoneModule;
} else if (Platform.OS == 'web') {
//暂未实现web功能
}
export default module;
然后在JavaScript文件中这样调用:
import CallPhone from '../../native/CallPhone';
CallPhone.callPhone('40077731xx');
到这里,整篇文章就结束了,疑问、建议或者指教欢迎讨论。

七 参考资料

native-modules-ios (https://facebook.github.io/rea ... s.html)
native-modules-android (https://facebook.github.io/rea ... d.html) 






微信公众号:IT大前端
关注可了解更多的大前端领域技术 查看全部
一 前言

由于前几个月公司2.0项目开发技术选型为React Native,技术部相关人员开始学习React Native相关的技术,笔者是一名Android开发者,下文所描述的React Native调用Android/iOS模块中关于iOS的部分如有误的地方,请指出。为了让从Android或iOS学习React Native的同志更加清楚的了解另一移动端,笔者尽可能写的详细点。

二 效果

下面两张图分别为iOS和Android上效果图,其中iOS效果图中点击电话号码会打印log,并不会调起iOS拨号界面,因为iOS模拟器不支持此功能,所以要想看效果只能用真机查看。这里打印log是为了证明React Native成功调起了原生iOS模块功能。

640.gif

 

640_(1).gif

 
 
三 实现方案

关于调用拨号功能以及调用浏览器、短信、邮箱等功能,可有两种实现方案。

一种是按照React Native提供的调用原生的过程方案来调用,这种适合大部分React Native调用原生功能的需求,掌握这种后,基本以后再有调用原生需求即可按照此过程方案解决,此文也会选用这种方案进行描述。

另一种是React Native帮我们封装的Linking模块可以实现这类的需求,这种相比上一种来说相对简单,主要适用于调用原生的电话、短信、邮箱、浏览器等功能。

四 实现原生Android模块

1.在自己新建的Reacat Native项目中android/app/src/main/java/xxx(项目包名)/ 目录下(为了和其他文件分离,笔者又在此目录下新建一个native文件夹),需要新建一个java类文件,例如文件名为CallPhoneModule.java,这个java类一定要继承RN提供的ReactContextBaseJavaModule抽象类,然后实现其构造函数,其中的参数要为ReactApplicationContext reactContext。
 
public class CallPhoneModule extends ReactContextBaseJavaModule {
public CallPhoneModule(ReactApplicationContext reactContext) {
super(reactContext);
}
}

 
2.然后实现NativeModule中定义的getName()方法,返回一个String类型字符串,这个返回结果将要在JavaScript中使用,例如返回“CallPhoneModule”,则可以在JavaScript中通过React.NativeModules.CallPhoneModule调用。
 
注意,如果返回的字符串中有RCT前缀,则会自动移除RCT前缀。例如返回“RCTCallPhoneModule”,则在JavaScript中依然可以通过React.NativeModules.CallPhoneModule调用。CallPhoneModule继承ReactContextBaseJavaModule,ReactContextBaseJavaModule继承BaseJavaModule,BaseJavaModule实现了NativeModule接口,这是CallPhoneModule与NativeModule的关系。 
@Override
public String getName() {
return "CallPhoneModule";
}

 
3.然后在CallPhoneModule类中写一个方法,这个方法提供给JavaScript调用,例如方法名为callPhone,里面传递String类型参数(非必须),特别的这个方法要使用@ReactMethod注解,以及返回类型必须为void。然后在callPhone方法中写入要实现的功能,这里写入了拨号功能的实现。
 
Intent intent = new Intent(Intent.ACTION_DIAL,  Uri.parse("tel:" + phoneString));
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
this.reactContext.startActivity(intent);

CallPhoneModule.java文件的全部代码如下:
package com.zhuku02;

import android.content.Intent;
import android.net.Uri;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.bridge.ReactMethod;
import java.lang.String;

public class CallPhoneModule extends ReactContextBaseJavaModule {

public ReactApplicationContext reactContext;

public CallPhoneModule(ReactApplicationContext reactContext) {
super(reactContext);
this.reactContext = reactContext;
}

@ReactMethod
public void callPhone(String phoneString) {
Intent intent = new Intent(Intent.ACTION_DIAL, Uri.parse("tel:" + phoneString));
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
this.reactContext.startActivity(intent);
}

@Override
public String getName() {
return "CallPhoneModule";
}
}

4.新建一个java类文件,例如文件名为CallPhoneReactPackage.java,这个类必须实现ReactPackage接口,然后实现createViewManagers、createNativeModules两个方法,特别的要在createNativeModules方法的返回值中add进刚才新建的CallPhoneModule类。

CallPhoneReactPackage.java全部代码如下:
package com.zhuku02;

import com.facebook.react.ReactPackage;
import com.facebook.react.bridge.NativeModule;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.uimanager.ViewManager;
import com.zhuku02.CallPhoneModule;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

public class CallPhoneReactPackage implements ReactPackage {

@Override
public List<ViewManager> createViewManagers(ReactApplicationContext reactContext) {
return Collections.emptyList();
}

@Override
public List<NativeModule> createNativeModules(ReactApplicationContext reactContext) {
List<NativeModule> modules = new ArrayList<>();

modules.add(new CallPhoneModule(reactContext));

return modules;
}
}

5.最后在MainApplication.java文件中的getPackages方法中加上刚才新建的CallPhoneReactPackage类。至此,原生Android模块书写完毕。关于JavaScript调用原生Android模块代码会在文末和调用原生iOS一起写出。

修改后的MainApplication.java文件代码如下:
package com.zhuku02;

import android.app.Application;
import com.facebook.react.ReactApplication;
import com.facebook.react.ReactNativeHost;
import com.facebook.react.ReactPackage;
import com.facebook.react.shell.MainReactPackage;
import com.facebook.soloader.SoLoader;

import com.zhuku02.CallPhoneReactPackage;

import java.util.Arrays;
import java.util.List;


public class MainApplication extends Application implements ReactApplication {

private final ReactNativeHost mReactNativeHost = new ReactNativeHost(this) {
@Override
public boolean getUseDeveloperSupport() {
return BuildConfig.DEBUG;
}

@Override
protected List<ReactPackage> getPackages() {
return Arrays.<ReactPackage>asList(
new MainReactPackage(),
new CallPhoneReactPackage()
);
}
};

@Override
public ReactNativeHost getReactNativeHost() {
return mReactNativeHost;
}

@Override
public void onCreate() {
super.onCreate();
SoLoader.init(this, /* native exopackage */ false);
}
}

 
五 实现原生iOS模块

1.在自己新建的Reacat Native项目中ios/xxx(项目包名)/ 目录下,需要新建一个后缀为m和一个后缀为h的文件。为了和其他文件分离以及和Android保持一致,笔者又在此目录下新建一个native文件夹。
 
在Xcode的此文件夹上右键New File,然后在弹出的页面中Cocoa Touch Class选项输入文件名,这样会建立出相同文件名的m和h文件。如果New File时,分别选择Objective-C File和Header File,则这两个文件名要相同。例如文件名称为CallPhoneModuleIos。

2.在CallPhoneModuleIos.h文件中,类要实现RN提供的RCTBridgeModule协议。RCT是ReaCT的缩写,React Native中Object-C相关的命名均以RCT开头。
 
RCTBridgeModule是定义好的protocol,实现该协议的类,会自动注册到Object-C对应的Bridge中。Object-C Bridge上层负责与Object-C通信,下层负责和JavaScript Bridge通信,而JavaScript Bridge负责和JavaScript通信。这样,通过Object-C Bridge和JavaScript Bridge就可以实现JavaScript和Object-C的相互调用。

CallPhoneModuleIos.h文件如下:
#import <UIKit/UIKit.h>
#import <React/RCTBridgeModule.h>
#import <React/RCTLog.h>

@interface CallPhoneModuleIos : NSObject <RCTBridgeModule>

@end

3.CallPhoneModuleIos.m文件中,类需要包含RCT_EXPORT_MODULE()宏,作用是自动注册一个module。这个宏可以添加一个参数,用来指定在JavaScript调用这个模块的名字,类似于上文中说的getName()方法。如果不添加这个参数,则默认就是这个类的名字。

4.然后需要在此类中写一个方法,提供给RN调用,方法通过RCT_EXPORT_METHOD()宏来实现。
RCT_EXPORT_METHOD(callPhone: (NSString *)phone){
NSLog(@"======%@",phone);
}

CallPhoneModuleIos.m完整代码:
#import "CallPhoneModuleIos.h"
#import <Foundation/Foundation.h>

@implementation CallPhoneModuleIos

RCT_EXPORT_MODULE(CallPhoneModuleIos);

RCT_EXPORT_METHOD(callPhone: (NSString *)phone){

NSLog(@"======%@",phone);
//去掉注释,下面代码就是实现拨号功能代码,但还未真机测试
// NSMutableString * str = [[NSMutableString alloc] initWithFormat:@"telprompt://%@",phone];
// [[UIApplication sharedApplication] openURL:[NSURL URLWithString:str]];

}

// -(dispath_queue_t)methodQueue{
// return dispath_get_main_queue();
// }

@end

至此,则原生iOS代码书写完成,现在即将开始调用。

六 React Native调用Android、iOS原生模块

为了在JavaScript端同时访问Android、iOS原生模块更加方便,笔者把原生模块的调用封装在一个JavaScript文件中,例如文件名为CallPhone.js,这样在需要调用的地方直接调用此JavaScript文件既可,同时在此文件中,处理好Android、iOS、Web(若有)的分别调用。
import {Platform, NativeModules} from 'react-native';

var module = null;
if (Platform.OS == 'ios') {
module = NativeModules.CallPhoneModuleIos;
} else if (Platform.OS == 'android') {
module = NativeModules.CallPhoneModule;
} else if (Platform.OS == 'web') {
//暂未实现web功能
}
export default module;

然后在JavaScript文件中这样调用:
import CallPhone from '../../native/CallPhone';
CallPhone.callPhone('40077731xx');

到这里,整篇文章就结束了,疑问、建议或者指教欢迎讨论。

七 参考资料

native-modules-ios (https://facebook.github.io/rea ... s.html)
native-modules-android (https://facebook.github.io/rea ... d.html) 


qrcode_for_gh_08bfa7313fb2_258.jpg

微信公众号:IT大前端
关注可了解更多的大前端领域技术
0
评论

WIFI 考勤打卡 浅析 WIFI Android

serge 发表了文章 • 310 次浏览 • 2019-03-04 22:11 • 来自相关话题

一、背景

最近产品部提出了在WEB端设置wifi考勤打卡新需求,根据管理员设置的wifi相关信息(主要是WIFI名称和MAC地址),员工用户利用移动端相连接的wifi进行wifi考勤打卡。

二、名词术语解释

下面的理解全是建立在无线路由器的基础上。如有错误请指出。

1、SS

SS(Service set)即服务集,是无线局域网中的一个术语,用以描述802.11无线网络的构成单位(一组互相有联系的无线设备),使用服务集标识符(SSID)作为识别。

可以分为独立基本服务集(IBSS)、基本服务集(BSS)和扩展服务集(ESS)三类。其中IBSS属于对等拓扑模式(又称Ad-Hoc模式、无线随意网络),而BSS和ESS属于基础架构模式。这些拓扑是原始的802.11规范中定义的,其他的如网桥、中继器等则是属于特定厂商的扩展或者WDS的拓扑模式。

2、SSID

SSID(Service Set Identifier)即服务集标识符,是一个或一组基础架构模式无线网络的标识,依照标识方式又可细分为两种:
基本服务集标识符(BSSID),表示的是AP的数据链路层的MAC地址。
扩展服务集标识符(ESSID),一个最长32字节区分大小写的字符串,ESSID标识与SSID相同的网络。术语SSID最常用。
在此可以理解为无线路由器发射的某个wifi的名称。(SSID=name of network)

3、BSS

BSS(Basic Service Set)即基本服务集,是一组能在PHY层相互通信的所有站。每个BSS都有一个称为BSSID的标识(ID),它是服务于BSS的接入点的MAC地址。
用在无线路由器发射出的wifi上可以这样理解:某一个无线路由器发射出的wifi信号所覆盖的范围可视为BSS。





 
4、BSSID

BSSID(Basic Service Set Identifier)即基本服务集标识符。

在上面的基础上可以这样理解:对某一个BSS基本服务集的唯一标识。例如,某无线路由器发射了一个名称为A的wifi热点,同一区域另一个无线路由器也发出了一个名称为A的wifi热点,当手机连接A热点时,如何辨别连接的是由哪一个路由器发射的wifi呢?

这时候就要用到BSSID了。一般情况下BSSID可以理解为无线路由器的MAC地址,通过查看手机连接wifi的MAC地址即可知道连接的是哪一个路由。(BSSID=AP MAC address)
其实准确来说手机得到的BSSID并不是路由器的基准(出厂)MAC地址。

例如,笔者公司的某款无线路由器B的出厂MAC地址为 XX:XX:XX:XX:XX:F1,当手机连接此wifi查看mac地址时发现是XX:XX:XX:XX:XX:F2,或者是XX:XX:XX:XX:XX:F3。

5、ESS ESSID

ESS(Extended Service Set )即扩展的基本服务集。
ESSID(Extended Service Set Identifier)即扩展的基本服务集标识符。
BSS+BSS+BSS+BSS+…=ESS。ESS为多个BSS的集合。ESS使用指定的ESSID作识别。

通过将多个BSS比邻安置,可以扩展网络的范围,如果这些BSS通过各种分布系统互联(无论是有线的还是无线的),拥有一致的ESSID,并且对于逻辑链路控制层来说可以认为是一个BSS的话,那么这些BSS可以被统一为一个ESS。

在同一个ESS中的不同BSS之间切换的过程称为漫游。一般而言,一个ESS中的BSS都会使用相同的SSID和安全机制以提供接近于无缝漫游的可能。两个BSS之间通常有15%左右的重叠范围来保证漫游时信号不会长时间丢失,并且设置在不同频段来防止相互干扰。





 
6、MAC

MAC地址采用十六进制数表示,共六个字节(48位)。(XX:XX:XX:XX:XX:XX )其中,前三个字节是由IEEE的注册管理机构RA负责给不同厂家分配的代码(高位24位),也称为“编制上唯一的标识符”(Organizationally Unique Identifier),后三个字节(低位24位)由各厂家自行指派给生产的适配器接口,称为扩展标识符(唯一性)。

三、历程

当产品部提出wifi考勤打卡需求时,普遍认为一个路由器有一个mac地址,手机连接wifi可以根据mac地址等信息进行打卡。当我们用多个手机连接公司名称为A(SSID)的wifi时,发现手机上展示的mac地址并不是一致的,这个就尴尬了,打翻了原有理念。
然后发现我们公司共有五个无线路由器,wifi名称都是A。哦,这时候才感觉到原来以前的知识还是靠谱的,可能是多个手机具体连接的路由器不是同一个。

然后把五个路由器wifi热点名称改为A、B、C、D、E,多个手机连接A热点时,发现手机得到的mac地址是一致的,到这里可以得出的结论是手机连接同一个wifi热点得到的mac地址是一致的。但是…..又尴尬了。

当多款手机连接B热点时,发现又出现了不一致的mac地址,查找原因发现,原来B无线路由器中可以设置2.4G Hz和5G Hz两个不同频段的wifi热点。B路由器中默认是开启2.4G Hz和5G Hz频段的wifi热点,并且wifi名称(SSID)是同一个。经过检查还有个问题是B路由器的出厂mac地址和手机连接得到的mac地址不一致。

例如上面举得例子:笔者公司的某款无线路由器B的出厂MAC地址为 XX:XX:XX:XX:XX:F1,当手机连接此wifi查看mac地址时发现是XX:XX:XX:XX:XX:F2,另一款手机连接时是XX:XX:XX:XX:XX:F3。由此可得出的结论是,路由器有一个基准(出厂)mac地址,然后发射出wifi的mac在基准mac地址上按照一定的算法进行变动,具体的变动算法不清楚,有清楚的请告知我,非常感谢。

另外还有一个问题是,C路由器设备后面所写的出厂说明mac地址是XX:XX:XX:XX:XX:56,但是通过路由器后台看到的出厂mac地址是XX:XX:XX:XX:XX:57,手机连接后得到的mac地址是XX:XX:XX:XX:XX:56。这就尴尬了,是厂家写错了还是根据特定的算法算的?

除了根据wifi设备分析外,我们也对具有wifi考勤打卡功能的软件进行了分析。比如现在比较火爆的由阿里团队研发的钉钉,以及纷享销客APP,在Android端,他们的处理都是获取周围wifi信息(并不是当前手机连接的wifi)进行打卡。在iOS端,他们的处理都是根据当前手机连接的wifi信息进行打卡。据iOS同事说,iOS获取周围wifi信息需要申请此功能,并最低支持版本是iOS 9。另外据可靠消息,分享逍客对mac地址的处理也是通过忽略低4位进行匹配。

四、结论

经过上述分析,手机获取的无线路由器MAC地址的低4位是变化的。那我们实现这个需求时,除了匹配虚拟位置、手机信息、wifi相关等其他信息外,只针对mac地址,我们可以忽略mac地址的低4位来做匹配。

五、参考资料

http://www.juniper.net/documentation/en_US/junos-space-apps12.3/network-director/topics/concept/wireless-ssid-bssid-essid.html 






微信公众号:IT大前端
关注可了解更多的大前端领域技术 查看全部
一、背景

最近产品部提出了在WEB端设置wifi考勤打卡新需求,根据管理员设置的wifi相关信息(主要是WIFI名称和MAC地址),员工用户利用移动端相连接的wifi进行wifi考勤打卡。

二、名词术语解释

下面的理解全是建立在无线路由器的基础上。如有错误请指出。

1、SS

SS(Service set)即服务集,是无线局域网中的一个术语,用以描述802.11无线网络的构成单位(一组互相有联系的无线设备),使用服务集标识符(SSID)作为识别。

可以分为独立基本服务集(IBSS)、基本服务集(BSS)和扩展服务集(ESS)三类。其中IBSS属于对等拓扑模式(又称Ad-Hoc模式、无线随意网络),而BSS和ESS属于基础架构模式。这些拓扑是原始的802.11规范中定义的,其他的如网桥、中继器等则是属于特定厂商的扩展或者WDS的拓扑模式。

2、SSID

SSID(Service Set Identifier)即服务集标识符,是一个或一组基础架构模式无线网络的标识,依照标识方式又可细分为两种:
基本服务集标识符(BSSID),表示的是AP的数据链路层的MAC地址。
扩展服务集标识符(ESSID),一个最长32字节区分大小写的字符串,ESSID标识与SSID相同的网络。术语SSID最常用。
在此可以理解为无线路由器发射的某个wifi的名称。(SSID=name of network)

3、BSS

BSS(Basic Service Set)即基本服务集,是一组能在PHY层相互通信的所有站。每个BSS都有一个称为BSSID的标识(ID),它是服务于BSS的接入点的MAC地址。
用在无线路由器发射出的wifi上可以这样理解:某一个无线路由器发射出的wifi信号所覆盖的范围可视为BSS。

WechatIMG363.jpeg

 
4、BSSID

BSSID(Basic Service Set Identifier)即基本服务集标识符。

在上面的基础上可以这样理解:对某一个BSS基本服务集的唯一标识。例如,某无线路由器发射了一个名称为A的wifi热点,同一区域另一个无线路由器也发出了一个名称为A的wifi热点,当手机连接A热点时,如何辨别连接的是由哪一个路由器发射的wifi呢?

这时候就要用到BSSID了。一般情况下BSSID可以理解为无线路由器的MAC地址,通过查看手机连接wifi的MAC地址即可知道连接的是哪一个路由。(BSSID=AP MAC address)
其实准确来说手机得到的BSSID并不是路由器的基准(出厂)MAC地址。

例如,笔者公司的某款无线路由器B的出厂MAC地址为 XX:XX:XX:XX:XX:F1,当手机连接此wifi查看mac地址时发现是XX:XX:XX:XX:XX:F2,或者是XX:XX:XX:XX:XX:F3。

5、ESS ESSID

ESS(Extended Service Set )即扩展的基本服务集。
ESSID(Extended Service Set Identifier)即扩展的基本服务集标识符。
BSS+BSS+BSS+BSS+…=ESS。ESS为多个BSS的集合。ESS使用指定的ESSID作识别。

通过将多个BSS比邻安置,可以扩展网络的范围,如果这些BSS通过各种分布系统互联(无论是有线的还是无线的),拥有一致的ESSID,并且对于逻辑链路控制层来说可以认为是一个BSS的话,那么这些BSS可以被统一为一个ESS。

在同一个ESS中的不同BSS之间切换的过程称为漫游。一般而言,一个ESS中的BSS都会使用相同的SSID和安全机制以提供接近于无缝漫游的可能。两个BSS之间通常有15%左右的重叠范围来保证漫游时信号不会长时间丢失,并且设置在不同频段来防止相互干扰。

WechatIMG362.jpeg

 
6、MAC

MAC地址采用十六进制数表示,共六个字节(48位)。(XX:XX:XX:XX:XX:XX )其中,前三个字节是由IEEE的注册管理机构RA负责给不同厂家分配的代码(高位24位),也称为“编制上唯一的标识符”(Organizationally Unique Identifier),后三个字节(低位24位)由各厂家自行指派给生产的适配器接口,称为扩展标识符(唯一性)。

三、历程

当产品部提出wifi考勤打卡需求时,普遍认为一个路由器有一个mac地址,手机连接wifi可以根据mac地址等信息进行打卡。当我们用多个手机连接公司名称为A(SSID)的wifi时,发现手机上展示的mac地址并不是一致的,这个就尴尬了,打翻了原有理念。
然后发现我们公司共有五个无线路由器,wifi名称都是A。哦,这时候才感觉到原来以前的知识还是靠谱的,可能是多个手机具体连接的路由器不是同一个。

然后把五个路由器wifi热点名称改为A、B、C、D、E,多个手机连接A热点时,发现手机得到的mac地址是一致的,到这里可以得出的结论是手机连接同一个wifi热点得到的mac地址是一致的。但是…..又尴尬了。

当多款手机连接B热点时,发现又出现了不一致的mac地址,查找原因发现,原来B无线路由器中可以设置2.4G Hz和5G Hz两个不同频段的wifi热点。B路由器中默认是开启2.4G Hz和5G Hz频段的wifi热点,并且wifi名称(SSID)是同一个。经过检查还有个问题是B路由器的出厂mac地址和手机连接得到的mac地址不一致。

例如上面举得例子:笔者公司的某款无线路由器B的出厂MAC地址为 XX:XX:XX:XX:XX:F1,当手机连接此wifi查看mac地址时发现是XX:XX:XX:XX:XX:F2,另一款手机连接时是XX:XX:XX:XX:XX:F3。由此可得出的结论是,路由器有一个基准(出厂)mac地址,然后发射出wifi的mac在基准mac地址上按照一定的算法进行变动,具体的变动算法不清楚,有清楚的请告知我,非常感谢。

另外还有一个问题是,C路由器设备后面所写的出厂说明mac地址是XX:XX:XX:XX:XX:56,但是通过路由器后台看到的出厂mac地址是XX:XX:XX:XX:XX:57,手机连接后得到的mac地址是XX:XX:XX:XX:XX:56。这就尴尬了,是厂家写错了还是根据特定的算法算的?

除了根据wifi设备分析外,我们也对具有wifi考勤打卡功能的软件进行了分析。比如现在比较火爆的由阿里团队研发的钉钉,以及纷享销客APP,在Android端,他们的处理都是获取周围wifi信息(并不是当前手机连接的wifi)进行打卡。在iOS端,他们的处理都是根据当前手机连接的wifi信息进行打卡。据iOS同事说,iOS获取周围wifi信息需要申请此功能,并最低支持版本是iOS 9。另外据可靠消息,分享逍客对mac地址的处理也是通过忽略低4位进行匹配。

四、结论

经过上述分析,手机获取的无线路由器MAC地址的低4位是变化的。那我们实现这个需求时,除了匹配虚拟位置、手机信息、wifi相关等其他信息外,只针对mac地址,我们可以忽略mac地址的低4位来做匹配。

五、参考资料

http://www.juniper.net/documentation/en_US/junos-space-apps12.3/network-director/topics/concept/wireless-ssid-bssid-essid.html 


qrcode_for_gh_08bfa7313fb2_258.jpg

微信公众号:IT大前端
关注可了解更多的大前端领域技术
0
回复

cocoapods 导入问题 Cocoapods 导入SDK 报错 导入 iOS

回复

Kion♚ 发起了问题 • 1 人关注 • 534 次浏览 • 2019-02-28 10:34 • 来自相关话题