写在前面,因为公司做的项目需要聊天功能,所以在网上找了下方案后,果断选择了融云,因为免费,有100个测试位。
本来没想着写这个文章,无奈融云的文档写的相(fei)当(chang)简(la)单(ji),在问客服问题,回复太慢了,并且没有解决问题。
所以记录下来,防止大家继续踩坑。
文章目录
目录
1.集成步骤
2.集成SDK
3.会话列表
1. 静态注册
2. 动态加载
4.会话
5.启动会话和会话列表
6.用户信息
7.退出融云
8.通知
9.服务器连接状态监听
10.发送信息监听
11.会话列表刷新头像
12.会话列表界面操作监听
13.会话界面操作监听
14.会话中历史消息头像更新
15.问题处理
1.注册
去融云官方网站注册,官方网址在这里
2.获取app key 和 app secert
登录成功后创建应用后可以看到app key和 app secret。注意这个app key 很重要,在后面要用到!
3.下载SDK
找到SDK进行下载,这里需要根据开发功能进行下载,因为我的项目只需要进行单聊和会话列表的功能,不需要其他功能(红包,位置,视频,语音),根据需求选择sdk,注意,IMKit、IMLib 都要下载,IMkit是界面模块开发,IMlib是通讯模块开发。
4.功能开发
到这一步就已经成功一大半了,功能开发可以参考融云官方文档,下面是针对我项目中涉及的功能开发进行介绍说明。
1.将下载好的sdk导入项目中
2.在项目中的build.gradle中添加imkit和imlib的依赖
然后在项目AndroidManifest.xml 文件中,添加 FileProvider 配置,修改 android:authorities 为应用的 “ApplicationId”.FileProvider,如下:
3.设置App key
打开 IMLib Module 的 AndroidManifest.xml 文件,将meta-data中的RONG_CLOUD_APP_KEY 修改为在融云登录成功后创建好的的app key。
4.初始化
融云官方文档中建议在application create的时候进行初始化,这里按照建议进行初始化。
在这里需要注意,所有的融云请求或操作都是在init后才能操作的,如果没有进行init直接请求或者操作必然失败。
if (getApplicationInfo().packageName.equals(getCurProcessName(getApplicationContext()))){
RongIM.init(this);
}
5.连接融云服务器
在init成功后,需要进行服务器的链接,这个建议放在baseactivity中作为公用方法,在登录成功后主动调用,在进入会话列表界面主动调用,在进入会话界面主动调用,因为在使用融云服务之前要确保和融云服务器的连接是存在且正常的。
在onSucceess的回调中,返回了userid,userid是我们在融云的身份id,可以这样理解:两个用户之间的聊天,就是用云的两个userid在互相发送消息。
/**
* 连接服务器,在整个应用程序全局,只需要调用一次,需在 {@link #init(Context)} 之后调用。
* 如果调用此接口遇到连接失败,SDK 会自动启动重连机制进行最多10次重连,分别是1, 2, 4, 8, 16, 32, 64, 128, 256, 512秒后。
* 在这之后如果仍没有连接成功,还会在当检测到设备网络状态变化时再次进行重连
*
* @param token 从服务端获取的用户身份令牌(Token)。
* @param callback 连接回调。
* @return RongIM 客户端核心类的实例。
*/
private void connect(String token) {
if (getApplicationInfo().packageName.equals(App.getCurProcessName(getApplicationContext()))) {
RongIM.connect(token, new RongIMClient.ConnectCallback() {
/**
* Token 错误。可以从下面两点检查 1. Token 是否过期,如果过期您需要向 App Server 重新请求一个新的 Token
* 2. token 对应的 appKey 和工程里设置的 appKey 是否一致
*/
@Override
public void onTokenIncorrect() {
//在这里处理链接失败的问题,如果token错误,重新获取token,进行连接
}
/**
* 连接融云成功
* @param userid 当前 token 对应的用户 id
*/
@Override
public void onSuccess(String userid) {
Log.d("LoginActivity", "--onSuccess" + userid);
}
/**
* 连接融云失败
* @param errorCode 错误码,可到官网 查看错误码对应的注释
*/
@Override
public void onError(RongIMClient.ErrorCode errorCode) {
}
});
}
}
按融云的介绍,会话列表有两种实现方式,一种是静态注册,一种是动态加载。
a. 会话列表所在的Activity 对应的布局文件:conversationlist.xml。注意 android:name 固定为融云的 ConversationListFragment
b.新建Activity
public class ConversationListActivity extends FragmentActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.conversationlist);
}
}
c.Activity对应Intent-Filter配置
在这里需要注意的是host一定要设置成自己的application id,如果没有特别设置,application id就是自己的包名。
但是,我们如果把会话列表放在fragment中如何弄呢?一般App都是用viewpage+fragment来进行多界面显示,所以这就到了动态加载的时候了。
a. 所在fragment中的布局文件
b.配置Uri
可以根据我们要显示的会话类型,进行配置
ConversationListFragment conversationListFragment = new ConversationListFragment();
Uri uri = Uri.parse("rong://" + getActivity().getApplicationInfo().packageName).buildUpon()
.appendPath("conversationlist")
.appendQueryParameter(Conversation.ConversationType.PRIVATE.getName(), "false") //设置私聊会话,该会话聚合显示
.appendQueryParameter(Conversation.ConversationType.SYSTEM.getName(), "true")//设置系统会话,该会话非聚合显示
.build();
c.将Uri设置给fragment
conversationListFragment.setUri(uri);
d.将会话列表加载到fragment中
FragmentManager fragmentManager = getChildFragmentManager();
FragmentTransaction transaction = fragmentManager.beginTransaction();
transaction.add(R.id.rong_container,conversationListFragment);
transaction.commit();
会话 Fragment 跟会话列表是完全一致的,添加方式完全一致,这里就不需要考虑加载在fragment中了,因为基本上每个会话都是单独的一个界面。
1.会话所在的Activity 对应的布局文件:conversationlist.xml。注意 android:name 固定为融云的 ConversationFragment
2.新建Activity
public class ConversationActivity extends FragmentActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.conversation);
}
}
3.Activity对应Intent-Filter配置
这里同样和会话列表一样,host一定要设置成自己的application id。
1.启动会话
/**
*启动会话界面
* @param context 应用上下文。
* @param conversationType 会话类型。
* @param targetId 根据不同的 conversationType,可能是用户 Id、群组 Id 或聊天室 Id。
* @param title 聊天的标题,开发者可以在聊天界面通过 intent.getData().getQueryParameter("title") 获取该值, 再手动设置为标题。
*/
public void startConversation(Context context, Conversation.ConversationType conversationType, String targetId, String title)
/**
* 启动单聊界面。
*
* @param context 应用上下文。
* @param targetUserId 要与之聊天的用户 Id。
* @param title 聊天的标题,开发者需要在聊天界面通过 intent.getData().getQueryParameter("title")
* 获取该值, 再手动设置为聊天界面的标题。
*/
RongIM.getInstance().startPrivateChat(getActivity(), targetUserid, title);
2.启动会话列表
/**
* 启动会话列表界面。
*
* @param context 应用上下文。
* @param supportedConversation 定义会话列表支持显示的会话类型,及对应的会话类型是否聚合显示。
* 例如:supportedConversation.put(Conversation.ConversationType.PRIVATE.getName(), false) 非聚合式显示 private 类型的会话。
*/
public void startConversationList(Context context, Map supportedConversation)
在融云的官方介绍中,对用户信息做了说明。
融云认为,每一个设计良好且功能健全的 App 都应该能够在本地获取、缓存并更新用户信息。所以,融云不维护用户基本信息(用户
Id、昵称、头像)。此外,App 提供用户信息也避免了由于缓存导致的用户信息更新不及时,App 中不同界面上的用户信息不统一(比如:一部分
App 从 App 服务器上获取并显示,一部分由融云服务器获取并显示),能够获得更好的用户体验。
融云提供了两种方式从 App 的数据源显示用户昵称和头像。
1.用户信息提供者
2.发送消息的时候携带用户信息
两种方式实现任意一种就可以,安卓和IOS两端必须保持统一,即选择的实现方式必须一致,否则可能报错
由于第一种实现方式比较麻烦,在项目中我选择了第二种实现方式。
结果这里埋了个坑,在更新头像后发现会话中的历史消息头像并没有更新,在后面讲我是如何处理的
a.在init后设置发送消息的时候是否携带信息为true,我是放在application的create中了
/**
* 设置消息体内是否携带用户信息。
*
* @param state 是否携带用户信息,true 携带,false 不携带。
*/
RongIM.getInstance().setMessageAttachedUserInfo(true);
b.设置用户信息
/**
* @param id 用户在融云的id
* @param name 用户在融云的名称
* @param uri 用户的头像
*/
RongIM.getInstance().setCurrentUserInfo(new io.rong.imlib.model.UserInfo(id, name, Uri.parse(img)));
c.启动私聊或会话列表
RongIM.getInstance().startPrivateChat(context,userid ,title);
与融云服务器断开连接有两种方式:
RongIM.logout()
RongIM.disconnect();
logout表示与融云的服务器断开连接后,有别人发过来的消息,不接收通知。
disconncet表示与融云的服务器断开连接后,有别人发送过来的消息,接受通知。
应用切后台,在有消息过来的时候,往往会有通知提示。
我们需要创建一个Receiver继承PushMessageReceiver,如下:
public class SealNotificationReceiver extends PushMessageReceiver {
@Override
public boolean onNotificationMessageArrived(Context context, PushNotificationMessage message) {
return false; // 返回 false, 会弹出融云 SDK 默认通知; 返回 true, 融云 SDK 不会弹通知, 通知需要由您自定义。
}
@Override
public boolean onNotificationMessageClicked(Context context, PushNotificationMessage message) {
return false; // 返回 false, 会走融云 SDK 默认处理逻辑, 即点击该通知会打开会话列表或会话界面; 返回 true, 则由您自定义处理逻辑。
}
}
然后在Androidmanifest中静态注册接收器
这样,在融云推送消息的时候,通知栏就能弹出消息,更多关于推送的内容可以去官网查看,点击这里
在进行融云相关请求和操作的时候,我们往往需要知道与服务器的连接状态,连接正常才有必要进行下一步。
1.创建自己的状态监听文件,实现融云的状态监听方法
public class RongYunConnectionStatusListener implements RongIMClient.ConnectionStatusListener {
private Context context;
public RongYunConnectionStatusListener(Context context){
this.context = context;
}
@Override
public void onChanged(ConnectionStatus connectionStatus) {
switch (connectionStatus){
case CONNECTED://连接成功。
break;
case DISCONNECTED://断开连接。
break;
case CONNECTING://连接中。
break;
case NETWORK_UNAVAILABLE://网络不可用。
break;
case KICKED_OFFLINE_BY_OTHER_CLIENT://用户账户在其他设备登录,本机会被踢掉线
new Handler(Looper.getMainLooper()).post(new Runnable() {
@Override
public void run() {
/**
* 被踢掉后,弹框提示
*/
ToastUtil.shortToast(context,"账号在其他设备登录,请重新登录");
/**
* 被踢掉后,跳转到登录界面
*/
Intent intent=new Intent(context,LoginActivity.class);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS);
context.startActivity(intent);
}
});
break;
}
}
}
2.在application的create中设置链接状态监听
/**
* 融云链接状态监听
*/
RongIM.setConnectionStatusListener(new RongYunConnectionStatusListener(this));
这样对设备与融云服务器的链接状态进行了监听,当出现上面的几种状态,便对应进行逻辑处理。
在进行会话的过程中,有时候需要对发送消息的结果进行捕获,这时候应该实现的是SendMessageListener
1.创建自己的信息发送监听,实现融云的监听方法
public class RongYunSendMessageListener implements RongIM.OnSendMessageListener {
private Context context;
public RongYunSendMessageListener(BaseApplication context) {
this.context = context;
}
@Override
public Message onSend(Message message) {
return message;
}
@Override
public boolean onSent(Message message, RongIM.SentMessageErrorCode sentMessageErrorCode) {
if(message.getSentStatus()== Message.SentStatus.FAILED){
if(sentMessageErrorCode== RongIM.SentMessageErrorCode.NOT_IN_CHATROOM){
//不在聊天室
}else if(sentMessageErrorCode== RongIM.SentMessageErrorCode.NOT_IN_DISCUSSION){
//不在讨论组
}else if(sentMessageErrorCode== RongIM.SentMessageErrorCode.NOT_IN_GROUP){
//不在群组
}else if(sentMessageErrorCode== RongIM.SentMessageErrorCode.REJECTED_BY_BLACKLIST){
//你在他的黑名单中
}else{
ToastUtil.shortToast(context,"与服务器失去链接,请重新登录");
}
}
return false;
}
}
2.在application的create中设置消息发送 监听
/**
* 融云发送消息监听
*/
RongIM.getInstance().setSendMessageListener(new RongYunSendMessageListener(this));
这样对消息发送进行了监听,当出现上面的几种状态,便对应进行逻辑处理。
在跳转会话列表界面时,可以先进行一次connect,因为在刷新会话列表的时候需要保证与融云服务器链接正常。a. 通过接口获取会话列表数据
b.针对每个会话,通过userid在app服务器获取用户信息
/**
* 更新会话列表头像和标题
*/
RongIM.getInstance().getConversationList(new RongIMClient.ResultCallback>() {
@Override
public void onSuccess(List conversations) {
if (conversations == null || conversations.size() <= 0){
return;
}
for (int i = 0 ; i < conversations.size() ; i ++){
Conversation conversation = conversations.get(i);
if (conversation != null){
//这里通过token 和useid在我们自己服务器请求用户信息
mPresenter.GetUserInfo(token,conversation.getTargetId());
}
}
}
@Override
public void onError(RongIMClient.ErrorCode errorCode) {
}
});
c.获取用户信息成功后通过接口更新用户信息
io.rong.imlib.model.UserInfo userInfo = new io.rong.imlib.model.UserInfo(
result.getPhone(),
result.getName(),
Uri.parse(result.getImg()));
RongIM.getInstance().refreshUserInfoCache(userInfo);
通过这样设置后,会话列表会自动获取最新的用户信息,显示在会话列表上。
1.定义自己的操作监听类
private class MyConversationListBehaviorListener implements RongIM.ConversationListBehaviorListener{
/**
* 当点击会话头像后执行。
*
* @param context 上下文。
* @param conversationType 会话类型。
* @param targetId 被点击的用户id。
* @return 如果用户自己处理了点击后的逻辑处理,则返回 true,否则返回 false,false 走融云默认处理方式。
*/
boolean onConversationPortraitClick(Context context, Conversation.ConversationType conversationType, String targetId){
ToastUtils.shorttoast(context,"跳转到用户详情");
}
/**
* 当长按会话头像后执行。
*
* @param context 上下文。
* @param conversationType 会话类型。
* @param targetId 被点击的用户id。
* @return 如果用户自己处理了点击后的逻辑处理,则返回 true,否则返回 false,false 走融云默认处理方式。
*/
boolean onConversationPortraitLongClick(Context context, Conversation.ConversationType conversationType, String targetId){
return false;
}
/**
* 长按会话列表中的 item 时执行。
*
* @param context 上下文。
* @param view 触发点击的 View。
* @param uiConversation 长按时的会话条目。
* @return 如果用户自己处理了长按会话后的逻辑处理,则返回 true, 否则返回 false,false 走融云默认处理方式。
*/
@Override
public boolean onConversationLongClick(Context context, View view, UIConversation uiConversation) {
return false;
}
/**
* 点击会话列表中的 item 时执行。
*
* @param context 上下文。
* @param view 触发点击的 View。
* @param uiConversation 会话条目。
* @return 如果用户自己处理了点击会话后的逻辑处理,则返回 true, 否则返回 false,false 走融云默认处理方式。
*/
@Override
public boolean onConversationClick(Context context, View view, UIConversation uiConversation) {
return false;
}
}
2.在application的create中设置监听
/**
* 融云会话列表界面监听
*/
RongIM.setConversationListBehaviorListener(new MyConversationListBehaviorListener());
通过这样设置后,针对在设置列表界面的不同操作可以进行不同的逻辑处理。
会话界面操作监听和会话列表界面相同,都是对该界面的操作进行处理,实现自己的功能。1.定义自己的会话界面操作监听类,实现融云的方法
private class MyConversationClickListener implements RongIM.ConversationClickListener {
/**
* 当点击用户头像后执行。
*
* @param context 上下文。
* @param conversationType 会话类型。
* @param user 被点击的用户的信息。
* @param targetId 会话 id
* @return 如果用户自己处理了点击后的逻辑处理,则返回 true,否则返回 false,false 走融云默认处理方式。
*/
@Override
public boolean onUserPortraitClick(Context context, Conversation.ConversationType conversationType, UserInfo user, String targetId) {
return false;
}
/**
* 当长按用户头像后执行。
*
* @param context 上下文。
* @param conversationType 会话类型。
* @param user 被点击的用户的信息。
* @param targetId 会话 id
* @return 如果用户自己处理了点击后的逻辑处理,则返回 true,否则返回 false,false 走融云默认处理方式。
*/
@Override
public boolean onUserPortraitLongClick(Context context, Conversation.ConversationType conversationType, UserInfo user, String targetId) {
return false;
}
/**
* 当点击消息时执行。
*
* @param context 上下文。
* @param view 触发点击的 View。
* @param message 被点击的消息的实体信息。
* @return 如果用户自己处理了点击后的逻辑处理,则返回 true, 否则返回 false, false 走融云默认处理方式。
*/
@Override
public boolean onMessageClick(Context context, View view, Message message) {
return false;
}
/**
* 当点击链接消息时执行。
*
* @param context 上下文。
* @param link 被点击的链接。
* @param message 被点击的消息的实体信息
* @return 如果用户自己处理了点击后的逻辑处理,则返回 true, 否则返回 false, false 走融云默认处理方式。
*/
@Override
public boolean onMessageLinkClick(Context context, String link, Message message) {
return false;
}
/**
* 当长按消息时执行。
*
* @param context 上下文。
* @param view 触发点击的 View。
* @param message 被长按的消息的实体信息。
* @return 如果用户自己处理了长按后的逻辑处理,则返回 true,否则返回 false,false 走融云默认处理方式。
*/
@Override
public boolean onMessageLongClick(Context context, View view, Message message) {
return false;
}
}
2.在application的create中设置监听
/**
* 融云会话界面监听
*/
RongIM.getInstance().setConversationClickListener(new MyConversationClickListener(this));
通过这样设置后,针对在设置列表界面的不同操作可以进行不同的逻辑处理。
在本地更新头像后,我们一般会refreshUserInfoCache来更新本地内存中保存的用户信息,但是当我们进入单个会话后会发现,历史消息中的头像并没有更新。
上面我们提过的,当你的用户信息选择第二种方式的时候出现的坑,这就是坑啊。
上面用户信息的部分说过了,选择第二种方式,那么在发送消息的时候会携带用户信息,这时候显示的头像是A,然后更新头像为B后,发送消息,携带的头像是B,单个消息看,是没有问题的,每个消息都显示了正确的头像,但是整体来看,历史消息中应该显示最新的头像,这就是问题。
知道来源后,我们就要着手解决这个问题了,融云API中提供了获取历史消息的接口
public void getHistoryMessages(ConversationType conversationType, String targetId, int oldestMessageId
,int count
,ResultCallback> callback) {
RongIMClient.getInstance().getHistoryMessages(conversationType, targetId, oldestMessageId, count, callback);
}
conversationType表示会话类型,一般有私聊,聊天室等,我这边是单聊。
targetId 是想获取历史消息携带的userinfo的userid。
oldestMessageId 是获取时间距离现在最久的messageid,如果是第一次获取的话,可以传入-1。
count 是想获取的message个数,我在这边传入的是Integer.Max_Value.
callback 中返回了获取的所有消息。
通过这个接口,我们拿到所有的Message,然后在Message中的content中的Userinfo中修改portialUrl即可。
下面是我的项目中处理逻辑
RongIM.getInstance().getHistoryMessages(Conversation.ConversationType.PRIVATE,
targetId,
-1,
Integer.MAX_VALUE,
new RongIMClient.ResultCallback>() {
@Override
public void onSuccess(List messages) {
for (int i = 0 ; i < messages.size() ; i++){
UserInfo userInfo = messages.get(i).getContent().getUserInfo();
if (userInfo.getUserId().equals(targetId)){
userInfo.setPortraitUri(Uri.parse(targetImg));
}else if (userInfo.getUserId().equals(senderId)){
userInfo.setPortraitUri(Uri.parse(senderImg));
}
}
}
@Override
public void onError(RongIMClient.ErrorCode errorCode) {
}
});
至此,算是填了上面的用户信息选择第二种方式的坑。
账号切换会话列表刷新会出现问题,针对我的项目,业务有三个:a.没登录情况下,不显示会话列表界面
b.登录情况下,有在融云注册,显示会话列表界面
c.登录情况下,没在融云注册,不显示会话列表界面
由于会话列表是动态加载的,如下:
ConversationListFragment conversationListFragment = new ConversationListFragment();
Uri uri = Uri.parse("rong://" + getActivity().getApplicationInfo().packageName).buildUpon()
.appendPath("conversationlist")
.appendQueryParameter(Conversation.ConversationType.PRIVATE.getName(), "false") //设置私聊会话,该会话聚合显示
.appendQueryParameter(Conversation.ConversationType.SYSTEM.getName(), "true")//设置系统会话,该会话非聚合显示
.build();
conversationListFragment.setUri(uri);
FragmentManager fragmentManager = getChildFragmentManager();
FragmentTransaction transaction = fragmentManager.beginTransaction();
transaction.add(R.id.rong_container,conversationListFragment);
transaction.commit();
官方文档中说,在退出账号后,回到会话列表界面的时候重设Uri,然后将uri设置给fragment,再调用融云接口onRefreshUI。
Uri uri = Uri.parse("rong://" + getActivity().getApplicationInfo().packageName).buildUpon()
.appendPath("conversationlist")
.appendQueryParameter(Conversation.ConversationType.PRIVATE.getName(), "false") //设置私聊会话,该会话聚合显示
.appendQueryParameter(Conversation.ConversationType.SYSTEM.getName(), "true")//设置系统会话,该会话非聚合显示
.build();
conversationListFragment.setUri(uri);
conversationListFragment.onRefreshUI();
实际上并不能刷新,显示的还是之前账号的会话列表,或者我没搞懂,有知道的小伙伴教我下。
在这里我做如下处理,因为我发现Uri创建的时候传入的parameter可以为空字符串,按我的理解,如果传入空字符串,那查询结果肯定没有内容啊,这样在没登录的时候不显示会话列表。
ConversationListFragment conversationListFragment = new ConversationListFragment();
Uri uri = Uri.parse("rong://" + getActivity().getApplicationInfo().packageName).buildUpon()
.appendPath("conversationlist")
.appendQueryParameter("", "false")
//本应该是私聊,这里传入空,那查询肯定没有结果,反应在界面上就是没有内容,只有一个没有会话的背景提示
.build();
conversationListFragment.setUri(uri);
FragmentManager fragmentManager = getChildFragmentManager();
FragmentTransaction transaction = fragmentManager.beginTransaction();
transaction.add(R.id.rong_container,conversationListFragment);
transaction.commit();
然后在切换账号刷新的时候做如下处理,即可实现强制刷新会话列表的效果:
可以看到会话列表界面实际上是加载到我们的frament中了,那我们可以这样想,切换账号后我能不能主动去判断是否加载会话列表还是不加载会话列表或者remove掉会话列表,这样在界面上显示出来不就是界面更新了吗。
if (!isLogged()){
if (fragment != null){
transaction.remove(fragment);//没登录状态下,会话列表如果不是null,则remove掉会话列表界面
}
}else{
fragment = new ConversationListFragment();
Uri uri = Uri.parse("rong://" + getContext().getApplicationInfo().packageName).buildUpon()
.appendPath("conversationlist")
.appendQueryParameter(Conversation.ConversationType.PRIVATE.getName(), "false")
.build();
fragment.setUri(uri); //设置 ConverssationListFragment 的显示属性
transaction.add(R.id.rong_content, fragment);
}
//根据登录状态的不同,进行不同逻辑处理
transaction.commit();
这样就实现了不同账号登录后,会话列表的强制刷新了。
不过这有个问题就是如果没有登录的情况下,整个会话列表的背景是空白的,这个需要改下。
通过阅读demo源码可知,融云在demo中的处理是添加了个layout,在会话列表有内容的情况下是不显示的状态。
所以,在我们的代码中也加上这部分代码就可以了,如下:
尊重作者劳动成果:https://blog.csdn.net/jinlu7611/article/details/90055868