打造強大的BaseModel(4):使用Swift反射

ButterFly發表於2016-04-25

本文「打造強大的BaseModel」的第四篇,《打造強大的BaseModel(1):讓Model自我描述》、《打造強大的BaseModel(2):讓Model實現自動對映,將字典轉化成Model》、《打造強大的BaseModel(3):讓Model實現自動歸檔》。如果你沒有看過前面三篇文章的話,建議在看這篇文章之前先去看看,熟悉一下iOS Runtime的一些東西以及純Swift型別和Objc型別的異同。

而這篇文章則討論了Swift的反射功能,與iOS Runtime不一樣,Swift的反射用了另一套API,實現機制也完全不一樣,倒是和目前其他的面嚮物件語言有些相似,比如C#和Java。不過Swift作為一門很新的語言,目前正在高速發展中,其反射功能和和主流的高階語言還沒法比,但是我相信Apple會在將來大大加強Swift的反射功能,以追上目前主流語言的腳步。


iOS Runtime目前存在的問題

關於iOS Runtime的文章有很多,一搜就能找出一大堆,但是大多數都是介紹什麼是iOS Runtime及怎麼使用Runtime。其實基於Objc的Runtime是iOS開發的黑魔法,甚至可以是說奇技淫巧,比如神奇的Method Swizzle可以交換任何iOS的系統方法,在裡面加上自己定義的一些功能。再比如訊息轉發機制,又比如說一些位於中的方法,比如class_copyIvarList等方法,可以動態獲取一個類裡面所有的方法和屬性,還有就是動態給一個類新增屬性和方法。Objc的Runtime是如此的強大,再加上KVC和KVO這兩個利器,可以實現很多你根本就想不到的功能,給iOS開發帶來極大的便捷。

使用iOS Runtime好處非常多,但缺點也是顯而易見的,主要有下面幾個:

  • 基於Objc的Runtime不是型別安全的,需要開發者保證所有資料都是正確的型別。
  • Runtime還是需要進行資料型別的檢查,影響了執行效率。
  • Apple推出全新的Swift語言後,單純的Swift型別不再相容原先的Objc的Runtime,

其中前面兩個問題影響不大,關鍵在於第三個。基於Swift作為一門靜態語言,所有資料的型別都是在編譯時就確定好了的,但是Apple為了讓Swift相容Objc,讓Swift也使用了Runtime。這顯然會拖累Swift的執行效率,和Apple所宣稱Swift具有超越Objective-C的效能的觀點完全不符。而Swift在將來是會慢慢替代 Objective-C的成為iOS或者OSX開發的主流語言,所以為了效能,我們應該儘量使用原生的Swift資料型別,避免讓Runtime進行Swift型別->Objc型別的隱式轉換。

所以目前的問題是使用Swift原生的資料型別和想要使用Objc的Runtime有了衝突,那麼Swift語言裡有沒有類似於Objc的Runtime的一套機制,讓Swift資料型別也能實現Objc的Runtime的一些功能呢?

很遺憾,這個答案是NO,Swift目前只有有限的反射功能,完全不能和Objc的Runtime相比。

什麼是反射

反射是一種計算機處理方式。是程式可以訪問、檢測和修改它本身狀態或行為的一種能力。

上面的話來自百度百科。使用反射有什麼用,看一些iOS Runtime的文章應該會很明白。下面再列舉一下

  • 動態地建立物件和屬性,
  • 動態地獲取一個類裡面所有的屬性,方法。
  • 獲取它的父類,或者是實現了什麼樣的介面(協議)
  • 獲取這些類和屬性的訪問限制(Public 或者 Private)
  • 動態地獲取執行中物件的屬性值,同時也能給它賦值(KVC)
  • 動態呼叫例項方法或者類方法
  • 動態的給類新增方法或者屬性,還可以交換方法(只限於Objective-C)

上面的一系列功能的細節和計算機語言的不同而不同。對於Objective-C來說,位於中的一系列方法就是完成這些功能的,嚴格來說Runtime並不是反射。而Swift真正擁有了反射功能,但是功能非常弱,目前只能訪問和檢測它本身,還不能修改。

Swift的反射

Swift的反射機制是基於一個叫Mirror的Stuct來實現的。具體的操作方式為:首先建立一個你想要反射的類的例項,再傳給Mirror的構造器來例項化一個Mirror物件,最後使用這個Mirror來獲取你想要的東西。

首先我們來寫一些測試用的類

可以看出,和第一篇文章一樣,列印出個每個屬性和其值。不同的是,對於自定義物件,不能自動打出裡面的屬性內容。

在利用Swift的反射來改進BaseModel之前,讓我們來看看Mirror裡面都有什麼東西吧

一個一個來看

  • 第一個是構造器,它傳入的引數型別是Any型別。說明Mirror支援對任意型別的反射。
  • 下面定義了兩個typealias,分別是Child和Children,Child是個元組(label: String?, value: Any),label是指屬性名,是個可空值,因為不是所有支援反射的資料結構都包含有名字的子節點。 struct 會以屬性的名字做為 label,但是 Collection 只有下標,沒有名字。Tuple 同樣也可能沒有給它們的條目指定名字。是Value是個Any,也就是說屬性可以是任意型別。
  • DisplayStyle是個enum,它會告訴你物件的型別。這裡面其他囊括了所有Cocoa的型別,唯一的例外是個Closure,或者Block。
  • AncestorRepresentation也是個enum,這個enum用來定義被反射的物件的父類應該如何被反射。也就是說,這隻應用於 class 型別的物件。預設情況(正如你所見)下 Swift 會為每個父類生成額外的 mirror。然而,如果你需要做更復雜的操作,你可以使用 AncestorRepresentation enum 來定義父類被反射的細節。具體都有什麼樣的型別看上面的註釋。
  • subjectType是個Any.Type,從上面列印出來的東西可以看出,它應該是AnyClass的名稱,不同的是AnyClass是AnyObject.Type.後面可以寫程式碼驗證
  • children是個AnyForwardCollection型別,也就是個Child的集合。AnyForwardCollection是個什麼玩意呢,Apple是這麼說的

    這個確實有點難以理解,我試著翻譯過來:一個型別可擦除的包裝器,適用於任何支援向前遍歷的集合,前向遍歷操作任意一個有著相同“元素”型別的底層集合,隱藏底層集合型別的細節。翻譯過來還是不明白什麼意思,也許需要大神指點,我只好把它看作一個可以遍歷的普通集合算了。
  • displayStyle是DisplayStyle enum,表示反射的物件屬於什麼樣的型別
  • superclassMirror()是獲取父類的Mirror,如果沒有父類,則為nil

下面看看用各種型別來看看Mirror的各個屬性可以列印出什麼

Colsure或者Block的displayStyle是nil,而typealias則轉化成了正確的型別。其他所有型別都正確在獲取並列印出來了。

Mirror類還有一個些其他的構造器,還有一些擴充套件,可能一些特殊場合會用到,這裡就省略了,總之來說,Swift的反射可以用在以下場景

  • 遍歷Turple
  • 對類做分析,獲取屬性和值
  • 執行時分析物件的一至性

總體來說,因為功能比較弱,使用場景也比較窄。遠遠比不上Objc的Runtime,更別說Java和C#了。
但是相對於Objc的Runtime,Swift的反射是可以獲取全部屬性的,而且API相對於Objc也簡單許多。有時侯,可以用來代替Runtime中獲取所有屬性名的方法

可以用這個來重寫description方法,程式碼看起來要簡潔不少。但是—–這個根本就沒法用。

Mirror中的children無法識別複雜型別,對於自定義型別,或者是沒有賦值的可空型別,獲取的value是nil,這樣也就無法像第一篇文章中那樣正確地列印出各種個屬性名和值,我的如意算盤落空了。

總結

就目前而言,怪異的API,功能的缺失,讓Swift的反射功能難堪大用。我們之所以要用反射,就是要利用基於反射動態程式設計的特性,實現在執行時動態地給屬性賦值,動態呼叫方法等。不知道是Apple認為Objc的Runtime功能已經足夠強大好用,還是認為Swift的安全更重要,目前在Swift2.2版本反射還是根本沒法用。我希望未來Apple可以好好的加強Swift的反射,參考一下C#和Java的機制,設計出更強大更好用的反射功能

打造強大的BaseModel系列文章到此就完結了,在寫這些文章的過程中,我發現自己對iOS的Runtime有了更深的理解,也明白了Swift和Objc是怎麼相互操作的,可見寫文章的最大收穫還是自己。所以我還會寫更多的iOS開發技巧系列文章,請大家期待。

相關文章