Android 蓝牙开发 入门级(史上最全)

04-13 1261阅读

第一节:了解蓝牙

1. 蓝牙基础

蓝牙是一种无线技术标准,用于短距离内的数据交换

在Android设备上,蓝牙技术允许进行设备发现、配对、连接以及数据传输。

技术始于爱立信公司 1994 方案,它是研究在移动电话和其他配件间进行低功耗、低成本无线通信连接的方法。发明者希望为设备间的通讯创造一组统一规则(标准化协议)用来解决用户间相互不兼容的移动电子设备。

Android 蓝牙开发 入门级(史上最全)

2.蓝牙分类 

Android 蓝牙开发 入门级(史上最全)

3. 蓝牙权限

在开始开发之前,需要在Android项目的AndroidManifest.xml文件中声明蓝牙相关的权限。对于基本的蓝牙操作,需要以下权限:



从Android 6.0(API级别23)开始,蓝牙扫描需要定位权限,因为蓝牙扫描可以被用来粗略地定位用户。

因此,如果你的应用目标是API级别23或更高,并且需要进行蓝牙扫描,你还需要添加:

 

从Android 10(API级别29)开始,ACCESS_FINE_LOCATION权限被分为更细粒度的权限,如果你的应用需要在后台访问位置,还需要声明:

 
4. 用户权限请求

对于运行时权限(如位置权限),你需要在应用运行时请求用户授权。这通常在你的应用尝试进行蓝牙扫描之前完成

这是请求权限的一个基本示例:

(1)安卓原生检查权限和申请权限

if (ContextCompat.checkSelfPermission(thisActivity, Manifest.permission.ACCESS_FINE_LOCATION)
        != PackageManager.PERMISSION_GRANTED) {
    ActivityCompat.requestPermissions(thisActivity,
            new String[]{Manifest.permission.ACCESS_FINE_LOCATION},
            MY_PERMISSIONS_REQUEST_ACCESS_FINE_LOCATION);
}

在这里,MY_PERMISSIONS_REQUEST_ACCESS_FINE_LOCATION是一个应用定义的整数常量。将在回调方法onRequestPermissionsResult中使用它来接收请求结果。

(2)第三方库xxPermissions

if (ActivityCompat.checkSelfPermission(getContext(), Manifest.permission.BLUETOOTH_CONNECT) != PackageManager.PERMISSION_GRANTED) {
                // 检查蓝牙连接权限,如果没有,则请求权限
                getPermission(Manifest.permission.BLUETOOTH_CONNECT);
                return;
            }
// 获取权限的方法
    private void getPermission(String permission) {
        // 请求权限
        XXPermissions.with(getContext())
                .permission(permission) // 申请单个权限
                .request(new OnPermissionCallback() {
                    @Override
                    public void onGranted(@NonNull List permissions, boolean allGranted) {
                        if (!allGranted) {
                            // 如果未全部授予权限,则显示提示信息
                            Toast.makeText(getContext(), "获取部分权限成功,但部分权限未正常授予", Toast.LENGTH_SHORT).show();
                            Log.d("TAG", "获取部分权限成功");
                            return;
                        }
                        // 权限全部成功授予后的操作
                        Log.d("TAG", "获取权限成功");
                    }
                    @Override
                    public void onDenied(@NonNull List permissions, boolean doNotAskAgain) {
                        if (doNotAskAgain) {
                            // 如果用户选择不再询问,则提示用户手动开启权限,并跳转到应用设置页面
                            Toast.makeText(getContext(), "被永久拒绝授权,请手动授予权限", Toast.LENGTH_SHORT).show();
                            Log.d("TAG", "被永久拒绝授权,请手动授予权限");
                            XXPermissions.startPermissionActivity(getContext(), permissions);
                        } else {
                            // 如果权限被拒绝但没有选择不再询问,则显示失败提示
                            Toast.makeText(getContext(), "获取权限失败", Toast.LENGTH_SHORT).show();
                            Log.d("TAG", "获取权限失败");
                        }
                    }
                });
    }

经典蓝牙BT开发流程

Android 蓝牙开发 入门级(史上最全)

备注:后面几节讲解的都是以BLE为例子

这就是开始使用蓝牙开发所需了解的基础知识。

我们继续下一节。

第二节:启用蓝牙和检查设备支持

在这一节中,我们将学习如何在你的应用中启用蓝牙功能,并检查用户的设备是否支持蓝牙。

1. 获取蓝牙适配器

首先,你需要获取BluetoothAdapter的实例,它是所有蓝牙操作的入口点。

BluetoothAdapter bluetoothAdapter = BluetoothAdapter.getDefaultAdapter();

如果getDefaultAdapter()返回null,则表示设备不支持蓝牙。

2. 检查蓝牙是否启用

接下来,检查蓝牙是否已经启用。使用BluetoothAdapter的isEnabled()方法可以检查蓝牙是否启用:

if (bluetoothAdapter != null && !bluetoothAdapter.isEnabled()) {
    // 蓝牙未启用
}

bluetoothAdapter != null 说明设备支持蓝牙

!bluetoothAdapter.isEnabled() 说明蓝牙没启用

3. 请求用户启用蓝牙

如果蓝牙未启用,你可以请求用户启用它。

这可以通过启动一个Intent来请求用户启用蓝牙,而不是直接调用enable()方法,因为enable()方法无需用户同意即可启用蓝牙,这可能不是最佳用户体验

Intent enableBtIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
startActivityForResult(enableBtIntent, REQUEST_ENABLE_BT);

在这里,REQUEST_ENABLE_BT是应用定义的整数请求码,用于在你的Activity的onActivityResult回调中接收结果。

4. onActivityResult回调

当用户响应启用蓝牙的请求时,系统会调用你的Activity的onActivityResult方法。

你可以在这里检查请求的结果:

@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
    super.onActivityResult(requestCode, resultCode, data);
    if (requestCode == REQUEST_ENABLE_BT) {
        if (resultCode == RESULT_OK) {
            // 用户启用了蓝牙
        } else {
            // 用户未启用蓝牙
        }
    }
}

这就是如何在你的应用中启用蓝牙并检查设备是否支持蓝牙的基本步骤,理解这些步骤是进行蓝牙开发的基础。

第三节:发现设备和获取已配对设备

在这一节中,我们将学习如何发现附近的蓝牙设备以及如何获取已经与你的设备配对的蓝牙设备列表。

1. 获取已配对设备

你的Android设备可能已经与一些蓝牙设备配对过了。

要获取这些已配对的设备列表,你可以使用BluetoothAdapter的getBondedDevices()方法:

Set pairedDevices = bluetoothAdapter.getBondedDevices();
if (pairedDevices.size() > 0) {
    // 至少有一个已配对设备
    for (BluetoothDevice device : pairedDevices) {
        String deviceName = device.getName();
        String deviceHardwareAddress = device.getAddress(); // MAC地址
         Log.d("TAG", "蓝牙设备名称:"+deviceName);
         Log.d("TAG", "蓝牙设备地址:"+deviceHardwareAddress);
    
     }
}
2. 发现新设备

要发现附近的蓝牙设备,你需要调用BluetoothAdapter的startDiscovery()方法。这个方法是异步的,发现过程通常会持续12秒。

需要注册一个BroadcastReceiver来监听BluetoothDevice.ACTION_FOUND广播,这个广播会在发现新设备时发送。

// 注册广播接收器以监听发现的设备
IntentFilter filter = new IntentFilter(BluetoothDevice.ACTION_FOUND);
registerReceiver(receiver, filter);
// 发现设备
if (bluetoothAdapter.startDiscovery()) {
    // 发现过程成功启动
}

疑问:

BluetoothDevice.ACTION_FOUND 是一个什么值?

解答:

  • BluetoothDevice.ACTION_FOUND 是一个字符串常量,用于在广播中标识已找到一个蓝牙设备,值是"android.bluetooth.device.action.FOUND"。
  • 当你的应用调用BluetoothAdapter的startDiscovery()方法开始扫描附近的蓝牙设备时,每发现一个设备,系统就会发送这个ACTION_FOUND的广播
  • 你的应用可以通过注册一个BroadcastReceiver来监听这个广播,以便获取每个发现的蓝牙设备的信息。

    在你的BroadcastReceiver中,你可以获取发现的设备信息:

    private final BroadcastReceiver receiver = new BroadcastReceiver() {
        public void onReceive(Context context, Intent intent) {
            String action = intent.getAction();
            if (BluetoothDevice.ACTION_FOUND.equals(action)) {
                // 从Intent中获取发现的BluetoothDevice
                BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
                String deviceName = device.getName();
                String deviceHardwareAddress = device.getAddress(); // MAC地址
            }
        }
    };

    Android 蓝牙开发 入门级(史上最全)

    疑问:

    action有几种?

    解答:

    - BluetoothDevice.ACTION_ACL_CONNECTED:当与远程设备建立低级别(ACL)连接时发送的广播。

    - BluetoothDevice.ACTION_ACL_DISCONNECT_REQUESTED:当系统要断开与远程设备的低级别(ACL)连接时发送的广播。

    - BluetoothDevice.ACTION_ACL_DISCONNECTED:当与远程设备的低级别(ACL)连接断开时发送的广播。

    - BluetoothDevice.ACTION_BOND_STATE_CHANGED:当远程设备的配对状态发生变化时发送的广播。

    - BluetoothDevice.ACTION_NAME_CHANGED:当远程设备的名称发生变化时发送的广播。

    - BluetoothDevice.ACTION_PAIRING_REQUEST:在需要配对时发送的广播,通常是因为配对过程需要输入PIN码或确认配对。

    - BluetoothAdapter.ACTION_DISCOVERY_STARTED:当设备发现开始时发送的广播。

    - BluetoothAdapter.ACTION_DISCOVERY_FINISHED:当设备发现结束时发送的广播。

    - BluetoothAdapter.ACTION_STATE_CHANGED:当蓝牙适配器的状态发生变化时发送的广播,例如蓝牙被开启或关闭。

     

    监听这些action可以让你的应用更好地与蓝牙设备交互。

    例如,

    通过监听ACTION_BOND_STATE_CHANGED,你的应用可以知道何时一个设备已经成功配对,或者配对失败。

    通过监听ACTION_ACL_CONNECTED和ACTION_ACL_DISCONNECTED,你的应用可以知道何时设备连接或断开连接

    可以在这些节点做一些处理。

    3. 停止发现

    由于发现过程会消耗大量资源,一旦你找到了你感兴趣的设备,或者你想停止扫描,你应该使用cancelDiscovery()方法来停止发现过程:

    bluetoothAdapter.cancelDiscovery();
    4. 清理

    记得在不再需要时,unregisterReceiver(receiver)注销你的BroadcastReceiver,例如,在你的Activity或Fragment的onDestroy()方法中:

    @Override
    protected void onDestroy() {
        super.onDestroy();
        // 确保我们不再监听广播
        unregisterReceiver(receiver);
    }

    这就是如何发现新设备以及获取已配对设备的基本步骤。

    理解这些步骤对于开发能够与其他蓝牙设备交互的应用是非常重要的。

    当你准备好了,请告诉我,以便我们继续下一节。

    第四节:连接到蓝牙设备

    在这一节中,我们将学习如何使用BluetoothSocket来连接到一个蓝牙设备。连接到设备是进行数据交换的前提。

    1. 创建BluetoothSocket

    要与远程蓝牙设备建立连接,首先需要创建一个BluetoothSocket。

    BluetoothSocket代表了蓝牙套接字的接口,它是两个设备之间通信的通道。

    对于大多数用途,你将使用createRfcommSocketToServiceRecord(UUID)方法来创建BluetoothSocket。

    这个方法需要一个UUID参数,这个UUID必须是你想要连接的远程设备上的蓝牙服务的UUID。UUID代表通用唯一标识符,用于唯一标识你的应用的蓝牙服务。

    BluetoothDevice device = ... // 获取一个BluetoothDevice对象
    UUID MY_UUID = UUID.fromString("00001101-0000-1000-8000-00805f9b34fb"); // 示例UUID
    BluetoothSocket socket = device.createRfcommSocketToServiceRecord(MY_UUID);

    有几个概念:

    • 服务UUID
    • 特征UUID
    • 描述UUID
    • 公共标准UUID
      2. 连接到远程设备

      创建BluetoothSocket后,你可以调用它的connect()方法来尝试与远程设备建立连接。

      这个方法是阻塞调用(直到连接成功或抛出异常)。

      try {
          socket.connect();
          // 连接成功,可以开始进行数据交换
      } catch (IOException e) {
          // 连接失败
          e.printStackTrace();
      }
      3. 数据交换

      连接成功后,你可以通过BluetoothSocket的输入输出流来进行数据的读写。

      try {
          InputStream inputStream = socket.getInputStream();
          OutputStream outputStream = socket.getOutputStream();
          // 从InputStream读取数据
          byte[] buffer = new byte[1024];
          int bytes;
          bytes = inputStream.read(buffer);
          String receivedData = new String(buffer, 0, bytes);
          // 向OutputStream写入数据
          String dataToSend = "Hello, Bluetooth!";
          outputStream.write(dataToSend.getBytes());
      } catch (IOException e) {
          // 处理异常
      }
      4. 断开连接

      完成数据交换后,应该关闭BluetoothSocket来释放资源。

      try {
          socket.close();
      } catch (IOException e) {
          // 处理异常
      }

      注意事项

      - 连接过程可能会花费一些时间,建议在一个单独的线程中进行连接操作,以避免阻塞UI线程。

      - 确保你的UUID与远程设备提供的服务UUID匹配。

      - 在连接过程中和之后,要妥善处理异常。

      疑问:

      我听同事说发送数据还有片段发送,是怎么样的

      解答:

      在蓝牙通信中,由于蓝牙协议本身的限制以及不同设备的性能差异,发送大量数据时直接发送整个数据包可能会导致数据传输失败或者效率低下。

      因此,将大数据分割成小片段(fragments)进行发送是一种常见的做法,这就是所谓的“片段发送”或“数据分片”。

      1.数据分片的基本思想

      数据分片的基本思想,是将大的数据包分割成多个较小的数据片段,然后逐个片段地发送这些数据。接收方收到所有片段后,再将它们重新组合成原始的数据包。

      2.如何实现数据分片?

      实现数据分片的一个简单方法是:

      • (1)确定片段大小:
        • 首先,你需要确定每个数据片段的大小。
          • 这个大小需要根据你的应用需求和蓝牙硬件的能力来决定。一个常见的做法是使用512字节作为每个片段的大小。
      • (2)分割数据:
        • 将你的数据分割成多个片段。
          • 如果数据大小不是片段大小的整数倍,最后一个片段可能会更小。
      • (3)发送数据片段:
        • 逐个发送每个数据片段。
          • 在每个片段发送之后,可能需要等待接收方的确认,再发送下一个片段
      • (4)接收方重组数据:
        • 接收方需要按照发送顺序接收并存储每个数据片段。
          • 一旦所有片段都被接收,接收方就可以将它们组合成原始的数据包。
        示例代码

        以下是一个简化的示例,展示了如何将一个大的字节数组分割成多个片段并发送:

        OutputStream outputStream = bluetoothSocket.getOutputStream();
        byte[] largeData = ...; // 假设这是要发送的大数据
        int chunkSize = 512; // 片段大小
        int offset = 0;
        while (offset  0 && isConnected) {
                            String deviceName = device .getName();
                          //  LogUtils.d(deviceName + "    电量:  " + level);
                            Log.e(TAG, deviceName + "    电量:  " + level);
                        }
                    }
                } else {
                   // ToastUtils.showLong("No Connected Bluetooth Devices Found");
                    Log.e(TAG,  "No Connected Bluetooth Devices Found");
                }
            } catch (Exception e) {
                Log.e(TAG,  "Exception"+e.toString());
                e.printStackTrace();
            }
        }


        总之,这就是如何连接到蓝牙设备并进行数据交换的基本步骤。理解这些步骤对于开发能够与其他蓝牙设备交互的应用是非常重要的。

        当你准备好了,请告诉我,以便我们继续下一节。

        第五节:管理蓝牙连接

        在这一节中,我们将讨论如何管理蓝牙连接,包括:数据的接收处理连接的维护断开连接的正确方式。

        1. 接收数据

        在前一节中,我们讨论了如何发送数据。接收数据同样重要,通常涉及到从BluetoothSocket的输入流中读取数据。

        由于数据接收是阻塞操作,推荐在单独的线程中进行读取操作,以避免阻塞UI线程。

        class ConnectedThread extends Thread {
            private final BluetoothSocket socket;
            private final InputStream inputStream;
            private final OutputStream outputStream;
            public ConnectedThread(BluetoothSocket socket) {
                this.socket = socket;
                InputStream tmpIn = null;
                OutputStream tmpOut = null;
                try {
                    tmpIn = socket.getInputStream();
                    tmpOut = socket.getOutputStream();
                } catch (IOException e) {
                    e.printStackTrace();
                }
                inputStream = tmpIn;
                outputStream = tmpOut;
            }
            public void run() {
                byte[] buffer = new byte[1024];  // 缓冲区
                int bytes; // 读取的字节数
                while (true) {
                    try {
                        // 从InputStream读取数据
                        bytes = inputStream.read(buffer);
                        String receivedData = new String(buffer, 0, bytes);
                        // 处理接收到的数据
                    } catch (IOException e) {
                        // 连接丢失时的处理
                        break;
                    }
                }
            }
            // 调用此方法从外部发送数据
            public void write(byte[] bytes) {
                try {
                    outputStream.write(bytes);
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            // 调用此方法关闭连接
            public void cancel() {
                try {
                    socket.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
        2. 维护连接

        维护蓝牙连接的关键,在于处理各种可能导致连接中断的情况。

        例如,设备的距离过远、干扰或设备关闭。

        在ConnectedThread的run方法中,如果inputStream.read(buffer)抛出IOException,通常意味着连接已经丢失,此时应当关闭socket并尝试重新连接或更新UI。

        3. 断开连接

        当不再需要蓝牙连接时,正确的断开连接是很重要的。这包括关闭BluetoothSocket,以及与之关联的输入输出流。

        在ConnectedThread类中,我们提供了一个cancel方法来做这件事。调用cancel可以安全地关闭socket并终止线程。

        public void cancel() {
            try {
                socket.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        4. 重连策略

        在实际应用中,可能需要实现自动重连的逻辑。这通常涉及到在连接丢失后等待一段时间,然后尝试重新连接到设备。

        重连策略的实现会根据具体的应用需求而有所不同,但基本思路是在捕获到连接丢失的异常后,延迟一段时间后再次尝试连接。

        这就是关于管理蓝牙连接的基本知识。通过正确地管理连接,你的应用可以更加稳定,为用户提供更好的体验。

        当你准备好了,请告诉我,以便我们继续下一节。

        第六节:蓝牙服务和特征

        在这一节中,我们将探讨使用蓝牙低功耗(BLE)时的高级概念,特别是蓝牙服务和特征的概念。这些概念对于理解和开发BLE应用至关重要。

        1. BLE简介

        蓝牙低功耗(BLE),也称为蓝牙4.0 或 蓝牙Smart,是蓝牙技术的一个版本,专为低功耗设备设计,适用于需要长期运行在电池供电下的设备。与经典蓝牙相比,BLE在保持通信能力的同时,大大降低了能耗。

        2. 服务和特征

        在BLE中,数据交换是通过服务(Services)和特征(Characteristics)的概念来组织的。

        - 服务(Service):

                服务是一组功能相关的特征的集合。每个服务都由一个唯一的UUID来标识。


        - 特征(Characteristic):

                特征表示服务中的一个数据点,例如一个传感器的值或者一个配置参数。特征包含一个值以及0到多个描述符(Descriptors),用于描述特征的属性。特征也由UUID标识。

        3. 发现服务和特征

        当你的设备与一个BLE设备建立连接后,下一步通常是发现远程设备提供的服务和特征。这可以通过调用BluetoothGatt对象的discoverServices()方法来完成。

        服务发现完成后,你可以通过调用getServices()方法来获取服务列表,然后遍历这些服务以查找特定的服务和特征。

        @Override
        public void onServicesDiscovered(BluetoothGatt gatt, int status) {
            if (status == BluetoothGatt.GATT_SUCCESS) {
                for (BluetoothGattService service : gatt.getServices()) {
                    UUID serviceUUID = service.getUuid();
                    // 检查是否是我们感兴趣的服务UUID
                    for (BluetoothGattCharacteristic characteristic : service.getCharacteristics()) {
                        UUID characteristicUUID = characteristic.getUuid();
                        // 检查是否是我们感兴趣的特征UUID
                    }
                }
            } else {
                Log.w(TAG, "onServicesDiscovered received: " + status);
            }
        }
        4. 读写特征

        一旦找到了感兴趣的特征,你可以读取特征的值或向其写入值。

        - 读取特征值:

                        调用BluetoothGatt的readCharacteristic()方法。


        - 写入特征值:

                        首先设置特征的值,然后调用BluetoothGatt的writeCharacteristic()方法。

        // 读取特征值
        gatt.readCharacteristic(characteristic);
        // 写入特征值
        characteristic.setValue(value);
        gatt.writeCharacteristic(characteristic);
        5. 订阅特征通知

        对于一些特征,你可能希望当特征的值发生变化时得到通知。

        这可以通过对特征启用通知来实现 BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE

        这通常涉及到写入特征的一个相关描述符。

        BluetoothGattDescriptor descriptor = characteristic.getDescriptor(
                UUID.fromString(CLIENT_CHARACTERISTIC_CONFIG));
        descriptor.setValue(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE);
        gatt.writeDescriptor(descriptor);

        这就是BLE服务和特征的基本概念。

        通过理解和使用这些概念,你可以开发出能够与BLE设备进行复杂交互的应用。

        当你准备好了,请告诉我,以便我们继续下一节。

        第七节:蓝牙安全性和最佳实践

        在这一节中,我们将讨论蓝牙通信的安全性问题以及开发蓝牙应用时的一些最佳实践。

        1. 蓝牙安全性概述

        蓝牙技术包含多种安全特性,旨在保护通信不被未授权访问。这些安全特性包括配对过程中的认证和加密。然而,蓝牙设备和通信仍可能面临一些安全威胁,如中间人攻击(MITM)、身份欺骗等。因此,开发者在使用蓝牙技术时需要采取适当的安全措施。

        2. 使用安全的配对模式


        - 配对过程:

                在设备之间建立连接前,通常会进行配对过程,这个过程可以提供认证和加密。确保使用安全的配对模式,如Passkey Entry或Numeric Comparison,这些模式提供更高级别的安全性。


        - 注意保护PIN码:

                如果你的应用需要用户输入PIN码来配对设备,确保这个过程是安全的,避免泄露PIN码。

        3. 管理已配对设备


        - 信任的设备:

                        只允许已知和信任的设备连接。对于不再使用的设备,应从系统中删除其配对信息。

        - 定期检查配对设备:应用应定期检查已配对设备列表,移除不再需要的设备。

        4. 使用加密的数据传输


        - 加密通信:

                        确保数据传输过程中使用加密。对于BLE,一旦设备配对,数据传输通常是加密的。但是,开发者应验证加密确实被启用。


        - 敏感信息处理:

                        对于传输敏感信息,考虑在应用层再次加密数据,以增加安全性。

        5. 最佳实践


        - 最小权限原则:应用应仅请求完成其功能所必需的最小权限集合。
        - 注意隐私问题:在处理用户数据和设备信息时,要注意用户隐私,遵守相关法律法规。
        - 错误处理:妥善处理错误和异常,避免泄露敏感信息。
        - 更新和维护:定期更新应用和蓝牙设备的固件,以修复已知的安全漏洞。

        6. 性能优化


        - 节能策略:

                        蓝牙操作可能会消耗大量电量,特别是在进行设备发现时。合理安排发现过程,减少不必要的操作,可以帮助节省电量。


        - 连接管理:

                        合理管理蓝牙连接,避免不必要的连接和断开,可以提高应用性能和用户体验。

        通过遵循这些安全性指南和最佳实践,你可以开发出既安全又高效的蓝牙应用。这些措施有助于保护用户数据,提高应用的稳定性和可靠性。

        这标志着我们蓝牙开发基础教程的结束。如果你有任何问题,或者想要深入了解某个特定主题,请随时留言、点赞、加关注。

VPS购买请点击我

文章版权声明:除非注明,否则均为主机测评原创文章,转载或复制请以超链接形式并注明出处。

目录[+]