? ? 在文章iOS 文件導入(OTG)開發(fā)問題隨記1中記錄了開發(fā)OTG的一些基本的問題,接下來記錄一下OTG開發(fā)步驟以及遇到的問題。
? ? 我在開發(fā)文件導入(OTG)中主要分成了兩個部分。1.從U盤拷貝文件到手機,2.將拷貝出來的文件進行編解碼。
1.從U盤拷貝文件到手機。
? ? 拷貝文件一開始使用的是系統(tǒng)提供的FileManager類中的?copyItem(atPath srcPath:String, toPath dstPath:String)方法。在應用之間互相拷貝時使用這個方法沒有問題,但是如果是從U盤到手機,這個方法就不適用了,主要有以下幾個問題:
1).拷貝過程中無法暫停,針對大文件拷貝,拷貝暫停是剛需。
2).報錯信息很委婉,U盤拔出這個異常是需要重點考慮的,但是針對這塊的報錯信息不明確,而且報錯信息需要從FileManagerDelegate中去撈,信息不明確,
3).大量數據會先緩存到內存中,然后進行拷貝,導致U盤拔出之類的error不及時。這個是我猜測的,現象是在拷貝過程中,把U盤拔出,這時并沒有及時進入到error的代理中,過了一段時間才進入error的delegate中,所以猜測是緩存到內存中的數據并沒有拷貝完,在拷貝完成之后,再次去U盤中讀取數據讀取不到時才會報錯。無論原因是什么,導致的問題就是error的delegate沒有及時執(zhí)行。
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循環(huán)中進行讀取解碼時,會導致CPU暴增,一度會增加到100%,在文件比較大時,最終App會被看門狗殺掉。所以在while循環(huán)讀取中,需要適當sleep來控制一下CPU。
? ? 整個OTG的主線問題就是以上幾個問題,但是在開發(fā)過程中,真正頭疼的都是一些支線問題,支線問題是根據業(yè)務的需求而不同的,我主要碰到以下幾個問題:
1).有關url的操作權限問題。
2).有關Realm的數據存儲問題。
? ? ?針對問題1),因為在documentPicker(_controller:UIDocumentPickerViewController, didPickDocumentsAt urls: [URL])代理中,會返回選中文件的url路徑,而且在iOS11以上開始支持多選,所以返回的數據是一個數組。而且在上一篇文章中說過,OTG形式的文件導入,只能采用UIDocumentPickerMode為open。在這種模式下,對文件路徑的使用url都需要通過startAccessingSecurityScopedResource()請求權限,在執(zhí)行完成之后,需要通過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,到合適的時候將它們取出來,通過請求關閉權限對其進行操作。但是在這個過程中發(fā)現,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的主要步驟和主要問題就是上面所述,如果還想起其他問題會及時更新。