Runtime of Objective-C

原文出處:http://blog.csdn.net/jasonblog/article/details/7246822


[0] Outline

  --  [1] 版本和平臺

  --  [2] 與Runtime System交互

  --  [3] 方法的動態決議

  --  [4] 消息轉發

  --  [5] 類型編碼

  --  [6] 屬性聲明


[1] 版本和平臺

Runtime System對於Objective-C來說就好比是它的操作系統,或者說是運行的支撐平臺,它使得Objective-C代碼能夠按照既定的語言特性跑起來。相對於C/C++來說,Objective-C儘可能地把一些動作推遲到運行時來執行,即儘可能動態地做事情。因此,它不僅需要一個編譯器,還需要一個運行時環境來執行編譯後的代碼。

Runtime System分爲Legacy和Modern兩個版本,一般來說,我們現在用的都是Modern版本。Modern版本的Runtime System有一個顯著的特徵就是“non-fragile”,即父類的成員變量的佈局發生改變時,子類不需要重新編譯。此外,還支持爲聲明的屬性進行合成操作(即@property@synthesis)。

下面會討論NSObject類Objective-C程序如何與Runtime System交互運行時動態地加載類發消息給其它對象,以及運行時如何獲取對象信息


[2] 與Runtime System交互

Objective-C程序和Runtime System在三個不同層次進行交互:通過Objective-C源碼;通過NSObject定義的函數;以及通過直接調用runtime functions


通常來講,Runtime System都是在幕後工作,我們需要做的就是編寫Objective-C代碼,然後編譯。編譯器會爲我們創建相應的數據結構和函數調用來實現語言的動態特性。這些數據結構保存着在類、Category定義和Protocol聲明中所能找到的信息,包括成員變量模板、selectors,以及其它從源碼中提取到的信息。

Runtime System是一個動態共享庫,位於/usr/include/objc,擁有一套公共的接口,由一系列函數和數據結構組成。開發人員可以使用純C調用一些函數來做編譯器做的事情,或者擴展Runtime System,爲開發環境製作一些工具等等。儘管一般情況下,編寫Objective-C並不需要了解這些內容,但有時候會很有用。所有的函數都在Objective-C Runtime Reference有文檔化信息。


Cocoa中大部分對象都是NSObject的子類(NSProxy是一個例外),繼承了NSObject的方法。因此在這個繼承體系中,子類可以根據需求重新實現NSObject定義的一些函數,實現多態和動態性,比如description方法(返回描述自身的字符串,類似Python中開頭的三引號)。

一些NSObject定義的方法只是簡單地詢問Runtime System獲得信息,使得對象可以進行自省(introspection),比如用來確定類類型的isKindOfClass:,確定對象在繼承體系中的位置的isMemberOfClass:,判斷一個對象是否能接收某個特定消息的respondsToSelector:,判斷一個對象是否遵循某個協議的conformsToProtocol:,以及提供方法實現地址的methodForSelector:。這些方法讓一個對象可以進行自省(introspect about itself)。


最主要的Runtime函數是用來發送消息的,它由源碼中的消息表達式激發。發送消息是Objective-C程序中最經常出現的表達式,而該表達式最終會被轉換成objc_msgSend函數調用。比如一個消息表達式[receiver message]會被轉換成objc_msgSend(receiver, selector),如果有參數則爲objc_msgSend(receiver, selector, arg1, arg2, …)

消息只有到運行時纔會和函數實現綁定起來:首先objc_msgSend在receiver中查找selector對應的函數實現;然後調用函數過程,將receiving object(即this指針)和參數傳遞過去;最後,返回函數的返回值。

發送消息的關鍵是編譯器爲類和對象創建的結構,包含兩個主要元素,一個是指向superclass的指針,另一個是類的dispatch table,該dispatch table中的表項將selector和對應的函數入口地址關聯起來。

當一個對象被創建時,內存佈局中的第一個元素是指向類結構的指針,isa。通過isa指針,一個對象可以訪問它的類結構,進而訪問繼承的類結構。示例圖可參見此處

當向一個對象發送消息時,objc_msgSend先通過isa指針在類的dispatch table中查找對應selector的函數入口地址,如果沒有找到,則沿着class hierarchy(類的繼承體系)尋找,直到NSObject類。這就是在運行時選擇函數實現,用OOP的行話來說,就是動態綁定。

爲了加速發送消息的速度,Runtime System爲每個類創建了一個cache,用來緩存selector和對應函數入口地址的映射。


當objc_msgSend找到對應的函數實現時,它除了傳遞函數參數,還傳遞了兩個隱藏參數:receiving objectselector。之所以稱之爲隱藏參數,是因爲這兩個參數在源代碼中沒有顯示聲明,但還是可以通過self和_cmd來訪問。

當一個消息要被髮送給某個對象很多次的時候,可以直接使用methodForSelector:來進行優化,比如下述代碼:

  1. //////////////////////////////////////////////////////////////  
  2. void (*setter)(id, SEL, BOOL);  
  3. int i;  
  4.   
  5. setter = (void (*)(id, SEL, BOOL))[target  
  6.      methodForSelector:@selector(setFilled:)];  
  7. for ( i = 0; i < 1000, i++ )   
  8.      setter(targetList[i], @selector(setFilled:), YES);  
  9. //////////////////////////////////////////////////////////////  

其中,methodForSelector:是由Cocoa Runtime System提供的,而不是Objective-C本身的語言特性。這裏需要注意轉換過程中函數類型的正確性,包括返回值和參數,而且這裏的前兩個參數需要顯示聲明爲id和SEL。


[3] 方法的動態決議

有時候我們想要爲一個方法動態地提供實現,比如Objective-C的@dynamic指示符,它告訴編譯器與屬性對應的方法是動態提供的。我們可以利用resolveInstanceMethod:resolveClassMethod:分別爲對象方法和類方法提供動態實現。

一個Objective-C方法本質上是一個擁有至少兩個參數(self和_cmd)的C函數,我們可以利用class_addMethod向一個類添加一個方法。比如對於下面的函數:

  1. //////////////////////////////////////////////////////////////  
  2. void dynamicMethodIMP(id self, SEL _cmd) {  
  3.      // implementation ….  
  4. }  
  5. //////////////////////////////////////////////////////////////  

我們可以利用resolveInstanceMethod:將它添加成一個方法(比如叫resolveThisMethodDynamically):

  1. //////////////////////////////////////////////////////////////  
  2. @implementation MyClass  
  3. + (BOOL)resolveInstanceMethod:(SEL)aSEL  
  4. {  
  5.      if (aSEL == @selector(resolveThisMethodDynamically)) {  
  6.           class_addMethod([self class], aSEL, (IMP) dynamicMethodIMP, "v@:");  
  7.           return YES;  
  8.      }  
  9.      return [super resolveInstanceMethod:aSEL];  
  10. }  
  11. @end  
  12. //////////////////////////////////////////////////////////////  

動態決議和發送消息並不衝突,在消息機制起作用之前,一個類是有機會動態決議一個方法的。當respondsToSelector:或者instancesRespondToSelector:被激活時,dynamic method resolver會優先有個機會爲這個selector提供一份實現。如果實現了resolveInstanceMethod:,對於不想動態決議而想讓其遵循消息轉發機制的selectors,返回NO即可。

Objective-C程序可以在運行時鏈接新的類和category。動態加載可以用來做很多不同的事情,比如System Preferences裏頭各種模塊就是動態加載的。儘管有運行時函數可以動態加載Objective-C模塊(objc/objc-load.h中的objc_loadModules),但Cocoa的NSBundle類提供了更方便的動態加載接口。


[4] 消息轉發

向一個對象發送它不處理的消息是一個錯誤,不過在報錯之前,Runtime System給了接收對象第二次的機會來處理消息。在這種情況下,Runtime System會向對象發一個消息,forwardInvocation:,這個消息只攜帶一個NSInvocation對象作爲參數——這個NSInvocation對象包裝了原始消息和相應參數。

通過實現forwardInvocation:方法(繼承於NSObject),可以給不響應的消息一個默認處理方式。正如方法名一樣,通常的處理方式就是轉發該消息給另一個對象:

  1. //////////////////////////////////////////////////////////////  
  2. - (void)forwardInvocation:(NSInvocation *)anInvocation  
  3. {  
  4.      if ([someOtherObject respondsToSelector:[anInvocation selector]])  
  5.           [anInvocation invokeWithTarget:someOtherObject];  
  6.      else  
  7.           [super forwardInvocation:anInvocation];  
  8. }  
  9. //////////////////////////////////////////////////////////////  

對於不識別的消息(在dispatch table中找不到),forwardInvocation:就像一箇中轉站,想繼續投遞或者停止不處理,都由開發人員決定。


[5] 類型編碼

爲了支持Runtime System,編譯器將返回值類型、參數類型進行編碼,相應的編譯器指示符是@encode

比如,void編碼爲v,char編碼爲c,對象編碼爲@,類編碼爲#,選擇符編碼爲:,而符合類型則由基本類型組成,比如

  1. typedef struct example {  
  2.      id     anObject;  
  3.      char *aString;  
  4.      int anInt;  
  5. } Example;  

編碼爲{example=@*i}。


[6] 屬性聲明

當編譯器遇到屬性聲明時,它會生成一些可描述的元數據(metadata),將其與相應的類、category和協議關聯起來。存在一些函數可以通過名稱在類或者協議中查找這些metadata,通過這些函數,我們可以獲得編碼後的屬性類型(字符串),複製屬性的attribute列表(C字符串數組)。因此,每個類和協議的屬性列表我們都可以獲得。


與類型編碼類似,屬性類型也有相應的編碼方案,比如readonly編碼爲R,copy編碼爲C,retain編碼爲&等。

通過property_getAttributes函數可以後去編碼後的字符串,該字符串以T開頭,緊接@encode type和逗號,接着以V和變量名結尾。比如:

  1. @property char charDefault;  

描述爲:Tc,VcharDefault

  1. @property(retain)ididRetain;  

描述爲:T@,&,VidRetain


Property結構體定義了一個指向屬性描述符的不透明句柄:typedef struct objc_property *Property;

通過class_copyPropertyList和protocol_copyPropertyList函數可以獲取相應的屬性數組:

  1. objc_property_t *class_copyPropertyList(Class cls, unsigned int *outCount)  
  2. objc_property_t *protocol_copyPropertyList(Protocol *proto, unsigned int *outCount)  

通過property_getName函數可以獲取屬性名稱。

通過class_getProperty和protocol_getProperty可以相應地根據給定名稱獲取到屬性引用:

  1. objc_property_t class_getProperty(Class cls, const char *name)  
  2. objc_property_t protocol_getProperty(Protocol *proto, const char *name, BOOL isRequiredProperty, BOOL isInstanceProperty)  

通過property_getAttributes函數可以獲取屬性的@encode type string:

const char *property_getAttributes(objc_property_t property)


以上函數組合成一段示例代碼:

  1. @interface Lender : NSObject {  
  2.      float alone;  
  3. }  
  4. @property float alone;  
  5. @end  
  6.   
  7. id LenderClass = objc_getClass("Lender");  
  8. unsigned int outCount, i;  
  9. objc_property_t *properties = class_copyPropertyList(LenderClass, &outCount);  
  10. for (i = 0; i < outCount; i++) {  
  11.      objc_property_t property = properties[i];  
  12.      fprintf(stdout, "%s %s\n", property_getName(property), property_getAttributes(property));  
  13. }  


[Last Updated] 2012-03-17

參考資料:Objective-C Runtime Programming Guide


Jason Lee @ 杭州

博客:http://blog.csdn.net/jasonblog

微博:http://weibo.com/jasonmblog

GitHub:https://github.com/siqin


發佈了11 篇原創文章 · 獲贊 2 · 訪問量 5萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章