Objective-C Runtime 執行時之一:類與物件

南峰子的技術部落格發表於2014-11-11

Objective-C語言是一門動態語言,它將很多靜態語言在編譯和連結時期做的事放到了執行時來處理。這種動態語言的優勢在於:我們寫程式碼時更具靈活性,如我們可以把訊息轉發給我們想要的物件,或者隨意交換一個方法的實現等。

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

Runtime庫主要做下面幾件事:

  1. 封裝:在這個庫中,物件可以用C語言中的結構體表示,而方法可以用C函式來實現,另外再加上了一些額外的特性。這些結構體和函式被runtime函式封裝後,我們就可以在程式執行時建立,檢查,修改類、物件和它們的方法了。
  2. 找出方法的最終執行程式碼:當程式執行[object doSomething]時,會向訊息接收者(object)傳送一條訊息(doSomething),runtime會根據訊息接收者是否能響應該訊息而做出不同的反應。這將在後面詳細介紹。

Objective-C runtime目前有兩個版本:Modern runtime和Legacy runtime。Modern Runtime 覆蓋了64位的Mac OS X Apps,還有 iOS Apps,Legacy Runtime 是早期用來給32位 Mac OS X Apps 用的,也就是可以不用管就是了。

在這一系列文章中,我們將介紹runtime的基本工作原理,以及如何利用它讓我們的程式變得更加靈活。在本文中,我們先來介紹一下類與物件,這是物件導向的基礎,我們看看在Runtime中,類是如何實現的。

類與物件基礎資料結構

Class

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

typedef struct objc_class *Class;

檢視objc/runtime.h中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;  // 類的版本資訊,預設為0
    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;

在這個定義中,下面幾個欄位是我們感興趣的

  1. isa:需要注意的是在Objective-C中,所有的類自身也是一個物件,這個物件的Class裡面也有一個isa指標,它指向metaClass(元類),我們會在後面介紹它。
  2. super_class:指向該類的父類,如果該類已經是最頂層的根類(如NSObject或NSProxy),則super_class為NULL。
  3. cache:用於快取最近使用的方法。一個接收者物件接收到一個訊息時,它會根據isa指標去查詢能夠響應這個訊息的物件。在實際使用中,這個物件只有一部分方法是常用的,很多方法其實很少用或者根本用不上。這種情況下,如果每次訊息來時,我們都是methodLists中遍歷一遍,效能勢必很差。這時,cache就派上用場了。在我們每次呼叫過一個方法後,這個方法就會被快取到cache列表中,下次呼叫的時候runtime就會優先去cache中查詢,如果cache沒有,才去methodLists中查詢方法。這樣,對於那些經常用到的方法的呼叫,但提高了呼叫的效率。
  4. version:我們可以使用這個欄位來提供類的版本資訊。這對於物件的序列化非常有用,它可是讓我們識別出不同類定義版本中例項變數佈局的改變。

針對cache,我們用下面例子來說明其執行過程:

NSArray *array = [[NSArray alloc] init];

其流程是:

  1. [NSArray alloc]先被執行。因為NSArray沒有+alloc方法,於是去父類NSObject去查詢。
  2. 檢測NSObject是否響應+alloc方法,發現響應,於是檢測NSArray類,並根據其所需的記憶體空間大小開始分配記憶體空間,然後把isa指標指向NSArray類。同時,+alloc也被加進cache列表裡面。
  3. 接著,執行-init方法,如果NSArray響應該方法,則直接將其加入cache;如果不響應,則去父類查詢。
  4. 在後期的操作中,如果再以[[NSArray alloc] init]這種方式來建立陣列,則會直接從cache中取出相應的方法,直接呼叫。

objc_object與id

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

struct objc_object {
    Class isa  OBJC_ISA_AVAILABILITY;
};

typedef struct objc_object *id;

可以看到,這個結構體只有一個字型,即指向其類的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類的一個繼承體系了,如下圖所示:

image

對於NSObject繼承體系來說,其例項方法對體系中的所有例項、類和meta-class都是有效的;而類方法對於體系內的所有類和meta-class都是有效的。

講了這麼多,我們還是來寫個例子吧:

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]));
}

#pragma mark -

@implementation Test

- (void)ex_registerClassPair {

    Class newClass = objc_allocateClassPair([NSError class], "TestClass", 0);
    class_addMethod(newClass, @selector(testMetaClass), (IMP)TestMetaClass, "v@:");
    objc_registerClassPair(newClass);

    id instance = [[newClass alloc] initWithDomain:@"some domain" code:0 userInfo:nil];
    [instance performSelector:@selector(testMetaClass)];
}

@end

這個例子是在執行時建立了一個NSError的子類TestClass,然後為這個子類新增一個方法testMetaClass,這個方法的實現是TestMetaClass函式。

執行後,列印結果是

2014-10-20 22:57:07.352 mountain[1303:41490] This objcet is 0x7a6e22b0
2014-10-20 22:57:07.353 mountain[1303:41490] Class is TestStringClass, super class is NSError
2014-10-20 22:57:07.353 mountain[1303:41490] Following the isa pointer 0 times gives 0x7a6e21b0
2014-10-20 22:57:07.353 mountain[1303:41490] Following the isa pointer 1 times gives 0x0
2014-10-20 22:57:07.353 mountain[1303:41490] Following the isa pointer 2 times gives 0x0
2014-10-20 22:57:07.353 mountain[1303:41490] Following the isa pointer 3 times gives 0x0
2014-10-20 22:57:07.353 mountain[1303:41490] NSObject's class is 0xe10000
2014-10-20 22:57:07.354 mountain[1303:41490] NSObject's meta class is 0x0

我們在for迴圈中,我們通過objc_getClass來獲取物件的isa,並將其列印出來,依此一直回溯到NSObject的meta-class。分析列印結果,可以看到最後指標指向的地址是0×0,即NSObject的meta-class的類地址。

這裡需要注意的是:我們在一個類物件呼叫class方法是無法獲取meta-class,它只是返回類而已。

類與物件操作函式

runtime提供了大量的函式來操作類與物件。類的操作方法大部分是以class為字首的,而物件的操作方法大部分是以objc或object_為字首。下面我們將根據這些方法的用途來分類討論這些方法的使用。

類相關操作函式

我們可以回過頭去看看objc_class的定義,runtime提供的操作類的方法主要就是針對這個結構體中的各個欄位的。下面我們分別介紹這一些的函式。並在最後以例項來演示這些函式的具體用法。

類名(name)

類名操作的函式主要有:

// 獲取類的類名
const char * class_getName ( Class cls );

● 對於class_getName函式,如果傳入的cls為Nil,則返回一個字字串。

父類(super_class)和元類(meta-class)

父類和元類操作的函式主要有:

// 獲取類的父類
Class class_getSuperclass ( Class cls );

// 判斷給定的Class是否是一個元類
BOOL class_isMetaClass ( Class cls );

● class_getSuperclass函式,當cls為Nil或者cls為根類時,返回Nil。不過通常我們可以使用NSObject類的superclass方法來達到同樣的目的。

● class_isMetaClass函式,如果是cls是元類,則返回YES;如果否或者傳入的cls為Nil,則返回NO。

例項變數大小(instance_size)

例項變數大小操作的函式有:

// 獲取例項大小
size_t class_getInstanceSize ( Class cls );

成員變數(ivars)及屬性

在objc_class中,所有的成員變數、屬性的資訊是放在連結串列ivars中的。ivars是一個陣列,陣列中每個元素是指向Ivar(變數資訊)的指標。runtime提供了豐富的函式來操作這一欄位。大體上可以分為以下幾類:

1.成員變數操作函式,主要包含以下函式:

// 獲取類中指定名稱例項成員變數的資訊
Ivar class_getInstanceVariable ( Class cls, const char *name );

// 獲取類成員變數的資訊
Ivar class_getClassVariable ( Class cls, const char *name );

// 新增成員變數
BOOL class_addIvar ( Class cls, const char *name, size_t size, uint8_t alignment, const char *types );

// 獲取整個成員變數列表
Ivar * class_copyIvarList ( Class cls, unsigned int *outCount );

● class_getInstanceVariable函式,它返回一個指向包含name指定的成員變數資訊的objc_ivar結構體的指標(Ivar)。

● class_getClassVariable函式,目前沒有找到關於Objective-C中類變數的資訊,一般認為Objective-C不支援類變數。注意,返回的列表不包含父類的成員變數和屬性。

● Objective-C不支援往已存在的類中新增例項變數,因此不管是系統庫提供的提供的類,還是我們自定義的類,都無法動態新增成員變數。但如果我們通過執行時來建立一個類的話,又應該如何給它新增成員變數呢?這時我們就可以使用class_addIvar函式了。不過需要注意的是,這個方法只能在objc_allocateClassPair函式與objc_registerClassPair之間呼叫。另外,這個類也不能是元類。成員變數的按位元組最小對齊量是1<<alignment。這取決於ivar的型別和機器的架構。如果變數的型別是指標型別,則傳遞log2(sizeof(pointer_type))。

● class_copyIvarList函式,它返回一個指向成員變數資訊的陣列,陣列中每個元素是指向該成員變數資訊的objc_ivar結構體的指標。這個陣列不包含在父類中宣告的變數。outCount指標返回陣列的大小。需要注意的是,我們必須使用free()來釋放這個陣列。

2.屬性操作函式,主要包含以下函式:

// 獲取指定的屬性
objc_property_t class_getProperty ( Class cls, const char *name );

// 獲取屬性列表
objc_property_t * class_copyPropertyList ( Class cls, unsigned int *outCount );

// 為類新增屬性
BOOL class_addProperty ( Class cls, const char *name, const objc_property_attribute_t *attributes, unsigned int attributeCount );

// 替換類的屬性
void class_replaceProperty ( Class cls, const char *name, const objc_property_attribute_t *attributes, unsigned int attributeCount );

這一種方法也是針對ivars來操作,不過只操作那些是屬性的值。我們在後面介紹屬性時會再遇到這些函式。

3.在MAC OS X系統中,我們可以使用垃圾回收器。runtime提供了幾個函式來確定一個物件的記憶體區域是否可以被垃圾回收器掃描,以處理strong/weak引用。這幾個函式定義如下:

const uint8_t * class_getIvarLayout ( Class cls );
void class_setIvarLayout ( Class cls, const uint8_t *layout );
const uint8_t * class_getWeakIvarLayout ( Class cls );
void class_setWeakIvarLayout ( Class cls, const uint8_t *layout );

但通常情況下,我們不需要去主動呼叫這些方法;在呼叫objc_registerClassPair時,會生成合理的佈局。在此不詳細介紹這些函式。

方法(methodLists)

方法操作主要有以下函式:

// 新增方法
BOOL class_addMethod ( Class cls, SEL name, IMP imp, const char *types );

// 獲取例項方法
Method class_getInstanceMethod ( Class cls, SEL name );

// 獲取類方法
Method class_getClassMethod ( Class cls, SEL name );

// 獲取所有方法的陣列
Method * class_copyMethodList ( Class cls, unsigned int *outCount );

// 替代方法的實現
IMP class_replaceMethod ( Class cls, SEL name, IMP imp, const char *types );

// 返回方法的具體實現
IMP class_getMethodImplementation ( Class cls, SEL name );
IMP class_getMethodImplementation_stret ( Class cls, SEL name );

// 類例項是否響應指定的selector
BOOL class_respondsToSelector ( Class cls, SEL sel );

● class_addMethod的實現會覆蓋父類的方法實現,但不會取代本類中已存在的實現,如果本類中包含一個同名的實現,則函式會返回NO。如果要修改已存在實現,可以使用method_setImplementation。一個Objective-C方法是一個簡單的C函式,它至少包含兩個引數—self和_cmd。所以,我們的實現函式(IMP引數指向的函式)至少需要兩個引數,如下所示:

void myMethodIMP(id self, SEL _cmd)
{
    // implementation ....
}

與成員變數不同的是,我們可以為類動態新增方法,不管這個類是否已存在。

另外,引數types是一個描述傳遞給方法的引數型別的字元陣列,這就涉及到型別編碼,我們將在後面介紹。

● class_getInstanceMethod、class_getClassMethod函式,與class_copyMethodList不同的是,這兩個函式都會去搜尋父類的實現。

● class_copyMethodList函式,返回包含所有例項方法的陣列,如果需要獲取類方法,則可以使用class_copyMethodList(object_getClass(cls), &count)(一個類的例項方法是定義在元類裡面)。該列表不包含父類實現的方法。outCount引數返回方法的個數。在獲取到列表後,我們需要使用free()方法來釋放它。

● class_replaceMethod函式,該函式的行為可以分為兩種:如果類中不存在name指定的方法,則類似於class_addMethod函式一樣會新增方法;如果類中已存在name指定的方法,則類似於method_setImplementation一樣替代原方法的實現。

● class_getMethodImplementation函式,該函式在向類例項傳送訊息時會被呼叫,並返回一個指向方法實現函式的指標。這個函式會比method_getImplementation(class_getInstanceMethod(cls, name))更快。返回的函式指標可能是一個指向runtime內部的函式,而不一定是方法的實際實現。例如,如果類例項無法響應selector,則返回的函式指標將是執行時訊息轉發機制的一部分。

● class_respondsToSelector函式,我們通常使用NSObject類的respondsToSelector:或instancesRespondToSelector:方法來達到相同目的。

協議(objc_protocol_list)

協議相關的操作包含以下函式:

// 新增協議
BOOL class_addProtocol ( Class cls, Protocol *protocol );

// 返回類是否實現指定的協議
BOOL class_conformsToProtocol ( Class cls, Protocol *protocol );

// 返回類實現的協議列表
Protocol * class_copyProtocolList ( Class cls, unsigned int *outCount );

● class_conformsToProtocol函式可以使用NSObject類的conformsToProtocol:方法來替代。

● class_copyProtocolList函式返回的是一個陣列,在使用後我們需要使用free()手動釋放。

版本(version)

版本相關的操作包含以下函式:

// 獲取版本號
int class_getVersion ( Class cls );

// 設定版本號
void class_setVersion ( Class cls, int version );

其它

runtime還提供了兩個函式來供CoreFoundation的tool-free bridging使用,即:

Class objc_getFutureClass ( const char *name );
void objc_setFutureClass ( Class cls, const char *name );

通常我們不直接使用這兩個函式。

例項(Example)

上面列舉了大量類操作的函式,下面我們寫個例項,來看看這些函式的例項效果:

//-----------------------------------------------------------
// MyClass.h

@interface MyClass : NSObject <NSCopying, NSCoding>

@property (nonatomic, strong) NSArray *array;

@property (nonatomic, copy) NSString *string;

- (void)method1;

- (void)method2;

+ (void)classMethod1;

@end

//-----------------------------------------------------------
// MyClass.m

#import "MyClass.h"

@interface MyClass () {
    NSInteger       _instance1;

    NSString    *   _instance2;
}

@property (nonatomic, assign) NSUInteger integer;

- (void)method3WithArg1:(NSInteger)arg1 arg2:(NSString *)arg2;

@end

@implementation MyClass

+ (void)classMethod1 {

}

- (void)method1 {
    NSLog(@"call method method1");
}

- (void)method2 {

}

- (void)method3WithArg1:(NSInteger)arg1 arg2:(NSString *)arg2 {

    NSLog(@"arg1 : %ld, arg2 : %@", arg1, arg2);
}

@end

//-----------------------------------------------------------
// main.h

#import "MyClass.h"
#import "MySubClass.h"
#import <objc/runtime.h>

int main(int argc, const char * argv[]) {
    @autoreleasepool {

        MyClass *myClass = [[MyClass alloc] init];
        unsigned int outCount = 0;

        Class cls = myClass.class;

        // 類名
        NSLog(@"class name: %s", class_getName(cls));

        NSLog(@"==========================================================");

        // 父類
        NSLog(@"super class name: %s", class_getName(class_getSuperclass(cls)));
        NSLog(@"==========================================================");

        // 是否是元類
        NSLog(@"MyClass is %@ a meta-class", (class_isMetaClass(cls) ? @"" : @"not"));
        NSLog(@"==========================================================");

        Class meta_class = objc_getMetaClass(class_getName(cls));
        NSLog(@"%s's meta-class is %s", class_getName(cls), class_getName(meta_class));
        NSLog(@"==========================================================");

        // 變數例項大小
        NSLog(@"instance size: %zu", class_getInstanceSize(cls));
        NSLog(@"==========================================================");

        // 成員變數
        Ivar *ivars = class_copyIvarList(cls, &outCount);
        for (int i = 0; i < outCount; i++) {
            Ivar ivar = ivars[i];
            NSLog(@"instance variable's name: %s at index: %d", ivar_getName(ivar), i);
        }

        free(ivars);

        Ivar string = class_getInstanceVariable(cls, "_string");
        if (string != NULL) {
            NSLog(@"instace variable %s", ivar_getName(string));
        }

        NSLog(@"==========================================================");

        // 屬性操作
        objc_property_t * properties = class_copyPropertyList(cls, &outCount);
        for (int i = 0; i < outCount; i++) {
            objc_property_t property = properties[i];
            NSLog(@"property's name: %s", property_getName(property));
        }

        free(properties);

        objc_property_t array = class_getProperty(cls, "array");
        if (array != NULL) {
            NSLog(@"property %s", property_getName(array));
        }

        NSLog(@"==========================================================");

        // 方法操作
        Method *methods = class_copyMethodList(cls, &outCount);
        for (int i = 0; i < outCount; i++) {
            Method method = methods[i];
            NSLog(@"method's signature: %s", method_getName(method));
        }

        free(methods);

        Method method1 = class_getInstanceMethod(cls, @selector(method1));
        if (method1 != NULL) {
            NSLog(@"method %s", method_getName(method1));
        }

        Method classMethod = class_getClassMethod(cls, @selector(classMethod1));
        if (classMethod != NULL) {
            NSLog(@"class method : %s", method_getName(classMethod));
        }

        NSLog(@"MyClass is%@ responsd to selector: method3WithArg1:arg2:", class_respondsToSelector(cls, @selector(method3WithArg1:arg2:)) ? @"" : @" not");

        IMP imp = class_getMethodImplementation(cls, @selector(method1));
        imp();

        NSLog(@"==========================================================");

        // 協議
        Protocol * __unsafe_unretained * protocols = class_copyProtocolList(cls, &outCount);
        Protocol * protocol;
        for (int i = 0; i < outCount; i++) {
            protocol = protocols[i];
            NSLog(@"protocol name: %s", protocol_getName(protocol));
        }

        NSLog(@"MyClass is%@ responsed to protocol %s", class_conformsToProtocol(cls, protocol) ? @"" : @" not", protocol_getName(protocol));

        NSLog(@"==========================================================");
    }
    return 0;
}

這段程式的輸出如下:

2014-10-22 19:41:37.452 RuntimeTest[3189:156810] class name: MyClass
2014-10-22 19:41:37.453 RuntimeTest[3189:156810] ==========================================================
2014-10-22 19:41:37.454 RuntimeTest[3189:156810] super class name: NSObject
2014-10-22 19:41:37.454 RuntimeTest[3189:156810] ==========================================================
2014-10-22 19:41:37.454 RuntimeTest[3189:156810] MyClass is not a meta-class
2014-10-22 19:41:37.454 RuntimeTest[3189:156810] ==========================================================
2014-10-22 19:41:37.454 RuntimeTest[3189:156810] MyClass's meta-class is MyClass
2014-10-22 19:41:37.455 RuntimeTest[3189:156810] ==========================================================
2014-10-22 19:41:37.455 RuntimeTest[3189:156810] instance size: 48
2014-10-22 19:41:37.455 RuntimeTest[3189:156810] ==========================================================
2014-10-22 19:41:37.455 RuntimeTest[3189:156810] instance variable's name: _instance1 at index: 0
2014-10-22 19:41:37.455 RuntimeTest[3189:156810] instance variable's name: _instance2 at index: 1
2014-10-22 19:41:37.455 RuntimeTest[3189:156810] instance variable's name: _array at index: 2
2014-10-22 19:41:37.455 RuntimeTest[3189:156810] instance variable's name: _string at index: 3
2014-10-22 19:41:37.463 RuntimeTest[3189:156810] instance variable's name: _integer at index: 4
2014-10-22 19:41:37.463 RuntimeTest[3189:156810] instace variable _string
2014-10-22 19:41:37.463 RuntimeTest[3189:156810] ==========================================================
2014-10-22 19:41:37.463 RuntimeTest[3189:156810] property's name: array
2014-10-22 19:41:37.463 RuntimeTest[3189:156810] property's name: string
2014-10-22 19:41:37.464 RuntimeTest[3189:156810] property's name: integer
2014-10-22 19:41:37.464 RuntimeTest[3189:156810] property array
2014-10-22 19:41:37.464 RuntimeTest[3189:156810] ==========================================================
2014-10-22 19:41:37.464 RuntimeTest[3189:156810] method's signature: method1
2014-10-22 19:41:37.464 RuntimeTest[3189:156810] method's signature: method2
2014-10-22 19:41:37.464 RuntimeTest[3189:156810] method's signature: method3WithArg1:arg2:
2014-10-22 19:41:37.465 RuntimeTest[3189:156810] method's signature: integer
2014-10-22 19:41:37.465 RuntimeTest[3189:156810] method's signature: setInteger:
2014-10-22 19:41:37.465 RuntimeTest[3189:156810] method's signature: array
2014-10-22 19:41:37.465 RuntimeTest[3189:156810] method's signature: string
2014-10-22 19:41:37.465 RuntimeTest[3189:156810] method's signature: setString:
2014-10-22 19:41:37.465 RuntimeTest[3189:156810] method's signature: setArray:
2014-10-22 19:41:37.466 RuntimeTest[3189:156810] method's signature: .cxx_destruct
2014-10-22 19:41:37.466 RuntimeTest[3189:156810] method method1
2014-10-22 19:41:37.466 RuntimeTest[3189:156810] class method : classMethod1
2014-10-22 19:41:37.466 RuntimeTest[3189:156810] MyClass is responsd to selector: method3WithArg1:arg2:
2014-10-22 19:41:37.467 RuntimeTest[3189:156810] call method method1
2014-10-22 19:41:37.467 RuntimeTest[3189:156810] ==========================================================
2014-10-22 19:41:37.467 RuntimeTest[3189:156810] protocol name: NSCopying
2014-10-22 19:41:37.467 RuntimeTest[3189:156810] protocol name: NSCoding
2014-10-22 19:41:37.467 RuntimeTest[3189:156810] MyClass is responsed to protocol NSCoding
2014-10-22 19:41:37.468 RuntimeTest[3189:156810] ==========================================================

動態建立類和物件

runtime的強大之處在於它能在執行時建立類和物件。

動態建立類

動態建立類涉及到以下幾個函式:

// 建立一個新類和元類
Class objc_allocateClassPair ( Class superclass, const char *name, size_t extraBytes );

// 銷燬一個類及其相關聯的類
void objc_disposeClassPair ( Class cls );

// 在應用中註冊由objc_allocateClassPair建立的類
void objc_registerClassPair ( Class cls );

● objc_allocateClassPair函式:如果我們要建立一個根類,則superclass指定為Nil。extraBytes通常指定為0,該引數是分配給類和元類物件尾部的索引ivars的位元組數。

為了建立一個新類,我們需要呼叫objc_allocateClassPair。然後使用諸如class_addMethod,class_addIvar等函式來為新建立的類新增方法、例項變數和屬性等。完成這些後,我們需要呼叫objc_registerClassPair函式來註冊類,之後這個新類就可以在程式中使用了。

例項方法和例項變數應該新增到類自身上,而類方法應該新增到類的元類上。

● objc_disposeClassPair函式用於銷燬一個類,不過需要注意的是,如果程式執行中還存在類或其子類的例項,則不能呼叫針對類呼叫該方法。

在前面介紹元類時,我們已經有接觸到這幾個函式了,在此我們再舉個例項來看看這幾個函式的使用。

Class cls = objc_allocateClassPair(MyClass.class, "MySubClass", 0);
class_addMethod(cls, @selector(submethod1), (IMP)imp_submethod1, "v@:");
class_replaceMethod(cls, @selector(method1), (IMP)imp_submethod1, "v@:");
class_addIvar(cls, "_ivar1", sizeof(NSString *), log(sizeof(NSString *)), "i");

objc_property_attribute_t type = {"T", "@\"NSString\""};
objc_property_attribute_t ownership = { "C", "" };
objc_property_attribute_t backingivar = { "V", "_ivar1"};
objc_property_attribute_t attrs[] = {type, ownership, backingivar};

class_addProperty(cls, "property2", attrs, 3);
objc_registerClassPair(cls);

id instance = [[cls alloc] init];
[instance performSelector:@selector(submethod1)];
[instance performSelector:@selector(method1)];

程式的輸出如下:

2014-10-23 11:35:31.006 RuntimeTest[3800:66152] run sub method 1
2014-10-23 11:35:31.006 RuntimeTest[3800:66152] run sub method 1

動態建立物件

動態建立物件的函式如下:

// 建立類例項
id class_createInstance ( Class cls, size_t extraBytes );

// 在指定位置建立類例項
id objc_constructInstance ( Class cls, void *bytes );

// 銷燬類例項
void * objc_destructInstance ( id obj );

● class_createInstance函式:建立例項時,會在預設的記憶體區域為類分配記憶體。extraBytes參數列示分配的額外位元組數。這些額外的位元組可用於儲存在類定義中所定義的例項變數之外的例項變數。該函式在ARC環境下無法使用。

呼叫class_createInstance的效果與+alloc方法類似。不過在使用class_createInstance時,我們需要確切的知道我們要用它來做什麼。在下面的例子中,我們用NSString來測試一下該函式的實際效果:

id theObject = class_createInstance(NSString.class, sizeof(unsigned));
id str1 = [theObject init];

NSLog(@"%@", [str1 class]);

id str2 = [[NSString alloc] initWithString:@"test"];
NSLog(@"%@", [str2 class]);

輸出結果是:

2014-10-23 12:46:50.781 RuntimeTest[4039:89088] NSString
2014-10-23 12:46:50.781 RuntimeTest[4039:89088] __NSCFConstantString

可以看到,使用class_createInstance函式獲取的是NSString例項,而不是類簇中的預設佔位符類__NSCFConstantString。

● objc_constructInstance函式:在指定的位置(bytes)建立類例項。

● objc_destructInstance函式:銷燬一個類的例項,但不會釋放並移除任何與其相關的引用。

例項操作函式

例項操作函式主要是針對我們建立的例項物件的一系列操作函式,我們可以使用這組函式來從例項物件中獲取我們想要的一些資訊,如例項物件中變數的值。這組函式可以分為三小類:

1.針對整個物件進行操作的函式,這類函式包含

// 返回指定物件的一份拷貝
id object_copy ( id obj, size_t size );

// 釋放指定物件佔用的記憶體
id object_dispose ( id obj );

有這樣一種場景,假設我們有類A和類B,且類B是類A的子類。類B通過新增一些額外的屬性來擴充套件類A。現在我們建立了一個A類的例項物件,並希望在執行時將這個物件轉換為B類的例項物件,這樣可以新增資料到B類的屬性中。這種情況下,我們沒有辦法直接轉換,因為B類的例項會比A類的例項更大,沒有足夠的空間來放置物件。此時,我們就要以使用以上幾個函式來處理這種情況,如下程式碼所示:

NSObject *a = [[NSObject alloc] init];
id newB = object_copy(a, class_getInstanceSize(MyClass.class));
object_setClass(newB, MyClass.class);
object_dispose(a);

2.針對物件例項變數進行操作的函式,這類函式包含:

// 修改類例項的例項變數的值
Ivar object_setInstanceVariable ( id obj, const char *name, void *value );

// 獲取物件例項變數的值
Ivar object_getInstanceVariable ( id obj, const char *name, void **outValue );

// 返回指向給定物件分配的任何額外位元組的指標
void * object_getIndexedIvars ( id obj );

// 返回物件中例項變數的值
id object_getIvar ( id obj, Ivar ivar );

// 設定物件中例項變數的值
void object_setIvar ( id obj, Ivar ivar, id value );

如果例項變數的Ivar已經知道,那麼呼叫object_getIvar會比object_getInstanceVariable函式快,相同情況下,object_setIvar也比object_setInstanceVariable快。

3.針對物件的類進行操作的函式,這類函式包含:

// 返回給定物件的類名
const char * object_getClassName ( id obj );

// 返回物件的類
Class object_getClass ( id obj );

// 設定物件的類
Class object_setClass ( id obj, Class cls );

獲取類定義

Objective-C動態執行庫會自動註冊我們程式碼中定義的所有的類。我們也可以在執行時建立類定義並使用objc_addClass函式來註冊它們。runtime提供了一系列函式來獲取類定義相關的資訊,這些函式主要包括:

// 獲取已註冊的類定義的列表
int objc_getClassList ( Class *buffer, int bufferCount );

// 建立並返回一個指向所有已註冊類的指標列表
Class * objc_copyClassList ( unsigned int *outCount );

// 返回指定類的類定義
Class objc_lookUpClass ( const char *name );
Class objc_getClass ( const char *name );
Class objc_getRequiredClass ( const char *name );

// 返回指定類的元類
Class objc_getMetaClass ( const char *name );

● objc_getClassList函式:獲取已註冊的類定義的列表。我們不能假設從該函式中獲取的類物件是繼承自NSObject體系的,所以在這些類上呼叫方法是,都應該先檢測一下這個方法是否在這個類中實現。

下面程式碼演示了該函式的用法:

int numClasses;
Class * classes = NULL;

numClasses = objc_getClassList(NULL, 0);
if (numClasses > 0) {
    classes = malloc(sizeof(Class) * numClasses);
    numClasses = objc_getClassList(classes, numClasses);

    NSLog(@"number of classes: %d", numClasses);

    for (int i = 0; i < numClasses; i++) {

        Class cls = classes[i];
        NSLog(@"class name: %s", class_getName(cls));
    }

    free(classes);
}

輸出結果如下:

2014-10-23 16:20:52.589 RuntimeTest[8437:188589] number of classes: 1282
2014-10-23 16:20:52.589 RuntimeTest[8437:188589] class name: DDTokenRegexp
2014-10-23 16:20:52.590 RuntimeTest[8437:188589] class name: _NSMostCommonKoreanCharsKeySet
2014-10-23 16:20:52.590 RuntimeTest[8437:188589] class name: OS_xpc_dictionary
2014-10-23 16:20:52.590 RuntimeTest[8437:188589] class name: NSFileCoordinator
2014-10-23 16:20:52.590 RuntimeTest[8437:188589] class name: NSAssertionHandler
2014-10-23 16:20:52.590 RuntimeTest[8437:188589] class name: PFUbiquityTransactionLogMigrator
2014-10-23 16:20:52.591 RuntimeTest[8437:188589] class name: NSNotification
2014-10-23 16:20:52.591 RuntimeTest[8437:188589] class name: NSKeyValueNilSetEnumerator
2014-10-23 16:20:52.591 RuntimeTest[8437:188589] class name: OS_tcp_connection_tls_session
2014-10-23 16:20:52.591 RuntimeTest[8437:188589] class name: _PFRoutines
......還有大量輸出

● 獲取類定義的方法有三個:objc_lookUpClass, objc_getClass和objc_getRequiredClass。如果類在執行時未註冊,則objc_lookUpClass會返回nil,而objc_getClass會呼叫類處理回撥,並再次確認類是否註冊,如果確認未註冊,再返回nil。而objc_getRequiredClass函式的操作與objc_getClass相同,只不過如果沒有找到類,則會殺死程式。

● objc_getMetaClass函式:如果指定的類沒有註冊,則該函式會呼叫類處理回撥,並再次確認類是否註冊,如果確認未註冊,再返回nil。不過,每個類定義都必須有一個有效的元類定義,所以這個函式總是會返回一個元類定義,不管它是否有效。

小結

在這一章中我們介紹了Runtime執行時中與類和物件相關的資料結構,通過這些資料函式,我們可以管窺Objective-C底層物件導向實現的一些資訊。另外,通過豐富的操作函式,可以靈活地對這些資料進行操作。

相關文章