Objective-C Runtime

jopen 9年前發布 | 38K 次閱讀 Objective-C開發 Objective-C

Objective-C

Objective-C 擴展了 C 語言,并加入了面向對象特性和 Smalltalk 式的消息傳遞機制。而這個擴展的核心是一個用 C 和 編譯語言 寫的 Runtime 庫。它是 Objective-C 面向對象和動態機制的基石。

Objective-C 是一個動態語言,這意味著它不僅需要一個編譯器,也需要一個運行時系統來動態得創建類和對象、進行消息傳遞和轉發。理解 Objective-C 的 Runtime 機制可以幫我們更好的了解這個語言,適當的時候還能對語言進行擴展,從系統層面解決項目中的一些設計或技術問題。了解 Runtime ,要先了解它的核心 - 消息傳遞 (Messaging)。

消息傳遞(Messaging)

I’m sorry that I long ago coined the term “objects” for this topic because it gets many people to focus on the lesser idea. The big idea is “messaging” – that is what the kernal[sic] of Smalltalk is all about... The key in making great and growable systems is much more to design how its modules communicate rather than what their internal properties and behaviors should be.

</blockquote>

Alan Kay 曾多次強調 Smalltalk 的核心不是面向對象,面向對象只是 the lesser ideas,消息傳遞才是 the big idea

在很多語言,比如 C ,調用一個方法其實就是跳到內存中的某一點并開始執行一段代碼。沒有任何動態的特性,因為這在編譯時就決定好了。而在 Objective-C 中,[object foo]語法并不會立即執行 foo 這個方法的代碼。它是在運行時給 object 發送一條叫 foo 的消息。這個消息,也許會由 object 來處理,也許會被轉發給另一個對象,或者不予理睬假裝沒收到這個消息。多條不同的消息也可以對應同一個方法實現。這些都是在程序運行的時候決定的。

事實上,在編譯時你寫的 Objective-C 函數調用的語法都會被翻譯成一個 C 的函數調用 -objc_msgSend()。比如,下面兩行代碼就是等價的:

[array insertObject:foo atIndex:5];

objc_msgSend(array, @selector(insertObject:atIndex:), foo, 5);</pre>

消息傳遞的關鍵藏于objc_object中的 isa 指針和objc_class中的 class dispatch table。

objc_object,objc_class以及Ojbc_method

在 Objective-C 中,類、對象和方法都是一個 C 的結構體,從objc/objc.h頭文件中,我們可以找到他們的定義:

struct objc_object {
Class isa OBJC_ISA_AVAILABILITY; };

struct objc_class {
Class isa OBJC_ISA_AVAILABILITY;

if !OBJC2

Class super_class;
const char *name;
long version;
long info;
long instance_size;
struct objc_ivar_list *ivars;
**struct objc_method_list **methodLists**;
**struct objc_cache *cache**;
struct objc_protocol_list *protocols;

endif

};

struct objc_method_list {
struct objc_method_list *obsolete; int method_count;

ifdef LP64

int space;

endif

/* variable length structure */
struct objc_method method_list[1];

};

struct objc_method {
SEL method_name; char method_types; / a string representing argument/return types */ IMP method_imp; };</pre>

objc_method_list本質是一個有objc_method元素的可變長度的數組。一個objc_method結構體中有函數名,也就是SEL,有表示函數類型的字符串 (見 Type Encoding) ,以及函數的實現IMP。

從這些定義中可以看出發送一條消息也就objc_msgSend做了什么事。舉objc_msgSend(obj, foo)這個例子來說:

  1. 首先,通過 obj 的 isa 指針找到它的 class ;
  2. 在 class 的 method list 找 foo ;
  3. 如果 class 中沒到 foo,繼續往它的 superclass 中找 ;
  4. 一旦找到 foo 這個函數,就去執行它的實現IMP .
  5. </ol>

    但這種實現有個問題,效率低。但一個 class 往往只有 20% 的函數會被經常調用,可能占總調用次數的 80% 。每個消息都需要遍歷一次objc_method_list并不合理。如果把經常被調用的函數緩存下來,那可以大大提高函數查詢的效率。這也就是objc_class中另一個重要成員objc_cache做的事情 - 再找到 foo 之后,把 foo 的method_name作為 key ,method_imp作為 value 給存起來。當再次收到 foo 消息的時候,可以直接在 cache 里找到,避免去遍歷objc_method_list.

    動態方法解析和轉發

    在上面的例子中,如果foo沒有找到會發生什么?通常情況下,程序會在運行時掛掉并拋出 unrecognized selector sent to … 的異常。但在異常拋出前,Objective-C 的運行時會給你三次拯救程序的機會:

    1. Method resolution
    2. Fast forwarding
    3. Normal forwarding
    4. </ol>

      Method Resolution

      首先,Objective-C 運行時會調用+resolveInstanceMethod:或者+resolveClassMethod:,讓你有機會提供一個函數實現。如果你添加了函數并返回 YES, 那運行時系統就會重新啟動一次消息發送的過程。還是以foo為例,你可以這么實現:

      void fooMethod(id obj, SEL _cmd)
      { NSLog(@"Doing foo"); }

      • (BOOL)resolveInstanceMethod:(SEL)aSEL { if(aSEL == @selector(foo:)){
          class_addMethod([self class], aSEL, (IMP)fooMethod, "v@:");
          return YES;
        
        } return [super resolveInstanceMethod]; }</pre>

        Core Data 有用到這個方法。NSManagedObjects 中 properties 的 getter 和 setter 就是在運行時動態添加的。

        如果 resolve 方法返回 NO ,運行時就會移到下一步:消息轉發(Message Forwarding)

        PS:iOS 4.3 加入很多新的 runtime 方法,主要都是以 imp 為前綴的方法,比如imp_implementationWithBlock()用 block 快速創建一個 imp 。
        上面的例子可以重寫成:

        IMP fooIMP = imp_implementationWithBlock(^(id _self) {
        NSLog(@"Doing foo"); });

      class_addMethod([self class], aSEL, fooIMP, "v@:"); </pre>

      Fast forwarding

      如果目標對象實現了-forwardingTargetForSelector:,Runtime 這時就會調用這個方法,給你把這個消息轉發給其他對象的機會。

      - (id)forwardingTargetForSelector:(SEL)aSelector
      {
          if(aSelector == @selector(foo:)){
              return alternateObject;
          }
          return [super forwardingTargetForSelector:aSelector];
      }

      只要這個方法返回的不是 nil 和 self,整個消息發送的過程就會被重啟,當然發送的對象會變成你返回的那個對象。否則,就會繼續 Normal Fowarding 。

      這里叫 Fast ,只是為了區別下一步的轉發機制。因為這一步不會創建任何新的對象,但下一步轉發會創建一個 NSInvocation 對象,所以相對更快點。

      Normal forwarding

      這一步是 Runtime 最后一次給你挽救的機會。首先它會發送-methodSignatureForSelector:消息獲得函數的參數和返回值類型。如果-methodSignatureForSelector:返回 nil ,Runtime 則會發出-doesNotRecognizeSelector:消息,程序這時也就掛掉了。如果返回了一個函數簽名,Runtime 就會創建一個 NSInvocation 對象并發送-forwardInvocation:消息給目標對象。

      NSInvocation 實際上就是對一個消息的描述,包括selector 以及參數等信息。所以你可以在-forwardInvocation:里修改傳進來的 NSInvocation 對象,然后發送-invokeWithTarget:消息給它,傳進去一個新的目標:

      - (void)forwardInvocation:(NSInvocation *)invocation
      {
          SEL sel = invocation.selector;

      if([alternateObject respondsToSelector:sel]) {
          [invocation invokeWithTarget:alternateObject];
      } 
      else {
          [self doesNotRecognizeSelector:sel];
      }
      

      }</pre>

      Cocoa 里很多地方都利用到了消息傳遞機制來對語言進行擴展,如 Proxies、NSUndoManager 跟 Responder Chain。NSProxy 就是專門用來作為代理轉發消息的;NSUndoManager 截取一個消息之后再發送;而 Responder Chain 保證一個消息轉發給合適的響應者。

      總結

      Objective-C 中給一個對象發送消息會經過以下幾個步驟:

      1. 在對象類的 dispatch table 中嘗試找到該消息。如果找到了,跳到相應的函數IMP去執行實現代碼;
      2. 如果沒有找到,Runtime 會發送+resolveInstanceMethod:或者+resolveClassMethod:嘗試去 resolve 這個消息;
      3. 如果 resolve 方法返回 NO,Runtime 就發送-forwardingTargetForSelector:允許你把這個消息轉發給另一個對象;
      4. 如果沒有新的目標對象返回, Runtime 就會發送-methodSignatureForSelector:和-forwardInvocation:消息。你可以發送-invokeWithTarget:消息來手動轉發消息或者發送-doesNotRecognizeSelector:拋出異常。
      5. </ol>

        利用 Objective-C 的 runtime 特性,我們可以自己來對語言進行擴展,解決項目開發中的一些設計和技術問題。下一篇文章,我會介紹 Method Swizzling 技術以及如何利用 Method Swizzling 做 Logging。

        Reference

        Message forwarding

        Objective-c-messaging

        The faster objc_msgSend

        Understanding objective-c runtime

        來自:http://tech.glowing.com/cn/objective-c-runtime/

 本文由用戶 jopen 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!