蓝牙权限与后台模式配置
本页介绍 Jieli iOS 蓝牙 Demo(CoreBluetoothClassicSample)中与蓝牙权限声明、后台运行模式以及运行时授权状态处理相关的全部配置与实现,帮助开发者理解 iOS 蓝牙应用从「声明权限」到「获得授权并开始使用」的完整链路。
Purpose and Scope
本页聚焦于蓝牙能力投入使用之前的准备工作,覆盖:
Info.plist中的NSBluetoothAlwaysUsageDescription权限用途声明(iOS 13+ 强制要求)UIBackgroundModes后台模式配置(bluetooth-central与bluetooth-peripheral)CentralViewController.swift中通过centralManagerDidUpdateState对蓝牙状态与授权状态(central.state/central.authorization)的运行时处理- 最小化
AppDelegate.swift入口结构与系统事件的关联
不属于本页范围(由同目录下其他页面承接):中心设备扫描与连接流程、外设广播流程、数据收发协议等具体业务逻辑。如需了解这些内容,请参阅对应页面(见文末 Related Links)。
Overview
iOS 将蓝牙(CoreBluetooth)视为受保护资源。App 若要使用蓝牙,必须同时满足两个层面的要求:
- 声明层面(静态配置):在
Info.plist中声明NSBluetoothAlwaysUsageDescription。从 iOS 13 开始,缺少该键会导致 App 在访问蓝牙时直接崩溃或被系统拒绝;该字符串会在系统权限弹窗中展示给用户,说明 App 使用蓝牙的用途。 - 授权层面(运行时状态):CoreBluetooth 的授权状态由系统管理,可能为
notDetermined、restricted、denied、allowedAlways。同时蓝牙硬件本身还有开关状态(poweredOff/poweredOn等)。开发者必须在centralManagerDidUpdateState(_:)回调中逐一处理这些状态,才能正确引导用户完成授权。
此外,如果 App 需要在退到后台后继续扫描、连接或广播蓝牙数据,必须在 Info.plist 的 UIBackgroundModes 数组中声明 bluetooth-central(中央设备角色)和/或 bluetooth-peripheral(外设角色)。本仓库的示例工程同时声明了两种模式,因为它同时包含 Central 与 Peripheral 两个示例界面。
Architecture
下图展示了本示例工程中蓝牙权限与后台模式的整体架构:Info.plist 的静态声明决定系统弹窗与后台能力,AppDelegate 作为应用入口,CBCentralManager 在运行时触发授权流程,最终由 CentralViewController 处理状态回调并启动蓝牙功能。
flowchart TD
subgraph sg_Config["配置层 Info.plist"]
NSBluetooth["NSBluetoothAlwaysUsageDescription"]
BGM["UIBackgroundModes<br/>bluetooth-central / bluetooth-peripheral"]
end
subgraph sg_Entry["应用入口"]
AppDelegate["AppDelegate"]
end
subgraph sg_Runtime["运行时 CoreBluetooth"]
CentralManager["CBCentralManager"]
StateCheck{"central.state"}
AuthCheck{"central.authorization"}
end
subgraph sg_UI["UI 层"]
CentralVC["CentralViewController"]
PeripheralVC["PeripheralViewController"]
end
NSBluetooth -->|"首次访问时系统弹出授权框"| CentralManager
BGM -->|"后台扫描/广播能力"| CentralManager
AppDelegate -->|"创建/持有管理器"| CentralManager
CentralManager -->|"centralManagerDidUpdateState 回调"| StateCheck
StateCheck -->|"unauthorized"| AuthCheck
StateCheck -->|"poweredOn"| CentralVC
AuthCheck -->|"restricted / denied 等"| CentralVC
CentralVC -->|"扫描并连接外设"| PeripheralVC
CentralVC -->|"os_log 记录状态"| StateCheck
各组件职责:
- Info.plist:静态声明蓝牙用途说明与后台模式,是系统权限弹窗文案和后台运行资格的唯一来源。缺少声明时,系统不会授予任何蓝牙能力。
- AppDelegate:示例工程中保持最小实现(仅持有
window),说明本工程的蓝牙能力完全由视图控制器驱动,App 生命周期不额外干预蓝牙管理器的创建时机。 - CBCentralManager:CoreBluetooth 的中央管理器。创建后系统异步回调
centralManagerDidUpdateState,其中包含蓝牙开关状态与授权状态。 - CentralViewController:状态回调的实际处理者。根据
central.state与central.authorization的组合决定是启动扫描、提示用户开启蓝牙,还是引导用户前往系统设置重新授权。
核心配置详解(Info.plist)
蓝牙使用权限声明
Info.plist 中的 NSBluetoothAlwaysUsageDescription 是 iOS 13 及以上版本访问蓝牙的强制前置条件。系统在 App 首次创建 CBCentralManager 或 CBPeripheralManager 时,会读取该字符串并弹出授权对话框;字符串内容应准确描述 App 使用蓝牙的具体用途,以提升用户授权意愿。
本工程声明的用途说明如下:
<key>NSBluetoothAlwaysUsageDescription</key>
<string>Use bluetooth to discover, connect to, and share information with nearby devices</string>
Source: Info.plist
设计意图:Always 级别的用途说明覆盖了 App 在前台和后台的所有蓝牙访问场景,与工程同时启用 bluetooth-central / bluetooth-peripheral 后台模式相匹配。若仅需前台短暂使用,可考虑 NSBluetoothPeripheralUsageDescription(iOS 12 及更早版本),但面向现代 iOS 的工程应统一使用 NSBluetoothAlwaysUsageDescription。
后台模式配置
UIBackgroundModes 数组声明了 App 支持的后台运行能力。本工程声明了两个蓝牙相关模式:
<key>UIBackgroundModes</key>
<array>
<string>bluetooth-central</string>
<string>bluetooth-peripheral</string>
</array>
Source: Info.plist
两个模式的含义与适用角色:
| 模式值 | 适用角色 | 后台能力 |
|---|---|---|
bluetooth-central | 中央设备(Central) | App 退到后台后仍可扫描、连接并维持与蓝牙外设的通信 |
bluetooth-peripheral | 外设(Peripheral) | App 退到后台后仍可广播服务、响应中央设备的连接与读写请求 |
该工程同时提供 CentralViewController(中央角色)与 PeripheralViewController(外设角色)两个示例界面,因此两种模式缺一不可。
设备能力声明
<key>UIRequiredDeviceCapabilities</key>
<array>
<string>armv7</string>
</array>
Source: Info.plist
该键限制 App 仅可在满足指定硬件能力的设备上安装(此处为 armv7 及以上的 ARM 架构设备),保证蓝牙相关硬件特性可用。
运行时权限状态处理
静态声明只是第一步。真正决定蓝牙能否使用的是运行时的授权状态。本工程在 CentralViewController.swift 的 centralManagerDidUpdateState(_:) 委托回调中集中处理这些状态:
func centralManagerDidUpdateState(_ central: CBCentralManager) {
// 在应用程序中,你需要处理每种 central.state 和 central.authorization 的可能值
switch central.state {
// ...
case .unauthorized:
switch central.authorization {
case .restricted:
// 系统级限制(如家长控制),无法通过用户设置解除
// ...
// ... 其他 authorization 分支
}
// ...
case .poweredOff:
os_log("蓝牙当前处于关闭状态")
case .poweredOn:
os_log("启动 cbManager")
// ...
}
}
Source: CentralViewController.swift(省略号处为未在本页展开的其余分支)
设计意图:这段代码体现了 iOS 蓝牙权限处理的两个关键实践:
- 先检查硬件状态,再检查授权状态。
central.state == .unauthorized时进一步细分central.authorization为.restricted(系统限制)还是.denied(用户拒绝),因为两者的处理方式完全不同——前者只能提示用户联系系统管理员/家长解除限制,后者可以引导用户前往「设置 → 隐私 → 蓝牙」重新授权。 poweredOff与poweredOn是用户最容易遇到的状态。蓝牙未开启时,即使权限为allowedAlways也无法使用,因此代码用os_log明确记录当前状态,便于开发调试时区分「没权限」和「没开蓝牙」。
应用入口(AppDelegate)
AppDelegate.swift 保持了系统模板的最小实现,仅声明窗口属性,未对蓝牙生命周期做任何干预:
@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
}
Source: AppDelegate.swift
设计意图:蓝牙管理器的创建与销毁完全由视图控制器(CentralViewController / PeripheralViewController)在各自生命周期中管理,AppDelegate 保持精简。这种设计适合示例工程——每个示例界面独立演示一个蓝牙角色;而在生产 App 中,通常会将 CBCentralManager 提升为 App 级单例,并把 centralManagerDidUpdateState 的状态处理上移到 AppDelegate 或专门的服务层,以保证多界面共享同一授权流程。
Core Flow:从启动到获得蓝牙授权
一次完整的蓝牙授权流程从 App 启动开始,到 centralManagerDidUpdateState 返回 .poweredOn 为止。时序如下:
sequenceDiagram
participant App as App (AppDelegate)
participant CB as CBCentralManager
participant OS as iOS 系统
participant User as 用户
participant VC as CentralViewController
App->>CB: 创建 CBCentralManager
CB->>OS: 请求蓝牙访问授权
OS->>User: 弹出授权框(展示 NSBluetoothAlwaysUsageDescription 文案)
User-->>OS: 允许 / 拒绝
OS-->>CB: 返回授权结果
CB-->>VC: centralManagerDidUpdateState(state)
VC->>VC: switch central.state
alt state == .poweredOn
VC->>VC: 检查 central.authorization 后启动扫描
else state == .unauthorized
VC->>VC: switch central.authorization(.restricted / .denied)
VC->>VC: 提示用户前往系统设置开启权限
else state == .poweredOff
VC->>VC: os_log 记录「蓝牙当前处于关闭状态」
end
关键观察点:
- 回调是异步且必然发生的。
CBCentralManager创建后,系统一定会调用一次centralManagerDidUpdateState,因此该方法是所有蓝牙状态判断的单一事实来源,不应在创建管理器后立即假设蓝牙可用。 - 状态判断是组合式的。硬件开关(
state)与授权级别(authorization)是正交的两个维度,必须分别处理。示例代码正是先switch central.state,再在.unauthorized分支内switch central.authorization。
授权状态的完整迁移关系如下:
stateDiagram-v2
[*] --> notDetermined: 首次启动,尚未请求
notDetermined --> allowedAlways: 用户点击「允许」
notDetermined --> denied: 用户点击「不允许」
notDetermined --> restricted: 系统限制(家长控制/MDM)
denied --> allowedAlways: 用户在系统设置中重新开启
restricted --> allowedAlways: 解除系统级限制(非用户可操作)
allowedAlways --> [*]: poweredOn 后开始扫描/广播
Usage Examples
示例一:完整蓝牙权限与后台模式声明
将以下片段合并到工程的 Info.plist 中,即可获得与示例工程一致的蓝牙权限声明:
<key>NSBluetoothAlwaysUsageDescription</key>
<string>Use bluetooth to discover, connect to, and share information with nearby devices</string>
<key>UIBackgroundModes</key>
<array>
<string>bluetooth-central</string>
<string>bluetooth-peripheral</string>
</array>
Source: Info.plist
示例二:运行时状态处理骨架
在视图控制器中实现 CBCentralManagerDelegate,用如下骨架覆盖所有关键状态(完整分支以省略号标注,实际工程中每个分支都应给出用户可感知的反馈):
func centralManagerDidUpdateState(_ central: CBCentralManager) {
// 在应用程序中,你需要处理每种 central.state 和 central.authorization 的可能值
switch central.state {
case .unauthorized:
switch central.authorization {
case .restricted:
// 系统限制,提示用户无法使用
case .denied:
// 引导用户前往「设置 → 隐私 → 蓝牙」重新授权
default:
break
}
case .poweredOff:
os_log("蓝牙当前处于关闭状态")
case .poweredOn:
os_log("启动 cbManager")
// 此处开始扫描/连接
default:
break
}
}
Source: CentralViewController.swift(省略号与注释为整理时补充,分支结构取自实际源码)
Configuration Options
Info.plist 中与本页主题相关的全部配置项:
| 配置项 | 类型 | 默认/示例值 | 说明 |
|---|---|---|---|
NSBluetoothAlwaysUsageDescription | String | "Use bluetooth to discover, connect to, and share information with nearby devices" | 必填。iOS 13+ 蓝牙用途说明,展示于系统授权弹窗;缺失会导致蓝牙 API 无法使用 |
UIBackgroundModes | Array<String> | ["bluetooth-central", "bluetooth-peripheral"] | 后台运行模式声明,数组形式 |
UIBackgroundModes → bluetooth-central | String | 已声明 | 允许后台扫描/连接(中央角色) |
UIBackgroundModes → bluetooth-peripheral | String | 已声明 | 允许后台广播(外设角色) |
UIRequiredDeviceCapabilities | Array<String> | ["armv7"] | 安装限制:要求设备满足指定硬件能力 |
CFBundleDisplayName | String | "CBClassic" | 桌面显示名称,便于区分多个示例工程 |
UIMainStoryboardFile / UILaunchStoryboardName | String | "Main" / "LaunchScreen" | Storyboard 驱动的界面入口,与蓝牙配置无直接关系 |
API Reference
本页涉及的公共接口均为 CoreBluetooth 框架自带 API,示例工程通过实现委托与读取属性来使用它们。
func centralManagerDidUpdateState(_ central: CBCentralManager)
CBCentralManagerDelegate 的必需回调,由系统在蓝牙管理器状态发生变化(含首次创建后)时调用。
参数:
central(CBCentralManager):触发回调的中央管理器实例,其state与authorization属性为当前状态快照。
说明: 该回调是蓝牙可用性的唯一权威信号。示例工程在此方法内完成所有状态分支处理(见 CentralViewController.swift)。
CBCentralManager.State(central.state)
| 枚举值 | 含义 | 示例工程处理 |
|---|---|---|
.unknown | 状态未知(初始化中) | 默认分支,忽略 |
.resetting | 蓝牙服务正在重置 | 默认分支,忽略 |
.unsupported | 设备不支持蓝牙 | 默认分支,忽略 |
.unauthorized | 未获得授权 | 进一步检查 central.authorization |
.poweredOff | 蓝牙已关闭 | os_log 记录提示 |
.poweredOn | 蓝牙可用 | 启动管理器开始扫描 |
CBManagerAuthorization(central.authorization)
在 central.state == .unauthorized 时用于细分原因:
| 枚举值 | 含义 | 处理建议 |
|---|---|---|
.restricted | 系统级限制(家长控制、MDM 等) | 提示用户权限受系统限制,无法自行解除 |
.denied | 用户拒绝授权 | 引导前往「设置 → 隐私 → 蓝牙」重新开启 |
.allowedAlways | 已获得始终允许 | 理论上不应出现在 .unauthorized 分支中 |
.notDetermined | 尚未决定 | 等待系统弹窗或再次触发请求 |
Failure Modes、边界情况与并发
权限被拒(.denied)
- 现象:
central.state == .unauthorized且central.authorization == .denied。 - 影响:所有扫描、连接、广播 API 均不可用,且不会再次自动弹窗。
- 处理:应提供明确的 UI 指引,引导用户前往「设置 → 隐私 → 蓝牙」手动开启;可考虑使用
UIApplication.openSettingsURLString跳转。示例工程在centralManagerDidUpdateState中预留了该分支(case .restricted/ 其他 authorization 分支,见源码 L72-L80 区域)。
系统限制(.restricted)
- 现象:授权状态为
.restricted,通常由家长控制、企业 MDM 或越狱环境导致。 - 处理:App 无法通过任何 API 改变该状态,只能提示用户联系设备管理者解除限制。这是与
.denied最重要的区别——不要把.restricted当作.denied处理,否则用户会陷入「去设置里也找不到开关」的困惑。
蓝牙硬件关闭(.poweredOff)
- 现象:
central.state == .poweredOff,此时即使授权为allowedAlways也无法使用。 - 处理:示例工程通过
os_log记录日志;生产环境建议监听该状态并在蓝牙重新开启(回到.poweredOn)时自动恢复扫描——由于centralManagerDidUpdateState会在开关状态变化时再次回调,天然支持「重连恢复」逻辑,无需额外监听。
状态判断的并发/时序注意点
centralManagerDidUpdateState回调一定发生在主线程,可直接操作 UI;但不要在创建CBCentralManager的同一条调用链上立即执行扫描,必须等待首次回调确认.poweredOn,否则扫描会静默失败。central.state与central.authorization是两个独立维度,判断顺序必须先 state 后 authorization(示例代码正是如此),避免在硬件关闭时误报权限问题。
性能与运维注意事项
- 弹窗时机:iOS 会在首次创建
CBCentralManager/CBPeripheralManager时触发权限弹窗。若在 App 启动瞬间就创建管理器,弹窗会过早出现,影响体验;建议在用户进入蓝牙功能页时再创建。示例工程将管理器交由视图控制器管理,天然符合这一实践。 - 后台功耗:声明
bluetooth-central/bluetooth-peripheral后,系统允许后台继续蓝牙活动,但扫描(尤其是高占空比扫描)会显著增加功耗。生产 App 应使用CBCentralManagerScanOptionAllowDuplicatesKey等参数控制扫描策略,并在不需要时及时停止扫描。 - 调试日志:示例工程用
os_log输出状态,可借助 Console.app / Xcode 的 unified logging 查看蓝牙状态流转,排查「权限 vs 开关」类问题。
Extension Points
- 授权流程上移:示例工程把状态处理放在视图控制器内。生产工程可将其抽象为
BluetoothAuthorizationService(单例),统一处理.denied跳转设置、.restricted提示等策略,供多个页面复用——只需保持CBCentralManagerDelegate回调仍落在主线程即可。 - 跨页面共享管理器:可将
CBCentralManager提升为 App 级单例,配合本页的权限/后台配置,实现「一次授权、全局可用」。 - 其他蓝牙框架衔接:若后续引入 External Accessory(MFi)或 CoreBluetooth 低功耗(BLE)能力,本页的权限声明策略依然适用——
NSBluetoothAlwaysUsageDescription同时覆盖 BLE 与经典蓝牙(该示例即基于 CoreBluetooth Classic,参见仓库目录UsingCoreBluetoothClassic)。
Related Links
- CentralViewController.swift — 中央设备角色:权限状态处理、扫描与连接逻辑
- PeripheralViewController.swift — 外设角色:广播与对外设连接的处理(对应
bluetooth-peripheral后台模式) - AppDelegate.swift — 应用入口
- Info.plist — 本页全部配置项的原始文件
- UsingCoreBluetoothClassic/README.md — 示例工程说明文档
- SampleCode.xcconfig — 工程构建配置(签名/标识符相关)
- 相关页面:中央设备扫描与连接流程、外设广播流程(本目录下其他 catalog 页面)