在 App 中支持本地网络隐私
Support local network privacy in your app
2020年6月23日
一句话判断
iOS 14 新增了本地网络权限弹窗,如果你的 App 会扫描局域网设备(如打印机、智能家居、AirPlay),必须在 Info.plist 中声明使用原因,否则网络请求会静默失败。
这场 Session 讲了什么
iOS 14 引入了一项重要的隐私保护:App 在访问本地网络之前必须获得用户授权。当你的 App 第一次尝试使用 Bonjour、mDNS、或直接访问本地 IP 地址时,系统会弹出一个权限请求对话框。用户可以选择允许或拒绝。如果用户拒绝了,所有本地网络流量会被静默丢弃——你的 App 不会收到任何错误提示。
Session 详细讲解了哪些网络行为会触发权限弹窗。主要包括:Bonjour 服务发现(NSBonjourServices)、本地 IP 地址通信(如 192.168.x.x 或 10.x.x.x)、多播和广播通信。同时,Session 介绍了如何在 Info.plist 中配置 NSLocalNetworkUsageDescription 来提供权限请求的理由文本,以及如何使用 NWPathMonitor 检测本地网络是否可用。
值得深挖的点
静默失败的陷阱
这是最需要警惕的行为。当用户拒绝本地网络权限后,你的网络请求不会返回错误码,而是直接超时或返回空数据。这意味着你可能花大量时间调试”为什么设备发现不工作”,最终发现是权限问题。建议在 App 中使用 NWPathMonitor 检测本地网络的可用性,在权限被拒绝时给用户明确的引导。
Bonjour 服务的精确声明
你需要在 Info.plist 的 NSBonjourServices 中列出你的 App 使用的所有 Bonjour 服务类型。比如 _http._tcp、_airplay._tcp 等。只声明你实际需要的服务类型,不要列出所有可能的服务。Apple 会审查这个列表,过多的服务声明可能被拒绝。
代码片段
配置 Info.plist
场景:为智能家居 App 声明本地网络权限。
<!-- 在 Info.plist 中添加 -->
<!-- 本地网络使用说明 -->
<key>NSLocalNetworkUsageDescription</key>
<string>需要访问本地网络来发现和控制您的智能家居设备</string>
<!-- 声明使用的 Bonjour 服务类型 -->
<key>NSBonjourServices</key>
<array>
<string>_http._tcp</string>
<string>_smartplug._tcp</string>
<string>_huebridge._tcp</string>
</array>
检测本地网络可用性
场景:在 App 启动时检查本地网络权限状态。
import Network
class LocalNetworkMonitor {
private let monitor = NWPathMonitor(requiredInterfaceType: .other)
private let queue = DispatchQueue(label: "network-monitor")
var isLocalNetworkAvailable: Bool = false
func startMonitoring() {
monitor.pathUpdateHandler = { [weak self] path in
// 检查是否有本地网络访问能力
let hasLocalNetwork = path.status == .satisfied
// 如果使用了本地地址,检查是否可达
DispatchQueue.main.async {
self?.isLocalNetworkAvailable = hasLocalNetwork
if !hasLocalNetwork {
print("本地网络不可用,可能是权限被拒绝")
self?.showPermissionGuidance()
}
}
}
monitor.start(queue: queue)
}
func stopMonitoring() {
monitor.cancel()
}
private func showPermissionGuidance() {
// 提示用户去设置中开启本地网络权限
let alert = UIAlertController(
title: "无法发现设备",
message: "请在「设置」中允许访问本地网络",
preferredStyle: .alert
)
alert.addAction(UIAlertAction(title: "去设置", style: .default) { _ in
if let url = URL(string: UIApplication.openSettingsURLString) {
UIApplication.shared.open(url)
}
})
alert.addAction(UIAlertAction(title: "取消", style: .cancel))
// 显示弹窗
if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
let rootVC = windowScene.windows.first?.rootViewController {
rootVC.present(alert, animated: true)
}
}
}
使用 Bonjour 发现设备
场景:发现局域网中的智能设备。
import Foundation
class DeviceDiscoveryManager: NSObject, NetServiceBrowserDelegate, NetServiceDelegate {
private var serviceBrowser: NetServiceBrowser?
private var discoveredServices: [NetService] = []
var onDeviceFound: ((String, String) -> Void)? // (名称, 地址)
func startDiscovery() {
serviceBrowser = NetServiceBrowser()
serviceBrowser?.delegate = self
// 搜索声明的 Bonjour 服务
serviceBrowser?.searchForServices(
ofType: "_smartplug._tcp",
inDomain: "local."
)
}
func stopDiscovery() {
serviceBrowser?.stop()
serviceBrowser = nil
}
// NetServiceBrowserDelegate
func netServiceBrowser(_ browser: NetServiceBrowser,
didFind service: NetService,
moreComing: Bool) {
print("发现设备: \(service.name)")
service.delegate = self
service.resolve(withTimeout: 5.0)
discoveredServices.append(service)
}
func netServiceBrowser(_ browser: NetServiceBrowser,
didRemove service: NetService,
moreComing: Bool) {
print("设备离线: \(service.name)")
discoveredServices.removeAll { $0 === service }
}
// NetServiceDelegate
func netServiceDidResolveAddress(_ sender: NetService) {
// 获取设备地址
if let hostName = sender.hostName {
print("设备地址: \(hostName):\(sender.port)")
onDeviceFound?(sender.name, "\(hostName):\(sender.port)")
}
}
}
使用 Network.framework 进行本地通信
场景:与局域网设备建立 TCP 连接。
import Network
class DeviceConnection {
private var connection: NWConnection?
func connect(to host: String, port: UInt16) {
let endpointHost = NWEndpoint.Host(host)
guard let endpointPort = NWEndpoint.Port(rawValue: port) else { return }
// 创建 TCP 连接
connection = NWConnection(
host: endpointHost,
port: endpointPort,
using: .tcp
)
// 设置状态回调
connection?.stateUpdateHandler = { state in
switch state {
case .ready:
print("连接成功")
case .failed(let error):
print("连接失败: \(error)")
// 如果是权限问题,不会返回特定错误
// 需要通过超时来判断
case .waiting(let error):
print("等待连接: \(error)")
default:
break
}
}
connection?.start(queue: .main)
}
func send(data: Data) {
connection?.send(content: data, completion: .contentProcessed { error in
if let error = error {
print("发送失败: \(error)")
}
})
}
}
最佳实践
已有项目:立即在 Info.plist 中添加 NSLocalNetworkUsageDescription 和 NSBonjourServices。否则 iOS 14 上你的设备发现功能会直接失效。在 App 中加入本地网络可用性检测,当用户拒绝权限时给出明确的引导。在每次 App 更新时检查 Bonjour 服务列表是否需要更新。
新项目:从第一天就配置好本地网络权限声明。NSLocalNetworkUsageDescription 的文案应该具体说明你的 App 需要访问本地网络的原因——“需要访问网络”这样的模糊描述会被 App Review 打回。只为你的 App 实际使用的服务声明 Bonjour 类型。
还有什么值得关注
- 本地网络权限只影响局域网通信(RFC 1918 地址段),不影响互联网通信。
- 用户可以随时在「设置 > 隐私 > 本地网络」中更改权限。
- AirPlay 和 AirDrop 不受这个权限影响,它们有独立的系统级权限管理。