在文章iOS 文件导入(OTG)开发问题随记1中记录了开发OTG的一些基本的问题,接下来记录一下OTG开发步骤以及遇到的问题。
我在开发文件导入(OTG)中主要分成了两个部分。1.从U盘拷贝文件到手机,2.将拷贝出来的文件进行编解码。
1.从U盘拷贝文件到手机。
拷贝文件一开始使用的是系统提供的FileManager类中的 copyItem(atPath srcPath:String, toPath dstPath:String)方法。在应用之间互相拷贝时使用这个方法没有问题,但是如果是从U盘到手机,这个方法就不适用了,主要有以下几个问题:
1).拷贝过程中无法暂停,针对大文件拷贝,拷贝暂停是刚需。
2).报错信息很委婉,U盘拔出这个异常是需要重点考虑的,但是针对这块的报错信息不明确,而且报错信息需要从FileManagerDelegate中去捞,信息不明确,
3).大量数据会先缓存到内存中,然后进行拷贝,导致U盘拔出之类的error不及时。这个是我猜测的,现象是在拷贝过程中,把U盘拔出,这时并没有及时进入到error的代理中,过了一段时间才进入error的delegate中,所以猜测是缓存到内存中的数据并没有拷贝完,在拷贝完成之后,再次去U盘中读取数据读取不到时才会报错。无论原因是什么,导致的问题就是error的delegate没有及时执行。
4).在文件拷贝过程中,无任何反馈,如果想要计算进度是没办法做到的。
考虑到以上几个问题,FileManager中的拷贝方法就行不通了,还得自己去实现文件拷贝的功能。自己实现文件拷贝的功能就是从一个文件中固定读多少数据,然后将读取到的数据拷贝到指定的目录。核心逻辑就是readDataOfLength,然后writeData。但是这一套还是无法解决U盘拔出的异常,当时没管这个,先实现了这一套,在实现这套之后,上面的问题1,4都得到了解决。但是如果在读取数据过程中将U盘拔出还是会出crash,在这里我踩了很多坑,比如判断文件读取路径是否存在,路径是否有效,路径是否有读取权限等等。但是最后都失败了,然后无意中进入NSFileHandle中看了一下readDataOfLength方法,豁然开朗,这几个方法写着都已经废弃了,建议我们用替代方法去实现功能,比如
- (NSData *)readDataOfLength:(NSUInteger)length
API_DEPRECATED_WITH_REPLACEMENT("readDataUpToLength:error:",
macos(10.0,API_TO_BE_DEPRECATED), ios(2.0,API_TO_BE_DEPRECATED),
watchos(2.0,API_TO_BE_DEPRECATED), tvos(9.0,API_TO_BE_DEPRECATED));
readDataOfLength有了替代方法readDataUpToLength:error:,然后我看了一下readDataUpToLength:error:,
- (nullableNSData*)readDataUpToLength:(NSUInteger)lengtherror:(outNSError**)error
API_AVAILABLE(macos(10.15), ios(13.0), watchos(6.0), tvos(13.0))NS_REFINED_FOR_SWIFT;
上述方法是从13.0开始支持的,然后iOS的OTG也是从iOS13开始支持的,然后我猜测就是为了考虑到外接盘符的插拔,然后更新了这个方法提供使用。我将App中的相关方法都更换成了iOS13支持的带error的方法,然后所有问题都解决了,在拷贝过程中,如果盘符拔出,则会报error,告知文件读取路径不存在,错误提示高效又明朗。一下子解决了上述的2,3问题。所以从U盘拷贝文件到手机基本就没啥问题了。
2.将拷贝出来的文件进行编解码。
这部分的功能主要是从本地文件读取数据然后进行一段一段的编解码,这部分的逻辑很正常,唯一要注意的点是,在while循环中进行读取解码时,会导致CPU暴增,一度会增加到100%,在文件比较大时,最终App会被看门狗杀掉。所以在while循环读取中,需要适当sleep来控制一下CPU。
整个OTG的主线问题就是以上几个问题,但是在开发过程中,真正头疼的都是一些支线问题,支线问题是根据业务的需求而不同的,我主要碰到以下几个问题:
1).有关url的操作权限问题。
2).有关Realm的数据存储问题。
针对问题1),因为在documentPicker(_controller:UIDocumentPickerViewController, didPickDocumentsAt urls: [URL])代理中,会返回选中文件的url路径,而且在iOS11以上开始支持多选,所以返回的数据是一个数组。而且在上一篇文章中说过,OTG形式的文件导入,只能采用UIDocumentPickerMode为open。在这种模式下,对文件路径的使用url都需要通过startAccessingSecurityScopedResource()请求权限,在执行完成之后,需要通过stopAccessingSecurityScopedResource()关闭权限。大致代码如下:
for url in urls {
let securitySucceeded = url.startAccessingSecurityScopedResource()
if securitySucceeded {
AudioFileManager.sharedInstance.importAudioFiles(urls: [url])
}
url.stopAccessingSecurityScopedResource()
}
如果选中了多个url,需求肯定希望是所有选中的url先展示出来,之后再一个一个处理url,进行导入,编解码。我一开始的实现是这样的,所有选中的url都会作为一条本地的记录,保存到Realm中,然后UI根据本地存储的Realm中的数据进行展示,最后再处理每个url。当时我先把url存入Realm,到合适的时候将它们取出来,通过请求关闭权限对其进行操作。但是在这个过程中发现,url转为String存入数据库中,再取出来转化为URL对其进行操作时,startAccessingSecurityScopedResource()始终获取不到url的使用权,这部分的原理我没有想通,其实那时候我应该多测试一下,在URL类下面有很多有关权限的方法,但是当时因为时间问题我没有尝试,我采用了一个最低级的方法,我把所有选中的url都保存在内存中,要用的时候取对应的url,这样就是可以获取使用权限,目前就是这种方案实现的,应该有更好的实现方案。
针对问题2),因为项目中有很多本地文件需要处理,为了方便,采用了Realm数据库,可能是Realm使用的还是不太熟,过程中遇到很多坑,多线程使用,数据得不到及时更新,在一边拷贝数据,一边更新数据库时,会导致拷贝的数据保存到数据库中。我在主线程中添加对应的url数据,但是在子线程中怎么都拿不到这个新添加的数据,后来我在每次获取数据库数据之前,都将Realm强制refresh,这样确实解决了问题,但是也带来了新的问题。我需要在一边将文件数据拷贝到指定目录之后,计算出拷贝的文件比例存入Realm,但是因为上面的refresh的原因,导致拷贝的数据也会存入Realm中变成垃圾数据,如果文件很大,就会导致Realm中的数据也很多,最终会crash。针对这个问题,我先是在Realm的初始化中的shouldCompactOnLaunch block中设置使用数据大于10M就return true,这意味着数据库会被压缩,这样确实解决了启动之前Realm就很大的问题,但是我的那种场景是使用过程中会导致垃圾数据很多,这种情况是不会主动压缩的,那这个问题还是解决不了。当时为了快速解决这个问题,我把进度拿出来单独处理了。。。
在实现U盘文件导入到iphone的主要步骤和主要问题就是上面所述,如果还想起其他问题会及时更新。