我的第一个Java工程(MQTT通信)
作为一个上班族,其实没有太多多余的时间去学习一门额外的语言,
即便有空闲时间,也是在深入的学习了解自己工作相关的嵌入式领域的东西,
这篇文章也是记录一下我当时为了能够测试一个MQTT方案学习设计的一个手机APP,
要特别感谢B站UP主 阿正啷个哩个啷,整个程序从0开始基本都是按照教程来写,只是后期向同事请教,添加了部分功能模块
本文针对Java小白 0基础,请大神轻喷,如果后期有时间,还会把文章点点完善 2021/10
- 1
- 2
- 3
- 4
- 5
一、环境安装
环境安装配置的具体步骤什么的,就不详细介绍了,网上有很多参考资料,而且现在的应用基本上都是傻瓜式安装,这边只是说明下需要安装的软件:
- 安装 Java JDK,从JDK 5.0开始,改名为Java SE;下载地址
- 安装 Android Studio; 官方下载地址
- 下图中有第三个文件,是Java 下的 mqtt 协议实现库,因为我这个程序中需要使用mqtt协议,所以我也放在这里了,只不过这是后面在 Android Studio 中导入的。
二、工程结构
2.1 新建工程
安装好环境以后,打开 Android Studio,点击新建一个工程 +Start a new Android Studio project
,选一个界面,进入如下配置,在这里设置 project 名字,保存路径,其他的东西默认就可以,点击Finish完成设置就可以进入到编辑界面:
在左边 project 栏目中,点击下面左图红色部分,可以切换工程的框架视图(就是工程结构是按照什么模式来布局的),如果 project 栏目不小心关闭了,可以通过勾选主菜单栏目下 View - Tool Window Bars 打开,如下面右图所示:
记得修改一个地方build.gradle
文件,原本是国外的地址,国外网址的速度大家是知道的,改成阿里云的地址,如下图:
maven{ url 'http://maven.aliyun.com/nexus/content/groups/public/'}
2.2 图标和名称
首先看一下AndroidManifest.xml
文件内容:
2.3 权限
在使用app的时候,相应的功能需要获取相应的权限,就是很多app在安装后,都会弹出向用户申请权限的那种东西,
我们这工程中得直接允许使用网络,蓝牙等权限,代码如下:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.mqtt_project">
<!--允许程序打开网络套接字-->
<uses-permission android:name="android.permission.INTERNET" />
<!--允许程序获取网络状态-->
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<!--使用蓝牙所需要的权限-->
<uses-permission android:name="android.permission.BLUETOOTH" />
<!--使用扫描和设置蓝牙的权限(要使用这一个权限必须申明BLUETOOTH权限)-->
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="人生得意"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity android:name=".MainActivity" android:theme="@style/Theme.AppCompat.Light.NoActionBar">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity android:name=".BlueTestActivity1" android:theme="@style/Theme.AppCompat.Light.NoActionBar">
</activity>
</application>
</manifest>
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
其中系统默认的名字本来是如下所示
android:label="@string/app_name"
- 1
那么他这个@string/app_name
是怎么识别的呢:
2.4 界面
2.4.1 界面配置
我们来看 MainActivity.java
文件,这个是我们app的主界面,因为在AndroidManifest.xml
中我们就设置过
<activity android:name=".MainActivity" android:theme="@style/Theme.AppCompat.Light.NoActionBar">
- 1
主界面配置:
如果需要其他界面我们可以让系统自动生成,比如我们直接在主界面对应的UI改成activity_main1
,这样系统会提示错误,我们按照下图的操作可以新建一个UI界面:
选中第一种办法,在弹出的对话框,点击OK即可:
可以看到系统自动新建了一个UI界面,可以对此界面进行编辑:
2.4.2 UI 配置
接下来我们来说说 activity_main.xml
:
2.4.3 控件
在UI设计的时候,需要用到很多的控件,比如按钮,图片,文本框等等,具体的种类和细节请参考对应资料,这里只介绍我使用到的:
按钮 :</Button>
图片: </ImageView>
文本框: </TextView>
</LinearLayout>
<Button
android:layout_width="wrap_content"
android:text="测试按钮"
android:id="@+id/btn_1"
android:layout_height="wrap_content">
</Button>
<LinearLayout
android:layout_width="match_parent"
android:orientation="horizontal"
android:gravity="center_vertical"
android:layout_height="wrap_content">
<ImageView
android:layout_width="40dp"
android:src="@drawable/th"
android:layout_marginLeft="10dp"
android:layout_height="40dp">
</ImageView>
<TextView
android:layout_width="wrap_content"
android:id="@+id/text_old"
android:text="我是原来的内容"
android:layout_height="wrap_content">
</TextView>
</LinearLayout>
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
上述代码中一般带有android:layout
的都是控件本身的属性,除了上面控件所定义的属性,每个控件都有很多属性,需要用到的时候再去查相关的内容。
控件ID
其中需要说明的是android:id
这个属性,这个id 是 java 文件与 XML 文件通讯的介质
2.4.4 布局
界面和布局的设置,需要自己多尝试,控件的属性自己不知道的可以直接测试效果,什么东西多试试知道了
充满父控件:android:layout_width="match_parent"
(这里是宽度,也有高度)
表示让当前控件的大小和父布局的大小一样,也就是由父布局来决定当前控件的大小
自适应:android:layout_height="wrap_content"
(高度自适应)
表示让当前的控件大小能够刚好包含里面的内容,也就是由控件内容决定当前控件的大小
垂直居中:android:gravity="center_vertical"
水平布局:android:orientation="horizontal"
(有水平垂直)
与边缘距离:android:layout_marginTop="10dp"
(有上下左右)
为了简单,整体布局采用线性布局</LinearLayout>
,整体布局是指整个页面的局部,但是一个页面中有很多控件,控件线性的排放,可以是垂直布局,也可以是水平布局(直接输入字母 o ,会自动弹出语句 ),如果使用水平布局,后面的控件图标会溢出屏幕外面,不能看到,这里只有使用垂直对齐,控件自适应,就能够按照手机屏幕大小顺序往下面排列,效果如下列图示:
对于布局,是有明确一层一层包含关系的,好像是分为父控件,子控件,在控件里面包含控件的情况下需要单独的对子控件进行设置对应的布局,这个布局是在父控件的范围内进行:
2.5 导入软件包
我们需要使用 MQTT相关的函数,需要导入 MQTT Java 包,我们使用下载好导入的方式,除了这种方式,还可以使用Gradle 方式在线下载,我们按照如下步骤:
1、切换至Project视图,将下载好的 Java 包直接拖到 libs 下面:
2、右击 Java 包,点击 Add As library,默认安装在 app 下面,点击确定,等待导入完成即可:
导入完成以后可以在build.gradle
中查看到:
三、程序编写
很快我们就可以开始写代码了,这边我并不会从 Jave 的基础开始介绍语法啊数据类型啊之类的,直接用到哪里改哪里,修改的地方做个说明。
3.1 基本快捷键
一些操作基本快捷键,持续补充:
**转到定义**
java中,按住Ctrl,然后单击,就能到定义了
**编辑界面切换**
Alt + ← 前一个编辑的页面 Alt + → 下一个编辑的页面
**自动解决错误**
Alt + Enter 就会出现建议解决办法
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
3.2 控件操作
控件操作是通过绑定 控件 ID 进行操作的,首先得定义一个按键 “变量”:
然后在初始化中,绑定按键变量到对应的控件ID:
//初始化,绑定对应的控件ID
private void ui_init() {
btn_1 = findViewById(R.id.btn_1);
image_open = findViewById(R.id.image_open);
image_door = findViewById(R.id.image_door);
image_lei = findViewById(R.id.image_lei);
image_th = findViewById(R.id.image_th);
image_wide = findViewById(R.id.image_wide);
image_pm = findViewById(R.id.image_pm);
text_old = findViewById(R.id.text_old);
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
然后开始按键的相关事件代码,选择setOnClickListener
,然后还要new View.OnClickListener()
,敲完之后就会自动生成一个单击事件代码框架:
完善一下单击事件,代码如下:
ui_init();//初始化
btn_1.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
//这里既是单击之后的操作
//System.out.println("这是一个单击操作"); java 中的打印操作
/*
更直观的方式用弹窗 : toast
context:界面选择 这里为 MainActivity.this : 当前界面
text: 显示的text
Toast.LENGTH_LONG 显示时长,这里选择 LENGTH_LONG
context 和 text 都不需要自己敲的
*/
Toast.makeText(MainActivity.this)
Toast.makeText(MainActivity.this,"这是一个单击操作",Toast.LENGTH_LONG).show();
text_old.setText("我是新的内容"); //两个控件联动,这里把 text_old 控件里面的内容改变了
}
});
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
3.3 MQTT通讯
MQTT服务器IP,订阅发布根据自己的测试环境修改,MQTT部分代码来自阿正,这里必须得贴一下阿正的B站主页:
订阅:client.subscribe(mqtt_sub_topic,1);
发布:client.publish(topic,message);
下面代码中发布封装了一个函数。
字符串截取:
msg.obj.toString().substring(msg.obj.toString().indexOf("temp:")+5,msg.obj.toString().indexOf("h"));
MQTT部分整体代码:
package com.example.mqtt_project;
import androidx.appcompat.app.AppCompatActivity;
import android.annotation.SuppressLint;
import android.content.Intent;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.view.View;
import android.widget.Button;
import android.widget.ImageView;
import android.widget.TextView;
import android.widget.Toast;
import org.eclipse.paho.client.mqttv3.IMqttDeliveryToken;
import org.eclipse.paho.client.mqttv3.MqttCallback;
import org.eclipse.paho.client.mqttv3.MqttClient;
import org.eclipse.paho.client.mqttv3.MqttConnectOptions;
import org.eclipse.paho.client.mqttv3.MqttException;
import org.eclipse.paho.client.mqttv3.MqttMessage;
import org.eclipse.paho.client.mqttv3.persist.MemoryPersistence;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
//
//这里是我的安卓测试,测试一下输入设置好了没有yes or no
//
public class MainActivity extends AppCompatActivity {
// private String host = "tcp://106.13.150.28:1883";
private String host = "tcp://116.62.28.158:1883";
// private String userName = "android";
// private String passWord = "android";
private String userName = "winshine";
private String passWord = "winshine123";
private String mqtt_id = "523266538";
private String mqtt_sub_topic = "523266538_PC"; //为了保证你不受到别人的消息
private String mqtt_pub_topic = "523266538"; //为了保证你不受到别人的消息
// private String mqtt_sub_topic = "hello"; //为了保证你不受到别人的消息
// private String mqtt_pub_topic = "523266538"; //为了保证你不受到别人的消息
private Button btn_1;
private Button btn_open;
private Button btn_close;
private ImageView image_open;
private ImageView image_door;
private ImageView image_wide;
private ImageView image_lei;
private ImageView image_pm;
private ImageView image_th;
private TextView text_old;
private MqttClient client;
private MqttConnectOptions options;
private Handler handler;
private ScheduledExecutorService scheduler;
private int led_flag = 1;
private String open_led ="LEDON";
private String close_led ="LEDOFF";
private int mqtt_flag = 0;
@SuppressLint("HandlerLeak")
@Override
protected void onCreate(Bundle savedInstanceState) {
//界面打开后,最先运行的地方
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);//对应界面UI
//用来界面初始化,控件初始化,某种意义上类似main()函数
ui_init();//初始化
btn_1.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
//这里既是单击之后的操作
//System.out.println("这是一个单击操作"); java 中的打印操作
/*
更直观的方式用弹窗 : toast
context:界面选择 这里为 MainActivity.this : 当前界面
text: 显示的text
Toast.LENGTH_LONG 显示时长,这里选择 LENGTH_LONG
context 和 text 都不需要自己敲的
*/
Toast.makeText(MainActivity.this,"这是一个单击操作",Toast.LENGTH_LONG).show();
text_old.setText("我是新的内容");
}
});
/*
学会按钮单击事件,图片也可以单击事件
单击第一个图片,通过mqtt发布一个消息
*/
image_open.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
try
{
if(led_flag == 0) {
publishmessageplus(mqtt_pub_topic, open_led);
led_flag =1;
Toast.makeText(MainActivity.this,"开灯",Toast.LENGTH_SHORT).show();
}
else
{
publishmessageplus(mqtt_pub_topic, close_led);
led_flag =0;
Toast.makeText(MainActivity.this,"关灯",Toast.LENGTH_SHORT).show();
}
}
catch (Exception e)
{
e.toString();
}
}
});
image_door.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Toast.makeText(MainActivity.this,"我是开门按钮",Toast.LENGTH_LONG).show();
}
});
image_lei.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Toast.makeText(MainActivity.this,"这里做蓝牙",Toast.LENGTH_LONG).show();
Intent intent = new Intent(MainActivity.this,
BlueTestActivity1.class);
startActivity(intent);
}
});
btn_open.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Toast.makeText(MainActivity.this,"打开mqtt重连",Toast.LENGTH_LONG).show();
mqtt_flag = 0;//打开重连
}
});
btn_close.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Toast.makeText(MainActivity.this,"关闭mqtt重连",Toast.LENGTH_LONG).show();
mqtt_flag = 1;//打开重连
}
});
//两个控件联动,按钮单击改变textview的值
//#########################################################//
Mqtt_init();
startReconnect();
handler = new Handler() {
@SuppressLint("SetTextI18n")
public void handleMessage(Message msg) {
super.handleMessage(msg);
switch (msg.what){
case 1: //开机校验更新回传
break;
case 2: // 反馈回传
break;
case 3: //MQTT 收到消息回传 UTF8Buffer msg=new UTF8Buffer(object.toString());
String temp=null;
try {
temp=msg.obj.toString();
if(temp!=null&&temp.contains("temp"))
{
String T_val = msg.obj.toString().substring(msg.obj.toString().indexOf("temp:")+5,msg.obj.toString().indexOf("h"));
String H_val = msg.obj.toString().substring(msg.obj.toString().indexOf("temp:")+16);
Toast.makeText(MainActivity.this,msg.obj.toString(),Toast.LENGTH_LONG).show();
String text_val="温度:"+T_val+""+"湿度:"+H_val;
text_old.setText(text_val);
}
}
catch (Exception e)
{
e.toString();
}
break;
case 30: //连接失败
Toast.makeText(MainActivity.this,"连接失败",Toast.LENGTH_LONG).show();
break;
case 31: //连接成功
Toast.makeText(MainActivity.this,"连接成功",Toast.LENGTH_LONG).show();
try {
client.subscribe(mqtt_sub_topic,1);
} catch (MqttException e) {
e.printStackTrace();
}
break;
default:
break;
}
}
};
}
//初始化,绑定对应的控件ID
private void ui_init() {
btn_1 = findViewById(R.id.btn_1);
image_open = findViewById(R.id.image_open);
image_door = findViewById(R.id.image_door);
image_lei = findViewById(R.id.image_lei);
image_th = findViewById(R.id.image_th);
image_wide = findViewById(R.id.image_wide);
image_pm = findViewById(R.id.image_pm);
text_old = findViewById(R.id.text_old);
btn_open = findViewById(R.id.btn_open);
btn_close = findViewById(R.id.btn_close);
}
private void Mqtt_init() {
try {
//host为主机名,test为clientid即连接MQTT的客户端ID,一般以客户端唯一标识符表示,MemoryPersistence设置clientid的保存形式,默认为以内存保存
client = new MqttClient(host, mqtt_id,
new MemoryPersistence());
//MQTT的连接设置
options = new MqttConnectOptions();
//设置是否清空session,这里如果设置为false表示服务器会保留客户端的连接记录,这里设置为true表示每次连接到服务器都以新的身份连接
options.setCleanSession(false);
//设置连接的用户名
options.setUserName(userName);
//设置连接的密码
options.setPassword(passWord.toCharArray());
// 设置超时时间 单位为秒
options.setConnectionTimeout(10);
// 设置会话心跳时间 单位为秒 服务器会每隔1.5*20秒的时间向客户端发送个消息判断客户端是否在线,但这个方法并没有重连的机制
options.setKeepAliveInterval(20);
//设置回调
client.setCallback(new MqttCallback() {
@Override
public void connectionLost(Throwable cause) {
//连接丢失后,一般在这里面进行重连
System.out.println("connectionLost----------");
//startReconnect();
}
@Override
public void deliveryComplete(IMqttDeliveryToken token) {
//publish后会执行到这里
System.out.println("deliveryComplete---------"
+ token.isComplete());
}
@Override
public void messageArrived(String topicName, MqttMessage message)
throws Exception {
//subscribe后得到的消息会执行到这里面
System.out.println("messageArrived----------");
//封装message包
Message msg = new Message();
msg.what = 3; //收到消息标志位
msg.obj = topicName + "---" + message.toString();
//发送message 到 handler
handler.sendMessage(msg); // hander 回传
}
});
} catch (Exception e) {
e.printStackTrace();
}
}
private void Mqtt_connect() {
new Thread(new Runnable() {
@Override
public void run() {
try {
if(!(client.isConnected()) ) //如果还未连接
{
if(mqtt_flag == 0) { //只有当flag == 0时才能一直重新连接
client.connect(options);
Message msg = new Message();
msg.what = 31;
handler.sendMessage(msg);
}
}
} catch (Exception e) {
e.printStackTrace();
Message msg = new Message();
msg.what = 30;
handler.sendMessage(msg);
}
}
}).start();
}
private void startReconnect() {
scheduler = Executors.newSingleThreadScheduledExecutor();
scheduler.scheduleAtFixedRate(new Runnable() {
@Override
public void run() {
if (!client.isConnected()) {
Mqtt_connect();
}
}
}, 0 * 1000, 10 * 1000, TimeUnit.MILLISECONDS);
}
private void publishmessageplus(String topic,String message2) {
if (client == null || !client.isConnected()) {
return;
}
MqttMessage message = new MqttMessage();
message.setQos(0);
message.setPayload(message2.getBytes());
try {
client.publish(topic,message);
} catch (MqttException e) {
e.printStackTrace();
}
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
- 69
- 70
- 71
- 72
- 73
- 74
- 75
- 76
- 77
- 78
- 79
- 80
- 81
- 82
- 83
- 84
- 85
- 86
- 87
- 88
- 89
- 90
- 91
- 92
- 93
- 94
- 95
- 96
- 97
- 98
- 99
- 100
- 101
- 102
- 103
- 104
- 105
- 106
- 107
- 108
- 109
- 110
- 111
- 112
- 113
- 114
- 115
- 116
- 117
- 118
- 119
- 120
- 121
- 122
- 123
- 124
- 125
- 126
- 127
- 128
- 129
- 130
- 131
- 132
- 133
- 134
- 135
- 136
- 137
- 138
- 139
- 140
- 141
- 142
- 143
- 144
- 145
- 146
- 147
- 148
- 149
- 150
- 151
- 152
- 153
- 154
- 155
- 156
- 157
- 158
- 159
- 160
- 161
- 162
- 163
- 164
- 165
- 166
- 167
- 168
- 169
- 170
- 171
- 172
- 173
- 174
- 175
- 176
- 177
- 178
- 179
- 180
- 181
- 182
- 183
- 184
- 185
- 186
- 187
- 188
- 189
- 190
- 191
- 192
- 193
- 194
- 195
- 196
- 197
- 198
- 199
- 200
- 201
- 202
- 203
- 204
- 205
- 206
- 207
- 208
- 209
- 210
- 211
- 212
- 213
- 214
- 215
- 216
- 217
- 218
- 219
- 220
- 221
- 222
- 223
- 224
- 225
- 226
- 227
- 228
- 229
- 230
- 231
- 232
- 233
- 234
- 235
- 236
- 237
- 238
- 239
- 240
- 241
- 242
- 243
- 244
- 245
- 246
- 247
- 248
- 249
- 250
- 251
- 252
- 253
- 254
- 255
- 256
- 257
- 258
- 259
- 260
- 261
- 262
- 263
- 264
- 265
- 266
- 267
- 268
- 269
- 270
- 271
- 272
- 273
- 274
- 275
- 276
- 277
- 278
- 279
- 280
- 281
- 282
- 283
- 284
- 285
- 286
- 287
- 288
- 289
- 290
- 291
- 292
- 293
- 294
- 295
- 296
- 297
- 298
- 299
- 300
- 301
- 302
- 303
- 304
- 305
- 306
- 307
- 308
- 309
- 310
3.4 界面切换
点击蓝牙图标,做了个界面切换,蓝牙单独的做了一个界面,这里得感谢同事 跳舞大佬,蓝牙部分的程序实现我就不放了,我只是拿过来用用,也没有时间去分析,只是了解下不同界面是如何切换的。
image_lei.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Toast.makeText(MainActivity.this,"这里做蓝牙",Toast.LENGTH_LONG).show();
Intent intent = new Intent(MainActivity.this,
BlueTestActivity1.class);
startActivity(intent);
}
});
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
3.5 前后台运行
在当时程序测试整体运行过程中,即便切换到后台(就是最小化以后,没有杀掉进程的情况下),mqtt重连方法也一直有弹窗,虽然在上面给的程序中,我加了个标志位处理了一下,但是也不是根本解决办法,还是请教了跳舞,告诉我这个基础的知识,感谢跳舞。
加入前后台运行的判断:
boolean isShow;
@Override
protected void onResume() {
super.onResume();
//前台
isShow = true;
}
@Override
protected void onPause() {
//后台
isShow = false;
super.onPause();
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
3.6 最终主界面图
最后主界面的效果图:
文章来源: blog.csdn.net,作者:矜辰所致,版权归原作者所有,如需转载,请联系作者。
原文链接:blog.csdn.net/weixin_42328389/article/details/120865541
- 点赞
- 收藏
- 关注作者
评论(0)