当前位置:   article > 正文

Android Bluetooth | 蓝牙配对源码分析_蓝牙协议及其源代码分析

蓝牙协议及其源代码分析

好厚米们,我又来了!

这次分享的是蓝牙设备执行配对动作时Android源码的执行流程。

下面先来说下,应用层是如何发起蓝牙配对的:

( ps:大多数业务逻辑,都是扫描到可用设备后,点击可用设备 -> 发起配对。)

这里我直接略过点击可用设备的步骤哈,扫描到第一个可用设备后,我直接通过扫描信息进行配对

  1. public class MainActivity extends AppCompatActivity {
  2. private BluetoothAdapter mBluetoothAdapter;
  3. private BluetoothDevice mBluetoothDevice;
  4. private BluetoothLeScanner scanner = BluetoothAdapter.getDefaultAdapter().getBluetoothLeScanner();
  5. @Override
  6. protected void onCreate(Bundle savedInstanceState) {
  7. super.onCreate(savedInstanceState);
  8. setContentView(R.layout.activity_main);
  9. ScanCallback scanCallback = new ScanCallback() {
  10. @SuppressLint("MissingPermission")
  11. @Override
  12. public void onScanResult(int callbackType, ScanResult result) {
  13. super.onScanResult(callbackType, result);
  14. //将扫描到的设备信息取出来,为蓝牙设备赋值
  15. mBluetoothDevice = result.getDevice();
  16. //通过蓝牙设备,调用配对方法
  17. mBluetoothDevice.createBond();
  18. }
  19. @Override
  20. public void onScanFailed(int errorCode) {
  21. super.onScanFailed(errorCode);
  22. // 扫描失败处理
  23. }
  24. };
  25. // 开始扫描
  26. if (scanner != null) {
  27. ScanSettings settings = new ScanSettings.Builder()
  28. .setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY)
  29. .build();
  30. // 添加过滤条件
  31. List<ScanFilter> filters = new ArrayList<>();
  32. //权限检查
  33. if (ActivityCompat.checkSelfPermission(this, Manifest.permission.BLUETOOTH_SCAN) != PackageManager.PERMISSION_GRANTED) {
  34. // TODO: Consider calling
  35. // ActivityCompat#requestPermissions
  36. // here to request the missing permissions, and then overriding
  37. // public void onRequestPermissionsResult(int requestCode, String[] permissions,
  38. // int[] grantResults)
  39. // to handle the case where the user grants the permission. See the documentation
  40. // for ActivityCompat#requestPermissions for more details.
  41. return;
  42. }
  43. scanner.startScan(filters, settings, scanCallback);
  44. }
  45. // 停止扫描
  46. if (scanner != null) {
  47. scanner.stopScan(scanCallback);
  48. }
  49. mBluetoothAdapter.startDiscovery();
  50. }
  51. }

由上面的代码可以看出,配对动作的执行依赖

  1. //通过蓝牙设备,调用配对方法
  2. BluetoothDevice.createBond();

下面就进入到了FWK层

执行BluetoothDevice.createBond()后,会进入到BluetoothDevice.java中执行

BluetoothDevice.java - OpenGrok cross reference for /frameworks/base/core/java/android/bluetooth/BluetoothDevice.java

  1. public boolean createBond() {
  2. final IBluetooth service = sService;
  3. if (service == null) {
  4. Log.e(TAG, "BT not enabled. Cannot create bond to Remote Device");
  5. return false;
  6. }
  7. try {
  8. Log.i(TAG, "createBond() for device " + getAddress()
  9. + " called by pid: " + Process.myPid()
  10. + " tid: " + Process.myTid());
  11. //通过service接口执行配对动作
  12. return service.createBond(this, TRANSPORT_AUTO);
  13. } catch (RemoteException e) {
  14. Log.e(TAG, "", e);
  15. }
  16. return false;
  17. }

而这个service,我们来看下其声明

  1. private static volatile IBluetooth sService;
  2. //后来在上面的配对方法中,为此接口赋值
  3. final IBluetooth service = sService;

本质上就是IBluetooth接口,不过在Android 10中,IBluetooth接口一共有两个。

应用层下发配对动作时,所用的IBludetooth接口是/system/bt/service/common/android/bluetooth/

此接口的具体代码如下(太多了,各位厚米自己点连接看下就行):

IBluetooth.aidl - OpenGrok cross reference for /system/bt/binder/android/bluetooth/IBluetooth.aidl

即通过这个AIDL接口调用蓝牙远程服务。

下面进入了Bluetooth 服务层

而这个接口的实现类,在AdaperServiceBinder中,部分代码如下:

PS:AdaperServiceBinder写在AdapterService.java中

AdapterService.java - OpenGrok cross reference for /packages/apps/Bluetooth/src/com/android/bluetooth/btservice/AdapterService.java

  1. private static class AdapterServiceBinder extends IBluetooth.Stub {
  2. //在Binder类中继承了IBluetooth.Stub
  3. private AdapterService mService;
  4. AdapterServiceBinder(AdapterService svc) {
  5. mService = svc;
  6. }
  7. public void cleanup() {
  8. mService = null;
  9. }
  10. public AdapterService getService() {
  11. if (mService != null && mService.isAvailable()) {
  12. return mService;
  13. }
  14. return null;
  15. }
  16. //发起配对
  17. @Override
  18. public boolean createBond(BluetoothDevice device, int transport) {
  19. if (!Utils.checkCallerAllowManagedProfiles(mService)) {
  20. Log.w(TAG, "createBond() - Not allowed for non-active user");
  21. return false;
  22. }
  23. //实例化蓝牙服务
  24. AdapterService service = getService();
  25. if (service == null) {
  26. return false;
  27. }
  28. //调用蓝牙服务中的createBond方法
  29. return service.createBond(device, transport, null);
  30. }
  31. }

 即,应用层触发的配对动作,最后会通过AIDL接口以及AIDL实现类,最终传递到蓝牙服务层的AdapterService.java中,下面看下AdapterService.java中是如何进行配对的,部分代码如下:

  1. boolean createBond(BluetoothDevice device, int transport, OobData oobData) {
  2. //检查蓝牙相关的配对,连接,发现,配对,等权限是否持有
  3. enforceCallingOrSelfPermission(BLUETOOTH_ADMIN_PERM, "Need BLUETOOTH ADMIN permission");
  4. //获取下设备的属性
  5. DeviceProperties deviceProp = mRemoteDevices.getDeviceProperties(device);
  6. if (deviceProp != null && deviceProp.getBondState() != BluetoothDevice.BOND_NONE) {
  7. //如果当前设备已经配对或者设备属性为空那么返回false
  8. return false;
  9. }
  10. //设置本地设备的绑定状态
  11. mRemoteDevices.setBondingInitiatedLocally(Utils.getByteAddress(device));
  12. // Pairing is unreliable while scanning, so cancel discovery
  13. // Note, remove this when native stack improves
  14. //取消扫描,具体参见上边的英文。
  15. cancelDiscoveryNative();
  16. //构建一个含有配对动作的MSG消息
  17. Message msg = mBondStateMachine.obtainMessage(BondStateMachine.CREATE_BOND);
  18. //并将设备以及transport传入消息中
  19. msg.obj = device;
  20. msg.arg1 = transport;
  21. if (oobData != null) {
  22. Bundle oobDataBundle = new Bundle();
  23. oobDataBundle.putParcelable(BondStateMachine.OOBDATA, oobData);
  24. msg.setData(oobDataBundle);
  25. }
  26. //将消息发送给配对相关的状态机,并返回true
  27. mBondStateMachine.sendMessage(msg);
  28. return true;
  29. }

到这里,就开始进入BondStateMachine.java中进行消息的处理了~ ,先来看下这个状态机中的状态:

BondStateMachine.java - OpenGrok cross reference for /packages/apps/Bluetooth/src/com/android/bluetooth/btservice/BondStateMachine.java

  1. //该状态机名为BondStateMachine.java
  2. //其中定义了两个状态
  3. //PendingCommandState是等待蓝牙配对命令的状态
  4. private PendingCommandState mPendingCommandState = new PendingCommandState();
  5. //StableState是指已经完成蓝牙配对的状态
  6. private StableState mStableState = new StableState();

综上所述,发送配对msg后,会进入PendingCommandState状态下进行处理,部分代码如下:

  1. private class PendingCommandState extends State {
  2. private final ArrayList<BluetoothDevice> mDevices = new ArrayList<BluetoothDevice>();
  3. @Override
  4. public void enter() {
  5. infoLog("Entering PendingCommandState State");
  6. BluetoothDevice dev = (BluetoothDevice) getCurrentMessage().obj;
  7. }
  8. @Override
  9. public boolean processMessage(Message msg) {
  10. BluetoothDevice dev = (BluetoothDevice) msg.obj;
  11. DeviceProperties devProp = mRemoteDevices.getDeviceProperties(dev);
  12. boolean result = false;
  13. if (mDevices.contains(dev) && msg.what != CANCEL_BOND
  14. && msg.what != BONDING_STATE_CHANGE && msg.what != SSP_REQUEST
  15. && msg.what != PIN_REQUEST) {
  16. deferMessage(msg);
  17. return true;
  18. }
  19. switch (msg.what) {
  20. case CREATE_BOND:
  21. //接到建立连接消息后
  22. OobData oobData = null;
  23. if (msg.getData() != null) {
  24. //为oob数据赋值
  25. oobData = msg.getData().getParcelable(OOBDATA);
  26. }
  27. //调用createBond方法,建立配对
  28. result = createBond(dev, msg.arg1, oobData, false);
  29. break;
  30. //其余代码已省略...
  31. }

        可以看到,接收到含有CREATE_BOND的msg之后,会调用createBond方法,部分代码如下:

  1. private boolean createBond(BluetoothDevice dev, int transport, OobData oobData,
  2. boolean transition) {
  3. //如果当前的配对状态为NONE,则开始执行配对,否则直接返回false
  4. if (dev.getBondState() == BluetoothDevice.BOND_NONE) {
  5. infoLog("Bond address is:" + dev);
  6. //将设备的地址转换为addr的数组
  7. byte[] addr = Utils.getBytesFromAddress(dev.getAddress());
  8. boolean result;
  9. //根据是否有oobData来选择对应的协议栈接口方法建立配对关系
  10. if (oobData != null) {
  11. result = mAdapterService.createBondOutOfBandNative(addr, transport, oobData);
  12. } else {
  13. //先看无oobdata的建立配对,此处直接调用了协议栈提供的接口,也就是调用到JNI层
  14. result = mAdapterService.createBondNative(addr, transport);
  15. }
  16. StatsLog.write(StatsLog.BLUETOOTH_BOND_STATE_CHANGED,
  17. mAdapterService.obfuscateAddress(dev), transport, dev.getType(),
  18. BluetoothDevice.BOND_BONDING,
  19. oobData == null ? BluetoothProtoEnums.BOND_SUB_STATE_UNKNOWN
  20. : BluetoothProtoEnums.BOND_SUB_STATE_LOCAL_OOB_DATA_PROVIDED,
  21. BluetoothProtoEnums.UNBOND_REASON_UNKNOWN);
  22. //如果建立成功 配对状态写入StatsLog
  23. if (!result) {
  24. StatsLog.write(StatsLog.BLUETOOTH_BOND_STATE_CHANGED,
  25. mAdapterService.obfuscateAddress(dev), transport, dev.getType(),
  26. BluetoothDevice.BOND_NONE, BluetoothProtoEnums.BOND_SUB_STATE_UNKNOWN,
  27. BluetoothDevice.UNBOND_REASON_REPEATED_ATTEMPTS);
  28. // Using UNBOND_REASON_REMOVED for legacy reason
  29. sendIntent(dev, BluetoothDevice.BOND_NONE, BluetoothDevice.UNBOND_REASON_REMOVED);
  30. return false;
  31. } else if (transition) {
  32. //如果传入了需要转换的状态,则进行转换
  33. transitionTo(mPendingCommandState);
  34. }
  35. return true;
  36. }
  37. return false;
  38. }

       根据oobData分别调用不同的协议栈接口,我们主要先分析下createBondNative(addr, transport)方法。

com_android_bluetooth_btservice_AdapterService.cpp - OpenGrok cross reference for /packages/apps/Bluetooth/jni/com_android_bluetooth_btservice_AdapterService.cpp

  1. //承接上面代码中调用JNI层接口的动作 result = mAdapterService.createBondNative(addr, transport);
  2. //最终会传递到一个.cpp文件中 -> com_android_bluetooth_btservice_AdapterService.cpp
  3. //代码如下:
  4. static jboolean createBondNative(JNIEnv* env, jobject obj, jbyteArray address,
  5. jint transport) {
  6. ALOGV("%s", __func__);
  7. if (!sBluetoothInterface) return JNI_FALSE;
  8. jbyte* addr = env->GetByteArrayElements(address, NULL);
  9. if (addr == NULL) {
  10. jniThrowIOException(env, EINVAL);
  11. return JNI_FALSE;
  12. }
  13. //调用hal(硬件层)的配对方法,传入要配对的地址以及传输方式
  14. int ret = sBluetoothInterface->create_bond((RawAddress*)addr, transport);
  15. env->ReleaseByteArrayElements(address, addr, 0);
  16. return (ret == BT_STATUS_SUCCESS) ? JNI_TRUE : JNI_FALSE;
  17. }

进入蓝牙协议栈处理

执行完,上述代码,就会进入蓝牙协议栈的逻辑处理。

执行create_bond方法,部分代码如下

bluetooth.cc - OpenGrok cross reference for /system/bt/btif/src/bluetooth.cc

  1. static int create_bond(const RawAddress* bd_addr, int transport) {
  2. /* sanity check */
  3. if (!interface_ready()) return BT_STATUS_NOT_READY;
  4. //返回btif_dm_create_bond方法的配对结果
  5. return btif_dm_create_bond(bd_addr, transport);
  6. }

而执行create_bond方法后会返回btif_dm_create_bond方法的配对结果,btif_dm_create_bond方法的部分代码如下:

btif_dm.cc - OpenGrok cross reference for /system/bt/btif/src/btif_dm.cc

  1. bt_status_t btif_dm_create_bond(const RawAddress* bd_addr, int transport) {
  2. btif_dm_create_bond_cb_t create_bond_cb;
  3. create_bond_cb.transport = transport;
  4. create_bond_cb.bdaddr = *bd_addr;
  5. BTIF_TRACE_EVENT("%s: bd_addr=%s, transport=%d", __func__,
  6. bd_addr->ToString().c_str(), transport);
  7. //如果配对状态不为NONE,则返回一个BUSY状态
  8. if (pairing_cb.state != BT_BOND_STATE_NONE) return BT_STATUS_BUSY;
  9. btif_stats_add_bond_event(*bd_addr, BTIF_DM_FUNC_CREATE_BOND,
  10. pairing_cb.state);
  11. //将BTIF_DM_CB_CREATE_BOND事件发送到btif_dm_generic_evt方法中进行处理
  12. btif_transfer_context(btif_dm_generic_evt, BTIF_DM_CB_CREATE_BOND,
  13. (char*)&create_bond_cb,
  14. sizeof(btif_dm_create_bond_cb_t), NULL);
  15. //表示对配动作启动
  16. return BT_STATUS_SUCCESS;
  17. }

事件传入 btif_dm_generic_evt后,btif_dm_generic_evt会根据不同的事件进行处理,当case到配对事件时,会执行如下代码:

  1. static void btif_dm_generic_evt(uint16_t event, char* p_param) {
  2. BTIF_TRACE_EVENT("%s: event=%d", __func__, event);
  3. switch (event) {
  4. //省略部分代码
  5. case BTIF_DM_CB_CREATE_BOND: {
  6. pairing_cb.timeout_retries = NUM_TIMEOUT_RETRIES;
  7. btif_dm_create_bond_cb_t* create_bond_cb =
  8. (btif_dm_create_bond_cb_t*)p_param;
  9. //这里会调用本地的btif_dm_cb_create_bond方法
  10. btif_dm_cb_create_bond(create_bond_cb->bdaddr, create_bond_cb->transport);
  11. } break;
  12. //省略部分代码
  13. }

而调用btif_dm_cb_create_bond方法时,会回调配对状态的变化,部分代码如下:

  1. tatic void btif_dm_cb_create_bond(const RawAddress& bd_addr,
  2. tBTA_TRANSPORT transport) {
  3. bool is_hid = check_cod(&bd_addr, COD_HID_POINTING);
  4. bond_state_changed(BT_STATUS_SUCCESS, bd_addr, BT_BOND_STATE_BONDING);
  5. //省略部分代码,咱们只看状态回调,上面就已经开始将状态转换为“绑定中”
  6. if (is_hid && (device_type & BT_DEVICE_TYPE_BLE) == 0) {
  7. bt_status_t status;
  8. status = (bt_status_t)btif_hh_connect(&bd_addr);
  9. if (status != BT_STATUS_SUCCESS)
  10. bond_state_changed(status, bd_addr, BT_BOND_STATE_NONE);
  11. } else {
  12. BTA_DmBondByTransport(bd_addr, transport);
  13. }
  14. /* Track originator of bond creation */
  15. pairing_cb.is_local_initiated = true;
  16. }

而最终会调用BTA_DmBondByTransport去向下传递进而完成配对动作的向下传递,这部分代码偏向硬件,我就不过多叙述了,主要还是看下状态如何回调到应用层的。(PS:主要是二两也不太会,怕说错了误人子弟)

配对状态是如何回调到应用层的呢?

回调的位置还是在http://aospxref.com/android-10.0.0_r47/xref/system/bt/btif/src/btif_dm.cc

 方法bond_state_changed负责接收状态的改变并回调给上层,部分代码如下:

  1. static void bond_state_changed(bt_status_t status, const RawAddress& bd_addr,
  2. bt_bond_state_t state) {
  3. btif_stats_add_bond_event(bd_addr, BTIF_DM_FUNC_BOND_STATE_CHANGED, state);
  4. //检查设备的配对是否在进行中
  5. if ((pairing_cb.state == state) && (state == BT_BOND_STATE_BONDING)) {
  6. // Cross key pairing so send callback for static address
  7. if (!pairing_cb.static_bdaddr.IsEmpty()) {
  8. auto tmp = bd_addr;
  9. //回调bond_state_changed_cb
  10. HAL_CBACK(bt_hal_cbacks, bond_state_changed_cb, status, &tmp, state);
  11. }
  12. return;
  13. }
  14. //判断是否为临时配对 如果是state设置为BT_BOND_STATE_NONE
  15. if (pairing_cb.bond_type == BOND_TYPE_TEMPORARY) state = BT_BOND_STATE_NONE;
  16. BTIF_TRACE_DEBUG("%s: state=%d, prev_state=%d, sdp_attempts = %d", __func__,
  17. state, pairing_cb.state, pairing_cb.sdp_attempts);
  18. auto tmp = bd_addr;
  19. HAL_CBACK(bt_hal_cbacks, bond_state_changed_cb, status, &tmp, state);
  20. int dev_type;
  21. if (!btif_get_device_type(bd_addr, &dev_type)) {
  22. dev_type = BT_DEVICE_TYPE_BREDR;
  23. }
  24. if (state == BT_BOND_STATE_BONDING ||
  25. (state == BT_BOND_STATE_BONDED && pairing_cb.sdp_attempts > 0)) {
  26. // Save state for the device is bonding or SDP.
  27. pairing_cb.state = state;
  28. pairing_cb.bd_addr = bd_addr;
  29. } else {
  30. pairing_cb = {};
  31. }
  32. }

而回调bond_state_changed_cb要通知到应用层,就需要从下往上,通过JNI然后再通过蓝牙服务最终传递到应用层。

而bond_state_changed_cb是如何通知到JNI的呢?这就需要了解另一个文件

bluetooth.h - OpenGrok cross reference for /system/bt/include/hardware/bluetooth.h

在bluetooth.h中,bond_state_changed_cb方法被声明成bond_state_changed_callback,如下:

  1. typedef struct {
  2. /** set to sizeof(bt_callbacks_t) */
  3. size_t size;
  4. adapter_state_changed_callback adapter_state_changed_cb;
  5. adapter_properties_callback adapter_properties_cb;
  6. remote_device_properties_callback remote_device_properties_cb;
  7. device_found_callback device_found_cb;
  8. discovery_state_changed_callback discovery_state_changed_cb;
  9. pin_request_callback pin_request_cb;
  10. ssp_request_callback ssp_request_cb;
  11. //在这里 在这里!
  12. bond_state_changed_callback bond_state_changed_cb;
  13. acl_state_changed_callback acl_state_changed_cb;
  14. callback_thread_event thread_evt_cb;
  15. dut_mode_recv_callback dut_mode_recv_cb;
  16. le_test_mode_callback le_test_mode_cb;
  17. energy_info_callback energy_info_cb;
  18. } bt_callbacks_t;

而最终这个bluetooth.h会被com_android_bluetooth_btservice_AdapterService.cpp引用,

com_android_bluetooth_btservice_AdapterService.cpp - OpenGrok cross reference for /packages/apps/Bluetooth/jni/com_android_bluetooth_btservice_AdapterService.cpp部分代码如下:

  1. #include <hardware/bluetooth.h>
  2. //引用bluetooth.h
  3. //配对状态改变回调
  4. static void bond_state_changed_callback(bt_status_t status, RawAddress* bd_addr,
  5. bt_bond_state_t state) {
  6. //用于处理回调函数
  7. CallbackEnv sCallbackEnv(__func__);
  8. if (!sCallbackEnv.valid()) return;
  9. if (!bd_addr) {
  10. ALOGE("Address is null in %s", __func__);
  11. return;
  12. }
  13. ScopedLocalRef<jbyteArray> addr(
  14. sCallbackEnv.get(), sCallbackEnv->NewByteArray(sizeof(RawAddress)));
  15. if (!addr.get()) {
  16. ALOGE("Address allocation failed in %s", __func__);
  17. return;
  18. }
  19. sCallbackEnv->SetByteArrayRegion(addr.get(), 0, sizeof(RawAddress),
  20. (jbyte*)bd_addr);
  21. //调用Java层的回调函数,将对应状态以及地址和绑定状态作为参数传递
  22. sCallbackEnv->CallVoidMethod(sJniCallbacksObj, method_bondStateChangeCallback,
  23. (jint)status, addr.get(), (jint)state);
  24. }

顺便看下,它是如何映射到JNI的回调方法的,代码如下:

  1. jclass jniCallbackClass =
  2. env->FindClass("com/android/bluetooth/btservice/JniCallbacks");
  3. method_bondStateChangeCallback =
  4. env->GetMethodID(jniCallbackClass, "bondStateChangeCallback", "(I[BI)V");

通过此回调就算是通知到了JNI的接口

JniCallbacks.java - OpenGrok cross reference for /packages/apps/Bluetooth/src/com/android/bluetooth/btservice/JniCallbacks.java

 其中回调部分的代码如下:

  1. void bondStateChangeCallback(int status, byte[] address, int newState) {
  2. mBondStateMachine.bondStateChangeCallback(status, address, newState);
  3. }

可以看到,这个状态的回调会再次通知到配对的状态机,而状态发生更新后,状态机通过接收对应的msg,来进行相应状态的转换或者其他代码的执行,代码如下:

  1. void bondStateChangeCallback(int status, byte[] address, int newState) {
  2. BluetoothDevice device = mRemoteDevices.getDevice(address);
  3. if (device == null) {
  4. infoLog("No record of the device:" + device);
  5. // This device will be added as part of the BONDING_STATE_CHANGE intent processing
  6. // in sendIntent above
  7. device = mAdapter.getRemoteDevice(Utils.getAddressStringFromByte(address));
  8. }
  9. infoLog("bondStateChangeCallback: Status: " + status + " Address: " + device + " newState: "
  10. + newState);
  11. Message msg = obtainMessage(BONDING_STATE_CHANGE);
  12. msg.obj = device;
  13. if (newState == BOND_STATE_BONDED) {
  14. msg.arg1 = BluetoothDevice.BOND_BONDED;
  15. } else if (newState == BOND_STATE_BONDING) {
  16. msg.arg1 = BluetoothDevice.BOND_BONDING;
  17. } else {
  18. msg.arg1 = BluetoothDevice.BOND_NONE;
  19. }
  20. msg.arg2 = status;
  21. sendMessage(msg);
  22. }

至此,整个配对的流程就梳理完了。

整个配对流程较冗长,但是我也实在不想分两篇来写,各位厚米多担待。

而且从JNI到蓝牙协议栈的处理梳理的并不到位,如果有懂行的大佬欢迎交流~

ps:我也是小白,刚看蓝牙源码不久,如果有哪里解释的不对,欢迎各位大神指点!

文章会同步上传到公众号上(二两仙气儿),欢迎同好一起交流学习。

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/知新_RL/article/detail/318556?site
推荐阅读
相关标签
  

闽ICP备14008679号