過去的幾年中涌現了大量的Objective-C開發者。有些是從動態語言轉過來的,好比Ruby或Python,有些是從強類型語言轉過來的,如Java或C#,固然也有直接以Objective-C做爲入門語言的。也就是說有很大一部分開發者都沒有使用Objective-C太長時間。當你接觸一門新語言時,更多地會關注基礎知識,如語法和特性等。但一般有一些更高級的,更不爲人知又有強大功能的特性等待你去開拓。html
這篇文章主要是來領略下Objective-C的運行時(runtime),同時解釋是什麼讓Objective-C如此動態,而後感覺下這些動態化的技術細節。但願這回讓你對Objective-C和Cocoa是如何運行的有更好的瞭解。python
Objective-C是一門簡單的語言,95%是C。只是在語言層面上加了些關鍵字和語法。真正讓Objective-C如此強大的是它的運行時。它很小但卻很強大。它的核心是消息分發。優化
若是你是從動態語言如Ruby或Python轉過來的,可能知道什麼是消息,能夠直接跳過進入下一節。那些從其餘語言轉過來的,繼續看。atom
執行一個方法,有些語言,編譯器會執行一些額外的優化和錯誤檢查,由於調用關係很直接也很明顯。但對於消息分發來講,就不那麼明顯了。在發消息前沒必要知道某個對象是否可以處理消息。你把消息發給它,它可能會處理,也可能轉給其餘的Object來處理。一個消息沒必要對應一個方法,一個對象可能實現一個方法來處理多條消息。spa
在Objective-C中,消息是經過objc_msgSend()
這個runtime方法及相近的方法來實現的。這個方法須要一個target,selector,還有一些參數。理論上來講,編譯器只是把消息分發變成objc_msgSend
來執行。好比下面這兩行代碼是等價的。代理
1指針 2code |
|
大多數面向對象的語言裏有 classes 和 objects 的概念。Objects經過Classes生成。可是在Objective-C中,classes自己也是objects(譯者注:這點跟python很像),也能夠處理消息,這也是爲何會有類方法和實例方法。具體來講,Objective-C中的Object是一個結構體(struct),第一個成員是isa
,指向本身的class。這是在objc/objc.h中定義的。
1 2 3 |
|
object的class保存了方法列表,還有指向父類的指針。但classes也是objects,也會有isa
變量,那麼它又指向哪兒呢?這裏就引出了第三個類型: metaclasses
。一個 metaclass被指向class,class被指向object。它保存了全部實現的方法列表,以及父類的metaclass。若是想更清楚地瞭解objects,classes以及metaclasses是如何一塊兒工做地,能夠閱讀這篇文章。
咱們知道了運行時會發消息給對象。咱們也知道一個對象的class保存了方法列表。那麼這些消息是如何映射到方法的,這些方法又是如何被執行的呢?
C
lass的方法列表實際上是一個字典,key爲selectors,IMPs爲value。一個IMP是指向方法在內存中的實現。很重要的一點是,selector和IMP之間的關係是在運行時才決定的,而不是編譯時。這樣咱們就能玩出些花樣。
IMP一般是指向方法的指針,第一個參數是self,類型爲id,第二個參數是_cmd,類型爲SEL,餘下的是方法的參數。這也是self
和_cmd
被定義的地方。下面演示了Method和IMP
1 2 3 |
|
如今咱們知道了objects,classes,selectors,IMPs以及消息分發,那麼運行時到底能作什麼呢?主要有兩個做用:
建立、修改、自省classes和objects
消息分發
以前已經提過消息分發,不過這只是一小部分功能。全部的運行時方法都有特定的前綴。下面是一些有意思的方法:
class開頭的方法是用來修改和自省classes。方法如 class_addIvar、class_addMethod、 class_addProperty和class_addProtocol 容許重建classes。
class_copyIvarList、class_copyMethodList、class_copyProtocolList和class_copyPropertyList能拿到一個class的全部內容。而class_getClassMethod、class_getClassVariable、 class_getInstanceMethod, class_getInstanceVariable、class_getMethodImplementation 和 class_getProperty 返回單個內容。
也有一些通用的自省方法,如class_conformsToProtocol、class_respondsToSelector、 class_getSuperclass。最後,你可使用class_createInstance來建立一個object。
這些方法能讓你獲得名字,內存地址和Objective-C type encoding。
這些方法主要用來自省,好比method_getName
, method_getImplementation
, method_getReturnType
等等。也有一些修改的方法,包括method_setImplementation
和method_exchangeImplementations
,這些咱們後面會講到。
一旦拿到了object,你就能夠對它作一些自省和修改。你能夠get/set ivar, 使用object_copy
和object_dispose
來copy和free object的內存。最NB的不只是拿到一個class,而是可使用object_setClass
來改變一個object的class。待會就能看到使用場景。
屬性保存了很大一部分信息。除了拿到名字,你還可使用property_getAttributes
來發現property的更多信息,如返回值、是否爲atomic、getter/setter名字、是否爲dynamic、背後使用的ivar名字、是否爲弱引用。
Protocols有點像classes,可是精簡版的,運行時的方法是同樣的。你能夠獲取method, property, protocol列表, 檢查是否實現了其餘的protocol。
最後咱們有一些方法能夠處理 selectors,好比獲取名字,註冊一個selector等等。
如今咱們對Objective-C的運行時有了大概的瞭解,來看看它們能作哪些有趣的事情。
比較基礎的一個動態特性是經過String來生成Classes和Selectors。Cocoa提供了NSClassFromString
和NSSelectorFromString
方法,使用起來很簡單:
1 |
|
因而咱們就獲得了一個string class。接下來:
1 |
|
爲何要這麼作呢?直接使用Class不是更方便?一般狀況下是,但有些場景下這個方法會頗有用。首先,能夠得知是否存在某個class,NSClassFromString
會返回nil,若是運行時不存在該class的話。好比能夠檢查NSClassFromString(@"NSRegularExpression")
是否爲nil來判斷是否爲iOS4.0+。
另外一個使用場景是根據不一樣的輸入返回不一樣的class或method。好比你在解析一些數據,每一個數據項都有要解析的字符串以及自身的類型(String,Number,Array)。你能夠在一個方法裏搞定這些,也可使用多個方法。其中一個方法是獲取type,而後使用if來調用匹配的方法。另外一種是根據type來生成一個selector,而後調用之。如下是兩種實現方式:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
|
可一看到,你能夠把7行帶if的代碼變成1行。未來若是有新的類型,只需增長實現方法便可,而不用再去添加新的 else if。
以前咱們講過,方法由兩個部分組成。Selector至關於一個方法的id;IMP是方法的實現。這樣分開的一個便利之處是selector和IMP之間的對應關係能夠被改變。好比一個 IMP 能夠有多個 selectors 指向它。
而 Method Swizzling 能夠交換兩個方法的實現。或許你會問「什麼狀況下會須要這個呢?」。咱們先來看下Objective-C中,兩種擴展class的途徑。首先是 subclassing。你能夠重寫某個方法,調用父類的實現,這也意味着你必須使用這個subclass的實例,但若是繼承了某個Cocoa class,而Cocoa又返回了原先的class(好比 NSArray)。這種狀況下,你會想添加一個方法到NSArray,也就是使用Category。99%的狀況下這是OK的,但若是你重寫了某個方法,就沒有機會再調用原先的實現了。
Method Swizzling 能夠搞定這個問題。你能夠重寫某個方法而不用繼承,同時還能夠調用原先的實現。一般的作法是在category中添加一個方法(固然也能夠是一個全新的class)。能夠經過method_exchangeImplementations
這個運行時方法來交換實現。來看一個demo,這個demo演示瞭如何重寫addObject:
方法來紀錄每個新添加的對象。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
|
咱們把方法交換放到了load
中,這個方法只會被調用一次,並且是運行時載入。若是指向臨時用一下,能夠放到別的地方。注意到一個很明顯的遞歸調用logAddObject:
。這也是Method Swizzling容易把咱們搞混的地方,由於咱們已經交換了方法的實現,因此其實調用的是addObject:
咱們能夠在運行時建立新的class,這個特性用得很少,但其實它仍是很強大的。你能經過它建立新的子類,並添加新的方法。
但這樣的一個子類有什麼用呢?別忘了Objective-C的一個關鍵點:object內部有一個叫作isa
的變量指向它的class。這個變量能夠被改變,而不須要從新建立。而後就能夠添加新的ivar和方法了。能夠經過如下命令來修改一個object的class
1 |
|
這能夠用在Key Value Observing。當你開始observing an object時,Cocoa會建立這個object的class的subclass,而後將這個object的isa指向新建立的subclass。點擊這裏查看更詳細的解釋。
目前爲止,咱們討論了方法交換,以及已有方法的處理。那麼當你發送了一個object沒法處理的消息時會發生什麼呢?很明顯,」it breaks」。大多數狀況下確實如此,但Cocoa和runtime也提供了一些應對方法。
首先是動態方法處理。一般來講,處理一個方法,運行時尋找匹配的selector而後執行之。有時,你只想在運行時才建立某個方法,好比有些信息只有在運行時才能獲得。要實現這個效果,你須要重寫+resolveInstanceMethod:
和/或 +resolveClassMethod:
。若是確實增長了一個方法,記得返回YES。
1 2 3 4 5 6 7 |
|
那Cocoa在什麼場景下會使用這些方法呢?Core Data用得不少。NSManagedObjects有許多在運行時添加的屬性用來處理get/set屬性和關係。那若是Model在運行時被改變了呢?
若是 resolve method 返回NO,運行時就進入下一步驟:消息轉發。有兩種常見用例。1) 將消息轉發到另外一個能夠處理該消息的object。2) 將多個消息轉發到同一個方法。
消息轉發分兩步。首先,運行時調用-forwardingTargetForSelector:
,若是隻是想把消息發送到另外一個object,那麼就使用這個方法,由於更高效。若是想要修改消息,那麼就要使用-forwardInvocation:
,運行時將消息打包成NSInvocation,而後返回給你處理。處理完以後,調用invokeWithTarget:
。
Cocoa有幾處地方用到了消息轉發,主要的兩個地方是代理(Proxies)和響應鏈(Responder Chain)。NSProxy是一個輕量級的class,它的做用就是轉發消息到另外一個object。若是想要惰性加載object的某個屬性會頗有用。NSUndoManager也有用到,不過是截取消息,以後再執行,而不是轉發到其餘的地方。
響應鏈是關於Cocoa如何處理與發送事件與行爲到對應的對象。好比說,使用Cmd+C執行了copy命令,會發送-copy:
到響應鏈。首先是First Responder,一般是當前的UI。若是沒有處理該消息,則轉發到下一個-nextResponder
。這麼一直下去直到找到可以處理該消息的object,或者沒有找到,報錯。
iOS 4.3帶來了不少新的runtime方法。除了對properties和protocols的增強,還帶來一組新的以 imp 開頭的方法。一般一個 IMP 是一個指向方法實現的指針,頭兩個參數爲 object(self)和selector(_cmd)。iOS 4.0和Mac OS X 10.6 帶來了block,imp_implementationWithBlock()
能讓咱們使用block做爲 IMP,下面這個代碼片斷展現瞭如何使用block來添加新的方法。
1 2 3 4 |
|
能夠看到,Objective-C 表面看起來挺簡單,但仍是很靈活的,能夠帶來不少可能性。動態語言的優點在於在不擴展語言自己的狀況下作不少很靈巧的事情。好比Key Value Observing,提供了優雅的API能夠與已有的代碼無縫結合,而不須要新增語言級別的特性。