Objective-C RunTime概覽

本文為入門介紹,希望能讓第一次接觸Runtime概念的朋友有一個概貌了解。

一篇文章,不可能講完Runtime的全部,但是,分成很多篇講,又有點「見樹木不見森林」的迷糊感覺——自己就是看了很多關(guān)于Runtime的文章,看完還是「迷霧重重」(當然,也可能因為資質(zhì)太過平庸)。

所以,這一篇,盡量涉及。

一句話概括

什么是Runtime?作以下引述。但也不要太奢望看完這些說明后,就會豁然開朗。

官方文檔Objective-C Runtime

The Objective-C runtime is a runtime library that provides support for the dynamic properties of the Objective-C language, and as such is linked to by all Objective-C apps.

Objective-C的runtime是一個「運行時庫」,為OC這門語言提供動態(tài)的特性,所有OC應(yīng)用程序都與之相關(guān)聯(lián)。

The down low on Objective-C Runtime

The Objective-C Runtime is an open source library written in C and Assembler that adds the Object Oriented capabilities to C to create the Objective-C language.

Objective-C的Runtime,是一個用C和匯編寫的「開源庫」,它為C添加了面向?qū)ο蟮奶匦?,從而成就了Objrctive-C這門語言。

The Objective-C languages defers as many decisions as it can from compile time and link time to runtime. Whenever possible, it does things dynamically. This means that the language requires not just a compiler, but also a runtime system to execute the compiled code. The runtime system acts as a kind of operating system for the Objective-C language, it’s what makes the language work.

Objective-C可以從『編譯時』、『鏈接時』再到『運行時』,hold住盡可能多的決策。只要有可能,它都是動態(tài)地干活兒的。這就意味著,這門語言不僅需要一個編譯器,還需要一個runtime系統(tǒng),用來執(zhí)行編譯的代碼。這個runtime系統(tǒng)就好比如是Objective-C的「操作系統(tǒng)」,(runtime系統(tǒng))讓這門語言能工作起來。

簡單點理解,Runtime就是一個C和匯編寫的代碼庫——是Objective-C之所以成為Objective-C的一個庫。

用一圖以助理解:

Runtime概覽

另外,可參考: 重識 Objective-C Runtime - Smalltalk 與 C 的融合

Runtime的三個頭文件

Runtime這個庫是開源的。有興(能)趣(力)的朋友可以仔細研究。

而平時我們會用到的Runtime函數(shù),基本上在runtime.h, objc.h, message.h這三個頭文件中。代碼2500行+(主要是runtime.h)

runtime.h

runtime.h中定義了若干「類型(Types)」和「函數(shù)(Functions)」。

有我們比較熟悉的Method,IvarCategory,objc_property_t,objc_class類型,都在這里定義。

另外還有106個函數(shù)。如常見的:

object_copy(), class_respondsToSelector(), class_copyMethodList等都在這里面。

objc.h

objc.h中定義了Class, id, SEL, IMP類型。

另外還有6個函數(shù)。

message.h

聲明了一系列的方法執(zhí)行函數(shù)。

objc_msgSend()objc_msgSendSuper()都定義在這里。

名詞解釋

isa

isa是一個指針,隱式地存在于實例對象、類中,對象的isa指針指向所屬類——因此實例對象能知道自己屬于哪個類;類的isa指針指向一個叫「元類(Meta Class))」的玩意兒。

isa指針在三個地方有定義:

  • objc_class結(jié)構(gòu)體有聲明,指向類的meta類。(在runtime.h)
  • objc_object結(jié)構(gòu)體有聲明,指向?qū)ο笏鶎兕?在objc.h)
  • NSObject類有有聲明,指向?qū)ο笏鶎兕悾?在NSObject.h)

Class

Class定義在objc.h中第37、38行,是一個指向objc_class結(jié)構(gòu)體的指針。

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

So, Class就是一個「指針變量」。

objc_class結(jié)構(gòu)體在runtime.h第55-70行中有定義:

struct objc_class {
    Class _Nonnull isa  OBJC_ISA_AVAILABILITY;// isa指針, 指向一個meta類,側(cè)面印證:「類也是對象」

#if !__OBJC2__
    Class _Nullable super_class                              OBJC2_UNAVAILABLE;// 指向父類
    const char * _Nonnull name                               OBJC2_UNAVAILABLE;
    long version                                             OBJC2_UNAVAILABLE;
    long info                                                OBJC2_UNAVAILABLE;
    long instance_size                                       OBJC2_UNAVAILABLE;
    struct objc_ivar_list * _Nullable ivars                  OBJC2_UNAVAILABLE;// 變量列表
    struct objc_method_list * _Nullable * _Nullable methodLists                    OBJC2_UNAVAILABLE;// 方法列表, 注意是有兩個星號的
    struct objc_cache * _Nonnull cache                       OBJC2_UNAVAILABLE;// 用于緩存方法
    struct objc_protocol_list * _Nullable protocols          OBJC2_UNAVAILABLE;// 協(xié)議
#endif

} OBJC2_UNAVAILABLE;
/* Use `Class` instead of `struct objc_class *` */

這個結(jié)構(gòu)體,包括:isa指針、父類指針、類名、成員變量、方法列表、緩存以及協(xié)議列表等。

解讀一下部分成員:

isa指針
上面介紹isa的時候,說過類也有一個isa指針,我們可以理解為:類本身也是一個對象——「類對象」。是「元類(Meta Class)」的實例(每個類的isa指針指向元類)。

我們熟知的「類方法」,也可以理解為是「類對象」的實例方法。

而這些「元類(Meta Class)」則是「根源類(Root Meta Class)」的實例——所有元類的isa指針最終都指向根元類。根元類的isa指針指向自己,最終完成閉環(huán)。

畫了一張示意圖幫助理解:

isa的指針的指向

struct objc_ivar_list
struct objc_ivar_list(ivars),是實例變量列表,保存類所聲明的所有實例變量。

objc_method_list
struct objc_method_list(methodLists)是方法列表,給某個對象發(fā)送消息,就是來這個列表中查找是否有相應(yīng)方法實現(xiàn)的。

可以動態(tài)修改methodLists的值來添加成員方法,這也是Category的實現(xiàn)原理。

struct objc_cache
struct objc_cache(cache),用于緩存方法,調(diào)用過的方法會緩存到這里,方便以后索引,提高速度。

Method

定義在runtime.h第44行,表示一個方法。

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

objc_method結(jié)構(gòu)體存儲了方法名、方法類型和方法實現(xiàn)。

SEL

定義在objc.h第49、50行中,表示一個方法選擇器(可以簡單點,理解為方法名,一個C語言的字符串)。

/// An opaque type that represents a method selector.
typedef struct objc_selector *SEL;

IMP

定義在objc.h第54行,表示一個方法的實現(xiàn)。由這個函數(shù)指針決定最終執(zhí)行哪段代碼。

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

Method , SELIMP有什么區(qū)別?

  • Method:表示一個方法,本質(zhì)是一個指向objc_method結(jié)構(gòu)體的指針。
  • SEL(Selector):在運行時用來代表一個方法的名字。
  • IMP(Implementation):表示方法的實現(xiàn)部分。第一個參數(shù)id指向調(diào)用方法的自身,第二個參數(shù)是方法的名字seletor,方法的參數(shù)緊隨其后。

在消息發(fā)送的過程中,這三個概念是可以互相轉(zhuǎn)換的。

可以這樣理解:

Runtime中,Class維護了一份分發(fā)列表(dispatch table),用于消息分發(fā);列表中每個入口,就是一個方法(Method),這份列表的key是selector(SEL),value是implementation(IMP)。

而后面介紹到的Method Swizzling,就是改變這份列表某兩個方法的SEL和IMP的對應(yīng)關(guān)系,讓seletor對應(yīng)一個不同的implementation。

(也有人比喻:SLE是門牌號碼,IMP是住戶)

id

定義在objc.h第45、46行中,表示一個類的實例對象。

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

objc_object這個結(jié)構(gòu)體,定義在objc.h中,這個結(jié)構(gòu)體只有一個指向類的isa指針。

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

Ivar

定義在runtime.h第44行,表示一個實例變量。

/// An opaque type that represents an instance variable.
typedef struct objc_ivar *Ivar;

Cache

定義在runtime.h第1841行。

typedef struct objc_cache *Cache OBJC2_UNAVAILABLE;

Cache的存在,是為方法調(diào)用時的性能優(yōu)化:實例對象收到消息后,會先從Cache中查找,看是否有方法的實現(xiàn)——Runtime會把調(diào)用過的方法緩存到Cache中。

objc_property_t

定義在runtime.h第52,53行。

/// An opaque type that represents an Objective-C declared property.
typedef struct objc_property *objc_property_t;

表示Objective-C中的屬性。

Category

runtime.h第49,50行:

/// An opaque type that represents a category.
typedef struct objc_category *Category;

objc_category結(jié)構(gòu)體定義在第1784-1790行。

struct objc_category {
    char * _Nonnull category_name                            OBJC2_UNAVAILABLE;
    char * _Nonnull class_name                               OBJC2_UNAVAILABLE;
    struct objc_method_list * _Nullable instance_methods     OBJC2_UNAVAILABLE;
    struct objc_method_list * _Nullable class_methods        OBJC2_UNAVAILABLE;
    struct objc_protocol_list * _Nullable protocols          OBJC2_UNAVAILABLE;
}                                                            OBJC2_UNAVAILABLE;

Category可以動態(tài)的地為已存在的類添加新的方法。

self & super

先做個實驗:

打?。?/p>

NSLog(@"[self class]:%@; [super class]:%@", NSStringFromClass([self class]), NSStringFromClass([super class]));

結(jié)果,[self class][super class]的值是一樣的。 Why?不應(yīng)該打印一個子類,一個父類嗎?

self,是一個隱藏參數(shù),隱藏在objc_msgSend(id _Nullable self, SEL _Nonnull op, ...)函數(shù)中,發(fā)送的所有方法,第一個參數(shù)都是self。

super不是隱藏參數(shù),是一個「編譯器標示符」,它告訴編譯器,調(diào)用父類的方法,而不是本類的方法。但是,這時候?qū)嶋H上的消息的接收者,還是self。

詳解解讀:

  • 執(zhí)行[super class],會先調(diào)用objc_msgSendSuper(struct objc_super * _Nonnull super, SEL _Nonnull op, ...)函數(shù);

  • 再根據(jù)objc_super結(jié)構(gòu)體的super_class去查找方法實現(xiàn),,最后調(diào)用objc_msgSend(id _Nullable self, SEL _Nonnull op, ...)執(zhí)行方法的實現(xiàn)。 所以,最后的接收器還是self。

因此,上述打印結(jié)果的值是一樣的。

消息的傳遞流程

關(guān)于OC中的消息傳遞流程,畫了一張圖以幫助理解(流程由下往上):

消息傳遞流程

Objective-C的消息傳遞流程,個人劃分為三部分

  • 正常的消息傳遞(Messaging)
  • 消息動態(tài)解析(Dynamic Method Resolution)
  • 消息轉(zhuǎn)發(fā)(Message Forwarding)又分2小步:
    • Fast forwarding
    • Normal forwarding

第一部分,叫做「正常的消息傳遞」,那理所當然,后面的就是「不正?!沽?。事實是:如果能找到方法的實現(xiàn)(IMP/implementation),就不會跳到后面。

Runtime應(yīng)用

1.獲取類的相關(guān)情況

比如,我想創(chuàng)建一個類似UITableView的類,然后打算參考一下官方的這個類都聲明了哪些方法,可以用以下方式查看(頭文件聲明的方法并不是全部方法):

    /* 獲取某個類的方法列表(所有方法) */
    // 這樣獲取是實例方法(不是類方法)
    Method *methods = class_copyMethodList([UITableView class], &outCount);
    for (NSUInteger methodIndex = 0 ; methodIndex < outCount; methodIndex ++) {
        SEL name = method_getName(methods[methodIndex]);
        NSLog(@"Human-例法方實-%@",NSStringFromSelector(name));
    }

還有很多其他函數(shù):class_getInstanceVariable(), objc_getMetaClass(), class_getClassVariable()等等。

2.動態(tài)添加方法的實現(xiàn)

比如,我們用了某個閉源的框架,不幸地,有個bug是:某方法沒有實現(xiàn),導致crash:

[Animal jump]: unrecognized selector sent to instance

這時候如果等閉源框架的debug更新,比較被動。而利用Runtime,可以動態(tài)地添加方法的實現(xiàn),防止crash:

#import "Bird.h"
#import <objc/runtime.h>

// 創(chuàng)建Animal的子類Bird
@implementation Bird

// 如果沒有找到實例方法的實現(xiàn), 就會回調(diào)跳到這里
+ (BOOL)resolveInstanceMethod:(SEL)sel {
    if (sel == @selector(jump)) {
        // 利用Runtime的class_addMethod()函數(shù), 動態(tài)添加方法的實現(xiàn)
        class_addMethod(self, sel, (IMP)jumpImp, "v@:");
        return YES;
    }
    return [super resolveInstanceMethod:sel];
}

void jumpImp(id obj, SEL _cmd) {
    NSLog(@"執(zhí)行了jumpImp(動態(tài)添加的方法實現(xiàn))");
}

@end

3.Method Swizzling

Method Swizzling,可以理解為「交換方法的實現(xiàn)(IMP)」,這是網(wǎng)友的說法,官方并沒有這種說法,可見蘋果官方應(yīng)該是不提倡這樣做的。

假如有個需求:需要記錄App每個頁面進入的次數(shù)(這個需求和Method Swizzling介紹的一樣)

我們可以在viewWillAppear:方法中作一些計數(shù)處理。但是,每個頁面都要寫重復的代碼。在這里就可以使用Method Swizzling,「動態(tài)地」在官方的基礎(chǔ)上增加一些代碼,以實現(xiàn)需求。

需要新建一個UIViewController的Category,在load方法中實現(xiàn)互換。

#import "UIViewController+Tracking.h"
#import <objc/runtime.h>

@implementation UIViewController (Tracking)

+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        Class class = [self class];
        
        // 拿到兩個Method對象
        SEL originalSelector = @selector(viewWillAppear:);
        SEL swizzledSelector = @selector(antony_viewWillAppear:);
        
        Method orignalMethod = class_getInstanceMethod(class, originalSelector);
        Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);

        BOOL didAddMethod = class_addMethod(class, originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod));
        
        if (didAddMethod == YES) {
            class_replaceMethod(class, swizzledSelector, method_getImplementation(orignalMethod), method_getTypeEncoding(orignalMethod));
        }
        else {
            // 利用method_exchangeImplementations()函數(shù)交換兩個Method的實現(xiàn)
            method_exchangeImplementations(orignalMethod, swizzledMethod);
        }
    });
}

#pragma mark - Method Swizzling
- (void)antony_viewWillAppear:(BOOL)animated {
    // 因為互換了方法, 這里實際調(diào)用的是viewWillAppear:的IMP(不會造成遞歸)
    [self antony_viewWillAppear:animated];

    // 在這里增加你要的功能
    NSLog(@"這是在viewWillAppear:新增的內(nèi)容");
}

@end

核心就是用method_exchangeImplementations()函數(shù),互換了viewWillAppear:和antony_viewWillAppear:的實現(xiàn)。

而如果現(xiàn)在創(chuàng)建控制器對象,實際流程是這樣的:

  • viewWillAppea:被執(zhí)行(實際上執(zhí)行上述Category的antony_viewWillAppear:方法)
  • antony_viewWillAppear:方法內(nèi)又調(diào)用了antony_viewWillAppear:(實際上執(zhí)行的是系統(tǒng)的viewWillAppear:方法——因為互換了)
  • 最后再執(zhí)行我們自己添加的代碼——這樣就實現(xiàn)了需求:所有UIViewController在執(zhí)行 viewWillAppear:時, 都會調(diào)用你增加的代碼。從而無須在所有的UIViewController中重復寫這部分代碼。

Github有個框架:Aspects,就是用Runtime的Method Swizzling實現(xiàn)的,它允許你往任意現(xiàn)存類或?qū)嵗砑宇~外的代碼。

4.動態(tài)添加屬性 - 利用Associated Objects(Associative References)

Associative References(關(guān)聯(lián)引用/對象),在runtime.h中定義的三個相關(guān)函數(shù):

  • objc_setAssociatedObject()
  • objc_getAssociatedObject()
  • objc_removeAssociatedObjects()

有什么作用呢?

網(wǎng)上有種說法:OC中的Category不能添加屬性。

其實嚴格來說:Category不能添加的是「實例變量」,而屬性其實是可以添加的:

  • 不能為Category添加實例變量;否則報錯:Instance variables may not be placed in categories

  • 但是可以為Category添加屬性,也可以自定義setter、getter,外部也可以訪問;但是,這個屬性是無意義的,因為不能保存數(shù)據(jù)(可以返回值,但是不能賦值)。而不能保存數(shù)據(jù)的原因,是因為沒有實例變量「裝」數(shù)據(jù);

而Associated Objects(關(guān)聯(lián)對象),則可以為Category提供保存數(shù)據(jù)的地方。

因此Associated Objects(關(guān)聯(lián)對象)就可以:給已有類(封閉的類)添加真正有意義的屬性——可以保存數(shù)據(jù)的屬性。

比如,我們要為一個叫做Human的類添加一個屬性nickName,就可以:

#import "Human+AdditionalProperties.h"
#import <objc/runtime.h>

@implementation Human (AdditionalProperties)
@dynamic nickName;

// 如果要刪除該屬性,調(diào)用objc_setAssociatedObject()賦值為nil即可,
// 不要用objc_removeAssociatedObjects(), 該函數(shù)會刪除所有添加的屬性
- (void)setNickName:(NSString *)nickName {
    // 參數(shù)1: 為哪個對象實現(xiàn)的關(guān)聯(lián)
    // 參數(shù)2: 這個關(guān)聯(lián)的key(可以用SEL作為key)
    // 參數(shù)3: 需要與對應(yīng)key(參數(shù)2)關(guān)聯(lián)的值(就是外部傳入的值)
    // 參數(shù)4: 關(guān)聯(lián)的策略(和屬性的attribute相對應(yīng))
    objc_setAssociatedObject(self, @selector(nickName), nickName, OBJC_ASSOCIATION_COPY_NONATOMIC);
}

- (NSString *)nickName {
    // 參數(shù)1: 為哪個對象實現(xiàn)的關(guān)聯(lián)
    // 參數(shù)2: 該關(guān)聯(lián)的key
    return objc_getAssociatedObject(self, @selector(nickName));
}
@end

需要再次強調(diào)的是:通過Associated Objects為類添加有意義的屬性,事實上并不是添加了實例變量,而是通過關(guān)聯(lián),使屬性有保存數(shù)據(jù)的能力。(可以用class_copyPropertyList()驗證,并沒有增加實例變量?;蛘邤帱c看該類的實例,并不會看到有添加了實例變量——雖然能用該屬性來存取數(shù)據(jù)。)

5.歸檔和解檔 一鍵序列化:

有用過NSKeyedArchiver固化自定義對象到沙盒的朋友應(yīng)該了解,當一個自定義對象有很多屬性,需要一個一個encode(編碼)或者decode(解碼),是很瑣屑的,比如:

自定義類有很多屬性

而利用Runtime,則可以簡化這個過程——無論類有多少屬性:

- (instancetype)initWithCoder:(NSCoder *)aDecoder {
    self = [super init];
    if (self) {
        unsigned int count = 0;
        // 利用class_copyIvarList()拿到類的所有實例變量
        Ivar *ivars = class_copyIvarList([self class], &count);
        // 再用for循環(huán)一次性解檔
        for (int i = 0; i < count; i++) {
            Ivar ivar = ivars[i];
            NSString *key = [NSString stringWithUTF8String:ivar_getName(ivar)];
            id value = [aDecoder decodeObjectForKey:key];
            [self setValue:value forKey:key];
        }
    }
    return self;
}

- (void)encodeWithCoder:(NSCoder *)aCoder {
    unsigned int count = 0;
    // 利用class_copyIvarList()拿到類的所有實例變量
    Ivar *ivars = class_copyIvarList([self class], &count);
    // 再用for循環(huán)一次性歸檔
    for (int i = 0; i < count; i++) {
        Ivar ivar = ivars[i];
        NSString *key = [NSString stringWithUTF8String:ivar_getName(ivar)];
        id object = [self valueForKey:key];
        [aCoder encodeObject:object forKey:key];
    }
}

Runtime還有很多應(yīng)用,有興趣可以繼續(xù)找相關(guān)資料學習。不過:

Objective-C的Runtime就像一把雙刃劍,使用它,風險高,回報也高。它賦予你很大的權(quán)力,但只要你犯了哪怕一丁點兒錯誤,都有可能讓程序掛掉。

所以,總原則:能不用,盡量不用。

Conclusion

到這里,估計還是有很多黑人問號:Runtime究竟是什么玩意兒? What the hell is Runtime??

這很正常,學習本來就是一個重復的過程——特別是面對學習曲線還比較陡峭的知識。繼續(xù)實踐、溫故知新,相信后面會有更好的了解。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 轉(zhuǎn)至元數(shù)據(jù)結(jié)尾創(chuàng)建: 董瀟偉,最新修改于: 十二月 23, 2016 轉(zhuǎn)至元數(shù)據(jù)起始第一章:isa和Class一....
    40c0490e5268閱讀 2,098評論 0 9
  • objc_getAssociatedObject返回與給定鍵的特定對象關(guān)聯(lián)的值。ID objc_getAssoci...
    有一種再見叫青春閱讀 1,774評論 0 7
  • 轉(zhuǎn)載:http://yulingtianxia.com/blog/2014/11/05/objective-c-r...
    F麥子閱讀 842評論 0 2
  • 本文詳細整理了 Cocoa 的 Runtime 系統(tǒng)的知識,它使得 Objective-C 如虎添翼,具備了靈活的...
    lylaut閱讀 869評論 0 4
  • 我們常常會聽說 Objective-C 是一門動態(tài)語言,那么這個「動態(tài)」表現(xiàn)在哪呢?我想最主要的表現(xiàn)就是 Obje...
    Ethan_Struggle閱讀 2,351評論 0 7

友情鏈接更多精彩內(nèi)容