杰理 SDK 文档中心
首页
首页
  • 快速开始

    • 环境要求与工程导入
    • 蓝牙权限与后台模式配置
  • GATT over BR/EDR 示例

    • 工程结构与核心组件
    • 中心设备:连接事件监听与设备列表
    • 外设交互:服务发现与特征读写

蓝牙权限与后台模式配置

本页介绍 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 若要使用蓝牙,必须同时满足两个层面的要求:

  1. 声明层面(静态配置):在 Info.plist 中声明 NSBluetoothAlwaysUsageDescription。从 iOS 13 开始,缺少该键会导致 App 在访问蓝牙时直接崩溃或被系统拒绝;该字符串会在系统权限弹窗中展示给用户,说明 App 使用蓝牙的用途。
  2. 授权层面(运行时状态):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 蓝牙权限处理的两个关键实践:

  1. 先检查硬件状态,再检查授权状态。central.state == .unauthorized 时进一步细分 central.authorization 为 .restricted(系统限制)还是 .denied(用户拒绝),因为两者的处理方式完全不同——前者只能提示用户联系系统管理员/家长解除限制,后者可以引导用户前往「设置 → 隐私 → 蓝牙」重新授权。
  2. 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 中与本页主题相关的全部配置项:

配置项类型默认/示例值说明
NSBluetoothAlwaysUsageDescriptionString"Use bluetooth to discover, connect to, and share information with nearby devices"必填。iOS 13+ 蓝牙用途说明,展示于系统授权弹窗;缺失会导致蓝牙 API 无法使用
UIBackgroundModesArray<String>["bluetooth-central", "bluetooth-peripheral"]后台运行模式声明,数组形式
UIBackgroundModes → bluetooth-centralString已声明允许后台扫描/连接(中央角色)
UIBackgroundModes → bluetooth-peripheralString已声明允许后台广播(外设角色)
UIRequiredDeviceCapabilitiesArray<String>["armv7"]安装限制:要求设备满足指定硬件能力
CFBundleDisplayNameString"CBClassic"桌面显示名称,便于区分多个示例工程
UIMainStoryboardFile / UILaunchStoryboardNameString"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 页面)
Prev
环境要求与工程导入