简介面向 Objective-C 开发者的 iOS 文件读取示例工程围绕系统文件选择器讲解如何从 iPhone 的文件 App 中读取所选文件。工程覆盖创建文件选择器、限定可选文件类型、呈现选择界面、在代理回调中取得文件地址以及使用二进制数据类或字符串类读取内容的完整流程并给出权限不足、读取失败等场景的异常处理思路针对图像、音频等不同类型数据也说明了如何结合对应框架进一步处理。压缩包共 1203 个文件其中头文件与实现文件合计超过 900 个承担主要业务逻辑另有图片资源、工程配置文件、界面描述文件与说明文档从文件类型即可看出工程包含源码、配置、界面与文档几类内容方便按类型查阅。包体约 5.67MB体量适中。目前已有 80 人学习示例代码完整、贴近真实开发场景可帮助理解文件选择器的调用细节并迁移到自身项目。1. OC 里的 DocumentPickeriPhone 文件 App 之外唯一合规的读文件入口做 iOS 开发的人都懂App 沙盒是个好东西它保证了数据隔离但也把「读取 iPhone 文件 App 里的文件」这件事变得麻烦。直接访问绝对路径沙盒之外你连路径都拿不到调私有接口审核直接打回。而 UIDocumentPickerViewController也就是开发者常说的 DocumentPicker是系统明牌给出的余地——它把文件 App 里用户主动选择的文件以「安全作用域 URL」的形式交给你后面能不能读、读多久全看你这次授权怎么用。这篇笔记要解决的问题是用 Objective-C 从 DocumentPicker 拿到文件后怎么稳定地读出内容、拷进沙盒以及过程中那些不跑一遍根本发现不了的问题。适合三类人第一次接文件选择的开发者、从 Swift 转 OC 维护老项目的熟手、被「明明拿到了 URL 却读不到数据」折磨的排查者。看完你能落地一个完整的读取闭环也知道每个参数动了会有什么后果。2. UIDocumentPickerViewController 的授权链路从安全作用域到数据读取2.1 一次选择背后发生了什么安全作用域 URL很多开发者第一次接触 DocumentPicker 时以为它和相册选择器一样选完把文件复制到临时目录就完事。实际不是。你从 delegate 回调里拿到的 URL是一个带有安全作用域security-scoped标记的文件 URL。这个标记是系统贴在这个 URL 扩展属性上的授权凭证它不等同于「你能访问沙盒外任意路径」而是只对被选中的这一个文件或目录生效。理解这条链路很重要选择文件 → 系统生成安全作用域 URL → 你的 App 对该 URL 获得了临时访问权 → 你在访问前必须调用startAccessingSecurityScopedResource→ 读完必须调用stopAccessingSecurityScopedResource释放授权。前面两步由 DocumentPicker 替你做后面两步是你代码里的事漏了任意一步都会出现「URL 看起来正常一读就失败」的诡异现象。另外要注意这个授权是有生命周期的。它跟随 URL 存活App 重启后失效。如果你希望记住用户曾经授权过的文件下次启动还能直接读就得把 URL 转成 bookmark书签数据存起来这个后面第 5 章单独讲。2.2 用 documentPicker 而不是 fileImporter 的选型考量iOS 14 之后系统新增了fileImporter相关能力很多新项目会问是不是应该用它替代 DocumentPicker我的结论是如果你维护的是 OC 代码库或者要兼容老版本DocumentPicker 仍然是更稳的选择。fileImporter本质上是 SwiftUI 封装的视图底层还是 UIDocumentPickerViewController但它的配置方式、错误处理都更偏向 Swift 生态OC 里接起来要么桥接要么绕路收益不大。反过来如果只做一次性读取比如用户选一个 JSON、CSV、PDF 进来用 DocumentPicker 还能配合asCopy参数让系统直接帮你复制副本连安全作用域的管理都省了。但代价是拿到的副本在 tmp 目录系统可能随时清理也不适合做「记住这个文件、持续追踪它的变化」这类需求。所以选型逻辑很简单只读一次用asCopy为 YES需要长期持有引用、持续读取或监听外部修改用asCopy为 NO自己做安全作用域管理。这个决定要在初始化 Picker 之前做好因为asCopy是初始化参数创建之后改不了。2.3 delegate 回调与 UTTypeiOS 14 前后的两套写法OC 项目里最容易翻车的地方之一是 DocumentPicker 的 delegate 方法在不同 iOS 版本下有两套。iOS 14 之前Picker 的初始化要用initWithDocumentTypes:inMode:对应的回调是documentPicker:didPickDocumentAtURL:。iOS 14 之后初始化改用initForOpeningContentTypes:asCopy:回调统一为documentPicker:didPickDocumentsAtURLs:旧的单 URL 回调被废弃。两套写法混用会出现什么结果在 iOS 14 以上跑旧方法Picker 正常弹出来但用户选完文件后 delegate 根本没反应。因为系统只认新的批量回调你实现的那个旧方法不会被调用。反过来说在老系统上只实现新方法回调同样不触发。UTType 参数也值得单独说。iOS 14 之后初始化时传的contentTypes数组直接决定哪些文件可选比如只让用户选 JSON[UTType.json]想全放开传UTType.data或UTType.item。注意 OC 里用的是UTType类对应 Swift 的UTType别写成老的kUTTypeJSON字符串常量那套在 iOS 14 里已经被标记废弃了。3. 用 OC 拉回文件的最小闭环代码流程与参数落点3.1 创建 Picker 的完整配置下面这段代码是从 OC 工程里初始化 DocumentPicker 的标准姿势。以「允许选择任意文件、只读一次」为例后续按需调整参数即可。#import UIKit/UIKit.h #import UniformTypeIdentifiers/UniformTypeIdentifiers.h // 初始化 DocumentPicker - (void)presentDocumentPicker { // 1. 指定可选文件类型这里放开为任意数据文件 NSArrayUTType * *contentTypes [UTType.data]; // 2. asCopy YES让系统直接复制文件副本到沙盒 tmp 目录 UIDocumentPickerViewController *picker [[UIDocumentPickerViewController alloc] initForOpeningContentTypes:contentTypes asCopy:YES]; // 3. 允许用户多选文件 picker.allowsMultipleSelection YES; // 4. 设置 delegate picker.delegate self; // 5. 弹出方式iPhone 上默认全屏 presentiPad 上必须配 popover 锚点 picker.modalPresentationStyle UIModalPresentationFormSheet; [self presentViewController:picker animated:YES completion:nil]; }参数说明contentTypes传UTType.data意味着扩展名五花八门的文件都能选如果你想只收 PDF改成[UTType.pdf]即可用户连灰色不可选状态都看不到。asCopy传 YES 时系统生成的是一个沙盒内副本的 URL不需要安全作用域管理适合多数「导入一次」的场景传 NO 则拿到原件 URL必须搭配第 3.2 节的startAccessingSecurityScopedResource才能读。3.2 delegate 里拿 URL 只是开始用户选完文件后系统回调你的 delegate。这一步要从didPickDocumentsAtURLs:里取出 URL 数组然后根据asCopy的取值决定后续路径。#pragma mark - UIDocumentPickerDelegate - (void)documentPicker:(UIDocumentPickerViewController *)controller didPickDocumentsAtURLs:(NSArrayNSURL * *)urls { // 1. 遍历用户选择的所有文件 for (NSURL *url in urls) { // 2. 判断 URL 是否带安全作用域如果是必须显式请求访问 BOOL accessed NO; if (url.isSecurityScopedResource) { accessed [url startAccessingSecurityScopedResource]; if (!accessed) { NSLog([Picker] 安全作用域访问失败: %, url); continue; } } // 3. 读取文件内容具体实现在下一节 NSData *data [self readFileDataFromURL:url]; // 4. 读完立刻释放安全作用域避免授权堆积 if (accessed) { [url stopAccessingSecurityScopedResource]; } // 5. 交给业务层处理 data [self handleImportedData:data fromURL:url]; } }逻辑说明isSecurityScopedResource是 URL 的一个属性用来判断这个 URL 是不是带安全作用域。asCopy为 YES 时系统返回的副本 URL 通常不是安全作用域所以这个判断会直接跳过start调用。但我在代码里仍然保留了判断分支因为某些系统版本或特殊文件类型下即使是副本也可能带作用域标记。startAccessingSecurityScopedResource返回 BOOL表示授权是否成功。一个容易忽略的点即使返回 NO后续也能打开文件但那是碰运气——系统可能因为某些缓存机制放行了等文件一移动或 App 重启这个问题就会以「之前好好的这次读不了」的形式爆发。3.3 文件协调器读取与沙盒副本拿到 URL 之后用NSData dataWithContentsOfURL:直接读是最省事的写法但对 iCloud 文件或在其他 App 里正被编辑的文件来说这种读取方式不保证拿到的是最新一致的数据。正确做法是套一层NSFileCoordinator让系统帮你处理文件当前的状态。- (NSData *)readFileDataFromURL:(NSURL *)url { __block NSData *data nil; __block NSError *coordinationError nil; // 1. 创建文件协调器指定读写类型 NSFileCoordinator *coordinator [[NSFileCoordinator alloc] initWithFilePresenter:nil]; // 2. 协调读取等待其他进程对该文件的写入操作完成 [coordinator coordinateReadingItemAtURL:url options:NSFileCoordinatorReadingWithoutChanges error:coordinationError byAccessor:^(NSURL *newURL) { // 3. 注意byAccessor 里拿到的 newURL 才是安全作用域内的可用地址 data [NSData dataWithContentsOfURL:newURL]; if (!data) { NSLog([Picker] 读取失败: %, newURL); } }]; if (coordinationError) { NSLog([Picker] 协调读取出错: %, coordinationError); } return data; }参数说明NSFileCoordinatorReadingWithoutChanges表示你只想读取、不修改文件这是最常用的选项。如果你接下来要做的是「读取并剪切原文件」要改用NSFileCoordinatorReadingForUploading等选项。byAccessor回调里拿到的newURL不一定是传入的url系统可能会在协调过程中调整 URL比如 iCloud 文件下载到本地后路径会变所以读取一定要用回调里的地址。如果你希望更彻底地把文件握在手里读完副本后把它从 tmp 目录搬到 Documents 或 Application Support 目录。tmp 里的文件在系统空间紧张时会被清理尤其用户选了多个大文件时风险更高。搬运用NSFileManager的moveItemAtURL:toURL:error:即可搬完记得确认目标目录的扩展文件保护属性。3.4 读完后收尾的规范动作读文件这件事最容易漏的是收尾。很多人调用了startAccessingSecurityScopedResource但忘记配对调用stopAccessingSecurityScopedResource。后果是什么授权对象一直悬在内存里一次两次没事用户连续选几十个文件、来回选目录内存里堆积的安全作用域会越来越多最终表现为系统弹窗卡顿、文件访问变慢甚至被系统强杀。// 收尾动作用一个专门的方法封装确保无论是正常结束还是异常退出都会执行 - (void)finishAccessingURL:(NSURL *)url { if (url.isSecurityScopedResource) { [url stopAccessingSecurityScopedResource]; } }规范流程是这样的在didPickDocumentsAtURLs:里对每个安全作用域 URL 先start读取完成后立即stop。如果你把 URL 存到了某个属性里准备后续用也要先拿到文件的完整数据、复制好副本再stop因为stop之后这个 URL 的访问权就失效了。另外当 DocumentPicker 以目录形式被打开用户选的是一个文件夹时URL 指向的是目录。读取目录里的文件时每个子文件 URL 也要单独走一遍安全作用域管理不能只对目录start一次就完事。4. 实战避坑DocumentPicker 读取文件最容易翻车的五个位置4.1 现象文件明明选中了open 和 dataWithContentsOfURL 都返回空这是我见过最多人踩的坑包括我自己早期也栽过。现象很统一delegate 正常回调、URL 也打印得出来但dataWithContentsOfURL:返回 nil或者openFile:返回 NO。很多开发者会怀疑是文件权限问题甚至去改 Info.plist 加权限描述加了也没用。原因创建 Picker 时asCopy传了 NO拿到的 URL 是原件带安全作用域但读取前没有调用startAccessingSecurityScopedResource。iOS 的沙盒机制在这里表现得很「阴」——你有 URL有文件名看起来一切正常可一旦访问沙盒外路径系统直接拒绝不抛异常、不给错误信息就一个静默失败。解决在读取前先判断isSecurityScopedResource是的话必须start读完stop。这就是第 3.2 节代码里那个判断分支的用途。如果你已经用asCopy:YES的写法这个问题理论上不会出现但建议代码里还是保留判断防的是系统行为变化。4.2 现象iPad 上点击按钮弹出 Picker 直接崩溃崩溃信息通常是UIPopoverPresentationController should have a non-nil sourceView or barButtonItem。iPhone 上跑得好好的一上 iPad 就崩原因和 DocumentPicker 本身无关而是系统的弹出方式要求。原因iPad 上的 FormSheet 弹窗依赖 popover 锚点也就是popoverPresentationController需要知道箭头指向哪里。如果你直接present而没有设置sourceView和sourceRect系统不知道锚点在哪直接崩溃。解决在 present 之前配置 popoverPresentationController 的锚点。picker.popoverPresentationController.sourceView self.view; picker.popoverPresentationController.sourceRect CGRectMake(self.view.bounds.size.width / 2, self.view.bounds.size.height / 2, 1.0, 1.0); picker.popoverPresentationController.permittedArrowDirections UIPopoverArrowDirectionAny;这段代码放在 iPhone 上也没副作用所以我一般会直接写上省得后期适配 iPad 时再排查。4.3 现象文件复制到一半源文件在另一个 App 里被改了这是一个比较隐蔽的问题场景是用户一边在文件 App 里开着这个文档编辑一边又在我们的 App 里导入同一个文件。你正在复制对方正在写最终复制出来的数据可能是半新半旧的混合体。原因没有使用文件协调器就直接读取。普通dataWithContentsOfURL:不会去和文件当前的所有者协商读写顺序它认为自己有完全的读取权实际拿到的可能是写入中途的快照。解决用第 3.3 节的NSFileCoordinator包一层。coordinateReadingItemAtURL:会等待其他进程的写入协调器完成操作后再读取保证你拿到的数据是写入完成后的版本。4.4 现象选了个 iCloud 文件读的时候卡了十几秒然后返回空数据iCloud 上未下载的文件本地只有一个占位文件。你用安全作用域 URL 去读系统会尝试下载这个下载动作是异步的如果你在主线程同步等待结果界面直接卡死下载慢一点就卡个十几秒最后还可能因为下载失败返回空数据。原因没有处理 iCloud 文件的状态。URL 对应的文件可能是NSURLUbiquitousItemIsDownloadedKey为 NO 的状态直接读相当于请求系统当场下载但下载完成时间不可控。解决读取前先检查文件的 iCloud 状态如果还没下载先触发下载再等待完成。NSURL *url /* 从 delegate 拿到的 URL */; NSNumber *downloaded nil; [url getResourceValue:downloaded forKey:NSURLUbiquitousItemIsDownloadedKey error:nil]; if (downloaded !downloaded.boolValue) { // 触发下载 NSError *downloadError nil; [[NSFileManager defaultManager] startDownloadingUbiquitousItemAtURL:url error:downloadError]; // 等待 NSURLUbiquitousItemDownloadingStatusKey 变为最新 }注意startDownloadingUbiquitousItemAtURL:也是异步的你需要通过 KVO 监听文件的下载状态或者用轮询配合等它真正下载完成后再读取。4.5 现象asCopy:YES拿到副本放 tmp 目录隔天文件没了这是一个容易忽视的坑。asCopy:YES把副本放到 tmp 目录tmp 会被系统在空间不足、App 被杀等场景下清空。很多开发者会把 tmp 里的文件路径存到数据库当作永久路径用结果第二天用户打开 App路径还在文件已经被系统清掉。原因tmp 目录就不是给长期存储用的。副本是给「读一次就完事」设计的要么立刻消费要么搬到 Documents。解决拿到副本 URL 后立即用NSFileManager把文件移动到 Documents 或 Application Support 子目录。搬移是同步操作不会额外弹窗搬完再存路径、做后续处理才能真正把文件握在手里。5. 进阶把「一次性授权」变成「长效访问权限」的两个技巧5.1 用 bookmark 保住安全作用域开发者接触 DocumentPicker 到后期通常都会遇到一个需求用户上次导入了一个文件希望下次打开 App 时还能直接读到它的更新。普通 URL 做不到因为安全作用域授权不跨启动。而 bookmark 就是系统提供的后悔药——它能把 URL 连同安全作用域一起序列化成 data存进数据库或 UserDefaults下次启动时解出来重新获得访问权。// 保存 bookmark在 stop 之前生成并存储 NSData *bookmark [url bookmarkDataWithOptions:NSURLBookmarkCreationWithSecurityScope includingResourceValuesForKeys:nil relativeToURL:nil error:nil]; [[NSUserDefaults standardUserDefaults] setObject:bookmark forKey:lastImportedFile]; // 恢复 bookmark拿到 URL 后照常 startAccessing NSData *savedBookmark [[NSUserDefaults standardUserDefaults] objectForKey:lastImportedFile]; NSURL *restoredURL [NSURL URLByResolvingBookmarkData:savedBookmark options:NSURLBookmarkResolutionWithSecurityScope relativeToURL:nil bookmarkDataIsStale:isStale error:nil]; BOOL accessed [restoredURL startAccessingSecurityScopedResource];生成 bookmark 的时机应该在stop之前因为stop之后 URL 的作用域失效生成的书签可能不完整。恢复时也要走一遍start因为这个 URL 虽然被 bookmark 恢复了权限但访问前仍需显式请求。这套机制在我做「记住用户最近导入的文件」这类功能时必用比每次让用户重新选一遍体验好得多。5.2 用文件协调器监听外部文件的修改最后一个技巧如果用户导入的是一个会在外部持续更新的文件比如一个别人通过 AirDrop 发来、正在协作编辑的文档你可以注册一个NSFilePresenter监听它的变动。这样即使文件在沙盒外只要 bookmark 还在、授权没过期你的 App 就能拿到「文件被修改了」的通知然后重新读取内容。interface MyFilePresenter : NSObject NSFilePresenter property (strong) NSURL *presentedItemURL; - (void)presentedItemDidChange; end implementation MyFilePresenter - (void)presentedItemDidChange { // 文件外部变化重新协调读取 NSFileCoordinator *coordinator [[NSFileCoordinator alloc] initWithFilePresenter:self]; [coordinator coordinateReadingItemAtURL:self.presentedItemURL options:NSFileCoordinatorReadingWithoutChanges error:nil byAccessor:^(NSURL *newURL) { // 在这里重新读取 }]; } end这个用法有一个前提文件 URL 必须来自 bookmark 恢复且授权成功否则presentedItemURL你会因为沙盒限制收不到变更通知。做监听时记得把 presenter 对象持有住别用临时变量否则注册后立刻被释放通知也自然丢失。OC 项目里这套组合我用下来最顺手也是「文件导入」功能从能用走向好用的分水岭。总结起来就一个习惯凡是用 DocumentPicker 拿文件先问自己一句「这次要一次性读还是要长期跟踪」再决定asCopy和安全作用域的组合方式。URLL 拿到手也不要急着开心第一步判断是不是安全作用域第二步协调器读取第三步搬到沙盒第四步存 bookmark。这套流程跑顺了文件导入就不会再出幺蛾子。希望帮到你。本文还有配套的精品资源点击获取