標籤:
Objective-C總Runtime的那點事兒(一)訊息機制 最近在找工作,Objective-C中的Runtime是經常被問到的一個問題,幾乎是面試大公司必問的一個問題。當然還有一些其他問題也幾乎必問,例如:RunLoop,Block,記憶體管理等。其他的問題如果有機會我會在其他文章中介紹。本篇文章主要介紹RunTime。
RunTime簡稱運行時。就是系統在啟動並執行時候的一些機制,其中最主要的是訊息機制。對於C語言,函數的調用在編譯的時候會決定調用哪個函數( C語言的函數調用請看這裡 )。編譯完成之後直接順序執行,無任何二義性。
OC的函數調用成為訊息發送。屬於動態調用過程。在編譯的時候並不能決定真正調用哪個函數(事實證明,在編譯階段,OC可以調用任何函數,即使這個函數並未實現,只要申明過就不會報錯。而C語言在編譯階段就會報錯)。只有在真正啟動並執行時候才會根據函數的名稱找到對應的函數來調用。
那OC是怎麼實現動態調用的呢?下面我們來看看OC通過發送訊息來達到動態調用的秘密。假如在OC中寫了這樣的一個代碼:
其中obj是一個對象,makeText是一個函數名稱。對於這樣一個簡單的調用。在編譯時間RunTime會將上述代碼轉化成
| 1 |
objc_msgSend(obj,@selector(makeText)); |
首先我們來看看obj這個對象,iOS中的obj都繼承於NSObject。
| 123 |
@interface NSObject <NSOBJECT> { Class isa OBJC_ISA_AVAILABILITY; } |
在NSObjcet中存在一個Class的isa指標。然後我們看看Class:
| 1234567891011121314 |
typedef struct objc_class *Class; struct objc_class { Class isa; // 指向metaclass Class super_class ; // 指向其父類 const char *name ; // 類名 long version ; // 類的版本資訊,初始化預設為0,可以通過runtime函數class_setVersion和class_getVersion進行修改、讀取 long info; // 一些標識資訊,如CLS_CLASS (0x1L) 表示該類為普通 class ,其中包含對象方法和成員變數;CLS_META (0x2L) 表示該類為 metaclass,其中包含類方法; long instance_size ; // 該類的執行個體變數大小(包括從父類繼承下來的執行個體變數); struct objc_ivar_list *ivars; // 用於儲存每個成員變數的地址 struct objc_method_list **methodLists ; // 與 info 的一些標誌位有關,如CLS_CLASS (0x1L),則儲存物件方法,如CLS_META (0x2L),則儲存類方法; struct objc_cache *cache; // 指向最近使用的方法的指標,用於提升效率; struct objc_protocol_list *protocols; // 儲存該類遵守的協議 } |
我們可以看到,對於一個Class類中,存在很多東西,下面我來一一解釋一下:
Class isa:指向metaclass,也就是靜態Class。一般一個Obj對象中的isa會指向普通的Class,這個Class中儲存普通成員變數和對象方法(“-”開頭的方法),普通Class中的isa指標指向靜態Class,靜態Class中儲存static類型成員變數和類方法(“+”開頭的方法)。
Class super_class:指向父類,如果這個類是根類,則為NULL。
下面一張圖片很好的描述了類和對象的繼承關係:
注意:所有metaclass中isa指標都指向跟metaclass。而跟metaclass則指向自身。Root metaclass是通過繼承Root class產生的。與root class結構體成員一致,也就是前面提到的結構。不同的是Root metaclass的isa指標指向自身。
Class類中其他的成員這裡就先不做過多解釋了,下面我們來看看:
@selector (makeText):這是一個SEL方法選取器。SEL其主要作用是快速的通過方法名字(makeText)尋找到對應方法的函數指標,然後調用其函數。SEL其本身是一個Int類型的一個地址,地址中存放著方法的名字。對於一個類中。每一個方法對應著一個SEL。所以iOS類中不能存在2個名稱相同的方法,即使參數類型不同,因為SEL是根據方法名字產生的,相同的方法名稱只能對應一個SEL。
下面我們就來看看具體訊息發送之後是怎麼來動態尋找對應的方法的。
首先,編譯器將代碼[obj makeText];轉化為objc_msgSend(obj, @selector (makeText));,在objc_msgSend函數中。首先通過obj的isa指標找到obj對應的class。在Class中先去cache中通過SEL尋找對應函數method(猜測cache中method列表是以SEL為key通過hash表來儲存的,這樣能提高函數尋找速度),若 cache中未找到。再去methodList中尋找,若methodlist中未找到,則取superClass中尋找。若能找到,則將method加入到cache中,以方便下次尋找,並通過method中的函數指標跳轉到對應的函數中去執行。
iOS開發:詳解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編程的確有很多功能,它應該成為實際應用代碼編寫的重要工具。
原文地址:http://bl
iOS開發:詳解Objective-C runTime