参考:
https://developer.android.com/guide/topics/connectivity/bluetooth-le
http://a1anwang.com/post-47.html

一.Android 低功耗蓝牙(BLE)的API简介

从Android 4.3(API 18)才支持低功耗蓝牙(Bluetooth Low Energy, BLE)的核心功能,
BLE蓝牙协议是GATT协议, BLE相关类不多, 全都位于android.bluetooth包和android.bluetooth.le包的几个类:
android.bluetooth..BluetoothGattService  包含多个Characteristic(属性特征值), 含有唯一的UUID作为标识.BluetoothGattCharacteristic  包含单个值和多个Descriptor, 含有唯一的UUID作为标识.BluetoothGattDescriptor  对Characteristic进行描述, 含有唯一的UUID作为标识.BluetoothGatt   客户端相关.BluetoothGattCallback  客户端连接回调     .BluetoothGattServer  服务端相关.BluetoothGattServerCallback 服务端连接回调android.bluetooth.le..AdvertiseCallback  服务端的广播回调.AdvertiseData  服务端的广播数据.AdvertiseSettings 服务端的广播设置.BluetoothLeAdvertiser 服务端的广播.BluetoothLeScanner  客户端扫描相关(Android5.0新增).ScanCallback  客户端扫描回调.ScanFilter 客户端扫描过滤.ScanRecord 客户端扫描结果的广播数据.ScanResult 客户端扫描结果.ScanSettings 客户端扫描设置BLE设备分为两种设备: 客户端(也叫主机/中心设备/Central), 服务端(也叫从机/外围设备/peripheral)
客户端的核心类是 BluetoothGatt
服务端的核心类是 BluetoothGattServer 和 BluetoothLeAdvertiser
BLE数据的核心类是 BluetoothGattCharacteristic 和 BluetoothGattDescriptor

二.低功耗蓝牙(BLE)-手机同时作为BLE客户端和BLE服务端,读写Characteristic数据

完整源码: https://github.com/lioilwin/Bluetooth

1.蓝牙权限

BLE权限增加了BEL支持检查,其它与上篇的经典蓝牙相同,不再写了
(1).在manifest中添加权限
<!-- true 表示手机必须支持BLE,否则无法安装!这里设为false, 运行后在Activity中检查-->
<uses-featureandroid:name="android.hardware.bluetooth_le"android:required="false" />(2).在Activity中设置蓝牙
// 检查是否支持BLE蓝牙
if (!getPackageManager().hasSystemFeature(PackageManager.FEATURE_BLUETOOTH_LE)) {Util.toast(this, "本机不支持低功耗蓝牙!");finish();return;
}

2.BLE客户端(也叫主机/中心设备/Central)

(1).扫描BLE设备(不包含经典蓝牙)

注意: BLE设备地址是动态变化(每隔一段时间都会变化),而经典蓝牙设备是出厂就固定不变了!
BluetoothAdapter bluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
// 下面使用Android5.0新增的扫描API,扫描返回的结果更友好,比如BLE广播数据以前是byte[] scanRecord,而新API帮我们解析成ScanRecord类\
// 旧API是BluetoothAdapter.startLeScan(...)
final BluetoothLeScanner bluetoothLeScanner = bluetoothAdapter.getBluetoothLeScanner();
bluetoothLeScanner.startScan(mScanCallback);
mHandler.postDelayed(new Runnable() {@Overridepublic void run() {bluetoothLeScanner.stopScan(mScanCallback); //停止扫描isScanning = false;}
}, 3000);// 扫描结果Callback
private final ScanCallback mScanCallback = new ScanCallback() {@Overridepublic void onScanResult(int callbackType, ScanResult result) {、BluetoothDevice dev = result.getDevice() 获取BLE设备信息// result.getScanRecord() 获取BLE广播数据}
};

(2).建立连接

// 获取扫描设备,建立连接
closeConn();
BluetoothDevice dev = result.getDevice()
mBluetoothGatt = dev.connectGatt(BleClientActivity.this, false, mBluetoothGattCallback); // 连接蓝牙设备// BLE中心设备连接外围设备的数量有限(大概2~7个),在建立新连接之前必须释放旧连接资源,否则容易出现连接错误133
private void closeConn() {if (mBluetoothGatt != null) {mBluetoothGatt.disconnect();mBluetoothGatt.close();}
}// 与服务端连接的Callback
public BluetoothGattCallback mBluetoothGattCallback = new BluetoothGattCallback() {@Overridepublic void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) {BluetoothDevice dev = gatt.getDevice();Log.i(TAG, String.format("onConnectionStateChange:%s,%s,%s,%s", dev.getName(), dev.getAddress(), status, newState));if (status == BluetoothGatt.GATT_SUCCESS && newState == BluetoothProfile.STATE_CONNECTED) {isConnected = true;gatt.discoverServices(); //启动服务发现} else {isConnected = false;closeConn();}logTv(String.format(status == 0 ? (newState == 2 ? "与[%s]连接成功" : "与[%s]连接断开") : ("与[%s]连接出错,错误码:" + status), dev));}@Overridepublic void onServicesDiscovered(BluetoothGatt gatt, int status) {Log.i(TAG, String.format("onServicesDiscovered:%s,%s,%s", gatt.getDevice().getName(), gatt.getDevice().getAddress(), status));if (status == BluetoothGatt.GATT_SUCCESS) { //BLE服务发现成功// 遍历获取BLE服务Services/Characteristics/Descriptors的全部UUIDfor (BluetoothGattService service : gatt.getServices()) {StringBuilder allUUIDs = new StringBuilder("UUIDs={\nS=" + service.getUuid().toString());for (BluetoothGattCharacteristic characteristic : service.getCharacteristics()) {allUUIDs.append(",\nC=").append(characteristic.getUuid());for (BluetoothGattDescriptor descriptor : characteristic.getDescriptors())allUUIDs.append(",\nD=").append(descriptor.getUuid());}allUUIDs.append("}");Log.i(TAG, "onServicesDiscovered:" + allUUIDs.toString());logTv("发现服务" + allUUIDs);}}}@Overridepublic void onCharacteristicRead(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) {UUID uuid = characteristic.getUuid();String valueStr = new String(characteristic.getValue());Log.i(TAG, String.format("onCharacteristicRead:%s,%s,%s,%s,%s", gatt.getDevice().getName(), gatt.getDevice().getAddress(), uuid, valueStr, status));logTv("读取Characteristic[" + uuid + "]:\n" + valueStr);}@Overridepublic void onCharacteristicWrite(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) {UUID uuid = characteristic.getUuid();String valueStr = new String(characteristic.getValue());Log.i(TAG, String.format("onCharacteristicWrite:%s,%s,%s,%s,%s", gatt.getDevice().getName(), gatt.getDevice().getAddress(), uuid, valueStr, status));logTv("写入Characteristic[" + uuid + "]:\n" + valueStr);}@Overridepublic void onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) {UUID uuid = characteristic.getUuid();String valueStr = new String(characteristic.getValue());Log.i(TAG, String.format("onCharacteristicChanged:%s,%s,%s,%s", gatt.getDevice().getName(), gatt.getDevice().getAddress(), uuid, valueStr));logTv("通知Characteristic[" + uuid + "]:\n" + valueStr);}@Overridepublic void onDescriptorRead(BluetoothGatt gatt, BluetoothGattDescriptor descriptor, int status) {UUID uuid = descriptor.getUuid();String valueStr = Arrays.toString(descriptor.getValue());Log.i(TAG, String.format("onDescriptorRead:%s,%s,%s,%s,%s", gatt.getDevice().getName(), gatt.getDevice().getAddress(), uuid, valueStr, status));logTv("读取Descriptor[" + uuid + "]:\n" + valueStr);}@Overridepublic void onDescriptorWrite(BluetoothGatt gatt, BluetoothGattDescriptor descriptor, int status) {UUID uuid = descriptor.getUuid();String valueStr = Arrays.toString(descriptor.getValue());Log.i(TAG, String.format("onDescriptorWrite:%s,%s,%s,%s,%s", gatt.getDevice().getName(), gatt.getDevice().getAddress(), uuid, valueStr, status));logTv("写入Descriptor[" + uuid + "]:\n" + valueStr);}
};

(3).传输数据(读写Characteristic和Descriptor)

注意:1.每次读写数据最多20个字节,如果超过,只能分包2.连续频繁读写数据容易失败,读写操作间隔最好200ms以上,或等待上次回调完成后再进行下次读写操作!
// 读取数据成功会回调->onCharacteristicChanged()
public void read(View view) {BluetoothGattService service = getGattService(BleServerActivity.UUID_SERVICE);if (service != null) {BluetoothGattCharacteristic characteristic = service.getCharacteristic(BleServerActivity.UUID_CHAR_READ_NOTIFY);//通过UUID获取可读的CharacteristicmBluetoothGatt.readCharacteristic(characteristic);}
}// 写入数据成功会回调->onCharacteristicWrite()
public void write(View view) {BluetoothGattService service = getGattService(BleServerActivity.UUID_SERVICE);if (service != null) {String text = mWriteET.getText().toString();BluetoothGattCharacteristic characteristic = service.getCharacteristic(BleServerActivity.UUID_CHAR_WRITE);//通过UUID获取可写的Characteristiccharacteristic.setValue(text.getBytes()); //单次最多20个字节mBluetoothGatt.writeCharacteristic(characteristic);}
}// 获取Gatt服务
private BluetoothGattService getGattService(UUID uuid) {BluetoothGattService service = mBluetoothGatt.getService(uuid);if (service == null)Util.toast(this, "没有找到服务UUID=" + uuid);return service;
}

(4).设置通知,实时监听Characteristic变化

// Characteristic变化会回调->onCharacteristicChanged()
BluetoothGattService service = getGattService(BleServerActivity.UUID_SERVICE);
if (service != null) {// 设置Characteristic通知BluetoothGattCharacteristic characteristic = service.getCharacteristic(BleServerActivity.UUID_CHAR_READ_NOTIFY);//通过UUID获取可通知的CharacteristicmBluetoothGatt.setCharacteristicNotification(characteristic, true);// 向Characteristic的Descriptor属性写入通知开关,使蓝牙设备主动向手机发送数据BluetoothGattDescriptor descriptor = characteristic.getDescriptor(BleServerActivity.UUID_DESC_NOTITY);// descriptor.setValue(BluetoothGattDescriptor.ENABLE_INDICATION_VALUE);//和通知类似,但服务端不主动发数据,只指示客户端读取数据descriptor.setValue(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE);mBluetoothGatt.writeDescriptor(descriptor);
}

3.BLE服务端(也叫从机/外围设备/peripheral)

public static final UUID UUID_SERVICE = UUID.fromString("10000000-0000-0000-0000-000000000000"); //自定义UUID
public static final UUID UUID_CHAR_READ_NOTIFY = UUID.fromString("11000000-0000-0000-0000-000000000000");
public static final UUID UUID_DESC_NOTITY = UUID.fromString("11100000-0000-0000-0000-000000000000");
public static final UUID UUID_CHAR_WRITE = UUID.fromString("12000000-0000-0000-0000-000000000000");
private BluetoothLeAdvertiser mBluetoothLeAdvertiser; // BLE广播
private BluetoothGattServer mBluetoothGattServer; // BLE服务端@Override
protected void onCreate(Bundle savedInstanceState) {......BluetoothManager bluetoothManager = (BluetoothManager) getSystemService(Context.BLUETOOTH_SERVICE);// BluetoothAdapter bluetoothAdapter = bluetoothManager.getAdapter();BluetoothAdapter bluetoothAdapter = BluetoothAdapter.getDefaultAdapter();// ============启动BLE蓝牙广播(广告) =================================================================================//广播设置(必须)AdvertiseSettings settings = new AdvertiseSettings.Builder().setAdvertiseMode(AdvertiseSettings.ADVERTISE_MODE_LOW_LATENCY) //广播模式: 低功耗,平衡,低延迟.setTxPowerLevel(AdvertiseSettings.ADVERTISE_TX_POWER_HIGH) //发射功率级别: 极低,低,中,高.setConnectable(true) //能否连接,广播分为可连接广播和不可连接广播.build();//广播数据(必须,广播启动就会发送)AdvertiseData advertiseData = new AdvertiseData.Builder().setIncludeDeviceName(true) //包含蓝牙名称.setIncludeTxPowerLevel(true) //包含发射功率级别.addManufacturerData(1, new byte[]{23, 33}) //设备厂商数据,自定义.build();//扫描响应数据(可选,当客户端扫描时才发送)AdvertiseData scanResponse = new AdvertiseData.Builder().addManufacturerData(2, new byte[]{66, 66}) //设备厂商数据,自定义.addServiceUuid(new ParcelUuid(UUID_SERVICE)) //服务UUID//      .addServiceData(new ParcelUuid(UUID_SERVICE), new byte[]{2}) //服务数据,自定义.build();mBluetoothLeAdvertiser = bluetoothAdapter.getBluetoothLeAdvertiser();mBluetoothLeAdvertiser.startAdvertising(settings, advertiseData, scanResponse, mAdvertiseCallback);// 注意:必须要开启可连接的BLE广播,其它设备才能发现并连接BLE服务端!// =============启动BLE蓝牙服务端=====================================================================================BluetoothGattService service = new BluetoothGattService(UUID_SERVICE, BluetoothGattService.SERVICE_TYPE_PRIMARY);//添加可读+通知characteristicBluetoothGattCharacteristic characteristicRead = new BluetoothGattCharacteristic(UUID_CHAR_READ_NOTIFY,BluetoothGattCharacteristic.PROPERTY_READ | BluetoothGattCharacteristic.PROPERTY_NOTIFY, BluetoothGattCharacteristic.PERMISSION_READ);characteristicRead.addDescriptor(new BluetoothGattDescriptor(UUID_DESC_NOTITY, BluetoothGattCharacteristic.PERMISSION_WRITE));service.addCharacteristic(characteristicRead);//添加可写characteristicBluetoothGattCharacteristic characteristicWrite = new BluetoothGattCharacteristic(UUID_CHAR_WRITE,BluetoothGattCharacteristic.PROPERTY_WRITE, BluetoothGattCharacteristic.PERMISSION_WRITE);service.addCharacteristic(characteristicWrite);if (bluetoothManager != null)mBluetoothGattServer = bluetoothManager.openGattServer(this, mBluetoothGattServerCallback);mBluetoothGattServer.addService(service);
}// BLE广播Callback
private AdvertiseCallback mAdvertiseCallback = new AdvertiseCallback() {@Overridepublic void onStartSuccess(AdvertiseSettings settingsInEffect) {logTv("BLE广播开启成功");}@Overridepublic void onStartFailure(int errorCode) {logTv("BLE广播开启失败,错误码:" + errorCode);}
};// BLE服务端Callback
private BluetoothGattServerCallback mBluetoothGattServerCallback = new BluetoothGattServerCallback() {@Overridepublic void onConnectionStateChange(BluetoothDevice device, int status, int newState) {Log.i(TAG, String.format("onConnectionStateChange:%s,%s,%s,%s", device.getName(), device.getAddress(), status, newState));logTv(String.format(status == 0 ? (newState == 2 ? "与[%s]连接成功" : "与[%s]连接断开") : ("与[%s]连接出错,错误码:" + status), device));}@Overridepublic void onServiceAdded(int status, BluetoothGattService service) {Log.i(TAG, String.format("onServiceAdded:%s,%s", status, service.getUuid()));logTv(String.format(status == 0 ? "添加服务[%s]成功" : "添加服务[%s]失败,错误码:" + status, service.getUuid()));}@Overridepublic void onCharacteristicReadRequest(BluetoothDevice device, int requestId, int offset, BluetoothGattCharacteristic characteristic) {Log.i(TAG, String.format("onCharacteristicReadRequest:%s,%s,%s,%s,%s", device.getName(), device.getAddress(), requestId, offset, characteristic.getUuid()));String response = "CHAR_" + (int) (Math.random() * 100); //模拟数据mBluetoothGattServer.sendResponse(device, requestId, BluetoothGatt.GATT_SUCCESS, offset, response.getBytes());// 响应客户端logTv("客户端读取Characteristic[" + characteristic.getUuid() + "]:\n" + response);}@Overridepublic void onCharacteristicWriteRequest(BluetoothDevice device, int requestId, BluetoothGattCharacteristic characteristic, boolean preparedWrite, boolean responseNeeded, int offset, byte[] requestBytes) {// 获取客户端发过来的数据String requestStr = new String(requestBytes);Log.i(TAG, String.format("onCharacteristicWriteRequest:%s,%s,%s,%s,%s,%s,%s,%s", device.getName(), device.getAddress(), requestId, characteristic.getUuid(),preparedWrite, responseNeeded, offset, requestStr));mBluetoothGattServer.sendResponse(device, requestId, BluetoothGatt.GATT_SUCCESS, offset, requestBytes);// 响应客户端logTv("客户端写入Characteristic[" + characteristic.getUuid() + "]:\n" + requestStr);}@Overridepublic void onDescriptorReadRequest(BluetoothDevice device, int requestId, int offset, BluetoothGattDescriptor descriptor) {Log.i(TAG, String.format("onDescriptorReadRequest:%s,%s,%s,%s,%s", device.getName(), device.getAddress(), requestId, offset, descriptor.getUuid()));String response = "DESC_" + (int) (Math.random() * 100); //模拟数据mBluetoothGattServer.sendResponse(device, requestId, BluetoothGatt.GATT_SUCCESS, offset, response.getBytes()); // 响应客户端logTv("客户端读取Descriptor[" + descriptor.getUuid() + "]:\n" + response);}@Overridepublic void onDescriptorWriteRequest(final BluetoothDevice device, int requestId, BluetoothGattDescriptor descriptor, boolean preparedWrite, boolean responseNeeded, int offset, byte[] value) {// 获取客户端发过来的数据String valueStr = Arrays.toString(value);Log.i(TAG, String.format("onDescriptorWriteRequest:%s,%s,%s,%s,%s,%s,%s,%s", device.getName(), device.getAddress(), requestId, descriptor.getUuid(),preparedWrite, responseNeeded, offset, valueStr));mBluetoothGattServer.sendResponse(device, requestId, BluetoothGatt.GATT_SUCCESS, offset, value);// 响应客户端logTv("客户端写入Descriptor[" + descriptor.getUuid() + "]:\n" + valueStr);// 简单模拟通知客户端Characteristic变化if (Arrays.toString(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE).equals(valueStr)) { //是否开启通知final BluetoothGattCharacteristic characteristic = descriptor.getCharacteristic();new Thread(new Runnable() {@Overridepublic void run() {for (int i = 0; i < 5; i++) {SystemClock.sleep(3000);String response = "CHAR_" + (int) (Math.random() * 100); //模拟数据characteristic.setValue(response);mBluetoothGattServer.notifyCharacteristicChanged(device, characteristic, false);logTv("通知客户端改变Characteristic[" + characteristic.getUuid() + "]:\n" + response);}}}).start();}}@Overridepublic void onExecuteWrite(BluetoothDevice device, int requestId, boolean execute) {Log.i(TAG, String.format("onExecuteWrite:%s,%s,%s,%s", device.getName(), device.getAddress(), requestId, execute));}@Overridepublic void onNotificationSent(BluetoothDevice device, int status) {Log.i(TAG, String.format("onNotificationSent:%s,%s,%s", device.getName(), device.getAddress(), status));}@Overridepublic void onMtuChanged(BluetoothDevice device, int mtu) {Log.i(TAG, String.format("onMtuChanged:%s,%s,%s", device.getName(), device.getAddress(), mtu));}
};

};

简书: https://www.jianshu.com/p/8ac31a5070d4
CSDN: https://blog.csdn.net/qq_32115439/article/details/80643906
GitHub博客: http://lioil.win/2018/06/10/Android-BLE.html
Coding博客: http://c.lioil.win/2018/06/10/Android-BLE.html

Android-低功耗蓝牙(BLE)-客户端(主机/中心设备)和服务端(从机/外围设备)相关推荐

  1. Android低功耗蓝牙(BLE)开发(二)

    在上一篇文章Android低功耗蓝牙(BLE)开发(一)中我们了解了BLE的相关概念,这里我们来实际用代码演示安卓进行BLE连接和通讯的功能.本文代码基于Android5.0以上(API 21) 1. ...

  2. 使用BleLib的轻松搞定Android低功耗蓝牙Ble 4.0开发详解

    转载请注明来源: http://blog.csdn.net/kjunchen/article/details/50909410 使用BleLib的轻松搞定Android低功耗蓝牙Ble 4.0开发详解 ...

  3. Android低功耗蓝牙BLE

    低功耗蓝牙BLE与传统的蓝牙相比最大的优势是功耗降低90%,同时传输距离增大(超过100米).安全和稳定性提高(支持AES加密和CRC验证),允许Android应用程序与具有更严格电源要求的BLE设备 ...

  4. Android 低功耗蓝牙BLE连接通信

    目录 简介 蓝牙 4.0 BLE与蓝牙4.0的区别 BLE的特点 主要特性 技术细节 BLE的应用 BLE的体系结构 BLE设备链路层状态 就绪态 广播态 扫描态 发起态 连接状态 通信基本过程 两种 ...

  5. PyQt5之QtBluetooth模块:低功耗蓝牙BLE通信

    PyQt5之QtBluetooth模块:低功耗蓝牙BLE通信 最近使用PyQt5开发PC端工具,正巧手上有一个富芮坤的低功耗蓝牙,于是想在PC端试试与之通信,不过发现使用PyQt5开发低功耗蓝牙的教程 ...

  6. Android低功耗蓝牙(BLE)使用详解

    代码地址如下: http://www.demodashi.com/demo/13390.html 与普通蓝牙相比,低功耗蓝牙显著降低了能量消耗,允许Android应用程序与具有更严格电源要求的BLE设 ...

  7. android 连接蓝牙电子秤_电子秤蓝牙双模通讯Android低功耗蓝牙(蓝牙4.0)BLE开发(上)...

    电子秤蓝牙双模通讯Android低功耗蓝牙(蓝牙4.0)BLE开发(上) 前段时间,公司项目用到了手机APP和蓝牙设备的通讯开发,这里也正好对低功耗蓝牙(蓝牙4.0及以后标准)的开发,做一个总结. 蓝 ...

  8. 泰凌微ble mesh蓝牙模组天猫精灵学习之旅④如何在Android开发低功耗蓝牙ble控制 TB-02 模块,代码工程全部开源!(附带Demo)

    本<泰凌微ble mesh蓝牙模组天猫精灵学习之旅>系列博客学习由半颗心脏 潜心所力所写,仅仅做个人技术交流分享,不做任何商业用途.如有不对之处,请留言,本人及时更改. 1.小白也痴迷,如 ...

  9. c# 低功耗蓝牙_Android ble低功耗蓝牙开发-客户端

    什么是BLE(低功耗蓝牙) BLE(Bluetooth Low Energy,低功耗蓝牙)是对传统蓝牙BR/EDR技术的补充. 尽管BLE和传统蓝牙都称之为蓝牙标准,且共享射频,但是,BLE是一个完全 ...

最新文章

  1. 计算类class的sizeof大小
  2. C#前期绑定和后期绑定操作Excel-------实现简单打印功能
  3. excel winform 导入 导出_强大的 Excel 导入导出工具 hutool
  4. C++实现另一个猜数字游戏
  5. 判别学习与生成学习的区别
  6. t3软件怎么生成报表_临沂用友畅捷通T3财务通软件财税一体化
  7. 报告称相比南方 数字化平台对北方小微商家助力作用更大
  8. 随想录(平台软件和项目代码)
  9. 2008r2 php mysql_Win2008R2IIS7.5+PHP5(FastCGI)+MySQL5环境搭建教程_MySQL
  10. mysql嵌套select limit_MySQL嵌套查询“LIMIT IN/ALL/ANY/SOME”错误解决方法
  11. Java开发二维码扫一扫名片技术
  12. Ubuntu使用ZTE MF832S上网卡拨号上网
  13. OpenGL (太阳,地球,月亮 +太阳系八大行星)
  14. c语言大于一小于10,C语言首先输入一个大于2且小于10的整数
  15. 简易版 水下超声波通信
  16. 淘宝越来越重视的内容营销要怎么去做?
  17. Arduino压电震动传感器(二)
  18. 云栖科技评论第77期:Z世代“掌钱”
  19. Lintcode 428. Pow(x, n) (Medium) (Python)
  20. VQA(图像问答)数据集结构及大致内容

热门文章

  1. 关于知识图谱上下级概念建设的一点想法
  2. koa超卖解决之分布式锁
  3. GUI编程基础学习(三)——单选按钮、复选框、组合框
  4. macbook历代_逼格进化史,看看苹果MacBook发展历程
  5. css设置单位(字体大小,长度,宽度)
  6. AI极链云新手教程(最快上手极链云服务器)
  7. Flutter 弹出键盘认识
  8. pdfFactory PRO 8.33.0 / FinePrint 11.33.0软件下载附注册码
  9. C#并口热敏小票打印机打印位图
  10. prefetch已经不再支持Aspera了