iOS通过Container读取文件问题笔录
问题背景及描述
在Kotlin Multiplatform项目中,实现iOS平台的文件存储时,虽然可以通过导出.xcappdata
文件确认文件确实存在于目录下,但在调用NSData.dataWithContentsOfFile
读取时却始终返回空。错误信息显示找不到该文件或目录 (No such File or Directory
)。
示例代码如下:
val errorPtr: ObjCObjectVar<NSError?> = alloc()
NSData.dataWithContentsOfFile(fullPath, options = 0, error = errorPtr.ptr)?.let { bytes ->
val array = ByteArray(bytes.length.toInt())
bytes.getBytes(array.refTo(0).getPointer(this), bytes.length)
return@withContext array
}
println(errorPtr.value?.description.orEmpty())
return@withContext null
Note: 为了更好地定位问题,添加了errorPtr
以查看具体错误信息
问题定位
- 通过错误日志发现,
fullPath
(这指的是问题描述中kotlin代码上的fullPath)可能是错误的路径。尽管导出的.xcappdata
确认文件确实存在,但NSData.dataWithContentsOfFile
依旧找不到。 - 通过对比
fullPath
和 Swift 代码获取的目录路径,发现 Application 目录后的 UUID 并不一致。例如:
- fullPath路径是这样的
/var/mobile/Containers/Data/Application/3FC1CCE9-A788-47AB-902A-FA133FAA3D30/Documents/60F570BE-2A16-4EFB-96B3-9203C0A0ABCE.jpg
- 而文件url则是
file:///private/var/mobile/Containers/Data/Application/C5AC5178-6887-40F4-9EE5-8D56CB830CB3/Documents/60F570BE-2A16-4EFB-96B3-9203C0A0ABCE.jpg
Swift代码获取文件路径代码
let fileManager = FileManager.default
let documentsURL = fileManager.urls(for: .documentDirectory, in: .userDomainMask)[0]
do {
let fileURLs = try fileManager.contentsOfDirectory(at: documentsURL, includingPropertiesForKeys: nil)
print(fileURLs)
// process files
} catch {
print("Error while enumerating files \(documentsURL.path): \(error.localizedDescription)")
}
- 经过一番查阅了解,自 iOS 8.0 开始,每次用户卸载并重新安装应用时,应用的 沙盒目录路径(
Application
目录)中的 UUID 部分都会发生变化。因此,存储完整路径是不可靠的。
解决方案
通过上述,已经确认问题是由于路径不一致导致,只要解决路径问题即可。这里提供两种比较直观的解决方案
方法1. 遍历目录,通过fileName过滤拿到对应的文件
既然存储路径会变化,而文件存在,可以遍历 Documents 目录并通过文件名匹配来获取文件路径。
// 拿到目录的url
val url = fileManager.URLForDirectory(
directory = NSDocumentDirectory,
inDomain = NSUserDomainMask,
appropriateForURL = null,
create = false,
error = null
)!!
// 拿到目录下所有文件的url
val fileUrls = fileManager.contentsOfDirectoryAtURL(url = url, includingPropertiesForKeys = null, options = 0, error = errorPtr.ptr)
// 通过全路径拿到文件名,然后通过文件名过滤得到唯一的文件url
val currentFileUrl = fileUrls?.first { it.toString().contains(fileName) } as? NSURL
// 读取到NSData中
currentFileUrl?.let {
NSData.dataWithContentsOfURL(currentFileUrl)
?.let { bytes ->
val array = ByteArray(bytes.length.toInt())
bytes.getBytes(array.refTo(0).getPointer(this), bytes.length)
return@withContext array
}
}
方法2. 直接获取 Documents 目录路径并拼接文件名
遍历文件目录可能带来性能开销,优化方案是直接获取 Documents 目录的路径并拼接文件名:
val documentDirectory = NSSearchPathForDirectoriesInDomains(
directory = NSDocumentDirectory,
domainMask = NSUserDomainMask,
expandTilde = true
).first() as NSString
val fullPath = documentDirectory.stringByAppendingPathComponent(fileName)
println(fullPath)
这样可以保证 fullPath
始终指向当前应用实例的正确 Documents
目录,避免了 UUID 变化带来的问题。
总结
- 不要存储完整路径,因为 iOS 8.0 之后 Application 目录的 UUID 会发生变化。
- 推荐存储文件名,在使用时动态获取 Documents 目录并拼接文件名。
- 优先使用
NSSearchPathForDirectoriesInDomains
获取 Documents 目录路径,比遍历文件更高效。
通过上述方法,成功解决了 NSData.dataWithContentsOfFile 无法读取文件的问题。
附
安装/运行方式 | UUID 是否变化 |
---|---|
App 卸载后重新安装 | ✅ 变化(iOS 8+ 开始引入的安全机制) |
App 通过 Xcode 重新安装 | ✅ 变化(Xcode 重新部署应用时相当于卸载+安装) |
App 通过 Xcode 直接运行(不卸载) | ❌ 不变(如果 Xcode 仅执行 Build & Run ,不删除 App) |
App 在模拟器上运行 | ✅ 变化(某些情况下,每次启动可能会变化) |
App 通过 TestFlight 或 App Store 安装 | ✅ 变化(安装时生成新 UUID) |
iOS OTA(无线更新 App) | ❌ 不变(更新时不会改变 UUID) |