原文地址:http://blog.securemacprogramming.com/2013/12/by-your-_cmd/
感謝翻譯小組成員
wingpan
熱心翻譯。本篇文章是我們每週推薦優秀國外的技術類文章的其中一篇。如果您有不錯的原創或譯文,歡迎提交給我們,更歡迎其他朋友加入我們的翻譯小組(聯繫qq:2408167315)。本文是我在
Alt Tech Talks: London
上關於Objective-C
runtime
的演講總結,如果你對Objective-C runtime
感興趣的話,應該看看這篇文章,特別是文章中的鏈接,一定會受益匪淺。
什麼是Objective-C runtime?
簡單來說,Objective-C runtime
是一個實現Objective-C
語言的C
庫。對象可以用C
語言中的結構體表示,而方法(methods
)可以用C
函數實現。事實上,他們差不多也是這麼幹了,另外再加上了一些額外的特性。這些結構體和函數被runtime
函數封裝後,Objective-C
程序員可以在程序運行時創建,檢查,修改類,對象和它們的方法。
除了封裝,Objective-C runtime
庫也負責找出方法的最終執行代碼。當程序執行[object doSomething]
時,不會直接找到方法並調用。相反,一條消息(message
)會發送給對象(在這兒,我們通常叫它接收者)。runtime
庫給次機會讓對象根據消息決定該作出什麼樣的反應。Alan Kay
反覆強調消息傳遞(message-passing
)是Smalltalk
最重要的部分(Objective-C
根據Smalltalk
發展而來),而不是對象:
由於以前關於這個話題我創造了“對象”這個詞,現在很多人都對這個概念趨之若鶩,這讓我感到非常遺憾。
其實這裏面更爲重要的理念是“消息命令”(messaging
),這纔是Smalltalk
的核心內容(現在尚有一些內容還沒有全部完成)。日語中有個簡短的單詞叫做“ma
”,它用來表示兩個物體之間的東西,在英語中和它最相近的單詞也許是“interstitial
”。製造一個龐大且可擴展系統的關鍵是設計它各個模塊之間的通信方式,而不是關注它的內部屬性和行爲。
實際上,在一篇介紹Smalltalk
虛擬機的文章裏,這門編程技術被叫做消息傳遞或者消息傳送範式。“面向對象”通常用來描述內存管理系統。
在演講和文章中都使用ObjC runtime
這個詞,看似只有一個,實際上存在很多runtime
庫。雖然它們都支持對象的自省檢查和消息接收,但是它們卻有不同的特性和實現方式(例如,同樣是發送消息,Apple
的runtime
用一步完成,而GNU runtime
會先查詢這些消息,然後執行查找到的函數分兩步完成)。以下所有的討論,都是基於Apple
的最新runtime
庫(蘋果公司在OSX 10.5
和iOS
發佈時的版本)。
在那次演講中,我決定研究runtime
庫某些領域的功能。我找了一些希望更透徹瞭解的東西,然後把它們做成問答的形式組成我的演講。
動態創建類
如何實現Key-Value Observing?
當我在準備這次演講時,一篇叫做KVO considered harmful
的文章開始擁有很多擁躉。它提出了很多對KVO
正確的批評,但相對於捨棄觀察者模式不用,我更想探索出一種新的實現方式。
KVO
實現觀察者模式的關鍵是它偷偷摸摸將被觀察對象的類改變了,它子類化原來的類後,就能夠自定義該對象的方法來調用KVO
的回調方法。這些都是通過 objc_duplicateClass
這個方法完成,但很遺憾,這個方法並不公開,我們無法私自調用。
條條大路通羅馬,好在除了objc_duplicateClass
,還有其他方法可以通過使用祕密子類化的方式實現觀察者模式,比如創建和註冊“class pair
”。那麼什麼是class pair
呢?對於Objective-C
的類來說,都有一對Class
的對象來定義它:Class
對象定義了這個類的實例方法,而metaclass
定義了這個類的類方法。所以每個class
其實是它metaclass
的單例。
這個代碼展示了觀察者模式的工作原理。當你給對象增加觀察者時,這個對象首先會檢查自己是否可被觀察,如果是,它會新創建一個類,用我們自己的-dealloc
替代原來類的方法,同樣它也會把-class
方法替換掉,類似於KVO
被觀察對象,當你訪問被觀察對象的類名時,返回的是它原來的類名,而不是新生成的類。
創建完類後,我們需要照着 Key-Value Coding
爲屬性增加一個setter
方法:這個setter
方法會獲取這個屬性修改前的值和修改後的值,然後調用block
形式的回調函數,將這兩個值告訴觀察者。代碼中根據我們的意願,這個block
可以異步調用。
請注意, -addObserverForKey:withBlock:
會使用s object_setClass()
將被觀察對象的類替代爲新組建的類。這樣做最主要的目的是將消息轉變爲方法的方式改變,但是這需要非常小心,原來的類和新的類必須有相同的成員變量佈局。因爲成員變量也是用過runtime
訪問,修改某個對象的類可能導致runtime
無法找到對應的變量。
我們在存儲觀察者集合時遇到些麻煩,因爲沒地方去存它們。給ObserverPattern
這個類增加成員變量不起作用,因爲根本沒有生成這個類的對象。被觀察對象的成員變量是它原來類的,它並沒有考慮過這些觀察者。
Objective-C runtime
通過引入 associated objects
幫助我們擺脫這個困境。在runtime
裏,理論上所有對象都可以擁有包含其他對象的字典。通過associated references
,被觀察對象可以存儲和訪問他們的觀察者,而不需要額外的成員變量。
如果你運行多次後,你會發現ObserverPattern
還是有點小毛病的。由於觀察者回調是異步調用的,觀察者接
收到的變化事件也是亂序的。這意味着觀察者其實無法區分被觀察屬性的最終狀態是什麼,回調中的新值可能早已被修改。我這樣做的目的是爲了說明在KVO
中同步調用回調其實是個有用的特色,並非bug
。
創建對象
那些額外的字節都是幹啥用的?
當你創建一個 Objective-C
對象時,runtime
會在實例變量存儲區域後面再分配一點額外的空間。這麼做的目的是什麼呢?你可以獲取這塊空間起始指針(用 object_getIndexedIvars
),然後就可以索引實例變量(ivars
)。好吧,下面我會使用自定義數組來說明一下索引ivars
的用處。
讓我們創建一個數組!從這個SimpleArray
中可以看到兩件事情:最明顯的一件是它使用了類簇模式。當使用+alloc
方法返回對象時,一般情況下已經爲這個對象分配了所有的內存,但是在這個例子中,在+alloc
時並不知道需要多大的內存空間。只有當調用了 -initWithObjects:count:
以後,才能根據數組內對象數量計算出這個數組需要多大的內存,所以+alloc
只是返回一個佔位符,只有在初始化後纔會分配和返回真正的數組對象。
或許你會問爲什麼我們要用類簇把事情搞那麼複雜,使用 calloc()
另外分配一塊大小合適的緩存,然後把那些對象指針存到裏面不就得了?答案是希望利用局部性原理提高訪問性能。從數組的設計上我們可以看出,每次數組指針被訪問時,之後會有很大機率訪問到緩存指針,所以把它們肩並肩的放入內存意味着找到其中一個就是找到了另外一個。
消息派發
消息如何轉發?
Objective-C
其中一個強大特性是對象不需要實現某個方法,儘管它在編譯時聲明瞭該選擇符(selector
)。但它可以在運行時再決定方法實現,或者將這些消息轉發給其他對象,或者發出異常,亦或做一些其他事情。但是這個特性的某些方面曾經一直困擾我:消息轉發(message
forwarding
)會調用 -forwardInvocation:
,然後傳入一個NSInvocation
對象。但是這個NSInvocation
類是在Foundation
庫中定義的,難道說runtime
工作需要Foundation
配合?
我試着挖掘其中的原因,發現答案並不是我想的那樣,runtime
不需要知道Foundation
。runtime
會讓程序定義轉發函數(forwarding function
),當 objc_msgSend()
無法找到該selector
的實現時,那個轉發函數就會被調用。程序一啓動,CoreFoundation
就將 -forwardInvocation:
定義成轉發函數。
讓我們來創建一個Ruby
!當然並不是真的實現完整的Ruby
,Ruby
有一個叫做#method_missing
的函數,當對象收到一個它沒有實現的消息時,這個函數就會被調到,這和Smalltalk
的做法比較相似。使用objc_setForwardHandler
,我們也能在Objective-C
的類中實現類似Ruby
的methodMissing:
方法。
總結
Objective-C runtime
可以有效的幫助我們爲程序增加很多動態的行爲。一些開發者除了使用method swizzling
幫助調試程序,並不會在實際程序中使用它,但runtime
編程的確有很多功能,它應該成爲實際應用代碼編寫的重要工具。