android bluetooth开发

android 平台提供蓝牙网络协议栈的支持,允许一台设备与其它设备通过无线交换数据。应用框架通过android 蓝牙API提供对蓝牙功能的使用。这些API允许应用无线连接到其它的蓝牙设备,支持点对点、点对多的无线通信。

通过android API,应用程序可以做到:

  • 扫描其它的蓝牙设备
  • 查询蓝牙适配器已经配对的蓝牙设备
  • 建立RFCOMM信道(channels)
  • 通过服务发现连接到其它设备
  • 与其它设备间发送或者接收数据
  • 管理多个连接

基础

本文档主要描述用android API来实现蓝牙通信的四个主要步骤:初始化蓝牙设备、找到已配对或者附近可用的蓝牙设备、连接设备、设备间传输数据。

所有的蓝牙API都在android.bluetooth包中,下面是建立蓝牙连接主要需要用到的类和接口:

BluetoothAdapter

代表本地蓝牙适配器,BluetoothAdapter是所有蓝牙交互的入口点。通过该对象可以发现其它蓝牙设备,查询已配对设备,用已知的MAC地址实例化一个BluetoothDevice对象,建立一个BluetoothServerSocket用来监听来自其它蓝牙设备的消息。

BluetoothDevice

代表一个远程蓝牙设备,可以通过一个BluetoothSocket对象向一个远程设备请求连接,或者查询蓝牙设备的信息如名字、地址、类别(class)、配对状态。

BluetoothSocket

代表一个蓝牙socket接口(类似于TCP Socket)。它是应用程序与其它蓝牙设备通过IO流交换数据的连接点。

BluetoothServerSocket

代表一个监听请求的服务器socket(类似于TCP ServerSocket)。要连接两台android设备,必须有一个设备用该类打开一个服务器socket。当一台远程蓝牙设备向该设备发出连接请求时,连接建立时BluetoothServerSocket会返回一个连接后的BluetoothSocket

BluetoothClass

描述一台蓝牙设备的特点和能力。这是一组定义蓝牙主要和次要的类别和服务的只读的属性值。虽然它并不能完全可靠地描述该设备支持的配置和服务,但是用来分别设别类型很有用。

BluetoothProfile

描述bluetooth profile的接口。bluetooth profile是设备间蓝牙通信的无线接口规范。举一个例子就是Hands-Free profile。关于profile更多信息可以参见Working with Profiles

BluetoothHeadset

为手机使用蓝牙耳机提供支持。它同时包括Headset 和 Hands-Free (v1.5) profiles。

BluetoothA2dp

定义高品质音频如何通过蓝牙连接由一台设备传输到另外一台设备。"a2dp"代表Advanced Audio Distribution Profile。

BluetoothHealth

代表一个控制蓝牙服务的Health Device Profile proxy。

BluetoothHealthCallback

用于实现BluetoothHealth回调的抽象类。开发者必须继承这个类并且实现其中的回调方法用来接收应用注册和蓝牙信道状态的变化。

BluetoothHealthAppConfiguration

代表第三方的Bluetooth Health应用连接到远程Bluetooth health设备的配置。

BluetoothProfile.ServiceListener

通知BluetoothProfile IPC客户端与service(the internal service that runs a particular profile)连接或者断开连接的接口。

蓝牙权限

为了在应用程序中使用蓝牙特性,开发者至少需要定义这两个权限中的一个:BLUETOOTHBLUETOOTH_ADMIN

如果应用需要进行一些蓝牙通信比如请求连接、接受连接和传输数据,开发者应该请求BLUETOOTH权限。

如果应用需要执行发现设备以及蓝牙设定则应该请求BLUETOOTH_ADMIN权限。大部分应用程序为了发现附近蓝牙设备都需要这个权限。该权限所授予的其它功能不应该被使用,除非该应用是基于用户请求修改蓝牙设定。如果你使用BLUETOOTH_ADMIN权限,则一定需要同时请求BLUETOOTH权限。

在应用程序的manifest中可以以如下方式定义蓝牙权限:

<manifest ... >
  <uses-permission android:name="android.permission.BLUETOOTH" />
  ...
</manifest>

可以查看 <uses-permission>文档获取更详细的定义应用权限的说明。

初始化蓝牙

在你的应用程序进行蓝牙通信前,应该确定你的设备支持蓝牙,并且是打开状态的。

如果该设备不支持蓝牙,你应该在应用中避开蓝牙的使用。如果设备支持蓝牙但是处于关闭状态,你应该在当前应用中请求用户启用蓝牙。该过程可以用BluetoothAdapter分两步完成。

1.获取BluetoothAdapter

BluetoothAdapter对象是所有蓝牙Activity所必须的,开发者可以通过静态方法getDefaultAdapter()取得BluetoothAdapter对象。该方法返回一个代表本地蓝牙适配器的BluetoothAdapter对象。整个系统中只有一个蓝牙适配器,应用程序可以通过该对象来与它交互。如果getDefaultAdapter()方法返回为null,则表示该设备不支持蓝牙。

BluetoothAdapter mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
if (mBluetoothAdapter == null) {
    // Device does not support Bluetooth
}

2.打开蓝牙

接下来,你应该确保蓝牙是打开的。通过isEnabled()方法可以检查当前蓝牙是否处于打开状态,如果方法返回false,则蓝牙处于关闭状态。开发者可以通过使用带有ACTION_REQUEST_ENABLE这个action的Intent调用startActivityForResult()来请求用户打开蓝牙。这将会请求通过系统设置来打开蓝牙,并不会关闭当前应用。代码如下:

if (!mBluetoothAdapter.isEnabled()) {
    Intent enableBtIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
    startActivityForResult(enableBtIntent, REQUEST_ENABLE_BT);
}
接下来会弹出一个对话框请求用户打开蓝牙,如下图所示:

如果用户选择"Yes",系统将会开始打开蓝牙并且在结束操作时返回你的应用程序。

传递给startActivityForResult()的常量REQUEST_ENABLE_BT是一个本地定义的整数(必须大于0)。在常量会作为回调方法onActivityResult()中的requestCode参数返回。

如果打开蓝牙成功,Activity在onActivityResult()方法中会收到返回码RESULT_OK,如果应用由于某原因打开失败或者用户选择了"No",则Activity会收到返回码RESULT_CANCELED

另外,应用程序可以监听ACTION_STATE_CHANGED广播,系统会在蓝牙状态发生改变时发出该广播。该广播包含EXTRA_STATEEXTRA_PREVIOUS_STATE两个属性,分别代表当前状态和之前状态。属性的值可能为STATE_TURNING_ONSTATE_ONSTATE_TURNING_OFF以及STATE_OFF。当应用运行时监听该广播对检测蓝牙状态很有帮助。

注意:启用蓝牙的可发现(discoverability)将会自动打开蓝牙。如果你想在进行蓝牙活动前持续地打开蓝牙的可发现性,你可以跳过上面的步骤2。具体可以参考enabling discoverability

找到设备

使用BluetoothAdapter,你可以通过蓝牙发现服务或者已配对设备列表发现附近的蓝牙设备。

设备发现是一个搜索附近蓝牙设备并向每个设备请求基本信息的一个扫描过程。然而,附近的蓝牙设备只有在打开了可发现性后才会对发现请求作出回应。如果一台设备是可发现的,它会对发现请求回应一些基本信息,如设备名称、类别以及MAC地址。通过这些信息,执行发现服务的设备可以选择性的与已发现的设备进行连接。

当第一次与远程设备连接时,系统将会呈现给用户一个配对请求。当一台设备配对后,该设备的基本信息(设备名称、类别和MAC地址)都被保存下来并且可以通过蓝牙的API读取出来。知道了一台远程设备的MAC地址后,可以随时发起连接而不需要经过发现(假设设备在可连接范围内)。

记住已配对和已连接是有区别的。已配对表明两台设备知道对方的存在,有一个共同的可用于认证的link-key,并且能够与对方建立一个加密连接。已连接表明两台设备共享一个RFCOMM信道(channel)并且能够向对方发送数据。当前的android 蓝牙API在建立RFCOMM连接前需要进行配对操作。(当你用andorid 蓝牙API建立加密连接时配对操作会自动完成)。

接下来的部分描述如何找到已配对的设备以及通过设备发现服务发现其它设备。

注意:android设备默认都是不可发现的。用户可以通过系统设置让设备在一定时间内可以被发现,应用程序也可以在不离开当前应用的情况下请求用户打开设备的可发现性。在之后的内容中会讲到如何enable discoverability

查询已配对设备

在进行蓝牙发现之前,有必要查询已配对设备列表检查所期望设备是否是已知的。通过使用getBondedDevices()方法即可,它会返回一系列代表已配对设备的BluetoothDevice。例如,你可以查询所有的已配对设备然后用一个ArrayAdapter给用户显示每一个设备的名称。

Set<BluetoothDevice> pairedDevices = mBluetoothAdapter.getBondedDevices();
// If there are paired devices
if (pairedDevices.size() > 0) {
    // Loop through paired devices
    for (BluetoothDevice device : pairedDevices) {
        // Add the name and address to an array adapter to show in a ListView
        mArrayAdapter.add(device.getName() + "\n" + device.getAddress());
    }
}

如果需要建立连接,所有所需要的 只有BluetoothDevice对象中的MAC地址。在上面的例子中,它作为ArrayAdapter的一部分保存下来并展示给用户,之后MAC地址可以被提取出来用来建立连接。关于建立连接部分可以参见 Connecting Devices

发现设备

要开始发现设备,只需要调用startDiscovery()方法。该过程是异步的,方法会立即返回一个boolean值以表明发现服务是否成功开启。发现过程通常包括大约12秒的查询扫描,接下来是扫描每一个设备获取其名称。

应用程序必须为ACTION_FOUND这个Intent注册一个BroadcastReceiver用来接收每一台发现的设备的信息。每发现一台设备,系统都会广播ACTION_FOUND这个Intent。这个intent捆绑了含TRA_DEVICEEXTRA_CLASS两个属性,分别包含一个BluetoothDevice和一个BluetoothClass对象。下面的例子展示了开发者应该如何处理发现到设备的广播:

// Create a BroadcastReceiver for ACTION_FOUND
private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
    public void onReceive(Context context, Intent intent) {
        String action = intent.getAction();
        // When discovery finds a device
        if (BluetoothDevice.ACTION_FOUND.equals(action)) {
            // Get the BluetoothDevice object from the Intent
            BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
            // Add the name and address to an array adapter to show in a ListView
            mArrayAdapter.add(device.getName() + "\n" + device.getAddress());
        }
    }
};
// Register the BroadcastReceiver
IntentFilter filter = new IntentFilter(BluetoothDevice.ACTION_FOUND);
registerReceiver(mReceiver, filter); // Don't forget to unregister during onDestroy

注意:执行设备发现对蓝牙适配器是很耗费资源的,一旦你发现需要连接的设备,确保在建立连接前调用 cancelDiscovery()来停止发现服务。另外,如果你已经与一台设备建立了连接,执行设备发现服务将会明显地压缩该连接的可用带宽,所以在连接状态时你不应该执行设备发现服务。

启用可发现性

如果你希望你的设备可以被其它设备发现,调用startActivityForResult(Intent, int),在参数中加入ACTION_REQUEST_DISCOVERABLE这个Intent。这将会请求用户在系统设置中打开可发现性模式。设备默认会在120s内可以被其它设备发现,你也可以在intent中加入EXTRA_DISCOVERABLE_DURATION这个属性来定义可被发现的时间。应用可以定义的最大时间是3600s,0代表设备始终不可被发现,小于0或者大于3600的值都会默认设置为120s。例如如下代码设置300s内可以被发现:

Intent discoverableIntent = new
Intent(BluetoothAdapter.ACTION_REQUEST_DISCOVERABLE);
discoverableIntent.putExtra(BluetoothAdapter.EXTRA_DISCOVERABLE_DURATION, 300);
startActivity(discoverableIntent);

系统会弹出一个如下所示的对话框请求用户设置设备可被发现:

如果用户点击”Yes",设备在指定时间内将会可被发现,Activity将会调用onActivityResult())回调方法,返回码等于设置的可被发现时间。如果用户点击了"No"或者有异常,返回码将是RESULT_CANCELED

注意:如果设备蓝牙还没有打开,启用蓝牙的可发现性将会自动打开蓝牙。

设备将会在该时间内保持可被发现。如果你想在设备的可发现性改变时得到通知,你可以为ACTION_SCAN_MODE_CHANGED Intent注册一个BroadcastReceiver。这个intent包含EXTRA_SCAN_MODEEXTRA_PREVIOUS_SCAN_MODE属性,分别表示当前的和之前的扫描模式。可能的值为SCAN_MODE_CONNECTABLE_DISCOVERABLESCAN_MODE_CONNECTABLESCAN_MODE_NONE,分别代表设备处于可被发现模式、不处于可发现模式但是可以接收连接、不处于可发现模式并且不可以接收连接。

如果你想连接到一台远程设备你不必打开设备的可发现性。只有在你的应用程序在运行服务器Socket并且希望接收连接时才需要打开可被发现性,因为远程设备在建立连接前需要发现希望连接的设备。

连接设备

为了让你的应用在两台设备间建立连接,你必须同时实现server端和client端机制,因为其中一台设备必须打开server socket然后另外一台设备建立连接(通过server端设备的MAC地址来建立连接)。server端和client在同一RFCOMM信道上有连接好的BluetoothSocket则认为它们互相连接到了对方。此时,每台设备都可以获取IO流并且可以开始数据传输,在后面的Managing a Connection中会详细讲解。这部分描述如何在两台设备间建立连接。

server端和client端通过不同的方法获取BluetoothSocket对象,server端会在接受一个连接请求时取得它,client端会在向server端打开一个RFCOMM信道时取得它。

一种实现方法是每台设备都准备作为server端,每个设备都有打开的server socket在监听连接请求,然后其中一台设备可以向另一台设备发起连接并且成为client端。或者一台设备打开server socket明确地作为server端,另外一台设备只需要发起请求就可以。

注意:如果两台设备之前没有配对,系统会自动显示请求配对的通知或者显示如下对话框给用户:


所以在连接其它设备时,应用程序无需关心两台设备是否已经配对。你的连接尝试会被祖塞直到用户成功配对,或者在用户拒绝配对、配对失败、配对超时时失败。

作为server端

如果你想连接两台设备,其中一台设备必须持有BluetoothServerSocket对象以作为server,server socket的作用是监听连接请求并且在接收请求时提供一个连接好的BluetoothSocket对象。当从BluetoothServerSocket中取得BluetoothSocket对象后,BluetoothServerSocket对象就没用了,除非你需要接收更多的请求。

下面是建立一个server socket并接收一个请求的基本步骤

1.通过调用listenUsingRfcommWithServiceRecord(String, UUID)方法取得一个BluetoothServerSocket对象

关于UUID:通用唯一标识符(Universally Unique Identifier)是一种用来唯一标识信息的128位标准化格式的字符串ID。重点是UUID数字足够大,你可以任意选取随机数而不用担心重复。在这里,它用来唯一标识你的应用里的蓝牙服务。要在应用中使用UUID,你可以使用网上的UUID生成器,然后通过fromString(String)来生成一个UUID对象。

上面的string是你的服务的标识名,系统将会自动将它写入一个新的SDP(Service Discovery Protocol)数据库条目(名字是任意的,可以简单的写为应用程序名)。UUID也包含在SDP条目中并且将会是与客户端达成连接的基础。也就是,当客户端试图连接这一台设备时,它会带有一个UUID用来唯一标识它想连接的服务,要想连接被接受UUID必须匹配。

2.调用accept()开始监听连接请求

这是一个阻塞方法,只有在接收一个连接或者有异常发生时它才会返回。当远程设备发送的请求中的UUID与server端设备正在监听的server socket中注册的UUID一致时连接请求才会被接受。如果成功了,accept()方法会返回一个连接上的BluetoothSocket

3.除非你想接收其它连接,否则调用close()

该方法释放server socket的资源,但是不会关闭之前由accept()方法返回的连接上的BluetoothSocket。不同于TCP/IP,RFCOMM在同一时间同一信道只能允许一台client端连接。所以大多数情况下在BluetoothServerSocket接受一个请求后立即调用close()很重要。

accept()方法不应该在主要的activity UI线程中执行,因为它是一个阻塞方法并且会阻止应用的UI交互。通常情况下与BluetoothServerSocketBluetoothSocket的操作都放在新线程中进行。如果要退出一个阻塞的方法如accept(),则应该在另外一个线程中调用BluetoothServerSocket(或者BluetoothSocket)的close()方法,这样阻塞方法会立即返回。注意BluetoothServerSocketBluetoothSocket中的方法都是线程安全的。

实例

如下是一个简单的server端接收连接请求的线程:

private class AcceptThread extends Thread {
    private final BluetoothServerSocket mmServerSocket;
 
    public AcceptThread() {
        // Use a temporary object that is later assigned to mmServerSocket,
        // because mmServerSocket is final
        BluetoothServerSocket tmp = null;
        try {
            // MY_UUID is the app's UUID string, also used by the client code
            tmp = mBluetoothAdapter.listenUsingRfcommWithServiceRecord(NAME, MY_UUID);
        } catch (IOException e) { }
        mmServerSocket = tmp;
    }
 
    public void run() {
        BluetoothSocket socket = null;
        // Keep listening until exception occurs or a socket is returned
        while (true) {
            try {
                socket = mmServerSocket.accept();
            } catch (IOException e) {
                break;
            }
            // If a connection was accepted
            if (socket != null) {
                // Do work to manage the connection (in a separate thread)
                manageConnectedSocket(socket);
                mmServerSocket.close();
                break;
            }
        }
    }
 
    /** Will cancel the listening socket, and cause the thread to finish */
    public void cancel() {
        try {
            mmServerSocket.close();
        } catch (IOException e) { }
    }
}

在上面的例子中,只期望连接一个client端,只要 接受了一个连接,取得了BluetoothSocket对象,应用程序就会将取得的BluetoothSocket对象发送到另外一个线程中,关闭BluetoothServerSocket并且跳出循环。

注意当accept()方法返回BluetoothSocket时,该socket已经连接,你不应该再去调用connect()(在client端完成)。

代码中manageConnectedSocket()是一个虚构的方法,它会建立一个线程并用来传输数据,在下面的Managing a Connection中会讲到。

只要你完成接受连接请求就应该关闭BluetoothServerSocket。比如,取得BluetoothSocket对象后就应该调用close()方法。你也许需要在线程中提供一个公有方法来关闭私有的BluetoothSocket。You may also want to provide a public method in your thread that can close the privateBluetoothSocket in the event that you need to stop listening on the server socket.

作为client端

为了与远程设备(开有server socket的设备)连接,你首先必须获取一个代表远程设备的BluetoothDevice对象。如何获取BluetoothDevice对象在前面的Finding Devices部分已经讲到了。接下来你应该用BluetoothDevice对象获取一个BluetoothSocket对象然后建立连接。

下面是基本的步骤:

1.使用BluetoothDevice对象,调用createRfcommSocketToServiceRecord(UUID)方法取得一个BluetoothSocket对象,这样就实现了要连接到BluetoothDevice的一个BluetoothSocket对象。这里传入的UUID必须与server端用listenUsingRfcommWithServiceRecord(String, UUID)方法建立BluetoothServerSocket时使用的UUID一致。保证使用相同的UUID只需要在你的应用中将它写固定,然后在server端和client端的代码中都引用它即可。

2.调用connect()来建立连接

调用该方法后,系统会对远程设备执行SDP查找以匹配UUID。如果查找成功并且远程设备接受连接,它将会共享连接过程中的RFCOMM信道并且connect()方法会返回。该方法是一个阻塞方法,如果因为某些原因连接失败或者connect()方法超时(大约12秒钟),那么它会抛出一个异常。因为connect()方法是一个阻塞方法,所以连接过程在一个与主线程区分的线程中进行。

注意:你应该确保在执行connect()时你的设备没有在进行扫描,如果正在扫描,那么连接过程会非常慢并且很容易失败。

实例

下面是一个建立蓝牙连接的线程实例:

private class ConnectThread extends Thread {
    private final BluetoothSocket mmSocket;
    private final BluetoothDevice mmDevice;
 
    public ConnectThread(BluetoothDevice device) {
        // Use a temporary object that is later assigned to mmSocket,
        // because mmSocket is final
        BluetoothSocket tmp = null;
        mmDevice = device;
 
        // Get a BluetoothSocket to connect with the given BluetoothDevice
        try {
            // MY_UUID is the app's UUID string, also used by the server code
            tmp = device.createRfcommSocketToServiceRecord(MY_UUID);
        } catch (IOException e) { }
        mmSocket = tmp;
    }
 
    public void run() {
        // Cancel discovery because it will slow down the connection
        mBluetoothAdapter.cancelDiscovery();
 
        try {
            // Connect the device through the socket. This will block
            // until it succeeds or throws an exception
            mmSocket.connect();
        } catch (IOException connectException) {
            // Unable to connect; close the socket and get out
            try {
                mmSocket.close();
            } catch (IOException closeException) { }
            return;
        }
 
        // Do work to manage the connection (in a separate thread)
        manageConnectedSocket(mmSocket);
    }
 
    /** Will cancel an in-progress connection, and close the socket */
    public void cancel() {
        try {
            mmSocket.close();
        } catch (IOException e) { }
    }
}

注意在建立连接前调用了 cancelDiscovery()方法,在建立连接前你应该总是这么做而且不用考虑它是否有在扫描(如果想检查可以调用 isDiscovering()))。

代码中manageConnectedSocket()是一个虚构的方法,它会建立一个线程并用来传输数据,在下面的Managing a Connection中会讲到。

当你操作完BluetoothSocket时应该调用close()来清理资源,它将会立即关闭所连接的socket并清理内部资源。

管理连接

当你成功连接两台或者多台设备后,每一台设备都会拥有一个连接好的BluetoothSocket对象,通过它你可以在设备间共享数据。传输数据的通用步骤如下:

1.通过getInputStream()getOutputStream()分别取得InputStreamOutputStream对象。

2.用read(byte[])write(byte[])从/向数据流读取/写入数据。

有些实现上的细节需要考虑,最主要是为所有的流读写单独开启线程。read(byte[])方法会一直阻塞直到流中有内容可以读取,write(byte[])方法并不总是阻塞的,但是在远程设备调用read(byte[])方法比较慢并且缓冲区满了的情况下也会造成阻塞。所以,线程中的主循环应该专门用来读取InputStream中的数据,并且在线程中写一个公开的方法用来向OutputStream中写入数据。

下面是代码的写法:

 
private class ConnectedThread extends Thread {
    private final BluetoothSocket mmSocket;
    private final InputStream mmInStream;
    private final OutputStream mmOutStream;
 
    public ConnectedThread(BluetoothSocket socket) {
        mmSocket = socket;
        InputStream tmpIn = null;
        OutputStream tmpOut = null;
 
        // Get the input and output streams, using temp objects because
        // member streams are final
        try {
            tmpIn = socket.getInputStream();
            tmpOut = socket.getOutputStream();
        } catch (IOException e) { }
 
        mmInStream = tmpIn;
        mmOutStream = tmpOut;
    }
 
    public void run() {
        byte[] buffer = new byte[1024];  // buffer store for the stream
        int bytes; // bytes returned from read()
 
        // Keep listening to the InputStream until an exception occurs
        while (true) {
            try {
                // Read from the InputStream
                bytes = mmInStream.read(buffer);
                // Send the obtained bytes to the UI activity
                mHandler.obtainMessage(MESSAGE_READ, bytes, -1, buffer)
                        .sendToTarget();
            } catch (IOException e) {
                break;
            }
        }
    }
 
    /* Call this from the main activity to send data to the remote device */
    public void write(byte[] bytes) {
        try {
            mmOutStream.write(bytes);
        } catch (IOException e) { }
    }
 
    /* Call this from the main activity to shutdown the connection */
    public void cancel() {
        try {
            mmSocket.close();
        } catch (IOException e) { }
    }
}

线程的构造方法会获取IO流,当线程执行时,它会等待从输入流中来的数据。当 read(byte[])读取到字节返回时,数据会由一个Handler发送给Activity处理,然后会进入下一次循环,继续等待输入流的内容。

向其它设备发送数据只需要在Activity中调用线程的write()方法,在方法中传入字节。该方法会调用write(byte[])将数据发送到远程设备。

当不再使用蓝牙连接时,应该调用cancel()方法来关闭luetoothSocket

关于蓝牙API的使用实例,可以参考Bluetooth Chat sample app

原文地址:http://developer.android.com/guide/topics/connectivity/bluetooth.html

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值