Objective-C 特性:Runtime

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

Objective-C是基于C語言加入了 面向對象特性消息轉發機制 的動態語言,這意味著它不僅需要一個編譯器,還需要 Runtime系統 來動態創建類和對象,進行消息發送和轉發。下面通過分析Apple開源的 Runtime代碼 (我使用的版本是objc4-646.tar)來深入理解Objective-C的Runtime機制。

Runtime數據結構

在Objective-C中,使用[receiver message]語法并不會馬上執行receiver對象的message方法的代碼,而是向receiver發送一條message消息,這條消息可能由receiver來處理,也可能由轉發給其他對象來處理,也有可能假裝沒有接收到這條消息而沒有處理。其實[receiver message]被編譯器轉化為:

id objc_msgSend ( id self, SEL op, ... );

下面從兩個數據結構id和SEL來逐步分析和理解Runtime有哪些重要的數據結構。

SEL

SEL是函數objc_msgSend第二個參數的數據類型,表示 方法選擇器 ,按下面路徑打開objc.h文件

Objective-C 特性:Runtime

SEL Data Structure

查看到SEL數據結構如下:

typedef struct objc_selector *SEL;

其實它就是映射到方法的C字符串,你可以通過Objc編譯器命令@selector()或者Runtime系統的sel_registerName函數來獲取一個SEL類型的方法選擇器。

如果你知道selector對應的方法名是什么,可以通過NSString* NSStringFromSelector(SEL aSelector)方法將SEL轉化為字符串,再用NSLog打印。

id

接下來看objc_msgSend第一個參數的數據類型id,id是通用類型指針,能夠表示任何對象。按下面路徑打開objc.h文件

Objective-C 特性:Runtime

id Data Structure.png

查看到id數據結構如下:

/// Represents an instance of a class.
struct objc_object {
    Class isa  OBJC_ISA_AVAILABILITY;
};

/// A pointer to an instance of a class.
typedef struct objc_object *id;

id其實就是一個指向objc_object結構體指針,它包含一個Class isa成員,根據isa指針就可以順藤摸瓜找到 對象所屬的類

注意:根據Apple的官方文檔 Key-Value Observing Implementation Details 提及,key-value observing是使用isa-swizzling的技術實現的,isa指針在運行時被修改,指向一個中間類而不是真正的類。所以,你不應該使用isa指針來確定類的關系,而是使用 class 方法來確定實例對象的類。

Class

isa指針的數據類型是Class,Class表示對象所屬的類,按下面路徑打開objc.h文件

Objective-C 特性:Runtime

Class Data Structure

/// An opaque type that represents an Objective-C class.
typedef struct objc_class *Class;

可以查看到Class其實就是一個objc_class結構體指針,但這個頭文件找不到它的定義,需要在runtime.h才能找到objc_class結構體的定義。

按下面路徑打開runtime.h文件

Objective-C 特性:Runtime

objc_class Data Structure

查看到objc_class結構體定義如下:

struct objc_class {
    Class isa  OBJC_ISA_AVAILABILITY;
#if !__OBJC2__
    Class super_class                                       OBJC2_UNAVAILABLE;
    const char *name                                         OBJC2_UNAVAILABLE;
    long version                                             OBJC2_UNAVAILABLE;
    long info                                               OBJC2_UNAVAILABLE;
    long instance_size                                     OBJC2_UNAVAILABLE;
    struct objc_ivar_list *ivars                             OBJC2_UNAVAILABLE;
    struct objc_method_list **methodLists                   OBJC2_UNAVAILABLE;
    struct objc_cache *cache                                 OBJC2_UNAVAILABLE;
    struct objc_protocol_list *protocols                     OBJC2_UNAVAILABLE;
#endif
} OBJC2_UNAVAILABLE;
/* Use `Class` instead of `struct objc_class *` */

注意:OBJC2_UNAVAILABLE是一個Apple對Objc系統運行版本進行約束的宏定義,主要為了兼容非Objective-C 2.0的遺留版本,但我們仍能從中獲取一些有用信息。

讓我們分析一些重要的成員變量表示什么意思和對應使用哪些數據結構。

  • isa表示一個Class對象的Class,也就是 Meta Class 。在面向對象設計中,一切都是對象,Class在設計中本身也是一個對象。我們會在objc-runtime-new.h文件找到證據,發現objc_class有以下定義:

    struct objc_class : objc_object {
      // Class ISA;
      Class superclass;
      cache_t cache;             // formerly cache pointer and vtable
      class_data_bits_t bits;    // class_rw_t * plus custom rr/alloc flags
    
      ......
    }

    由此可見,結構體objc_class也是繼承objc_object,說明 Class在設計中本身也是一個對象

    其實Meta Class也是一個Class,那么它也跟其他Class一樣有自己的isa和super_class指針,關系如下:

    Objective-C 特性:Runtime

    Class isa and superclass relationship from Google

    上圖 實線 是super_class指針, 虛線 是isa指針。有幾個關鍵點需要解釋以下:

    • Root class (class)其實就是NSObject,NSObject是沒有超類的,所以Root class(class)的superclass指向nil。
    • 每個Class都有一個isa指針指向唯一的Meta class
    • Root class(meta)的superclass指向Root class(class),也就是NSObject,形成一個回路。
    • 每個Meta class的isa指針都指向Root class (meta)。
  • super_class表示實例對象對應的父類
  • name表示類名
  • ivars表示多個成員變量,它指向objc_ivar_list結構體。在runtime.h可以看到它的定義:

    struct objc_ivar_list {
      int ivar_count                                           OBJC2_UNAVAILABLE;
    #ifdef __LP64__
      int space                                                OBJC2_UNAVAILABLE;
    #endif
      /* variable length structure */
      struct objc_ivar ivar_list[1]                            OBJC2_UNAVAILABLE;
    }

    objc_ivar_list其實就是一個鏈表,存儲多個objc_ivar,而objc_ivar結構體存儲類的單個成員變量信息。

  • methodLists表示方法列表,它指向objc_method_list結構體的二級指針,可以動態修改*methodLists的值來添加成員方法,也是Category實現原理,同樣也解釋Category不能添加屬性的原因。在runtime.h可以看到它的定義:

    struct objc_method_list {
      struct objc_method_list *obsolete                        OBJC2_UNAVAILABLE;
    
      int method_count                                         OBJC2_UNAVAILABLE;
    #ifdef __LP64__
      int space                                                OBJC2_UNAVAILABLE;
    #endif
      /* variable length structure */
      struct objc_method method_list[1]                        OBJC2_UNAVAILABLE;
    }

    同理,objc_method_list也是一個鏈表,存儲多個objc_method,而objc_method結構體存儲類的某個方法的信息。

  • cache用來緩存經常訪問的方法,它指向objc_cache結構體,后面會重點講到。

  • protocols表示類遵循哪些協議

Method

Method表示類中的某個方法,在runtime.h文件中找到它的定義:

/// An opaque type that represents a method in a class definition.
typedef struct objc_method *Method;

struct objc_method {
    SEL method_name                                          OBJC2_UNAVAILABLE;
    char *method_types                                       OBJC2_UNAVAILABLE;
    IMP method_imp                                           OBJC2_UNAVAILABLE;
}

其實Method就是一個指向objc_method結構體指針,它存儲了方法名(method_name)、方法類型(method_types)和方法實現(method_imp)等信息。而method_imp的數據類型是IMP,它是一個函數指針,后面會重點提及。

Ivar

Ivar表示類中的實例變量,在runtime.h文件中找到它的定義:

/// An opaque type that represents an instance variable.
typedef struct objc_ivar *Ivar;
struct objc_ivar {
    char *ivar_name                                       OBJC2_UNAVAILABLE;
    char *ivar_type                                       OBJC2_UNAVAILABLE;
    int ivar_offset                                       OBJC2_UNAVAILABLE;
#ifdef __LP64__
    int space                                               OBJC2_UNAVAILABLE;
#endif
}

Ivar其實就是一個指向objc_ivar結構體指針,它包含了變量名(ivar_name)、變量類型(ivar_type)等信息。

IMP

在上面講Method時就說過,IMP本質上就是一個函數指針,指向方法的實現,在objc.h找到它的定義:

/// A pointer to the function of a method implementation. 
#if !OBJC_OLD_DISPATCH_PROTOTYPES
typedef void (*IMP)(void /* id, SEL, ... */ ); 
#else
typedef id (*IMP)(id, SEL, ...); 
#endif

當你向某個對象發送一條信息,可以由這個函數指針來指定方法的實現,它最終就會執行那段代碼,這樣可以 繞開消息傳遞 階段而去執行另一個方法實現。

Cache

顧名思義,Cache主要用來緩存,那它緩存什么呢?我們先在runtime.h文件看看它的定義:

typedef struct objc_cache *Cache                             OBJC2_UNAVAILABLE;

struct objc_cache {
    unsigned int mask /* total = mask + 1 */                 OBJC2_UNAVAILABLE;
    unsigned int occupied                                    OBJC2_UNAVAILABLE;
    Method buckets[1]                                        OBJC2_UNAVAILABLE;
};

Cache其實就是一個存儲Method的鏈表,主要是為了優化 方法調用 的性能。當對象receiver調用方法message時,首先根據對象receiver的isa指針查找到它對應的類,然后在類的methodLists中搜索方法,如果沒有找到,就使用super_class指針到父類中的methodLists查找,一旦找到就調用方法。如果沒有找到,有可能消息轉發,也可能忽略它。但這樣查找方式效率太低,因為往往一個類大概只有20%的方法經常被調用,占總調用次數的80%。所以使用Cache來 緩存經常調用的方法 ,當調用方法時,優先在Cache查找,如果沒有找到,再到methodLists查找。

消息發送

前面從objc_msgSend作為入口,逐步深入分析Runtime的數據結構,了解每個數據結構的作用和它們之間關系后,我們正式轉入 消息發送 這個正題。

objc_msgSend函數

在前面已經提過,當某個對象使用語法[receiver message]來調用某個方法時,其實[receiver message]被編譯器轉化為:

id objc_msgSend ( id self, SEL op, ... );

現在讓我們看一下objc_msgSend它具體是如何發送消息:

  1. 首先根據receiver對象的isa指針獲取它對應的class
  2. 優先在class的cache查找message方法,如果找不到,再到methodLists查找
  3. 如果沒有在class找到,再到super_class查找
  4. 一旦找到message這個方法,就執行它實現的IMP。
Objective-C 特性:Runtime

Objc Message.gif

self與super

為了讓大家更好地理解self和super,借用 sunnyxx 博客的 ios程序員6級考試 一道題目:下面的代碼分別輸出什么?

@implementation Son : Father
- (id)init
{
    self = [super init];
    if (self)
    {
        NSLog(@"%@", NSStringFromClass([self class]));
        NSLog(@"%@", NSStringFromClass([super class]));
    }
    return self;
}
@end

self表示當前這個類的對象,而super是一個編譯器標示符,和self指向同一個消息接受者。在本例中,無論是[self class]還是[super class],接受消息者都是Son對象,但super與self不同的是,self調用class方法時,是在子類Son中查找方法,而super調用class方法時,是在父類Father中查找方法。

當調用[self class]方法時,會轉化為objc_msgSend函數,這個函數定義如下:

id objc_msgSend(id self, SEL op, ...)

這時會從當前Son類的方法列表中查找,如果沒有,就到Father類查找,還是沒有,最后在NSObject類查找到。我們可以從NSObject.mm文件中看到- (Class)class的實現:

- (Class)class {
    return object_getClass(self);
}

所以NSLog(@"%@", NSStringFromClass([self class]));會輸出 Son

當調用[super class]方法時,會轉化為objc_msgSendSuper,這個函數定義如下:

id objc_msgSendSuper(struct objc_super *super, SEL op, ...)

objc_msgSendSuper函數第一個參數super的數據類型是一個指向objc_super的結構體,從message.h文件中查看它的定義:

/// Specifies the superclass of an instance. 
struct objc_super {
    /// Specifies an instance of a class.
    __unsafe_unretained id receiver;
    /// Specifies the particular superclass of the instance to message. 
#if !defined(__cplusplus)  &&  !__OBJC2__
    /* For compatibility with old objc-runtime.h header */
    __unsafe_unretained Class class;
#else
    __unsafe_unretained Class super_class;
#endif
    /* super_class is the first class to search */
};
#endif

結構體包含兩個成員,第一個是receiver,表示某個類的實例。第二個是super_class表示當前類的父類。

這時首先會構造出objc_super結構體,這個結構體第一個成員是self,第二個成員是(id)class_getSuperclass(objc_getClass("Son")),實際上該函數會輸出Father。然后在Father類查找class方法,查找不到,最后在NSObject查到。此時,內部使用objc_msgSend(objc_super->receiver, @selector(class))去調用,與[self class]調用相同,所以結果還是 Son

隱藏參數self和_cmd

當[receiver message]調用方法時,系統會在運行時偷偷地動態傳入兩個隱藏參數self和_cmd,之所以稱它們為隱藏參數,是因為在源代碼中沒有聲明和定義這兩個參數。至于對于self的描述,上面已經解釋非常清楚了,下面我們重點講解_cmd。

_cmd表示當前調用方法,其實它就是一個方法選擇器SEL。一般用于判斷方法名或在Associated Objects中唯一標識鍵名,后面在Associated Objects會講到。

方法解析與消息轉發

[receiver message]調用方法時,如果在message方法在receiver對象的類繼承體系中沒有找到方法,那怎么辦?一般情況下,程序在運行時就會Crash掉,拋出 unrecognized selector sent to … 類似這樣的異常信息。但在拋出異常之前,還有 三次機會 按以下順序讓你拯救程序。

  1. Method Resolution
  2. Fast Forwarding
  3. Normal Forwarding
Objective-C 特性:Runtime

Message Forward from Google

Method Resolution

首先Objective-C在運行時調用+ resolveInstanceMethod:或+ resolveClassMethod:方法,讓你添加方法的實現。如果你添加方法并返回YES,那系統在運行時就會重新啟動一次消息發送的過程。

舉一個簡單例子,定義一個類Message,它主要定義一個方法sendMessage,下面就是它的設計與實現:

@interface Message : NSObject

- (void)sendMessage:(NSString *)word;

@end
@implementation Message

- (void)sendMessage:(NSString *)word
{
    NSLog(@"normal way : send message = %@", word);
}

@end

如果我在viewDidLoad方法中創建Message對象并調用sendMessage方法:

- (void)viewDidLoad {
    [super viewDidLoad];

    Message *message = [Message new];
    [message sendMessage:@"Sam Lau"];
}

控制臺會打印以下信息:

normal way : send message = Sam Lau

但現在我將原來sendMessage方法實現給 注釋掉 ,覆蓋resolveInstanceMethod方法:

#pragma mark - Method Resolution

/// override resolveInstanceMethod or resolveClassMethod for changing sendMessage method implementation
+ (BOOL)resolveInstanceMethod:(SEL)sel
{
    if (sel == @selector(sendMessage:)) {
        class_addMethod([self class], sel, imp_implementationWithBlock(^(id self, NSString *word) {
            NSLog(@"method resolution way : send message = %@", word);
        }), "v@*");
    }

    return YES;
}

控制臺就會打印以下信息:

method resolution way : send message = Sam Lau

注意到上面代碼有這樣一個字符串"v@*,它表示方法的參數和返回值,詳情請參考 Type Encodings

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

Fast Forwarding

如果目標對象實現- forwardingTargetForSelector:方法,系統就會在運行時調用這個方法,只要這個方法返回的不是nil或self,也會重啟消息發送的過程,把這消息轉發給其他對象來處理。否則,就會繼續 Normal Fowarding

繼續上面Message類的例子,將sendMessage和resolveInstanceMethod方法 注釋掉 ,然后添加forwardingTargetForSelector方法的實現:

#pragma mark - Fast Forwarding
- (id)forwardingTargetForSelector:(SEL)aSelector
{
    if (aSelector == @selector(sendMessage:)) {
        return [MessageForwarding new];
    }

    return nil;
}

此時還缺一個轉發消息的類MessageForwarding,這個類的設計與實現如下:

@interface MessageForwarding : NSObject

- (void)sendMessage:(NSString *)word;

@end
@implementation MessageForwarding

- (void)sendMessage:(NSString *)word
{
    NSLog(@"fast forwarding way : send message = %@", word);
}

@end

此時,控制臺會打印以下信息:

fast forwarding way : send message = Sam Lau

這里叫Fast,是因為這一步不會創建NSInvocation對象,但Normal Forwarding會創建它,所以相對于更快點。

Normal Forwarding

如果沒有使用Fast Forwarding來消息轉發,最后只有使用Normal Forwarding來進行消息轉發。它首先調用methodSignatureForSelector:方法來獲取函數的參數和返回值,如果返回為nil,程序會Crash掉,并拋出 unrecognized selector sent to instance 異常信息。如果返回一個函數簽名,系統就會創建一個 NSInvocation 對象并調用-forwardInvocation:方法。

繼續前面的例子,將forwardingTargetForSelector方法 注釋掉 ,添加methodSignatureForSelector和forwardInvocation方法的實現:

#pragma mark - Normal Forwarding
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
{
    NSMethodSignature *methodSignature = [super methodSignatureForSelector:aSelector];
    if (!methodSignature) {
        methodSignature = [NSMethodSignature signatureWithObjCTypes:"v@:*"];
    }
    return methodSignature;
}
- (void)forwardInvocation:(NSInvocation *)anInvocation
{
    MessageForwarding *messageForwarding = [MessageForwarding new];
    if ([messageForwarding respondsToSelector:anInvocation.selector]) {
        [anInvocation invokeWithTarget:messageForwarding];
    }
}

關于這個例子的示例代碼請到 github 下載。

三種方法的選擇

Runtime提供三種方式來將原來的方法實現代替掉,那該怎樣選擇它們呢?

  • Method Resolution: 由于Method Resolution不能像消息轉發那樣可以交給其他對象來處理,所以只適用于在 原來的類 中代替掉。
  • Fast Forwarding: 它可以將消息處理轉發給 其他對象 ,使用范圍更廣,不只是限于原來的對象。
  • Normal Forwarding: 它跟Fast Forwarding一樣可以消息轉發,但它能通過NSInvocation對象獲取 更多消息發送 的信息,例如:target、selector、arguments和返回值等信息。

Associated Objects

當使用Category對某個類進行擴展時,有時需要 存儲屬性 ,Category是不支持的,這時需要使用Associated Objects來給已存在的類Category添加自定義的屬性。Associated Objects提供三個API來向對象 添加、獲取和刪除 關聯值:

  • void objc_setAssociatedObject (id object, const void *key, id value, objc_AssociationPolicy policy )
  • id objc_getAssociatedObject (id object, const void *key )
  • void objc_removeAssociatedObjects (id object )

其中objc_AssociationPolicy是個枚舉類型,它可以指定Objc內存管理的引用計數機制。

typedef OBJC_ENUM(uintptr_t, objc_AssociationPolicy) {
    OBJC_ASSOCIATION_ASSIGN = 0,           /**< Specifies a weak reference to the associated object. */
    OBJC_ASSOCIATION_RETAIN_NONATOMIC = 1, /**< Specifies a strong reference to the associated object. 
        *   The association is not made atomically. */
    OBJC_ASSOCIATION_COPY_NONATOMIC = 3,   /**< Specifies that the associated object is copied. 
        *   The association is not made atomically. */
    OBJC_ASSOCIATION_RETAIN = 01401,       /**< Specifies a strong reference to the associated object.
        *   The association is made atomically. */
    OBJC_ASSOCIATION_COPY = 01403          /**< Specifies that the associated object is copied.
        *   The association is made atomically. */
};

下面有個關于NSObject+AssociatedObjectCategory添加屬性associatedObject的 示例代碼 :

NSObject+AssociatedObject.h

@interface NSObject (AssociatedObject)

@property (strong, nonatomic) id associatedObject;

@end

NSObject+AssociatedObject.m

@implementation NSObject (AssociatedObject)
- (void)setAssociatedObject:(id)associatedObject
{
    objc_setAssociatedObject(self, @selector(associatedObject), associatedObject, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
- (id)associatedObject
{
    return objc_getAssociatedObject(self, _cmd);
}
@end

Associated Objects的key要求是 唯一 并且是 常量 ,而SEL是滿足這個要求的,所以上面的采用隱藏參數_cmd作為key。

Method Swizzling

Method Swizzling就是在運行時將一個方法的實現代替為另一個方法的實現。如果能夠利用好這個技巧,可以寫出 簡潔、有效且維護性更好 的代碼。可以參考兩篇關于Method Swizzling技巧的文章:

Aspect-Oriented Programming(AOP)

類似記錄日志、身份驗證、緩存等事務非常瑣碎,與業務邏輯無關,很多地方都有,又很難抽象出一個模塊,這種程序設計問題,業界給它們起了一個名字叫 橫向關注點(Cross-cutting concern)AOP 作用就是分離 橫向關注點(Cross-cutting concern) 來提高模塊復用性,它可以在既有的代碼添加一些額外的行為(記錄日志、身份驗證、緩存)而無需修改代碼。

危險性

Method Swizzling就像一把瑞士小刀,如果使用得當,它會有效地解決問題。但使用不當,將帶來很多麻煩。在stackoverflow上有人已經提出這樣一個問題: What are the Dangers of Method Swizzling in Objective C? ,它的 危險性 主要體現以下幾個方面:

  • Method swizzling is not atomic
  • Changes behavior of un-owned code
  • Possible naming conflicts
  • Swizzling changes the method's arguments
  • The order of swizzles matters
  • Difficult to understand (looks recursive)
  • Difficult to debug

總結

雖然在平時項目不是經常用到Objective-C的Runtime特性,但當你閱讀一些iOS開源項目時,你就會發現很多時候都會用到。所以深入理解Objective-C的Runtime數據結構、消息轉發機制有助于你更容易地閱讀和學習開源項目。

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