問題描述
使用環境:
Xcode 10.1
Swift 4.0
描述:
使用 NSCoding
進行 archive
和 unarchive
歸檔。舊的工程名叫 A, 新的工程名叫 B。A 曾經在裝置上執行過,並使用 NSUserDefault
針對序列化後的Data
進行持久化儲存。
當更換工程名後,B 在執行時從userDefault
中取出這個NSData
來做解檔報錯了
'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'
自測後發現
測試過發現瞭如下問題:
- 使用
OC
建立工程,變更工程名之後,呼叫[NSkeyedUnarchiver unarchiveobject]
是沒有問題的。 - 在
Swift
上發現一定會造成閃退 - ios 11 和 ios 12之後的版本,unarchive 分別新增了一個 函式,用來配合解檔失敗時候的處理(try catch)
- 看了我們使用的第三方登入/分享的平臺,他們也有使用到 歸檔和解檔。只是用的是 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中沒有名稱空間的概念,在進行應用開發時,所有的程式碼和引用的靜態庫最終會被編譯到同一個域和二進位制檔案中。這樣當兩個類名重複的時候,就會導致編譯衝突和失敗。這也就是為什麼我們在寫OC程式碼的時候要新增類名字首的原因。比如蘋果本身保留的字首UI和NS 還有各個系統框架的字首AF、SD等,這樣做可以大大降低引起衝突的機率,但是風險仍然存在,如果你在專案中同時載入進兩個不同的庫,而這兩個庫都分別引用了同一個第三方庫而沒有修改名字,這樣就會發生衝突。 Swift由於名稱空間的存在,既是兩個名稱相同的類,只要他們來自不同的名稱空間就不會產生編譯時的衝突。 "在 Swift 中,由於可以使用名稱空間了,即使是名字相同的型別,只要是來自不同的名稱空間的話,都是可以和平共處的。
—— 參考出處 —— 喵神寫的名稱空間的文件
結論
由於 Swift
的機制原因,建立的類都會帶上 名稱空間
(簡單的理解就是工程名,在 Info.plist
中檢視原始碼,看到的那個 CFBundleName
就是名稱空間,實際上就是工程名)。當我們變更了工程的同事,也就意味著名稱空間跟著變了。在上一個工程中歸檔自定義類時,帶上了舊工程的名稱空間。因此在新工程做解檔事,找不到對應的名稱空間,早場了 crash
解決方案(預防為主):
預防方法一
如果我們在 Swift
中針對一個自定義類使用 NSKeyedUnarchiver
進行歸檔,那麼這個自定義類建議定義為 oc
的類,在建立類的時候, class
前要加上 @objc
關鍵字,這樣累就不會帶上名稱空間了。
@objc(MyClass) class MyClass: NSObject, NSCoding {
//... 略去,下面要去實現 NSCoding 的 decode 和 encode delegate
}
複製程式碼
預防方法二
如果我們就是想要保證 swift
的名稱空間,能做的是在工程初始的時候,不使用業務名給主 target
命名。
例如我們要寫一個計算器的應用,那麼很自然的我們在新建工程名的時候,會給他命名為 Calculator
。後續如果想要修改工程名,肯定也會修改這個 target
。
事實上我們可以換一種做法,將主target
命名為 Main
,而在使用 pod
的時候,指定他生成的 workspace
名. 這樣,後續如果要修改工程名,也只需要修改 workspace
的名字就能達到目的而不去影響 名稱空間
platform :ios, '8.0'
# inhibit_all_warnings!
use_frameworks!
workspace 'Calculator'
target 'Main' do
# pod ...
end
複製程式碼
規避方法
如果你之前已經發布了一個 沒有使用過 @objc
關鍵字的類,這時候新發的版本肯定無法將這個已經存在 NSUserDefault
中的類變為不帶名稱空間。意味著即使升級版本,也會造成崩潰。
那麼怎麼辦呢?
我看過一些社交類的第三方應用提供的 sdk
,如facebook, vk, twitter
等等,他們的歸檔物件型別目前還是用 OC
的。但是我在 vk
的程式碼中看到了 try catch
檢視了一下 NSKeyedUnarchiver
的 swift
官方 API
,發現了兩個帶有 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?
複製程式碼
因為大多數的app
應用都需要從 ios 8.0
開始支援,所以最好的方法是我們自己去新增一個 try catch
處理解檔失敗時候的崩潰。