Android 12.0蓝牙适配

  • 前言
  • 正文
    • 一、Android版本中蓝牙简介
    • 二、新建项目
      • ① 配置settings.gradle和build.gradle
      • ② 配置AndroidManifest.xml
    • 三、打开蓝牙
      • ① 打开蓝牙意图
      • ② 请求BLUETOOTH_CONNECT权限意图
    • 四、蓝牙扫描
      • ① 扫描者
      • ② 扫描回调
      • ③ 扫描方法
      • ④ 执行扫描
      • ⑤ 应用不推导物理位置
    • 五、页面显示扫描设备
      • ① 蓝牙设备适配器
      • ② 显示列表设备
    • 六、适配Android12.0以下设备
    • 七、源码

前言

  在我的申请下,公司终于购买了一台基于Android12.0的手机,然后我就开心的拿去安装测试了,发现程序崩溃了,于是我这里就写下来,Android12.0的蓝牙适配方法。

正文

  在Android系统版本中,蓝牙的变化有,但是不多,这里简要说明一下。

一、Android版本中蓝牙简介

  • Android1.5 中增加了蓝牙功能,立体声 Bluetooth 支持:A2DP [Advanced Audio Distribution Profile]、AVCRP [Audio/Video Remote Control Profile],自动配对。
  • Android2.0 中支持Bluetooth2.1协议。
  • Android3.0 中能让应用查询已经连接上 Bluetooth 设备的 Bluetooth Profile、音频状态等,然后通知用户。
  • Android3.1 中系统可以通过 Bluetooth HID 方式同时接入一到多款输入设备。
  • Android4.0 中新增支持连接 Bluetooth HDP [Health Device Profile)] 设备,通过第三方应用的支持,用户可以连接到医院、健身中心或者家庭等场合中的无线医疗设备和传感器。
  • Android4.2 中引入了一种新的针对 Android 设备优化的 Bluetooth 协议栈 BlueDroid,从而取代 BlueZ 协议栈。Bluedroid 协议栈由 Google 和 Broadcom 公司共同开发,相对于 BlueZ 协议栈,BlueDroid 提升了兼容性和可靠性。
  • Android4.3 中增加了对低功耗蓝牙的支持,内置支持 Bluetooth AVRCP 1.3,基于 Google 和 Broadcom 公司功能研发的针对于 Android 设备优化的新的蓝牙协议栈 BlueDroid。
  • Android4.4 中新增两种新 Proifle 支持:HID [Human Interface Device]、MAP [Message Access Profile]
  • Android5.0 中支持Bluetooth4.1协议。
  • Android6.0 中扫描蓝牙需要动态获取定位才行。
  • Android7.0 中支持Bluetooth4.2协议。
  • Android8.0 中支持Bluetooth5.0协议,强化了蓝牙音频的表现。比如编码/传输格式可选SBC、AAC、aptX/aptX HD、LDAC等四种,音质依次提高。
  • Android10.0 中支持Bluetooth5.1协议,在5.0的基础上,增加了侧向功能和厘米级定位服务,大幅度提高了定位精度。使室内定位更精准。
  • Android11.0 中支持Bluetooth5.2协议,增强版ATT协议,LE功耗控制和信号同步,连接更快,更稳定,抗干扰性更好。
  • Android12.0 中支持Bluetooth5.3协议,增强了经典蓝牙BR/EDR(基础速率和增强速率)的安全性。蓝牙5.3的延迟更低、抗干扰性更强、提升了电池续航时间。系统引入了新的运行时权限 BLUETOOTH_SCAN、BLUETOOTH_ADVERTISE 和 BLUETOOTH_CONNECT权限,用于更好地管理应用于附近蓝牙设备的连接。

二、新建项目

  在Android12.0中新增加了三个运行时权限,我们依次来说明一下,这里我们依然创建一个项目来说明,新建一个Android12Bluetooth项目,如下图所示:

这里使用Kotlin来写,点击Finish。

① 配置settings.gradle和build.gradle

然后来配置一下项目的依赖库,首先是在工程的settings.gradle中增加如下依赖:

 maven { url "https://jitpack.io" }

增加位置如下图所示:

然后在app的build.gradle中增加

 buildFeatures {viewBinding truedataBinding true}implementation 'com.github.CymChad:BaseRecyclerViewAdapterHelper:3.0.4'

增加位置如下图所示:

然后Sync Now。

② 配置AndroidManifest.xml

下面配置AndroidMainfest.xml,权限如下所示:

 <uses-permission android:name="android.permission.BLUETOOTH"/><uses-permission android:name="android.permission.BLUETOOTH_ADMIN"/><!--Android12 的蓝牙权限 如果您的应用与已配对的蓝牙设备通信或者获取当前手机蓝牙是否打开--><uses-permission android:name="android.permission.BLUETOOTH_CONNECT"/><!--Android12 的蓝牙权限 如果您的应用查找蓝牙设备(如蓝牙低功耗 (BLE) 外围设备)--><uses-permission android:name="android.permission.BLUETOOTH_SCAN"/><!--Android12 的蓝牙权限 如果您的应用使当前设备可被其他蓝牙设备检测到--><uses-permission android:name="android.permission.BLUETOOTH_ADVERTISE"/>

增加位置如下图所示:

三、打开蓝牙

  下面我们构建一下activity_main.xml中的代码:

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"xmlns:app="http://schemas.android.com/apk/res-auto"xmlns:tools="http://schemas.android.com/tools"android:layout_width="match_parent"android:layout_height="match_parent"tools:context=".MainActivity"><Buttonandroid:id="@+id/btn_open_bluetooth"android:layout_width="0dp"android:layout_height="wrap_content"android:layout_marginStart="10dp"android:layout_marginTop="20dp"android:layout_marginEnd="10dp"android:insetTop="0dp"android:insetBottom="0dp"android:text="打开蓝牙"app:layout_constraintEnd_toEndOf="parent"app:layout_constraintStart_toStartOf="parent"app:layout_constraintTop_toTopOf="parent" /></androidx.constraintlayout.widget.ConstraintLayout>

这里就是写一个按钮,用来点击打开系统蓝牙的开关的。在Android12.0之前打开蓝牙的之前需要先判断蓝牙是否打开,我们可以这样来写,在MainActivity中增加如下代码:

 private fun isOpenBluetooth(): Boolean {val manager = getSystemService(BLUETOOTH_SERVICE) as BluetoothManagerval adapter = manager.adapter ?: return falsereturn adapter.isEnabled}

同样我们还需要一个方法判断当前是否为Android12及以上版本。

 private fun isAndroid12() = Build.VERSION.SDK_INT >= Build.VERSION_CODES.S

同样还有一个检查此权限是否授予的方法和一个显示Toast的方法:

 private fun hasPermission(permission: String) =checkSelfPermission(permission) == PackageManager.PERMISSION_GRANTEDprivate fun showMsg(msg: String) {Toast.makeText(this, msg, Toast.LENGTH_SHORT).show()}

① 打开蓝牙意图

在MainActivity中新增如下代码:

 //打开蓝牙意图val enableBluetooth = registerForActivityResult(StartActivityForResult()) {if (it.resultCode == Activity.RESULT_OK) {showMsg(if (isOpenBluetooth()) "蓝牙已打开" else "蓝牙未打开")}}

此方法就替代了之前startActivityForResult,现在我们使用registerForActivityResult。再返回中可以得知当前是否打开了蓝牙。

② 请求BLUETOOTH_CONNECT权限意图

registerForActivityResult不光能用于页面获取值,也能用于请求权限。

 //请求BLUETOOTH_CONNECT权限意图val requestBluetoothConnect = registerForActivityResult(ActivityResultContracts.RequestPermission()) {if (it) {//打开蓝牙enableBluetooth.launch(Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE))} else {showMsg("Android12中未获取此权限,则无法打开蓝牙。")}}

请求权限返回无非就是同意不同意,如果同意了我们就调用

enableBluetooth.launch(Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE))

去打开系统蓝牙,不同意就提示一下。下面再用ViewBinding来配置一下:

这里有一个initView的函数,在这个函数中我们对按钮的点击事件进行操作,新增initView()函数,代码如下:

 private fun initView() {binding.btnOpenBluetooth.setOnClickListener {//蓝牙是否已打开if (isOpenBluetooth()){showMsg("蓝牙已打开")return@setOnClickListener}//是Android12if (isAndroid12()) {//检查是否有BLUETOOTH_CONNECT权限if (hasPermission(Manifest.permission.BLUETOOTH_CONNECT)) {//打开蓝牙enableBluetooth.launch(Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE))} else {//请求权限requestBluetoothConnect.launch(Manifest.permission.BLUETOOTH_CONNECT)}return@setOnClickListener}//不是Android12 直接打开蓝牙enableBluetooth.launch(Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE))}}

  这里的代码就比较好理解,首先判断蓝牙是否已经打开了,打开了就不往下执行,没打开,再判断当前是否为Android12,不是就直接打开系统蓝牙,是Android12,再去检查是否授予BLUETOOTH_CONNECT权限,授予了就打开系统蓝牙,没有授予就去请求此权限,下面我们运行一下:

四、蓝牙扫描

  在Android6.0 - Android11.0之间,扫描蓝牙都是需要打开定位权限的,而在Android12中则不需要了,换成了BLUETOOTH_SCAN权限,那么我们下面来看看,怎么操作的。

这里扫描蓝牙就以低功耗蓝牙为例子。

① 扫描者

在MainActivity中定义如下变量

 private val TAG = MainActivity::class.java.simpleName//获取系统蓝牙适配器private lateinit var mBluetoothAdapter: BluetoothAdapter//扫描者private lateinit var scanner: BluetoothLeScanner//是否正在扫描var isScanning = false

获取系统蓝牙适配器,要在onCreate回调中,如下图所示:

② 扫描回调

扫描设备时会有扫描的结果,在MainActivity中增加如下代码:

 //扫描结果回调private val scanCallback = object : ScanCallback() {override fun onScanResult(callbackType: Int, result: ScanResult) {val device = result.deviceLog.d(TAG, "name: ${device.name}, address: ${device.address}")}}

这里可能你的device.name下面会有一个红线,这是因为AS会检查你这里需要一个BLUETOOTH_CONNECT权限,而这个权限我们在打开蓝牙时已经请求过了,那么为了避免麻烦,我们在当前MainActivity上面增加如下注解。

@SuppressLint("MissingPermission")

如下图所示:

这个注解加上去之后你需要小心蓝牙权限的问题。

③ 扫描方法

下面我们写一个开始扫描和停止扫描的方法,代码如下:

 private fun startScan() {if (!isScanning) {scanner.startScan(scanCallback)isScanning = truebinding.btnScanBluetooth.text = "停止扫描"}}private fun stopScan() {if (isScanning) {scanner.stopScan(scanCallback)isScanning = falsebinding.btnScanBluetooth.text = "扫描蓝牙"}}

扫描和停止扫描时修改一下变量值并且改动按钮的文字以表示当前是否正在扫描中。

④ 执行扫描

执行扫描就很简单了,首先我们需要在MainActivity中创建扫描意图:

 //请求BLUETOOTH_SCAN权限意图private val requestBluetoothScan =registerForActivityResult(ActivityResultContracts.RequestPermission()) {if (it) {//进行扫描startScan()} else {showMsg("Android12中未获取此权限,则无法扫描蓝牙。")}}

这个意图我们将在点击扫描按钮的时候会用到,下面我们在initView中增加扫描按钮点击的代码:

     //扫描蓝牙binding.btnScanBluetooth.setOnClickListener {if (isAndroid12()) {if (hasPermission(Manifest.permission.BLUETOOTH_SCAN)) {//扫描或者停止扫描if (isScanning) stopScan() else startScan()} else {//请求权限requestBluetoothScan.launch(Manifest.permission.BLUETOOTH_SCAN)}}}

相对来说还是比较的简洁,其实还可以更简洁。我在扫描回调中打印了日志,如果有扫描到设备的话,就会有日志,下面我们扫描一下看看:

扫描启动了,但是没有设备被扫描到,可我附近明明有蓝牙设备正在广播,这是为什么呢?

⑤ 应用不推导物理位置

  这个说起来就和之前的Android 6.0 至 Android 11.0中需要定位权限才能扫描有关系了,就是因为这个推导物理位置,手机是可以通过扫描到的设备知道设备的具体位置的,所以之前需要定位权限,那么现在我没有定位权限了,你扫不到设备就很离谱,怎么解决呢?

如果您的应用不推导物理位置,那么您可以坚定地断言您的应用绝不会使用蓝牙权限来推导物理位置。为此,请完成以下步骤:

将 android:usesPermissionFlags 属性添加到 BLUETOOTH_SCAN 权限声明,并将此属性的值设为 neverForLocation。

注意:如果 android:usesPermissionFlags 中包含 neverForLocation,则会从扫描结果中过滤出某些 BLE 信标。

这是官方的说明,操作起来很简单,如下图所示:

意思很明显,就是说你如果不需要推导物理地址,那么就设置一下这个权限的标识即可。下面我们再来运行一下:

设备就扫描到了,可以看到这里有设备的Mac地址,再点一下就可以停止扫描了。

不过我们这里是控制台显示了设备,并没有在页面显示设备,下面我们完成这一步。

五、页面显示扫描设备

  显示蓝牙设备首先我们需要修改一下activity_main.xml布局,代码如下:

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"xmlns:app="http://schemas.android.com/apk/res-auto"xmlns:tools="http://schemas.android.com/tools"android:layout_width="match_parent"android:layout_height="match_parent"tools:context=".MainActivity"><Buttonandroid:id="@+id/btn_open_bluetooth"android:layout_width="0dp"android:layout_height="wrap_content"android:layout_marginStart="10dp"android:layout_marginTop="20dp"android:layout_marginEnd="10dp"android:insetTop="0dp"android:insetBottom="0dp"android:text="打开蓝牙"app:layout_constraintEnd_toEndOf="parent"app:layout_constraintStart_toStartOf="parent"app:layout_constraintTop_toTopOf="parent" /><Buttonandroid:id="@+id/btn_scan_bluetooth"android:layout_width="0dp"android:layout_height="wrap_content"android:layout_marginStart="10dp"android:layout_marginTop="20dp"android:layout_marginEnd="10dp"android:insetTop="0dp"android:insetBottom="0dp"android:text="扫描蓝牙"app:layout_constraintEnd_toEndOf="parent"app:layout_constraintHorizontal_bias="0.0"app:layout_constraintStart_toStartOf="parent"app:layout_constraintTop_toBottomOf="@+id/btn_open_bluetooth" /><androidx.recyclerview.widget.RecyclerViewandroid:id="@+id/rv_device"android:layout_width="0dp"android:layout_height="0dp"android:layout_marginTop="20dp"android:overScrollMode="never"app:layout_constraintBottom_toBottomOf="parent"app:layout_constraintEnd_toEndOf="parent"app:layout_constraintStart_toStartOf="parent"app:layout_constraintTop_toBottomOf="@+id/btn_scan_bluetooth" /></androidx.constraintlayout.widget.ConstraintLayout>

① 蓝牙设备适配器

这个里的适配器使我们自己去写的,需要显示数据的,首先我们需要创建一个蓝牙图标,在drawable包下新建一个icon_bluetooth.xml,里面的代码如下:

<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"android:width="48dp"android:height="48dp"android:autoMirrored="true"android:tint="#000000"android:viewportWidth="24.0"android:viewportHeight="24.0"><pathandroid:fillColor="@android:color/white"android:pathData="M14.24,12.01l2.32,2.32c0.28,-0.72 0.44,-1.51 0.44,-2.33 0,-0.82 -0.16,-1.59 -0.43,-2.31l-2.33,2.32zM19.53,6.71l-1.26,1.26c0.63,1.21 0.98,2.57 0.98,4.02s-0.36,2.82 -0.98,4.02l1.2,1.2c0.97,-1.54 1.54,-3.36 1.54,-5.31 -0.01,-1.89 -0.55,-3.67 -1.48,-5.19zM15.71,7.71L10,2L9,2v7.59L4.41,5 3,6.41 8.59,12 3,17.59 4.41,19 9,14.41L9,22h1l5.71,-5.71 -4.3,-4.29 4.3,-4.29zM11,5.83l1.88,1.88L11,9.59L11,5.83zM12.88,16.29L11,18.17v-3.76l1.88,1.88z" /></vector>

因为我们的设备需要显示信号强度,那么我们创建一个数据类,在com.llw.bluetooth包下新建一个MyDevice类,代码如下:

data class MyDevice(val device: BluetoothDevice, var rssi: Int)

然后我们构建适配器的item布局,在layout包下新建一个item_device.xml,代码如下:

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"xmlns:app="http://schemas.android.com/apk/res-auto"xmlns:tools="http://schemas.android.com/tools"><data><variablename="device"type="com.llw.bluetooth.MyDevice" /></data><androidx.constraintlayout.widget.ConstraintLayoutandroid:layout_width="match_parent"android:layout_height="70dp"><ImageViewandroid:id="@+id/imageView"android:layout_width="50dp"android:layout_height="50dp"android:layout_marginStart="10dp"android:layout_marginTop="10dp"android:padding="8dp"app:layout_constraintStart_toStartOf="parent"app:layout_constraintTop_toTopOf="parent"app:srcCompat="@drawable/icon_bluetooth" /><TextViewandroid:id="@+id/tv_name"android:layout_width="0dp"android:layout_height="wrap_content"android:layout_marginStart="10dp"android:layout_marginTop="4dp"android:layout_marginEnd="10dp"android:text="@{device.device.name ?? `Unknown` }"android:textColor="@color/black"android:textSize="14sp"android:textStyle="bold"app:layout_constraintEnd_toStartOf="@+id/tv_rssi"app:layout_constraintStart_toEndOf="@+id/imageView"app:layout_constraintTop_toTopOf="@+id/imageView" /><TextViewandroid:id="@+id/tv_address"android:layout_width="0dp"android:layout_height="wrap_content"android:layout_marginStart="10dp"android:layout_marginEnd="10dp"android:layout_marginBottom="4dp"android:text="@{device.device.address}"android:textSize="12sp"app:layout_constraintBottom_toBottomOf="@+id/imageView"app:layout_constraintEnd_toStartOf="@+id/tv_rssi"app:layout_constraintStart_toEndOf="@+id/imageView" /><TextViewandroid:id="@+id/tv_rssi"android:layout_width="wrap_content"android:layout_height="wrap_content"android:layout_marginEnd="10dp"android:text="@{device.rssi+`dBm`}"app:layout_constraintBottom_toBottomOf="parent"app:layout_constraintEnd_toEndOf="parent"app:layout_constraintTop_toTopOf="parent" /></androidx.constraintlayout.widget.ConstraintLayout>
</layout>

这里的布局数据采用了DataBinding。下面我们去写适配器,在com.llw.bluetooth包新建一个MyDeviceAdapter类,里面的代码如下:

class MyDeviceAdapter(data: MutableList<MyDevice>) :BaseQuickAdapter<MyDevice, BaseDataBindingHolder<ItemDeviceBinding>>(R.layout.item_device, data) {override fun convert(holder: BaseDataBindingHolder<ItemDeviceBinding>, item: MyDevice) {holder.dataBinding?.apply {device = itemexecutePendingBindings()}}
}

这里就用到之前build.gradle中第三方依赖库的适配器,相当好用。下面我们回到MainActivity中。

② 显示列表设备

在MainActivity中创建两个变量:

 //设备列表private val deviceList = mutableListOf<MyDevice>()//适配器private lateinit var myDeviceAdapter: MyDeviceAdapter

这里我们需要思考一个问题,那就是列表设备的唯一性,因为蓝牙设备是一直广播的,所以我们扫描到的结果会有重复的设备,重复的设备有信号强度上的差异,这个地方我们要做的就是判断当前列表中是否有此设备,有就更新rssi,没有就添加,我们新增一个findDeviceIndex()函数,代码如下:

 private fun findDeviceIndex(scanDevice: MyDevice, deviceList: List<MyDevice>): Int {var index = 0for (device in deviceList) {if (scanDevice.device.address.equals(device.device.address)) return indexindex += 1}return -1}

下面我们再新增一个addDeviceList()函数,代码如下:

 private fun addDeviceList(device: MyDevice) {val index = findDeviceIndex(device, deviceList)if (index == -1) {Log.d(TAG, "name: ${device.device.name}, address: ${device.device.address}")deviceList.add(device)myDeviceAdapter.notifyDataSetChanged()} else {deviceList[index].rssi = device.rssimyDeviceAdapter.notifyItemChanged(index)}}

最后我们在扫描回调中调用此方法:

最后别忘记了我们的适配器和列表都需要初始化的,我写在initView()函数中,如下图所示:

现在就可以运行了。

其实Android12蓝牙只有权限上要注意一下,虽然有三个动态权限,但是只要你同意了一个就都同意了,因为它们属于同一个权限组,所以如果你能确保当前拥有其中一个权限的话,蓝牙的操作就和之前一样的。不过还是有一些问题的,那就是在打开蓝牙之后要对变量进行赋值,如下图所示:

六、适配Android12.0以下设备

当前的代码我们在Android12上是没有问题了,但是Android12一下Android6.0以上还是扫描不到设备,因为需要定位权限,那么我们在AndroidManifest.xml中增加:

 <!--Android6-11 定位权限--><uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" /><uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />

然后我们回到MainActivity中,增加一个定位意图

 //请求定位权限意图private val requestLocation =registerForActivityResult(ActivityResultContracts.RequestPermission()) {if (it) {//扫描蓝牙startScan()} else {showMsg("Android12以下,6及以上需要定位权限才能扫描设备")}}

然后我们回到扫描按钮的点击事件。

下面我们在Android10.0上运行一下:

七、源码

如果你觉得代码对你有帮助的话,不妨Fork或者Star一下~
GitHub:Android12Bluetooth

Android 12 蓝牙适配相关推荐

  1. Android 12 自动适配 exported 深入解析避坑

    众所周知,从 Android 12 开始,使用了 TargetSDK 31 之后,四大组件如果使用了 intent-filter, 但是没显性质配置 exported App 将会无法安装,甚至编译不 ...

  2. Android 12 自动适配 exported 深入解析避坑 | 开发者说·DTalk

    本文原作者: 恋猫de小郭,原文发布于: GSYTech  众所周知,从 Android 12 开始,使用了 TargetSDK 31 之后,四大组件如果使用了 intent-filter,但是没显性 ...

  3. Android 12应用适配指南

    Android 12应用适配指南 1.Android 12上的主要变更 1.1 兼容性 1.1.1 前台服务启动限制 1.1.2 前台服务通知延迟 1.1.3 待处理 intent 必须声明可变性 1 ...

  4. 华为android o适配名单,Android 12首批适配名单公布:没有华为、荣耀

    谷歌已经正式发布了Android 12,新的系统底层带来了3个改动:更丰富的视觉元素,更全面的隐私保护,并且开始引入"多设备互联"的概念.说实话,Android 12的更新只能用& ...

  5. Android 12 快速适配要点

    Android 12 需要更新适配点并不多,本篇主要介绍最常见的两个需要适配的点:android:exported 和 SplashScreen . 一.android:exported 它主要是设置 ...

  6. Android 12 变更及适配攻略

    这几个月有点忙,一年一篇的适配文章来的有点晚了.但其实也还好,因为我们项目也是下半年才适配.我这边也是提前调研踩坑,评估一下工作量.这个时间点也完全跟得上Google Play的审核要求(11月1号) ...

  7. android 开放平台开发,OPPO开放平台上线Android 12适配文档

    OPPO 近日悄然在开放平台添加了 Android 12 适配文档,该文档可帮助 App 开发者更好地兼容 Android 12. 虽然文档内还没有任何内容,但这标志着 OPPO 已经开始了 Andr ...

  8. android制作开放平台,OPPO 开放平台上线 Android 12 适配文档

    IT之家 5 月 17 日消息OPPO 近日悄然在开放平台添加了 Android 12 适配文档,该文档可帮助 App 开发者更好地兼容 Android 12. 虽然文档内还没有任何内容,但这标志着 ...

  9. PermissionX 1.6发布,支持Android 12,可能是今年最大的版本升级

    本文同步发表于我的微信公众号,扫一扫文章底部的二维码或在微信搜索 郭霖 即可关注,每个工作日都有文章更新. 各位小伙伴们大家早上好. 没错,PermissionX又升级了,并且这次版本变化非常大,很有 ...

最新文章

  1. iOS RunLoop详解
  2. javascript常用小例子
  3. 读取工作流程中的表单物件
  4. Java集合之ArrayList
  5. 很强大的FFMPEG API Documentation
  6. Java运行原理研究(未完待续)
  7. C语言之extern关键字探究
  8. redis主线程阻塞的情形
  9. 2022年申请亳州市发明专利材料,专利说明书摘要写作技巧
  10. 史上最通俗易懂的并查集算法详解
  11. Linux环境下向github上传代码(生成token、生成本地密钥)
  12. 来成都旅游,千万不要做的22件事!
  13. DeepStream初步学习
  14. 达威尔液晶电子手写板儿童涂鸦画板写字板,培养小孩创造力和想象力的好工具!
  15. MPU6050原始数据分析——学习笔记
  16. amd显卡测试大风车软件md,肉眼可见的撕裂 AMD将推出FreeSync2 HDR测试工具
  17. MATLAB——Z变换与Z反变换
  18. Unity3d实现阿拉伯语适配,不规则特殊字符的处理
  19. BoT-SORT: Robust Associations Multi-Pedestrian Tracking
  20. 白领必备书写职场英文邮件参考

热门文章

  1. Linux指令英文全称(方便记忆)
  2. 基于AndroidStudio+Java+SQLite开发的背单词APP系统
  3. 林仕鼎:大数据需要三类人才
  4. nim游戏(判断必胜还是必败,必胜该怎么取)
  5. 电影 下载 地址收集
  6. 电音制作宿主软件-Ableton Live Suite v11.0.5 x64 WiN
  7. 腾讯云ubuntu远程桌面
  8. deeplabv3 + mobilenetv2 做语义分割并封装成c++部署到移动端,linux,windows等平台(史上最详细)
  9. js打印完毕后关闭窗口
  10. web前端设计与开发期末作品 用DIV CSS技术设计的网上书城网页与实现制作(大一Web课程设计)