天天看點

Objective-C Runtime 運作時(一):類與對象Runtime庫主要做下面幾件事:類與對象基礎資料結構

Objective-C語言是一門動态語言,動态語言相對靜态語言最大的特點是把很多靜态語言在編譯和連結時期做的事放到了運作時來處理。這種動态語言的優勢在于:我們寫代碼時更具靈活性,如我們可以把消息轉發給我們想要的對象,或者随意交換一個方法的實作等。

這種特性意味着Objective-C不僅需要一個編譯動态語言的編譯器,還需要一個運作時系統來執行編譯的代碼。對于Objective-C來說,這個運作時系統就像一個作業系統一樣:它讓所有的工作可以正常的運作。這個運作時系統即Objc Runtime。Objc Runtime其實是一個Runtime庫,它基本上是用C和彙編寫的,這個庫使得C語言有了面向對象的能力。

Runtime庫主要做下面幾件事:

1、封裝:在這個庫中,對象可以用C語言中的結構體表示,而方法可以用C函數來實作,另外再加上了一些額外的特性。這些結構體和函數被runtime函數封裝後,我們就可以在程式運作時建立,檢查,修改類、對象和它們的方法了。

2、找出方法的最終執行代碼:當程式執行[object doSomething]時,會向消息接收者(object)發送一條消息(doSomething),runtime會根據消息接收者是否能響應該消息而做出不同的反應。

類與對象基礎資料結構

Class

類本質上是一個結構體,Objective-C類是由Class類型來表示的,它實際上是一個指向objc_class結構體的指針。它的定義如下:

typedef struct objc_class *Class;
           

通過檢視objc/runtime.h中objc_class結構體的定義如下:

struct objc_class {      
        Class isa; // 指向metaclass(metaclass 後面解釋)       
        Class super_class ;   // 指向其父類      
        const charchar *name ;    // 類名      
        long version ;    // 類的版本資訊,初始化預設為0,可以通過runtime函數class_setVersion和class_getVersion進行修改、讀取      
        long info;   // 一些辨別資訊     
        long instance_size ;   // 該類的執行個體變量大小(包括從父類繼承下來的執行個體變量);      
        struct objc_ivar_list *ivars;   // 成員變量連結清單,通過ivars可以通路所有的成員變量      
        struct objc_method_list **methodLists ;   // 存儲類的方法連結清單      
        struct objc_cache *cache;   // 指向最近使用的方法清單,用于提升運作效率;      
        struct objc_protocol_list *protocols;   // 協定連結清單   
    }   
    /* Use `Class` instead of `struct objc_class */ 
           

objc_object與id

objc_object是表示一個類的執行個體的結構體,它的定義如下(objc/objc.h):

struct objc_object {
    Class isa  OBJC_ISA_AVAILABILITY;
};

typedef struct objc_object *id;
           

可以看到,這個結構體隻有一個成員屬性isa,isa指向該執行個體對象的類。這樣,當我們向一個Objective-C對象發送消息時,運作時庫會根據執行個體對象的isa指針找到這個執行個體對象所屬的類。Runtime庫會在類的方法清單及父類的方法清單中去尋找與消息對應的selector指向的方法。找到後即運作這個方法。

當建立一個特定類的執行個體對象時,配置設定的記憶體包含一個objc_object資料結構,然後是類的執行個體變量的資料。NSObject類的alloc和allocWithZone:方法使用函數class_createInstance來建立objc_object資料結構。

另外還有我們常見的id,其實它是一個objc_object結構類型的指針。它的存在可以讓我們實作類似于C++中泛型的一些操作。該類型的指針可以指向任何類型的指針,也就是說也接受任何類型的對象,有點類似于C語言中void *指針類型的作用,可以指向任何資料類型的記憶體。

objc_cache

上面提到了objc_class結構體中的cache字段,它用于緩存調用過的方法。這個字段是一個指向objc_cache結構體的指針,其定義如下

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

該結構體的字段描述如下:

1、mask:一個整數,指定配置設定的緩存bucket的總數。在方法查找過程中,Objective-C runtime使用這個字段來确定開始線性查找數組的索引位置。指向方法selector的指針與該字段做一個AND位操作(index = (mask & selector))。這可以作為一個簡單的hash雜湊演算法。

2、occupied:一個整數,指定實際占用的緩存bucket的總數。

3、buckets:指向Method資料結構指針的數組。這個數組可能包含不超過mask+1個元素。需要注意的是,指針可能是NULL,表示這個緩存bucket沒有被占用,另外被占用的bucket可能是不連續的。這個數組可能會随着時間而增長。

元類(Meta Class)

在上面我們提到,所有的類自身也是一個對象,我們可以向這個對象發送消息(即調用類方法)。如:

NSArray *array = [NSArray array];
           

這個例子中,+array消息發送給了NSArray類,而這個NSArray也是一個對象。既然是對象,那麼它也是一個objc_object指針,它包含一個指向其類的一個isa指針。那麼這些就有一個問題了,這個isa指針指向什麼呢?為了調用+array方法,這個類的isa指針必須指向一個包含這些類方法的一個objc_class結構體。這就引出了meta-class的概念,meta-class是一個類對象的類

當向一個對象發送消息時,runtime會在這個對象所屬的這個類的方法清單中查找方法;而向一個類發送消息時,會在這個類的meta-class的方法清單中查找。meta-class之是以重要,是因為它存儲着一個類的所有類方法。每個類都會有一個單獨的meta-class,因為每個類的類方法基本不可能完全相同。

再深入一下,meta-class也是一個類,也可以向它發送一個消息,那麼它的isa又是指向什麼呢?為了不讓這種結構無限延伸下去,Objective-C的設計者讓所有的meta-class的isa指向基類的meta-class,以此作為它們的所屬類。即,任何NSObject繼承體系下的meta-class都使用NSObject的meta-class作為自己的所屬類,而基類的meta-class的isa指針是指向它自己。這樣就形成了一個完美的閉環。

通過上面的描述,再加上對objc_class結構體中super_class指針的分析,我們就可以描繪出類及相應meta-class類的一個繼承體系了,如下圖所示:

Objective-C Runtime 運作時(一):類與對象Runtime庫主要做下面幾件事:類與對象基礎資料結構

下面通過例子來說明class、superClass、MetalClass

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


@implementation TestRuntime

/**
 *  類加載的時候會調用
 */
+ (void)load
{
    [super load];
    
    [self registerClassPair];
}


+ (void)registerClassPair
{
    Class newClass = objc_allocateClassPair([NSError class], "TestClass", 0);
    class_addMethod(newClass, @selector(testMetaClass), (IMP)TestMetaClass, "[email protected]:");
    objc_registerClassPair(newClass);
    
    id instance = [[newClass alloc] initWithDomain:@"some domain" code:0 userInfo:nil];
    [instance performSelector:@selector(testMetaClass)];
}

void TestMetaClass(id self, SEL _cmd) {
    
    NSLog(@"This objcet is %p", self);
    NSLog(@"Class is %@, super class is %@", [self class], [self superclass]);
    
    Class currentClass = [self class];
    for (int i = 0; i < 4; i++) {
        NSLog(@"Following the isa pointer %d times gives %p", i, currentClass);
        currentClass = objc_getClass((__bridge void *)currentClass);
    }
    
    NSLog(@"NSObject's class is %p", [NSObject class]);
    NSLog(@"NSObject's meta class is %p", objc_getClass((__bridge void *)[NSObject class]));
}

@end
           

上面的例子是在運作時建立了一個NSError的子類TestClass,然後為這個子類添加一個方法testMetaClass,這個方法的實作是TestMetaClass函數。

運作後,列印結果是

2015-08-30 21:16:58.415 runtime[4996:195361] This objcet is 0x100311630
2015-08-30 21:16:58.416 runtime[4996:195361] Class is TestClass, super class is NSError
2015-08-30 21:16:58.416 runtime[4996:195361] Following the isa pointer 0 times gives 0x100311400
2015-08-30 21:16:58.416 runtime[4996:195361] Following the isa pointer 1 times gives 0x0
2015-08-30 21:16:58.416 runtime[4996:195361] Following the isa pointer 2 times gives 0x0
2015-08-30 21:16:58.417 runtime[4996:195361] Following the isa pointer 3 times gives 0x0
2015-08-30 21:16:58.417 runtime[4996:195361] NSObject's class is 0x7fff732810f0
2015-08-30 21:16:58.417 runtime[4996:195361] NSObject's meta class is 0x0
           

我們在for循環中,我們通過objc_getClass來擷取對象的isa,并将其列印出來,依此一直回溯到NSObject的meta-class。分析列印結果,可以看到最後指針指向的位址是0x0,即NSObject的meta-class的類位址。

這裡需要注意的是:我們在一個類對象調用class方法是無法擷取meta-class,它隻是傳回類而已。