iOS 读取存储在Container的文件问题

问题描述

在使用KMM实现iOS平台文件存储时,通过导出container文件可以查看到文件确实存在目录下,但是在调用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

errorPtr是为了查看具体错误,所以加上这个为了更好定位问题

问题定位

  1. 通过上面的error日志中得知,大概率是因为fullPath这个目录是一个错误的目录了,而我又从device中导出了xxx.xcappdata
    在这个目录下确实能找到对应名字的文件,而且文件是没有问题的,这个可以确定问题是文件是存在的,但是目录又不一致
  2. 既然文件存在,但是目录上却找不到该文件,问题出在哪呢?于是想通过代码打印出fullPath和目录下所有文件的路径对比是否一致,上代码:
    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)")
    }
    这一块是Swift的代码,然后获取document Directory的url(因为我存储文件的目录就是这个),然后通过遍历将所有文件url打印并对比
  • 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

果然,Application后面的UUID并不一致,那全路径肯定也是不对的咯
通过查阅资料,得知在iOS8.0之后每次debug运行或者更新,这个UUID都会改变,而我又为了方便,一开始就存储了全路径,所以每次重新运行代码就会出现找不到这个文件,因为UUID的改变导致了整个目录都变了,自然找不到。

解决问题

既然知道了问题所在,解决问题就简单了

  1. 遍历目录,通过fileName过滤拿到对应的文件
    刚开始,由于对iOS这边的api之类的还不是很了解,解决思路也很直接,既然Swift上能拿到目录下所有文件的url了,在kotlin native层肯定有对应的API,于是开搞,上代码:
    // 拿到目录的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
        }
    }
    再次运行,成功拿到bytes(✌️)
    但是这样存在一个问题,随着时间,这个目录的文件肯定会越来越多,到时候遍历肯定是一个耗时任务。那有没有可以直接通过filename拿到对应文件的url呢?
    看了下代码,既然能拿到document目录的url,那是不是也有API可以直接拿到文件的url?一开始想通过url + filename组装一下
    他们类型不一致,相对麻烦,还好找到了,代码:
    val documentDirectory = NSSearchPathForDirectoriesInDomains(
        directory = NSDocumentDirectory,
        domainMask = NSUserDomainMask,
        expandTilde = true
    ).first() as NSString
    val fullPath = documentDirectory.stringByAppendingPathComponent(fileName)
    println(fullPath)
    路径正确,直接调用NSData.dataWithContentsOfFile传入文件路径即可

总结

在 iOS8.0 之后,由于Application(Bundle identifier)的UUID会随机改变,所以不能直接存全路径,可以直接存名字
而可以通过NSSearchPathForDirectoriesInDomains拿到对应的路径,再加上文件名得到当前文件准确的全路径,这时候读取即可。