你点了扫描小票按钮,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,而没有 NSCameraUsageDescription、NSMicrophoneUsageDescription 或 NSPhotoLibraryUsageDescription,会被立刻终结进程。
如何识别:崩溃栈里能看到 TCC(Transparency, Consent, Control)framework;console 行明确写出缺哪个 key。
2. 没配 NSLocationWhenInUseUsageDescription 就用定位
CLLocationManager.requestWhenInUseAuthorization() 需要 NSLocationWhenInUseUsageDescription。如果同时调用了 requestAlwaysAuthorization(),iOS 11+ 还要求两个 key 都在:NSLocationWhenInUseUsageDescription 和 NSLocationAlwaysAndWhenInUseUsageDescription。
如何识别: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 就用蓝牙
实例化 CBCentralManager 或 CBPeripheralManager 需要 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:NSContactsUsageDescription、NSCalendarsUsageDescription、NSRemindersUsageDescription、NSHealthShareUsageDescription、NSHealthUpdateUsageDescription、NSHomeKitUsageDescription、NSMotionUsageDescription。崩溃栈在 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_FILE、INFOPLIST_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 权限需要NSLocationWhenInUseUsageDescription加NSLocationAlwaysAndWhenInUseUsageDescription两个。 - 写”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