官方文檔:https://developer.android.com/guide/topics/connectivity/bluetooth
Android 中將藍牙分為傳統(tǒng)藍牙和低功耗藍牙(Bluetooth low energy)兩種。后者的優(yōu)勢在于快速搜索,快速連接,超低功耗保持連接和數(shù)據(jù)傳輸,同時低功耗帶來的缺點是數(shù)據(jù)傳輸速率低,所以多用在可穿戴式設(shè)備。
在這里我們主要介紹使用傳統(tǒng)藍牙來實現(xiàn)一個聊天的數(shù)據(jù)傳輸 demo。以下內(nèi)容基本都是基于官方文檔的二次闡述,以及一些疑惑的查找到的解答,最后在 demo 里面有對藍牙的相關(guān)操作進行了封裝。先貼個圖看看效果吧:

基礎(chǔ)知識
BluetoothAdapter: 本地藍牙適配器,我們在發(fā)現(xiàn)設(shè)備,配對的時候都得用上它。
BluetoothDevice: 遠程藍牙設(shè)備,就是代表著你可以連接的一個設(shè)備,里面存儲名字,MAC地址等信息。
BluetoothSocket 和 BluetoothServerSocket: 藍牙套接字,和 TCP 的 Socket 相似。一臺設(shè)備開啟一個 ServerSocket 并監(jiān)聽,另一臺設(shè)備開啟 Socket 進行連接,以此實現(xiàn)一個端對端的連接和數(shù)據(jù)傳輸。
UUID: 唯一識別符。它被用于唯一標(biāo)識應(yīng)用的藍牙服務(wù)(不是表示藍牙設(shè)備)。
Q1:為什么網(wǎng)上的大多數(shù)例子都是使用
00001101-0000-1000-8000-00805F9B34FB這個UUID?
A1:這是因為一個藍牙設(shè)備里面可以提供諸多服務(wù),如A2DP(藍牙音頻傳輸)、HEADFREE(免提)、SPP(串口通信) 等等。而上面的字符串碼就是 SPP 的 UUID,基本藍牙板上默認就是這個值,我們可以通過UUID.fromString("00001101-0000-1000-8000-00805F9B34FB")來將字符串轉(zhuǎn)成 UUID。
在連接藍牙串口板我們往往就會使用上面的UUID,但是如果 Android 端對端的話,建議自己自己設(shè)定 UUID,這樣別人的 UUID 就連不上了。
實現(xiàn)一個藍牙聊天demo
要實現(xiàn)一個藍牙聊天demo,首先我們有兩臺有藍牙功能的設(shè)備,這里我用了兩臺手機。按照流程一般來說要開啟藍牙-搜索設(shè)備-配對設(shè)備-連接-通信。如此就能實現(xiàn)一個基本的藍牙通信。
第一步:權(quán)限
在 Android 中沒有權(quán)限寸步難行。要使用藍牙,還需要聲明相應(yīng)的權(quán)限。
<manifest ... >
<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
...
</manifest>
BLUETOOTH 是基本的權(quán)限,用于你的藍牙連接,數(shù)據(jù)傳輸?shù)取?/p>
BLUETOOTH_ADMIN 一般應(yīng)只用于發(fā)現(xiàn)本地藍牙設(shè)備。
除非該應(yīng)用是將要應(yīng)用戶請求修改藍牙設(shè)置的“超級管理員”,否則不應(yīng)使用此權(quán)限所授予的其他能力。
另:如果要使用
BLUETOOTH_ADMIN權(quán)限,則還必須擁有BLUETOOTH權(quán)限。
此外會發(fā)現(xiàn)我這里比官方文檔還多了個 ACCESS_COARSE_LOCATION,這是因為我在實測過程中,我的測試機Android 8.0 系統(tǒng)中,藍牙掃描沒有掃描出信息,但是系統(tǒng)是有的。在網(wǎng)上一番尋找之后發(fā)現(xiàn)在 Android 6.0 之后還需要一個模糊定位的權(quán)限,否則掃描功能無效。
google 文檔:為給用戶提供更嚴(yán)格的數(shù)據(jù)保護,從此版本(6.0)開始,對于使用 WLAN API 和 Bluetooth API 的應(yīng)用,Android 移除了對設(shè)備本地硬件標(biāo)識符的編程訪問權(quán)。
WifiInfo.getMacAddress()方法和BluetoothAdapter.getAddress()方法現(xiàn)在會返回常量值02:00:00:00:00:00。
現(xiàn)在,要通過藍牙和 WLAN 掃描訪問附近外部設(shè)備的硬件標(biāo)識符,您的應(yīng)用必須擁有ACCESS_FINE_LOCATION或ACCESS_COARSE_LOCATION權(quán)限。
關(guān)于動態(tài)權(quán)限申請在此不作累述,小伙伴們可以自己去實現(xiàn)。
第二步:啟動藍牙
1、獲取 BluetoothAdapter
BluetoothAdapter mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
if (mBluetoothAdapter == null) {
//TODO 設(shè)備不支持藍牙,阻斷用戶操作
}
2、啟動藍牙
if(!mBlueAdapter.isEnabled()){
//請求藍牙
Intent enableBtIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
startActivityForResult(enableBtIntent, REQUEST_ENABLE_BT);
}
系統(tǒng)將會彈窗提示用戶是否開啟藍牙,用戶的選擇將在 onActivityResult() 中得到反饋。同意的時候收到 RESULT_OK,拒絕的時候收到 RESULT_CANCELED。
第三步:查找設(shè)備
1、查找已配對設(shè)備
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());
}
}
2、查找未知設(shè)備
mBlueAdapter.startDiscovery()
查找未知設(shè)備只需要調(diào)用 startDiscovery() 即可,這是一個異步操作,系統(tǒng)一般會在后臺進程進行一個 12 秒的查詢掃描。查找出來的信息我們需要在廣播中進行監(jiān)聽才可得知。
private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
if (BluetoothDevice.ACTION_FOUND.equals(action)) {
BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
mUnpaireList.add(device);
mUnpaireAdapter.notifyDataSetChanged();
}
}
};
//注冊廣播
IntentFilter filter = new IntentFilter(BluetoothDevice.ACTION_FOUND);
registerReceiver(mBtReceiver, filter);
//同時別忘了銷毀時注銷廣播
第四步:配對連接
在這里我們往往需要一臺設(shè)備做服務(wù)器端一臺做客戶端,實際上就是 app 開啟了一個服務(wù)器線程讓藍牙的 socket 可以連接。連接完成后再使用 I/O Stream 進行數(shù)據(jù)交互。
1、服務(wù)器線程
我們需要用 listenUsingInsecureRfcommWithServiceRecord(String,UUID) 獲取 BluetoothServerSocket。
Q2:
listenUsingRfcommWithServiceRecord()和listenUsingInsecureRfcommWithServiceRecord()有什么區(qū)別?
A2:從名字來看似乎是安全不安全的區(qū)別,但是實際上我并沒有找到相關(guān)資料佐證。也有文章描述客戶端的 socket 創(chuàng)建createRfcommSocketToServiceRecord是安卓2.3系統(tǒng)及以下用的,新的安卓要用createInsecureRfcommSocketToServiceRecord,所以對應(yīng)著服務(wù)器端也用Insercure吧。
服務(wù)器監(jiān)聽中,由于 accept() 方法是阻塞的,所以需要子線程中處理。
private class AcceptThread extends Thread {
private final BluetoothServerSocket mmServerSocket;
public AcceptThread() {
BluetoothServerSocket tmp = null;
try {
tmp = mBluetoothAdapter.listenUsingInsecureRfcommWithServiceRecord(NAME, MY_UUID);
} catch (IOException e) { }
mmServerSocket = tmp;
}
public void run() {
BluetoothSocket socket = mmServerSocket.accept();
mmServerSocket.clost();
mInputStream = socket.getInputStrem();
mOutputStream = socket.getOutputStream();
byte[] buffer = new byte[1024];
int bytes;
while (true) {
try{
//讀取buffer信息打印出來
bytes = mInputStream.read(buffer);
String s = new String(buffer, 0, bytes);
sendHandlerMsg(s);
} carch(IOException e){
break;
}
}
}
}
2、客戶端連接
客戶端連接和服務(wù)端連接相似。當(dāng)然首先你要獲取到要配對的設(shè)備 BluetoothDevice,然后獲取 BluetoothSocket ,使用 mSocket.connect() 連接即可。他們的邏輯基本相同,在官方文檔中也有相關(guān)的描述。
在這里因為實際上我的需求是使用手機連接一個硬件設(shè)備,所以我選擇封裝了一個藍牙工具類,把藍牙開啟連接等客戶端相關(guān)操作封裝到 BluetoothManager 中。其中 ConnectThread 和 ReadThread 抽成兩個Runnable 放在線程池中處理。當(dāng) socket 連接成功后獲取到 IO 流來進行讀寫操作。讀操作因為屬于阻塞操作放在子線程。代碼這里就不貼了,文末有此 demo 的地址。有興趣的也可以自己去實現(xiàn)一下。
第五步:其他
剩下的就是布局和交互邏輯的實現(xiàn),這里就不在一一闡述了。
總結(jié)
藍牙的相關(guān)操作感覺和 Socket 非常地相似,都是進行端對端綁定,然后進行數(shù)據(jù)傳輸。所以同理也應(yīng)該會存在類似 Socket 的各種問題,比如說丟包,斷開連接需要心跳檢測,重連機制等等。這個demo只是對API進行了一定程度的整合,還存有不少的問題。