問(wèn)題描述
使用環(huán)境:
Xcode 10.1Swift 4.0
描述:
使用 NSCoding 進(jìn)行 archive 和 unarchive 歸檔。舊的工程名叫 A, 新的工程名叫 B。A 曾經(jīng)在設(shè)備上運(yùn)行過(guò),并使用 NSUserDefault 針對(duì)序列化后的Data進(jìn)行持久化保存。
當(dāng)更換工程名后,B 在運(yùn)行時(shí)從userDefault中取出這個(gè)NSData來(lái)做解檔報(bào)錯(cuò)了
'NSInvalidUnarchiveOperationException', reason: '*** -[NSKeyedUnarchiver decodeObjectForKey:]: cannot decode object of class (MyProject.MyClass) for key (root); the class may be defined in source code or a library that is not linked'
自測(cè)后發(fā)現(xiàn)
測(cè)試過(guò)發(fā)現(xiàn)了如下問(wèn)題:
- 使用
OC創(chuàng)建工程,變更工程名之后,調(diào)用[NSkeyedUnarchiver unarchiveobject]是沒(méi)有問(wèn)題的。 - 在
Swift上發(fā)現(xiàn)一定會(huì)造成閃退 - ios 11 和 ios 12之后的版本,unarchive 分別添加了一個(gè) 函數(shù),用來(lái)配合解檔失敗時(shí)候的處理(try catch)
- 看了我們使用的第三方登錄/分享的平臺(tái),他們也有使用到 歸檔和解檔。只是用的是 OC
原因
When you use the @objc(name) attribute on a Swift class, the class is made available in Objective-C without any namespacing. As a result, this attribute can also be useful when you migrate an archivable Objective-C class to Swift. Because archived objects store the name of their class in the archive, you should use the @objc(name) attribute to specify the same name as your Objective-C class so that older archives can be unarchived by your new Swift class.
—— 參考出處
何為命名空間
OC中沒(méi)有命名空間的概念,在進(jìn)行應(yīng)用開(kāi)發(fā)時(shí),所有的代碼和引用的靜態(tài)庫(kù)最終會(huì)被編譯到同一個(gè)域和二進(jìn)制文件中。這樣當(dāng)兩個(gè)類(lèi)名重復(fù)的時(shí)候,就會(huì)導(dǎo)致編譯沖突和失敗。這也就是為什么我們?cè)趯?xiě)OC代碼的時(shí)候要添加類(lèi)名前綴的原因。比如蘋(píng)果本身保留的前綴UI和NS 還有各個(gè)系統(tǒng)框架的前綴AF、SD等,這樣做可以大大降低引起沖突的幾率,但是風(fēng)險(xiǎn)仍然存在,如果你在項(xiàng)目中同時(shí)加載進(jìn)兩個(gè)不同的庫(kù),而這兩個(gè)庫(kù)都分別引用了同一個(gè)第三方庫(kù)而沒(méi)有修改名字,這樣就會(huì)發(fā)生沖突。
Swift由于命名空間的存在,既是兩個(gè)名稱(chēng)相同的類(lèi),只要他們來(lái)自不同的命名空間就不會(huì)產(chǎn)生編譯時(shí)的沖突。
"在 Swift 中,由于可以使用命名空間了,即使是名字相同的類(lèi)型,只要是來(lái)自不同的命名空間的話(huà),都是可以和平共處的。
—— 參考出處
—— 喵神寫(xiě)的命名空間的文檔
結(jié)論
由于 Swift 的機(jī)制原因,創(chuàng)建的類(lèi)都會(huì)帶上 命名空間(簡(jiǎn)單的理解就是工程名,在 Info.plist 中查看源碼,看到的那個(gè) CFBundleName 就是命名空間,實(shí)際上就是工程名)。當(dāng)我們變更了工程的同事,也就意味著命名空間跟著變了。在上一個(gè)工程中歸檔自定義類(lèi)時(shí),帶上了舊工程的命名空間。因此在新工程做解檔事,找不到對(duì)應(yīng)的命名空間,早場(chǎng)了 crash
解決方案(預(yù)防為主):
預(yù)防方法一
如果我們?cè)?Swift 中針對(duì)一個(gè)自定義類(lèi)使用 NSKeyedUnarchiver 進(jìn)行歸檔,那么這個(gè)自定義類(lèi)建議定義為 oc的類(lèi),在創(chuàng)建類(lèi)的時(shí)候, class 前要加上 @objc 關(guān)鍵字,這樣累就不會(huì)帶上命名空間了。
@objc(MyClass) class MyClass: NSObject, NSCoding {
//... 略去,下面要去實(shí)現(xiàn) NSCoding 的 decode 和 encode delegate
}
預(yù)防方法二
如果我們就是想要保證 swift的命名空間,能做的是在工程初始的時(shí)候,不使用業(yè)務(wù)名給主 target 命名。
例如我們要寫(xiě)一個(gè)計(jì)算器的應(yīng)用,那么很自然的我們?cè)谛陆üこ堂臅r(shí)候,會(huì)給他命名為 Calculator。后續(xù)如果想要修改工程名,肯定也會(huì)修改這個(gè) target。
事實(shí)上我們可以換一種做法,將主target 命名為 Main,而在使用 pod的時(shí)候,指定他生成的 workspace名. 這樣,后續(xù)如果要修改工程名,也只需要修改 workspace 的名字就能達(dá)到目的而不去影響 命名空間
platform :ios, '8.0'
# inhibit_all_warnings!
use_frameworks!
workspace 'Calculator'
target 'Main' do
# pod ...
end
規(guī)避方法
如果你之前已經(jīng)發(fā)布了一個(gè) 沒(méi)有使用過(guò) @objc 關(guān)鍵字的類(lèi),這時(shí)候新發(fā)的版本肯定無(wú)法將這個(gè)已經(jīng)存在 NSUserDefault 中的類(lèi)變?yōu)椴粠臻g。意味著即使升級(jí)版本,也會(huì)造成崩潰。
那么怎么辦呢?
我看過(guò)一些社交類(lèi)的第三方應(yīng)用提供的 sdk,如facebook, vk, twitter 等等,他們的歸檔對(duì)象類(lèi)型目前還是用 OC 的。但是我在 vk 的代碼中看到了 try catch
查看了一下 NSKeyedUnarchiver 的 swift 官方 API,發(fā)現(xiàn)了兩個(gè)帶有 throws 的 API,但是他們支持的版本都在 11.0 之后。
@available(OSX 10.13, iOS 11.0, watchOS 4.0, tvOS 11.0, *) @nonobjc public static func unarchivedObject<DecodedObjectType>(ofClass cls: DecodedObjectType.Type, from data: Data) throws -> DecodedObjectType? where DecodedObjectType : NSObject, DecodedObjectType : NSCoding @available(OSX 10.13, iOS 11.0, watchOS 4.0, tvOS 11.0, *) @nonobjc public static func unarchivedObject(ofClasses classes: [AnyClass], from data: Data) throws -> Any?
因?yàn)榇蠖鄶?shù)的app應(yīng)用都需要從 ios 8.0 開(kāi)始支持,所以最好的方法是我們自己去添加一個(gè) try catch 處理解檔失敗時(shí)候的崩潰。