本文主要讲解经典蓝牙的开发,主要包含以下几个知识点:

  1. 蓝牙 API 简介
  2. 经典蓝牙开发的一般步骤

相信通过以上步骤,您会很快上手一个 Android 经典蓝牙开发的 App。

蓝牙 API 简介

Android 所有关于蓝牙开发的类都在 android.bluetooth 包下,只有 8 个类 :

BluetoothAdapter     本地蓝牙适配器
BluetoothClass      蓝牙类(主要包括服务和设备)
BluetoothClass.Device    蓝牙设备类
BluetoothClass.Device.Major    蓝牙设备管理
BluetoothClass.Service   蓝牙服务类
BluetoothDevice     蓝牙设备(远程蓝牙设备)
BluetoothServiceSocket    监听蓝牙连接的类
BluetoothSocket     蓝牙连接类

BluetoothAdapter

表示本地的蓝牙适配器 (蓝牙射频)。BluetoothAdapter 是为所有蓝牙交互的入口点。它可以发现其他蓝牙设备、 查询绑定 (配对) 设备的列表、 实例化已知的 MAC 地址的 BluetoothDevice(蓝牙设备) 和创建 BluetoothServerSocket 用于侦听来自其他设备的通信。直到我们建立 BluetoothSocket 连接之前,都要不断操作它 。BluetoothAdapter 里的方法很多,常用的有以下几个:

// 根据字面意思,是取消发现,也就是说当我们正在搜索设备的时候调用这个方法将不再继续搜索;
cancelDiscovery();// 关闭蓝牙
disable()
// 打开蓝牙
enable();
// 这个方法打开蓝牙不会弹出提示,更多的时候我们需要问下用户是否打开,
// 以下两行代码同样是打开蓝牙,不过会提示用户:
Intemtenabler = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
// 同 startActivity(enabler);
startActivityForResult(enabler , reCode);
// 获取本地蓝牙地址
getAddress();
// 获取默认 BluetoothAdapter,实际上,也只有这一种方法获取 BluetoothAdapter。
getDefaultAdapter();
// 获取本地蓝牙名称
getName();
// 根据蓝牙地址获取远程蓝牙设备
getRemoteDevice(String address);
// 获取本地蓝牙适配器当前状态(调试的时候更需要)
getState();
// 判断当前是否正在查找设备,如果是则返回 true
isDiscovering();
// 判断蓝牙是否打开,已打开返回 true,否则,返回 false。
isEnabled();
// 根据名称,UUID 创建并返回 BluetoothServerSocket 对象,
// 这是创建 BluetoothSocket 服务器端的第一步。
// 第一个参数表示蓝牙服务的名称,可以是任意字符串,第二个参数是 UUID。
listenUsingRfcommWithServiceRecord(String name , UUID uuid);
// 开始搜索,这是搜索的第一步
startDiscovery();

BluetoothDevice

表示远程蓝牙设备。使用此类并通过 BluetoothSocket 类可以请求连接远程设备,或查询这台设备的信息如其名称、 地址、 类和绑定状态。

createRfcommSocketToServiceRecord(UUID uuid);

根据 UUID 创建并返回一个 BluetoothSocket , 这个方法也是我们获取 BluetoothDevice 的目的——创建 BluetoothSocket。这个类其他的方法,如 getAddress()、getName(),同 BluetoothAdapter。

备注: 蓝牙—RFCOMM 协议

串口仿真协议(RFCOMM),RFCOMM 是一个简单的协议,其中针对 9 针 RS-232 串口仿真附加了部分条款,可支持在两个蓝牙设备之间同时保持高达 60 路的通信连接。RFCOMM 的目的是针对如何在两个不同设备上的应用之间保证一条完整的通信路径。

BluetoothServerSocket

表示打开服务器套接字侦听传入的请求 (类似于 TCP ServerSocket)。为了连接两台 Android 设备,一台设备必须用此类打开一个服务器套接字。当远程蓝牙设备向此设备发出连接请求时,而且当连接被接收时,BluetoothServerSocket 将返回连接的 BluetoothSocket。这个类有三个方法。

// 两者的区别在于后者指定了过时时间,
// 需要注意的是,执行这两个方法的时候,直到接收到了客户端的请求
//(或是过期之后),都会阻塞线程,应该放在新线程里运行!
// 还需要注意,这两个方法都返回一个 BluetoothSocket,
// 最后的连接也是服务器端与客户端这两个 BluetoothSocket 的连接。
accept();
accept(int timeout);
// 关闭
close();

BluetoothSocket

跟 BluetoothServerSocket 相对,是客户端。表示一个蓝牙套接字 (类似于 TCP Socket) 的接口。这是一个允许应用程序与另一台蓝牙设备通过 InputStream和 OutputStream 来交换数据的连接点。其一共5个方法,一般都会用到。

// 关闭
close();
// 连接
connect();
// 获取输入流
getInptuStream();
// 获取输出流
getOutputStream();
// 获取远程设备,这里指的是获取 BluetoothSocket 指定连接的那个远程蓝牙设备
getRemoteDevice();

BluetoothClass

描述的一般特征和蓝牙设备的功能。这是一整套只读的属性,用于定义设备的主要和次要设备类和它的服务。然而,这并不是支持所有蓝牙配置文件和服务的设备,但很适用于获取设备类型。

BluetoothProfile

表示一个蓝牙配置文件。蓝牙配置文件是基于蓝牙通信设备之间的无线接口规范。如免提规范 (Hands-Free profile)

BluetoothHeadset

蓝牙耳机与手机一起使用配置文件 ,这包括蓝牙耳机和免提(v1.5) 的配置文件

BluetoothA2dp

定义了如何高质量的音频可以进行流式处理,从一个设备到另一个通过蓝牙连接。”A2DP” 代表先进音频分配协议

BluetoothHealth

表示控制蓝牙服务健康设备协议。

BluetoothHealthCallback

BluetoothHealthCallback 是一个抽象类,您使用它来实现 BluetoothHealth 回调,你必须扩展此类并实现回调方法以接收有关更改的更新应用程序的注册和蓝牙通道状态。

BluetoothHealthAppConfiguration

表示一个蓝牙健康第三方应用程序注册与远程蓝牙健康设备进行通信的应用程序配置。

BluetoothProfile.ServiceListener

通知 BluetoothProfile IPC 客户端界面时已被连接或断开服务 (即运行一个特定的配置文件内部服务)

UUID(universal unique identifier , 全局唯一标识符)

格式如下:
UUID 格式一般是”xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx”。
UUID 分为 5 段,是一个 8-4-4-4-12 的字符串,这个字符串要求永不重复。

String uuid = java.util.UUID.randomUUID().toString();

一般在创建 Socket 时需要 UUID 作为端口的唯一性,如果两台 Android 设备互联,则没有什么特殊的,如果让非 Android 的蓝牙设备连接 Android 蓝牙设备,则 UUID 必须使用某个固定保留的 UUID。

Android 中创建 UUID 的方式如下:

UUID uuid = UUID.fromString("00001101-0000-1000-8000-00805F9B34FB");

常用固定的 UUID

蓝牙串口服务 ( SPP ):SerialPortServiceClass_UUID = '{00001101-0000-1000-8000-00805F9B34FB}'LANAccessUsingPPPServiceClass_UUID = '{00001102-0000-1000-8000-00805F9B34FB}'拨号网络服务:DialupNetworkingServiceClass_UUID = '{00001103-0000-1000-8000-00805F9B34FB}'信息同步服务:IrMCSyncServiceClass_UUID = '{00001104-0000-1000-8000-00805F9B34FB}'SDP_OBEXObjectPushServiceClass_UUID = '{00001105-0000-1000-8000-00805F9B34FB}'文件传输服务:OBEXFileTransferServiceClass_UUID = '{00001106-0000-1000-8000-00805F9B34FB}'IrMCSyncCommandServiceClass_UUID = '{00001107-0000-1000-8000-00805F9B34FB}'SDP_HeadsetServiceClass_UUID = '{00001108-0000-1000-8000-00805F9B34FB}'CordlessTelephonyServiceClass_UUID = '{00001109-0000-1000-8000-00805F9B34FB}'SDP_AudioSourceServiceClass_UUID = '{0000110A-0000-1000-8000-00805F9B34FB}'SDP_AudioSinkServiceClass_UUID = '{0000110B-0000-1000-8000-00805F9B34FB}'SDP_AVRemoteControlTargetServiceClass_UUID = '{0000110C-0000-1000-8000-00805F9B34FB}'SDP_AdvancedAudioDistributionServiceClass_UUID = '{0000110D-0000-1000-8000-00805F9B34FB}'SDP_AVRemoteControlServiceClass_UUID = '{0000110E-0000-1000-8000-00805F9B34FB}'VideoConferencingServiceClass_UUID = '{0000110F-0000-1000-8000-00805F9B34FB}'IntercomServiceClass_UUID = '{00001110-0000-1000-8000-00805F9B34FB}'蓝牙传真服务:FaxServiceClass_UUID = '{00001111-0000-1000-8000-00805F9B34FB}'HeadsetAudioGatewayServiceClass_UUID = '{00001112-0000-1000-8000-00805F9B34FB}'WAPServiceClass_UUID = '{00001113-0000-1000-8000-00805F9B34FB}'WAPClientServiceClass_UUID = '{00001114-0000-1000-8000-00805F9B34FB}'蓝牙打印服务:HCRPrintServiceClass_UUID = '{00001126-0000-1000-8000-00805F9B34FB}'HCRScanServiceClass_UUID = '{00001127-0000-1000-8000-00805F9B34FB}'CommonISDNAccessServiceClass_UUID = '{00001128-0000-1000-8000-00805F9B34FB}'VideoConferencingGWServiceClass_UUID = '{00001129-0000-1000-8000-00805F9B34FB}'UDIMTServiceClass_UUID = '{0000112A-0000-1000-8000-00805F9B34FB}'UDITAServiceClass_UUID = '{0000112B-0000-1000-8000-00805F9B34FB}'AudioVideoServiceClass_UUID = '{0000112C-0000-1000-8000-00805F9B34FB}'SIMAccessServiceClass_UUID = '{0000112D-0000-1000-8000-00805F9B34FB}'PnPInformationServiceClass_UUID = '{00001200-0000-1000-8000-00805F9B34FB}'GenericNetworkingServiceClass_UUID = '{00001201-0000-1000-8000-00805F9B34FB}'GenericFileTransferServiceClass_UUID = '{00001202-0000-1000-8000-00805F9B34FB}'GenericAudioServiceClass_UUID = '{00001203-0000-1000-8000-00805F9B34FB}'GenericTelephonyServiceClass_UUID = '{00001204-0000-1000-8000-00805F9B34FB}'个人局域网服务:PANUServiceClass_UUID = '{00001115-0000-1000-8000-00805F9B34FB}'NAPServiceClass_UUID = '{00001116-0000-1000-8000-00805F9B34FB}'GNServiceClass_UUID = '{00001117-0000-1000-8000-00805F9B34FB}'DirectPrintingServiceClass_UUID = '{00001118-0000-1000-8000-00805F9B34FB}'ReferencePrintingServiceClass_UUID = '{00001119-0000-1000-8000-00805F9B34FB}'ImagingServiceClass_UUID = '{0000111A-0000-1000-8000-00805F9B34FB}'ImagingResponderServiceClass_UUID = '{0000111B-0000-1000-8000-00805F9B34FB}'ImagingAutomaticArchiveServiceClass_UUID = '{0000111C-0000-1000-8000-00805F9B34FB}'ImagingReferenceObjectsServiceClass_UUID = '{0000111D-0000-1000-8000-00805F9B34FB}'SDP_HandsfreeServiceClass_UUID = '{0000111E-0000-1000-8000-00805F9B34FB}'HandsfreeAudioGatewayServiceClass_UUID = '{0000111F-0000-1000-8000-00805F9B34FB}'DirectPrintingReferenceObjectsServiceClass_UUID = '{00001120-0000-1000-8000-00805F9B34FB}'ReflectedUIServiceClass_UUID = '{00001121-0000-1000-8000-00805F9B34FB}'BasicPringingServiceClass_UUID = '{00001122-0000-1000-8000-00805F9B34FB}'PrintingStatusServiceClass_UUID = '{00001123-0000-1000-8000-00805F9B34FB}'人机输入服务:HumanInterfaceDeviceServiceClass_UUID = '{00001124-0000-1000-8000-00805F9B34FB}'HardcopyCableReplacementServiceClass_UUID = '{00001125-0000-1000-8000-00805F9B34FB}'

经典蓝牙使用步骤详解

配置权限

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

Android 6.0 之后,如果需要利用本机查找周围的 WiFi 和蓝牙设备,需要在配置文件中申请两个权限:

<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>

并在 java 代码中动态申请 runtime 权限:

        if (Build.VERSION.SDK_INT >= 6.0) {ActivityCompat.requestPermissions(this,new String[]{Manifest.permission.ACCESS_FINE_LOCATION},MY_PERMISSION_REQUEST_CONSTANT);}
public void onRequestPermissionsResult(int requestCode, String permissions[],
int[] grantResults) {switch (requestCode) {case MY_PERMISSION_REQUEST_CONSTANT: {// If request is cancelled, the result arrays are empty.if (grantResults.length > 0 && grantResults[0]== PackageManager.PERMISSION_GRANTED) {//permission granted!}return;}}}

获取本地蓝牙适配器

BluetoothAdapter mAdapter= BluetoothAdapter.getDefaultAdapter();

打开蓝牙

// 弹出对话框提示用户是否打开,REQUEST_ENABLE 为自定义请求码
Intent enabler = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
startActivityForResult(enabler, REQUEST_ENABLE);// 不做提示,强行打开
// mAdapter.enable();// 补充一下,使设备能够被搜索,REQUEST_DISCOVERABLE 为自定义请求码
Intent enabler = new Intent(BluetoothAdapter.ACTION_REQUEST_DISCOVERABLE);
startActivityForResult(enabler,REQUEST_DISCOVERABLE);

搜索设备

// 1) mAdapter.startDiscovery(); 是第一步
// 判断是否在搜索,如果在搜索,就取消搜索if (mBluetoothAdapter.isDiscovering()) {mBluetoothAdapter.cancelDiscovery();}// 开始搜索mBluetoothAdapter.startDiscovery();
// 可是你会发现没有返回的蓝牙设备,怎么知道查找到了呢?
// 2) 所以有第二步定义 BroadcastReceiver 接收搜索结果,代码如下
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);if (device.getBondState() != BluetoothDevice.BOND_BONDED) {Log.v(TAG, "find device:" + device.getName()+ device.getAddress());}}//搜索完成else if (BluetoothAdapter.ACTION_DISCOVERY_FINISHED.equals(action)){Log.v(TAG,"find over");}//执行更新列表的代码// updateList();}
};

这样,没查找到新设备或是搜索完成,相应的操作都在上段代码的两个 if 语句里执行了,不过前提是你要先注册
BroadcastReceiver,具体代码如下,该段代码,一般写在 onCreate() 里。

 // Register the BroadcastReceiverIntentFilter filter = new IntentFilter();filter.addAction(BluetoothDevice.ACTION_FOUND);// 搜索发现设备filter.addAction(BluetoothAdapter.ACTION_DISCOVERY_FINISHED);// 结束搜索设备filter.addAction(BluetoothDevice.ACTION_BOND_STATE_CHANGED);// 状态改变filter.addAction(BluetoothAdapter.ACTION_SCAN_MODE_CHANGED);// 行动扫描模式改变了filter.addAction(BluetoothAdapter.ACTION_STATE_CHANGED);// 动作状态发生了变化registerReceiver(mReceiver, filter);

当然,记得反注册哦

 // Don't forget to unregister during onDestroyunregisterReceiver(mReceiver);

建立连接

首先 Android SDK(2.0 以上版本)支持的蓝牙连接是通过 BluetoothSocket 建立连接。
服务器端(BluetoothServerSocket)和客户端(BluetoothSocket)需指定同样的 UUID,才能建立连接,因为建立连接的方法会阻塞线程,所以服务器端和客户端都应启动新线程连接。

// 1)服务器端:
BluetoothServerSocket serverSocket =
mAdapter.listenUsingRfcommWithServiceRecord(serverSocketName,UUID);
serverSocket.accept();// 2)客户端:
// device 为刚才在 BroadcastReceiver 中获取到的 BLuetoothDevice 对象
// 方式一:使用 UUID 形式直接连接
final String SPP_UUID = "00001101-0000-1000-8000-00805F9B34FB";
UUID uuid = UUID.fromString(SPP_UUID);
BluetoothSocket socket;
socket = device.createInsecureRfcommSocketToServiceRecord(uuid);
adapter.cancelDiscovery();
socket.connect();// 方式二:使用反射机制BluetoothSocket temp = null;try {// 利用反射机制Method m = mBluetoothDevice.getClass().getMethod("createRfcommSocket", int.class);// 这里端口默认为 1temp = (BluetoothSocket) m.invoke(mBluetoothDevice, 1);} catch (SecurityException | NoSuchMethodException| IllegalArgumentException| IllegalAccessException| InvocationTargetException e) {Log.e(TAG, "initSocket", e);}socket = temp;

关于建立连接的一个比较实用,可操作性较强的完整写法可以是:

// 定义内部类private boolean connecting;private class ConnectThread extends Thread {String macAddress;private ConnectThread(String mac) {macAddress = mac;}public void run() {connecting = true;boolean connected = false;if (mBluetoothAdapter == null) {mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter();}mBluetoothDevice = mBluetoothAdapter.getRemoteDevice(macAddress);mBluetoothAdapter.cancelDiscovery();initSocket();while (!connected && connTime <= 10) {try {assert socket != null;socket.connect();connected = true;} catch (IOException e1) {connTime++;connected = false;try {initSocket();assert socket != null;socket.connect();} catch (IOException e) {Log.e(TAG, "ConnectThread", e);}// 关闭 sockettry {socket.close();socket = null;} catch (IOException e2) {Log.e(TAG, "ConnectThread", e2);}} finally {connecting = false;}// connectDevice();}// 重置 ConnectThread// synchronized (BluetoothService.this) {// ConnectThread = null;//}}public void cancel() {try {socket.close();socket = null;} catch (Exception e) {Log.e(TAG, "ConnectThread", e);} finally {connecting = false;}}}

初始化 socket 的代码为:

   /*** 取得 BluetoothSocket*/private void initSocket() {BluetoothSocket temp = null;try {// 利用反射机制Method m = mBluetoothDevice.getClass().getMethod("createRfcommSocket", int.class);// 这里端口为 1temp = (BluetoothSocket) m.invoke(mBluetoothDevice, 1);} catch (SecurityException | NoSuchMethodException|IllegalArgumentException| IllegalAccessException | InvocationTargetException e) {Log.e(TAG, "initSocket", e);}socket = temp;}

以上连接线程的使用方式:

new ConnectThread(device.getAddress()).start();

启动一个线程,并传入搜索到的设备的 mac 地址即可发起连接请求。

数据传输

连接成功后,可以通过 Java 流的知识做一些类似文件传输、局域网聊天等操作。

// step 1: 获取流
inputStream = socket.getInputStream();
outputStream = socket.getOutputStream();
// step 2: 写出、读入(JAVA 常规操作)这里就不赘述
...
// step 3: 接收数据转换
// 使用 socket.getInputStream 接收到的数据是字节流,这样的数据是没法分析的,
// 所以很多情况需要一个 byte 转十六进制 String 的函数:public static String bytesToHex(byte[] bytes) {char[] hexChars = new char[bytes.length * 2];for ( int j = 0; j &lt; bytes.length; j++ ) {int v = bytes[j] &amp; 0xFF;hexChars[j * 2] = hexArray[v &gt;&gt;&gt; 4];hexChars[j * 2 + 1] = hexArray[v &amp; 0x0F];}return new String(hexChars);
}

好了,通过以上解析,基本可以开发一款经典蓝牙应用了,下次再和大家分享更多关于 BLE(低功耗蓝牙) 开发相关实用的知识。

参考文档

  1. Android 开发者文档
  2. 安卓 6.0 版本更新以后无法利用 BluetoothDevice.ACTION_FOUND 查找周围设备
  3. Android 蓝牙开发全面总结
  4. Android 蓝牙及蓝牙通讯讲解

Android 经典蓝牙开发相关推荐

  1. Android经典蓝牙开发全流程

    一.基本介绍   所谓蓝牙(Bluetooth)技术,实际上是一种短距离无线电技术,最初是由爱立信公司公司发明的.技术始于爱立信公司 1994 方案,它是研究在移动电话和其他配件间进行低功耗.低成本无 ...

  2. Android经典蓝牙开发简介(Google官网译文)

    公司的项目最近需要用到蓝牙开发的相关内容,因此特地查阅了Google官方文档的内容并进行二次整理,希望能对需要学习该部分的朋友有所帮助. 原文地址:http://developer.android.c ...

  3. 【Android】蓝牙开发——经典蓝牙:配对与解除配对 实现配对或连接时不弹出配对框

    目录 一.配对方法 二.解除配对方法 三.配对/解除配对结果 四.justwork配对模式下,不弹出配对框 五.pincode配对模式下,不弹出配对框 六.小结 在之前的文章[Android]蓝牙开发 ...

  4. 蓝牙配对模式 java_【Android】蓝牙开发—— 经典蓝牙配对介绍(Java代码实现演示)附Demo源码...

    目录 前言 一.连接&配对方法介绍 二.演示:第一次连接蓝牙设备  &  直接与蓝牙设备建立配对 三.总结 四.补充 五.Demo案例源码地址: 前言 前面两篇文章[Android]蓝 ...

  5. 【Android】蓝牙开发—— 经典蓝牙配对介绍(Java代码实现演示)附Demo源码

    目录 前言 一.连接&配对方法介绍 二.演示:第一次连接蓝牙设备  &  直接与蓝牙设备建立配对 三.总结 四.补充 五.Demo案例源码地址: 前言 前面两篇文章[Android]蓝 ...

  6. Android笔记---蓝牙开发经典蓝牙和低功耗蓝牙

    目录 前言 一般开发步骤 相关API介绍 一.通用API 1.BluetoothAdapter 2.BluetoothDevice 二.经典蓝牙(BT)API 1.BluetoothSocket 2. ...

  7. Android BLE蓝牙开发知识总结

    Android BLE蓝牙开发知识总结 1.蓝牙介绍 1.1什么是蓝牙?    蓝牙( Bluetooth® ):是一种无线技术标准,可实现固定设备.移动设备和楼宇个人域网之间的短距离数据交换(使用2 ...

  8. Android Ble蓝牙开发总结

    Android Ble蓝牙开发总结 前言 本文总结了ble的搜索,连接,读写操作.以及在开发过程中可能遇到的坑. 首先我们需要知道,什么是ble. 蓝牙发展至今经历了8个版本的更新.1.1.1.2.2 ...

  9. 【Android】蓝牙开发——BLE(低功耗蓝牙)(附完整Demo)

    目录 目录 前言 一.相关概念介绍 二.实战开发 三.项目演示 四.Demo案例源码地址 五.更新记录 1.2020/12/29 :修改 setupService()中错误 2.2021/05/14 ...

最新文章

  1. Linux常用指令---find | locate(查找)
  2. CISCO7200路由器MultiChannel配置介绍
  3. 【转】OGRE资源相关分析
  4. Socket编程小结(续)
  5. JZOJ 5933. 【NOIP2018模拟10.27】百鸽笼
  6. kafka消费端慢慢延迟(网络带宽不足)
  7. uva 12563——Jin Ge Jin Qu hao
  8. JavaScript代码片段
  9. linux安装opencv让输入密码,linux下安装opencv的全过程(对初学者或者linux不熟悉的童鞋,非常适合)...
  10. iOS中滤镜处理及相关内存泄漏问题的解决
  11. IOS基础学习日志(七)利用dispatch_once创建单例及使用
  12. 6那智机器人各轴旋转方向
  13. Unity 置顶点击的对象
  14. 农夫山泉下场当“烧水工”,熟水市场是“鸡肋”还是“机遇”?
  15. 端游一般用什么配置的服务器呢?
  16. Masonry 比例设置multipliedBy与dividedBy区别
  17. 快速抢占Shopee墨西哥广告蓝海,Shopee广告投放策略分享
  18. dpi、dp、sp、px、mm之间的关系
  19. 浅谈SRAM与DRAM的异同
  20. 什么是防火墙?防火墙基础知识讲解

热门文章

  1. xen server 添加硬盘步骤
  2. matlab画三维约束图命令,Matlab画三维图的方法
  3. 策略设计模式:英雄死亡之后的处理
  4. java中mvc事务_java核心技术第五篇之事务和MVC模式
  5. 认识这些词,老外都认为你无敌了~
  6. 全国行政边界json数据echarts地图geojson生成精确到城镇街道-20211208
  7. 我对暴风影音的几点建议
  8. 连Python产生器(Generator)的原理都解释不了,还敢说Python用了5年?
  9. CUDA教程: 2.初识CUDA---CUDA简介
  10. C# android 蓝牙、消息、socket传输文件 InTheHand 手机蓝牙连接电脑