缺失 NSXxxUsageDescription 导致 App 启动即崩溃

iOS 在你调用隐私敏感 API 而 Info.plist 缺少对应用途描述时立即终结进程。定位、补 key、再发版。

你点了扫描小票按钮,App 卡了一帧,然后消失了。没崩溃弹窗、没错误提示 —— 直接回到主屏。打开设备 Console 看到这样一行:“This app has crashed because it attempted to access privacy-sensitive data without a usage description. The app’s Info.plist must contain an NSCameraUsageDescription key with a string value explaining to the user how the app uses this data.” iOS 在这种情况下不抛可恢复的异常;它直接用 SIGABRT 终结进程。修复就一行 Info.plist 字符串,但搞清楚是哪个 key(30 多个备选)、写一段能通过 App Review 的描述,需要一遍方法化的排查。

常见原因

按最常踩到的 API 面排序。

1. 没配对应 key 就访问相机 / 麦克风 / 相册

调用 AVCaptureDevice.requestAccess(for: .video)PHPhotoLibrary.requestAuthorization、或用 .camera source type 实例化 UIImagePickerController,而没有 NSCameraUsageDescriptionNSMicrophoneUsageDescriptionNSPhotoLibraryUsageDescription,会被立刻终结进程。

如何识别:崩溃栈里能看到 TCC(Transparency, Consent, Control)framework;console 行明确写出缺哪个 key。

2. 没配 NSLocationWhenInUseUsageDescription 就用定位

CLLocationManager.requestWhenInUseAuthorization() 需要 NSLocationWhenInUseUsageDescription。如果同时调用了 requestAlwaysAuthorization(),iOS 11+ 还要求两个 key 都在:NSLocationWhenInUseUsageDescriptionNSLocationAlwaysAndWhenInUseUsageDescription

如何识别:Console:“Trying to start MapKit location updates without prompting for location authorization.” 或者首次 locationManager.startUpdatingLocation() 即崩。

3. iOS 14+ 用 IDFA 但没配 NSUserTrackingUsageDescription

ATTrackingManager.requestTrackingAuthorization 需要 NSUserTrackingUsageDescription。没有这个 key 调用会挂住或静默返回 .denied;某些 SDK(Facebook、Branch)在 init 时甚至会硬崩。

如何识别:AppDelegate 调用 Settings.shared.advertiserTrackingEnabled = true 之类 SDK init 代码时首次启动就崩。

4. 没配 NSBluetoothAlwaysUsageDescription 就用蓝牙

实例化 CBCentralManagerCBPeripheralManager 需要 NSBluetoothAlwaysUsageDescription(iOS 13+)。老的 NSBluetoothPeripheralUsageDescription 已弃用;iOS 13+ 上只设老 key 还是崩。

如何识别:崩溃栈带 CoreBluetooth,console 写出 key 名。

5. iOS 14+ 本地网络访问

任何稍微复杂点的 socket 调用(NWConnection、multipeer、Bonjour 浏览)需要 NSLocalNetworkUsageDescription。iOS 13 上一切正常的代码到 iOS 14 上要么崩、要么连接静默超时。

如何识别:iOS 13 模拟器上正常,iOS 14+ 真机上会弹*“App 想要查找并连接局域网内的设备”*;如果 Info.plist 缺 key,提示根本不出现,连接静默超时。

6. 通讯录 / 日历 / 提醒 / Health / HomeKit / Motion

每个受保护资源都有自己的 key:NSContactsUsageDescriptionNSCalendarsUsageDescriptionNSRemindersUsageDescriptionNSHealthShareUsageDescriptionNSHealthUpdateUsageDescriptionNSHomeKitUsageDescriptionNSMotionUsageDescription。崩溃栈在 console 里永远会指出缺哪一个。

如何识别:Console:“This app has crashed because it attempted to access privacy-sensitive data without a usage description. The app’s Info.plist must contain an <KEY>…“

7. 源码里有但归档里被剥掉

最痛苦的一种:项目能编译、模拟器能跑,TestFlight build 一启动就崩。Build settings(自定义 Info.plist 路径、条件构建、重新生成 Info.plist 的脚本)在归档时把 key 剥走了。

如何识别:本地 Debug 正常;只有 Release / Distribution 归档崩。从 .ipa 中提取 Info.plist 看不到那个 key。

开始之前

  • 在真机上复现崩溃(不只是模拟器)—— 某些受保护 API 在模拟器上返回 mock 数据,永远不崩。
  • 把崩溃日志做 symbolicate,或者直接看实时 Console.app —— iOS 打出的那行错误明确告诉你缺哪个 key。
  • 看看问题是否只在 Release build 出现;如果是,看 build settings 而不是源码。

需要收集的信息

  • Console.app 中那条*“This app has crashed because…”*的完整原文。
  • 每个 target 的 Info.plist 跑 grep -E "NS[A-Z][A-Za-z]+UsageDescription" Info.plist 的输出。
  • 触发崩溃的具体 API 调用(相机、定位、ATT、蓝牙等)。
  • 启动时初始化的所有 SDK —— 很多 SDK 在 init 里就请求权限。
  • Build settings:INFOPLIST_FILEINFOPLIST_PREPROCESS、任何动 Info.plist 的自定义 build phase。

修复步骤

步骤 1:从 console 读出确切的缺失 key

设备插上 Mac,打开 Console.app,按 App 名过滤,复现崩溃。那行非常明确:

This app has crashed because it attempted to access privacy-sensitive data
without a usage description. The app's Info.plist must contain an
NSCameraUsageDescription key with a string value explaining to the user
how the app uses this data.

把它当权威 —— 准确指出该加哪个 key。

步骤 2:加上 key,写一段面向用户、具体的描述

打开 App target 的 Info.plist(或现代 Xcode 中的 INFOPLIST_KEY_* build settings)。加上:

<key>NSCameraUsageDescription</key>
<string>Scan receipts and barcodes to add items to your expense list.</string>

App Review 对描述的硬要求:

  • 必须具体(不能只写”用于相机访问”—— 要说明对用户的好处)。
  • 必须用用户本地语言;为每种支持的语言提供 InfoPlist.strings
  • 不要写”可能”/“也许”—— 描述实际用途。
  • 必须与权限弹出当下 App 真的在做的事一致。

步骤 3:确认 key 真的进了归档

构建并归档之后提取:

xcrun -sdk iphoneos PackageApplication \
  -v $ARCHIVE_PATH/Products/Applications/YourApp.app
unzip -p YourApp.ipa "Payload/YourApp.app/Info.plist" \
  | plutil -convert xml1 -o - - \
  | grep -A1 UsageDescription

或者更简单 —— 右键 .xcarchive → 显示包内容 → Products/Applications/YourApp.app → Info.plist,直接看。

步骤 4:项目使用现代 Xcode(没有 Info.plist 文件)的情况

Xcode 14+ 把 Info.plist key 存在 Build Settings 里。加:

INFOPLIST_KEY_NSCameraUsageDescription = Scan receipts and barcodes to add items to your expense list.

或者在 target 的 Info tab 点 + 加 key。build 系统会在编译期合成 Info.plist。

步骤 5:为每种支持的语言做本地化

Info.plist 中英文字符串是 fallback。每个 locale 加一份 InfoPlist.strings

// en.lproj/InfoPlist.strings
"NSCameraUsageDescription" = "Scan receipts and barcodes to add items to your expense list.";

// zh-Hans.lproj/InfoPlist.strings
"NSCameraUsageDescription" = "扫描小票和条码,将物品添加到你的支出列表。";

// es.lproj/InfoPlist.strings
"NSCameraUsageDescription" = "Escanea recibos y códigos de barras para agregar artículos a tu lista de gastos.";

没有本地化字符串的话,App Review 可能以”非英语用户在系统权限弹窗中看到英文”为由拒掉。

步骤 6:审计每个 SDK 暗藏的权限请求

SDK 在 init 时会请求权限:

nm YourApp.app/YourApp | grep -E "requestAuthorization|requestAccess|requestTracking"

或者查看你依赖的 SDK 的 release notes。常见嫌疑:Facebook SDK(ATT)、Branch(ATT、定位)、Firebase Messaging(通知)、Adjust(ATT)、音频类 SDK(麦克风)。SDK 碰过的每一项权限都需要在你的 Info.plist 里有用途描述,哪怕你自己代码完全没碰那个 API。

步骤 7:在 CI 加一个缺 key 检查

5 行脚本就能防止这个回归:

REQUIRED_KEYS="NSCameraUsageDescription NSPhotoLibraryUsageDescription NSLocationWhenInUseUsageDescription"
for key in $REQUIRED_KEYS; do
  /usr/libexec/PlistBuddy -c "Print :$key" "$APP_PATH/Info.plist" \
    || { echo "Missing $key"; exit 1; }
done

跑在归档好的 .app 上,而不是源码 Info.plist 上,这样 build settings 漂移也能抓到。

验证

  • 真机冷启动,触发原本崩溃的功能;系统权限弹窗带着你写的描述出现。
  • 拒绝权限,App 优雅处理(不崩、给出 fallback UI)。
  • 切到另一种语言再启动,描述文案是那种语言。
  • 昨天 TestFlight build 的崩溃日志在新 build 上不再复现。
  • 归档 build 通过 CI 检查。

长期预防

  • 为每个 target 维护一份”用途 key 清单”,列出代码路径需要哪些;每次发版前过一遍。
  • 把新增 SDK 当作触发”用途描述审查”的事件;SDK 文档总会列出涉及哪些权限。
  • 每个 shipped 语言都本地化每一个用途描述;CI 步骤检测 InfoPlist.strings 是否缺 key。
  • DEBUG 下在 App 启动时加一个运行时检查,断言每个 key 存在;缺失时直接 fatal 在开发期暴露。
  • 把归档的 Info.plist 作为构建产物保留;跨版本对比,发现静默丢失。
  • 复杂 App 写集成测试,每次在干净模拟器(无任何已授权)上走一遍受权限保护的功能;缺 key 立即暴露。

常见坑

  • 在系统权限弹窗预览里改字符串 —— 那是运行时显示,不是真正的来源。要改源码 Info.plist。
  • iOS 11+ 单独用 NSLocationAlwaysUsageDescription;Always 权限需要 NSLocationWhenInUseUsageDescriptionNSLocationAlwaysAndWhenInUseUsageDescription 两个。
  • 写”We need camera access.”这种空洞描述。App Review 会拒 —— 要具体。
  • iOS App target 和 Watch target 都调用某 API,但只在其中一个 target 设了 key。
  • 重构时删了 key 没注意以前触发它的 SDK 还在 —— 下个版本崩溃回归。
  • 以为模拟器能抓到这种问题 —— 模拟器对很多 API 返回 mock 数据,永远触发不到缺 key 崩溃。

FAQ

Q:Console 说要 NSCameraUsageDescription,但我没用相机,为什么?

某个打包进来的 SDK 在用。跑 grep -RE "AVCapture|UIImagePicker|requestAccess.*video" Pods/ 找出来。要么给那个 SDK 加描述,要么如果你不需要那个功能就把 SDK 删了。

Q:描述字符串能写空吗?

不能。App Review 会拒掉空字符串或一个词的描述。苹果要求至少一句话说明 App 怎么用这份数据。

Q:我 Watch App 崩了,但我已经在手机 App 的 Info.plist 里加了 key。

Watch App 有自己的 Info.plist。Watch target 的 Info.plist 也要加。App Clips、Widgets、以及任何独立初始化 CLLocationManager 的 framework 同理。

Q:为什么模拟器不崩?

模拟器对部分 API 的 TCC 强制是宽松的;比如 AVCaptureDevice.requestAccess 在模拟器上可能不查 Info.plist 直接返回。一定要在真机上测。设备测试本身有问题时,参考TestFlight Build 在设备上看不到

Q:App Review 说”用途说明太含糊”,怎样算合格?

审核员希望你说出具体功能:“To scan receipts for expense tracking,” 而不是 “For camera access.”。要引用 App 中使用这份数据的具体功能。

相关阅读

标签: #排查 #ios #info-plist #privacy #crash