Objective-C Runtime 完整總結

mkvm202 7年前發布 | 30K 次閱讀 Objective-C開發 Objective-C

類和對象

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

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

Runtime庫主要做下面幾件事:

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

找出方法的最終執行代碼:當程序執行[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;</code></pre>

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

  • isa:需要注意的是在Objective-C中,所有的類自身也是一個對象,這個對象的Class里面也有一個isa指針,它指向metaClass(元類),我們會在后面介紹它。

  • super_class:指向該類的父類,如果該類已經是最頂層的根類(如NSObject或NSProxy),則super_class為NULL。

  • cache:用于緩存最近使用的方法。一個接收者對象接收到一個消息時,它會根據isa指針去查找能夠響應這個消息的對象。在實際使用中,這個對象只有一部分方法是常用的,很多方法其實很少用或者根本用不上。這種情況下,如果每次消息來時,我們都是methodLists中遍歷一遍,性能勢必很差。這時,cache就派上用場了。在我們每次調用過一個方法后,這個方法就會被緩存到cache列表中,下次調用的時候runtime就會優先去cache中查找,如果cache沒有,才去methodLists中查找方法。這樣,對于那些經常用到的方法的調用,但提高了調用的效率。

  • version:我們可以使用這個字段來提供類的版本信息。這對于對象的序列化非常有用,它可是讓我們識別出不同類定義版本中實例變量布局的改變。

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

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

其流程是:

  • [NSArray alloc]先被執行。因為NSArray沒有+alloc方法,于是去父類NSObject去查找。

  • 檢測NSObject是否響應+alloc方法,發現響應,于是檢測NSArray類,并根據其所需的內存空間大小開始分配內存空間,然后把isa指針指向NSArray類。同時,+alloc也被加進cache列表里面。

  • 接著,執行-init方法,如果NSArray響應該方法,則直接將其加入cache;如果不響應,則去父類查找。

  • 在后期的操作中,如果再以[[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;</code></pre>

可以看到,這個結構體只有一個字體,即指向其類的isa指針。這樣,當我們向一個Objective-C對象發送消息時,運行時庫會根據實例對象的isa指針找到這個實例對象所屬的類。Runtime庫會在類的方法列表及父類的方法列表中去尋找與消息對應的selector指向的方法。找到后即運行這個方法。

當創建一個特定類的實例對象時,分配的內存包含一個objc_object數據結構,然后是類的實例變量的數據。NSObject類的alloc和allocWithZone:方法使用函數class_createInstance來創建objc_object數據結構。

另外還有我們常見的id,它是一個objc_object結構類型的指針。它的存在可以讓我們實現類似于C++中泛型的一些操作。該類型的對象可以轉換為任何一種對象,有點類似于C語言中void 指針類型的作用。</p>

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

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

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

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

  • 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類的一個繼承體系了,如下圖所示:

對于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</code></pre>

這個例子是在運行時創建了一個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。分析打印結果,可以看到最后指針指向的地址是0x0,即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 );</code></pre>

  • 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 );</code></pre>

  • 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 );</code></pre>

這一種方法也是針對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 );</code></pre>

  • 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 );</code></pre>

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

  • class_copyProtocolList函數返回的是一個數組,在使用后我們需要使用free()手動釋放。

版本(version)

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

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

// 設置版本號 void class_setVersion ( Class cls, int version );</code></pre>

其它

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;

}</code></pre>

這段程序的輸出如下:

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 );</code></pre>

  • 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)];</code></pre>

程序的輸出如下:

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 );</code></pre>

  • 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]);</code></pre>

輸出結果是:

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 );</code></pre>

有這樣一種場景,假設我們有類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 );</code></pre>

如果實例變量的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 );</code></pre>

獲取類定義

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 );</code></pre>

  • 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);

}</code></pre>

輸出結果如下:

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底層面向對象實現的一些信息。另外,通過豐富的操作函數,可以靈活地對這些數據進行操作。

成員和成員屬性

在前面一篇文章中,我們介紹了Runtime中與類和對象相關的內容,從這章開始,我們將討論類實現細節相關的內容,主要包括類中成員變量,屬性,方法,協議與分類的實現。

本章的主要內容將聚集在Runtime對成員變量與屬性的處理。在討論之前,我們先介紹一個重要的概念:類型編碼。

類型編碼(Type Encoding)

作為對Runtime的補充,編譯器將每個方法的返回值和參數類型編碼為一個字符串,并將其與方法的selector關聯在一起。這種編碼方案在其它情況下也是非常有用的,因此我們可以使用@encode編譯器指令來獲取它。當給定一個類型時,@encode返回這個類型的字符串編碼。這些類型可以是諸如int、指針這樣的基本類型,也可以是結構體、類等類型。事實上,任何可以作為sizeof()操作參數的類型都可以用于@encode()。

在Objective-C Runtime Programming Guide中的Type Encoding一節中,列出了Objective-C中所有的類型編碼。需要注意的是這些類型很多是與我們用于存檔和分發的編碼類型是相同的。但有一些不能在存檔時使用。

注:Objective-C不支持long double類型。@encode(long double)返回d,與double是一樣的。

一個數組的類型編碼位于方括號中;其中包含數組元素的個數及元素類型。如以下示例:

float a[] = {1.0, 2.0, 3.0};
NSLog(@"array encoding type: %s", @encode(typeof(a)));

輸出是:

2014-10-28 11:44:54.731 RuntimeTest[942:50791] array encoding type: [3f]

其它類型可參考Type Encoding,在此不細說。

另外,還有些編碼類型,@encode雖然不會直接返回它們,但它們可以作為協議中聲明的方法的類型限定符。可以參考Type Encoding。

對于屬性而言,還會有一些特殊的類型編碼,以表明屬性是只讀、拷貝、retain等等,詳情可以參考Property Type String。

成員變量、屬性

Runtime中關于成員變量和屬性的相關數據結構并不多,只有三個,并且都很簡單。不過還有個非常實用但可能經常被忽視的特性,即關聯對象,我們將在這小節中詳細討論。

基礎數據類型

Ivar

Ivar是表示實例變量的類型,其實際是一個指向objc_ivar結構體的指針,其定義如下:

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

}</code></pre>

objc_property_t

objc_property_t是表示Objective-C聲明的屬性的類型,其實際是指向objc_property結構體的指針,其定義如下:

typedef struct objc_property *objc_property_t;

objc_property_attribute_t

objc_property_attribute_t定義了屬性的特性(attribute),它是一個結構體,定義如下:

typedef struct {
    const char *name;           // 特性名
    const char *value;          // 特性值
} objc_property_attribute_t;

關聯對象(Associated Object)

關聯對象是Runtime中一個非常實用的特性,不過可能很容易被忽視。

關聯對象類似于成員變量,不過是在運行時添加的。我們通常會把成員變量(Ivar)放在類聲明的頭文件中,或者放在類實現的@implementation后面。但這有一個缺點,我們不能在分類中添加成員變量。如果我們嘗試在分類中添加新的成員變量,編譯器會報錯。

我們可能希望通過使用(甚至是濫用)全局變量來解決這個問題。但這些都不是Ivar,因為他們不會連接到一個單獨的實例。因此,這種方法很少使用。

Objective-C針對這一問題,提供了一個解決方案:即關聯對象(Associated Object)。

我們可以把關聯對象想象成一個Objective-C對象(如字典),這個對象通過給定的key連接到類的一個實例上。不過由于使用的是C接口,所以key是一個void指針(const void *)。我們還需要指定一個內存管理策略,以告訴Runtime如何管理這個對象的內存。這個內存管理的策略可以由以下值指定:

OBJC_ASSOCIATION_ASSIGN
OBJC_ASSOCIATION_RETAIN_NONATOMIC
OBJC_ASSOCIATION_COPY_NONATOMIC
OBJC_ASSOCIATION_RETAIN
OBJC_ASSOCIATION_COPY

當宿主對象被釋放時,會根據指定的內存管理策略來處理關聯對象。如果指定的策略是assign,則宿主釋放時,關聯對象不會被釋放;而如果指定的是retain或者是copy,則宿主釋放時,關聯對象會被釋放。我們甚至可以選擇是否是自動retain/copy。當我們需要在多個線程中處理訪問關聯對象的多線程代碼時,這就非常有用了。

我們將一個對象連接到其它對象所需要做的就是下面兩行代碼:

static char myKey;

objc_setAssociatedObject(self, &myKey, anObject, OBJC_ASSOCIATION_RETAIN);</code></pre>

在這種情況下,self對象將獲取一個新的關聯的對象anObject,且內存管理策略是自動retain關聯對象,當self對象釋放時,會自動release關聯對象。另外,如果我們使用同一個key來關聯另外一個對象時,也會自動釋放之前關聯的對象,這種情況下,先前的關聯對象會被妥善地處理掉,并且新的對象會使用它的內存。

id anObject = objc_getAssociatedObject(self, &myKey);

我們可以使用objc_removeAssociatedObjects函數來移除一個關聯對象,或者使用objc_setAssociatedObject函數將key指定的關聯對象設置為nil。

我們下面來用實例演示一下關聯對象的使用方法。

假定我們想要動態地將一個Tap手勢操作連接到任何UIView中,并且根據需要指定點擊后的實際操作。這時候我們就可以將一個手勢對象及操作的block對象關聯到我們的UIView對象中。這項任務分兩部分。首先,如果需要,我們要創建一個手勢識別對象并將它及block做為關聯對象。如下代碼所示:

- (void)setTapActionWithBlock:(void (^)(void))block
{
    UITapGestureRecognizer *gesture = objc_getAssociatedObject(self, &kDTActionHandlerTapGestureKey);

if (!gesture)
{
    gesture = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(__handleActionForTapGesture:)];
    [self addGestureRecognizer:gesture];
    objc_setAssociatedObject(self, &kDTActionHandlerTapGestureKey, gesture, OBJC_ASSOCIATION_RETAIN);
}

objc_setAssociatedObject(self, &kDTActionHandlerTapBlockKey, block, OBJC_ASSOCIATION_COPY);

}</code></pre>

這段代碼檢測了手勢識別的關聯對象。如果沒有,則創建并建立關聯關系。同時,將傳入的塊對象連接到指定的key上。注意block對象的關聯內存管理策略。

手勢識別對象需要一個target和action,所以接下來我們定義處理方法:

- (void)__handleActionForTapGesture:(UITapGestureRecognizer *)gesture
{
    if (gesture.state == UIGestureRecognizerStateRecognized)
    {
        void(^action)(void) = objc_getAssociatedObject(self, &kDTActionHandlerTapBlockKey);

    if (action)
    {
        action();
    }
}

}</code></pre>

我們需要檢測手勢識別對象的狀態,因為我們只需要在點擊手勢被識別出來時才執行操作。

從上面的例子我們可以看到,關聯對象使用起來并不復雜。它讓我們可以動態地增強類現有的功能。我們可以在實際編碼中靈活地運用這一特性。

成員變量、屬性的操作方法

成員變量

成員變量操作包含以下函數:

// 獲取成員變量名
const char * ivar_getName ( Ivar v );

// 獲取成員變量類型編碼 const char * ivar_getTypeEncoding ( Ivar v );

// 獲取成員變量的偏移量 ptrdiff_t ivar_getOffset ( Ivar v );</code></pre>

  • ivar_getOffset函數,對于類型id或其它對象類型的實例變量,可以調用object_getIvar和object_setIvar來直接訪問成員變量,而不使用偏移量。

關聯對象

關聯對象操作函數包括以下:

// 設置關聯對象
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 ); 關聯對象及相關實例已經在前面討論過了,在此不再重復。</code></pre>

屬性

屬性操作相關函數包括以下:

// 獲取屬性名
const char * property_getName ( objc_property_t property );

// 獲取屬性特性描述字符串 const char * property_getAttributes ( objc_property_t property );

// 獲取屬性中指定的特性 char property_copyAttributeValue ( objc_property_t property, const char attributeName );

// 獲取屬性的特性列表 objc_property_attribute_t property_copyAttributeList ( objc_property_t property, unsigned int outCount );</code></pre>

  • property_copyAttributeValue函數,返回的char *在使用完后需要調用free()釋放。

  • property_copyAttributeList函數,返回值在使用完后需要調用free()釋放。

實例

假定這樣一個場景,我們從服務端兩個不同的接口獲取相同的字典數據,但這兩個接口是由兩個人寫的,相同的信息使用了不同的字段表示。我們在接收到數據時,可將這些數據保存在相同的對象中。對象類如下定義:

@interface MyObject: NSObject

@property (nonatomic, copy) NSString name;
@property (nonatomic, copy) NSString
status;

@end</code></pre>

接口A、B返回的字典數據如下所示:

@{@"name1": "張三", @"status1": @"start"}

@{@"name2": "張三", @"status2": @"end"}</code></pre>

通常的方法是寫兩個方法分別做轉換,不過如果能靈活地運用Runtime的話,可以只實現一個轉換方法,為此,我們需要先定義一個映射字典(全局變量)

static NSMutableDictionary *map = nil;

@implementation MyObject

  • (void)load { map = [NSMutableDictionary dictionary];

    map[@"name1"] = @"name"; map[@"status1"] = @"status"; map[@"name2"] = @"name"; map[@"status2"] = @"status"; }

@end</code></pre>

上面的代碼將兩個字典中不同的字段映射到MyObject中相同的屬性上,這樣,轉換方法可如下處理:

- (void)setDataWithDic:(NSDictionary )dic
{
    [dic enumerateKeysAndObjectsUsingBlock:^(NSString key, id obj, BOOL *stop) {

    NSString *propertyKey = [self propertyForKey:key];

    if (propertyKey)
    {
        objc_property_t property = class_getProperty([self class], [propertyKey UTF8String]);

        // TODO: 針對特殊數據類型做處理
        NSString *attributeString = [NSString stringWithCString:property_getAttributes(property) encoding:NSUTF8StringEncoding];

        ...

        [self setValue:obj forKey:propertyKey];
    }
}];

}</code></pre>

當然,一個屬性能否通過上面這種方式來處理的前提是其支持KVC。

小結

本章中我們討論了Runtime中與成員變量和屬性相關的內容。成員變量與屬性是類的數據基礎,合理地使用Runtime中的相關操作能讓我們更加靈活地來處理與類數據相關的工作。

方法和消息

SEL又叫選擇器,是表示一個方法的selector的指針,其定義如下:

typedef struct objc_selector *SEL;

objc_selector結構體的詳細定義沒有在頭文件中找到。方法的selector用于表示運行時方 法的名字。Objective-C在編譯時,會依據每一個方法的名字、參數序列,生成一個唯一的整型標識(Int類型的地址),這個標識就是SEL。如下 代碼所示:

SEL sel1 = @selector(method1);
NSLog(@"sel : %p", sel1);

上面的輸出為:

2014-10-30 18:40:07.518 RuntimeTest[52734:466626] sel : 0x100002d72

兩個類之間,不管它們是父類與子類的關系,還是之間沒有這種關系,只要方法名相同,那么方法的SEL就是一樣的。每一個方法都對應著一個SEL。所以在 Objective-C同一個類(及類的繼承體系)中,不能存在2個同名的方法,即使參數類型不同也不行。相同的方法只能對應一個SEL。這也就導致 Objective-C在處理相同方法名且參數個數相同但類型不同的方法方面的能力很差。如在某個類中定義以下兩個方法:

- (void)setWidth:(int)width;

  • (void)setWidth:(double)width;</code></pre>

    當然,不同的類可以擁有相同的selector,這個沒有問題。不同類的實例對象執行相同的selector時,會在各自的方法列表中去根據selector去尋找自己對應的IMP。

    工程中的所有的SEL組成一個Set集合,Set的特點就是唯一,因此SEL是唯一的。因此,如果我們想到這個方法集合中查找某個方法時,只需要去 找到這個方法對應的SEL就行了,SEL實際上就是根據方法名hash化了的一個字符串,而對于字符串的比較僅僅需要比較他們的地址就可以了,可以說速度 上無語倫比!!但是,有一個問題,就是數量增多會增大hash沖突而導致的性能下降(或是沒有沖突,因為也可能用的是perfect hash)。但是不管使用什么樣的方法加速,如果能夠將總量減少(多個方法可能對應同一個SEL),那將是最犀利的方法。那么,我們就不難理解,為什么 SEL僅僅是函數名了。

    本質上,SEL只是一個指向方法的指針(準確的說,只是一個根據方法名hash化了的KEY值,能唯一代表一個方法),它的存在只是為了加快方法的查詢速度。這個查找過程我們將在下面討論。

    我們可以在運行時添加新的selector,也可以在運行時獲取已存在的selector,我們可以通過下面三種方法來獲取SEL:

    1. sel_registerName函數

    2. Objective-C編譯器提供的@selector()

    3. NSSelectorFromString()方法

    IMP

    IMP實際上是一個函數指針,指向方法實現的首地址。其定義如下:

    id (*IMP)(id, SEL, ...)

    這個函數使用當前CPU架構實現的標準的C調用約定。第一個參數是指向self的指針(如果是實例方法,則是類實例的內存地址;如果是類方法,則是指向元類的指針),第二個參數是方法選擇器(selector),接下來是方法的實際參數列表。

    前面介紹過的SEL就是為了查找方法的最終實現IMP的。由于每個方法對應唯一的SEL,因此我們可以通過SEL方便快速準確地獲得它所對應的 IMP,查找過程將在下面討論。取得IMP后,我們就獲得了執行這個方法代碼的入口點,此時,我們就可以像調用普通的C語言函數一樣來使用這個函數指針 了。

    通過取得IMP,我們可以跳過Runtime的消息傳遞機制,直接執行IMP指向的函數實現,這樣省去了Runtime消息傳遞過程中所做的一系列查找操作,會比直接向對象發送消息高效一些。

    Method

    介紹完SEL和IMP,我們就可以來講講Method了。Method用于表示類定義中的方法,則定義如下:

    typedef struct objc_method *Method;

struct objc_method { SEL method_name OBJC2_UNAVAILABLE; // 方法名 char method_types OBJC2_UNAVAILABLE; IMP method_imp OBJC2_UNAVAILABLE; // 方法實現 }</code></pre>

我們可以看到該結構體中包含一個SEL和IMP,實際上相當于在SEL和IMP之間作了一個映射。有了SEL,我們便可以找到對應的IMP,從而調用方法的實現代碼。具體操作流程我們將在下面討論。

objc_method_description

objc_method_description定義了一個Objective-C方法,其定義如下:

struct objc_method_description { SEL name; char types; };</code></pre> 
  

方法相關操作函數

Runtime提供了一系列的方法來處理與方法相關的操作。包括方法本身及SEL。本節我們介紹一下這些函數。

方法操作相關函數包括下以:

// 調用指定方法的實現
id method_invoke ( id receiver, Method m, ... );

// 調用返回一個數據結構的方法的實現 void method_invoke_stret ( id receiver, Method m, ... );

// 獲取方法名 SEL method_getName ( Method m );

// 返回方法的實現 IMP method_getImplementation ( Method m );

// 獲取描述方法參數和返回值類型的字符串 const char * method_getTypeEncoding ( Method m );

// 獲取方法的返回值類型的字符串 char * method_copyReturnType ( Method m );

// 獲取方法的指定位置參數的類型字符串 char * method_copyArgumentType ( Method m, unsigned int index );

// 通過引用返回方法的返回值類型字符串 void method_getReturnType ( Method m, char *dst, size_t dst_len );

// 返回方法的參數的個數 unsigned int method_getNumberOfArguments ( Method m );

// 通過引用返回方法指定位置參數的類型字符串 void method_getArgumentType ( Method m, unsigned int index, char *dst, size_t dst_len );

// 返回指定方法的方法描述結構體 struct objc_method_description * method_getDescription ( Method m );

// 設置方法的實現 IMP method_setImplementation ( Method m, IMP imp );

// 交換兩個方法的實現 void method_exchangeImplementations ( Method m1, Method m2 );</code></pre>

  • method_invoke函數,返回的是實際實現的返回值。參數receiver不能為空。這個方法的效率會比method_getImplementation和method_getName更快。

  • method_getName函數,返回的是一個SEL。如果想獲取方法名的C字符串,可以使用sel_getName(method_getName(method))。

  • method_getReturnType函數,類型字符串會被拷貝到dst中。

  • method_setImplementation函數,注意該函數返回值是方法之前的實現。

方法選擇器

選擇器相關的操作函數包括:

// 返回給定選擇器指定的方法的名稱
const char * sel_getName ( SEL sel );

// 在Objective-C Runtime系統中注冊一個方法,將方法名映射到一個選擇器,并返回這個選擇器 SEL sel_registerName ( const char *str );

// 在Objective-C Runtime系統中注冊一個方法 SEL sel_getUid ( const char *str );

// 比較兩個選擇器 BOOL sel_isEqual ( SEL lhs, SEL rhs );</code></pre>

  • sel_registerName函數:在我們將一個方法添加到類定義時,我們必須在Objective-C Runtime系統中注冊一個方法名以獲取方法的選擇器。

方法調用流程

在Objective-C中,消息直到運行時才綁定到方法實現上。編譯器會將消息表達式[receiver message]轉化為一個消息函數的調用,即objc_msgSend。這個函數將消息接收者和方法名作為其基礎參數,如以下所示:

objc_msgSend(receiver, selector)

如果消息中還有其它參數,則該方法的形式如下所示:

objc_msgSend(receiver, selector, arg1, arg2, ...)

這個函數完成了動態綁定的所有事情:

  1. 首先它找到selector對應的方法實現。因為同一個方法可能在不同的類中有不同的實現,所以我們需要依賴于接收者的類來找到的確切的實現。

  2. 它調用方法實現,并將接收者對象及方法的所有參數傳給它。

  3. 最后,它將實現返回的值作為它自己的返回值。

消息的關鍵在于我們前面章節討論過的結構體objc_class,這個結構體有兩個字段是我們在分發消息的關注的:

  1. 指向父類的指針

  2. 一個類的方法分發表,即methodLists。

當我們創建一個新對象時,先為其分配內存,并初始化其成員變量。其中isa指針也會被初始化,讓對象可以訪問類及類的繼承體系。

下圖演示了這樣一個消息的基本框架:

當消息發送給一個對象時,objc_msgSend通過對象的isa指針獲取到類的結構體,然后在方法分發表里面查找方法的selector。如果 沒有找到selector,則通過objc_msgSend結構體中的指向父類的指針找到其父類,并在父類的分發表里面查找方法的selector。依 此,會一直沿著類的繼承體系到達NSObject類。一旦定位到selector,函數會就獲取到了實現的入口點,并傳入相應的參數來執行方法的具體實 現。如果最后沒有定位到selector,則會走消息轉發流程,這個我們在后面討論。

為了加速消息的處理,運行時系統緩存使用過的selector及對應的方法的地址。這點我們在前面討論過,不再重復。

隱藏參數

objc_msgSend有兩個隱藏參數:

  1. 消息接收對象

  2. 方法的selector

這兩個參數為方法的實現提供了調用者的信息。之所以說是隱藏的,是因為它們在定義方法的源代碼中沒有聲明。它們是在編譯期被插入實現代碼的。

雖然這些參數沒有顯示聲明,但在代碼中仍然可以引用它們。我們可以使用self來引用接收者對象,使用_cmd來引用選擇器。如下代碼所示:

- strange
{
    id  target = getTheReceiver();
    SEL method = getTheMethod();

if ( target == self || method == _cmd )
    return nil;
return [target performSelector:method];

}</code></pre>

當然,這兩個參數我們用得比較多的是self,_cmd在實際中用得比較少。

獲取方法地址

Runtime中方法的動態綁定讓我們寫代碼時更具靈活性,如我們可以把消息轉發給我們想要的對象,或者隨意交換一個方法的實現等。不過靈活性的提 升也帶來了性能上的一些損耗。畢竟我們需要去查找方法的實現,而不像函數調用來得那么直接。當然,方法的緩存一定程度上解決了這一問題。

我們上面提到過,如果想要避開這種動態綁定方式,我們可以獲取方法實現的地址,然后像調用函數一樣來直接調用它。特別是當我們需要在一個循環內頻繁地調用一個特定的方法時,通過這種方式可以提高程序的性能。

NSObject類提供了methodForSelector:方法,讓我們可以獲取到方法的指針,然后通過這個指針來調用實現代碼。我們需要將methodForSelector:返回的指針轉換為合適的函數類型,函數參數和返回值都需要匹配上。

我們通過以下代碼來看看methodForSelector:的使用:

void (*setter)(id, SEL, BOOL);
int i;

setter = (void (*)(id, SEL, BOOL))[target methodForSelector:@selector(setFilled:)]; for ( i = 0 ; i < 1000 ; i++ ) setter(targetList[i], @selector(setFilled:), YES);</code></pre>

這里需要注意的就是函數指針的前兩個參數必須是id和SEL。

當然這種方式只適合于在類似于for循環這種情況下頻繁調用同一方法,以提高性能的情況。另外,methodForSelector:是由Cocoa運行時提供的;它不是Objective-C語言的特性。

消息轉發

當一個對象能接收一個消息時,就會走正常的方法調用流程。但如果一個對象無法接收指定消息時,又會發生什么事呢?默認情況下,如果是以 [object message]的方式調用方法,如果object無法響應message消息時,編譯器會報錯。但如果是以perform…的形式來調用,則需要等到運 行時才能確定object是否能接收message消息。如果不能,則程序崩潰。

通常,當我們不能確定一個對象是否能接收某個消息時,會先調用respondsToSelector:來判斷一下。如下代碼所示:

if ([self respondsToSelector:@selector(method)]) {
    [self performSelector:@selector(method)];
}

不過,我們這邊想討論下不使用respondsToSelector:判斷的情況。這才是我們這一節的重點。

當一個對象無法接收某一消息時,就會啟動所謂”消息轉發(message forwarding)“機制,通過這一機制,我們可以告訴對象如何處理未知的消息。默認情況下,對象接收到未知的消息,會導致程序崩潰,通過控制臺,我們可以看到以下異常信息:

-[SUTRuntimeMethod method]: unrecognized selector sent to instance 0x100111940

*** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[SUTRuntimeMethod method]: unrecognized selector sent to instance 0x100111940'</code></pre>

這段異常信息實際上是由NSObject的”doesNotRecognizeSelector”方法拋出的。不過,我們可以采取一些措施,讓我們的程序執行特定的邏輯,而避免程序的崩潰。

消息轉發機制基本上分為三個步驟:

  1. 動態方法解析

  2. 備用接收者

  3. 完整轉發

下面我們詳細討論一下這三個步驟。

動態方法解析

對象在接收到未知的消息時,首先會調用所屬類的類方法+resolveInstanceMethod:(實例方法)或 者+resolveClassMethod:(類方法)。在這個方法中,我們有機會為該未知消息新增一個”處理方法”“。不過使用該方法的前提是我們已經 實現了該”處理方法”,只需要在運行時通過class_addMethod函數動態添加到類里面就可以了。如下代碼所示:

void functionForMethod1(id self, SEL _cmd) {
   NSLog(@"%@, %p", self, _cmd);
}

  • (BOOL)resolveInstanceMethod:(SEL)sel {

    NSString *selectorString = NSStringFromSelector(sel);

    if ([selectorString isEqualToString:@"method1"]) {

      class_addMethod(self.class, @selector(method1), (IMP)functionForMethod1, "@:");
    

    }

    return [super resolveInstanceMethod:sel]; }</code></pre>

    不過這種方案更多的是為了實現@dynamic屬性。

    備用接收者

    如果在上一步無法處理消息,則Runtime會繼續調以下方法:

    - (id)forwardingTargetForSelector:(SEL)aSelector

    如果一個對象實現了這個方法,并返回一個非nil的結果,則這個對象會作為消息的新接收者,且消息會被分發到這個對象。當然這個對象不能是self自身,否則就是出現無限循環。當然,如果我們沒有指定相應的對象來處理aSelector,則應該調用父類的實現來返回結果。

    使用這個方法通常是在對象內部,可能還有一系列其它對象能處理該消息,我們便可借這些對象來處理消息并返回,這樣在對象外部看來,還是由該對象親自處理了這一消息。如下代碼所示:

    @interface SUTRuntimeMethodHelper : NSObject

  • (void)method2;

@end

@implementation SUTRuntimeMethodHelper

  • (void)method2 { NSLog(@"%@, %p", self, _cmd); }

@end

pragma mark -

@interface SUTRuntimeMethod () { SUTRuntimeMethodHelper *_helper; }

@end

@implementation SUTRuntimeMethod

  • (instancetype)object { return [[self alloc] init]; }

  • (instancetype)init { self = [super init]; if (self != nil) {

      _helper = [[SUTRuntimeMethodHelper alloc] init];
    

    }

    return self; }

  • (void)test { [self performSelector:@selector(method2)]; }

  • (id)forwardingTargetForSelector:(SEL)aSelector {

    NSLog(@"forwardingTargetForSelector");

    NSString *selectorString = NSStringFromSelector(aSelector);

    // 將消息轉發給_helper來處理 if ([selectorString isEqualToString:@"method2"]) {

      return _helper;
    

    }

    return [super forwardingTargetForSelector:aSelector]; }

@end</code></pre>

這一步合適于我們只想將消息轉發到另一個能處理該消息的對象上。但這一步無法對消息進行處理,如操作消息的參數和返回值。

完整消息轉發

如果在上一步還不能處理未知消息,則唯一能做的就是啟用完整的消息轉發機制了。此時會調用以下方法:

- (void)forwardInvocation:(NSInvocation *)anInvocation

運行時系統會在這一步給消息接收者最后一次機會將消息轉發給其它對象。對象會創建一個表示消息的NSInvocation對象,把與尚未處理的消息 有關的全部細節都封裝在anInvocation中,包括selector,目標(target)和參數。我們可以在forwardInvocation 方法中選擇將消息轉發給其它對象。

forwardInvocation:方法的實現有兩個任務:

  1. 定位可以響應封裝在anInvocation中的消息的對象。這個對象不需要能處理所有未知消息。

  2. 使用anInvocation作為參數,將消息發送到選中的對象。anInvocation將會保留調用結果,運行時系統會提取這一結果并將其發送到消息的原始發送者。

不過,在這個方法中我們可以實現一些更復雜的功能,我們可以對消息的內容進行修改,比如追回一個參數等,然后再去觸發消息。另外,若發現某個消息不應由本類處理,則應調用父類的同名方法,以便繼承體系中的每個類都有機會處理此調用請求。

還有一個很重要的問題,我們必須重寫以下方法:

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector

消息轉發機制使用從這個方法中獲取的信息來創建NSInvocation對象。因此我們必須重寫這個方法,為給定的selector提供一個合適的方法簽名。

完整的示例如下所示:

- (NSMethodSignature )methodSignatureForSelector:(SEL)aSelector {
    NSMethodSignature signature = [super methodSignatureForSelector:aSelector];

if (!signature) {
    if ([SUTRuntimeMethodHelper instancesRespondToSelector:aSelector]) {
        signature = [SUTRuntimeMethodHelper instanceMethodSignatureForSelector:aSelector];
    }
}

return signature;

}

  • (void)forwardInvocation:(NSInvocation *)anInvocation { if ([SUTRuntimeMethodHelper instancesRespondToSelector:anInvocation.selector]) {
      [anInvocation invokeWithTarget:_helper];
    
    } }</code></pre>

    NSObject的forwardInvocation:方法實現只是簡單調用了doesNotRecognizeSelector:方法,它不會轉發任何消息。這樣,如果不在以上所述的三個步驟中處理未知消息,則會引發一個異常。

    從某種意義上來講,forwardInvocation:就像一個未知消息的分發中心,將這些未知的消息轉發給其它對象。或者也可以像一個運輸站一樣將所有未知消息都發送給同一個接收對象。這取決于具體的實現。

    消息轉發與多重繼承

    回過頭來看第二和第三步,通過這兩個方法我們可以允許一個對象與其它對象建立關系,以處理某些未知消息,而表面上看仍然是該對象在處理消息。通過這 種關系,我們可以模擬“多重繼承”的某些特性,讓對象可以“繼承”其它對象的特性來處理一些事情。不過,這兩者間有一個重要的區別:多重繼承將不同的功能 集成到一個對象中,它會讓對象變得過大,涉及的東西過多;而消息轉發將功能分解到獨立的小的對象中,并通過某種方式將這些對象連接起來,并做相應的消息轉 發。

    不過消息轉發雖然類似于繼承,但NSObject的一些方法還是能區分兩者。如respondsToSelector:和isKindOfClass:只能用于繼承體系,而不能用于轉發鏈。便如果我們想讓這種消息轉發看起來像是繼承,則可以重寫這些方法,如以下代碼所示:

    - (BOOL)respondsToSelector:(SEL)aSelector   {
    
     if ( [super respondsToSelector:aSelector] )
              return YES;     
     else {
               /* Here, test whether the aSelector message can
                *            
                * be forwarded to another object and whether that  
                *            
                * object can respond to it. Return YES if it can.  
                */      
     }
     return NO;  
    
    }</code></pre>

    小結

    在此,我們已經了解了Runtime中消息發送和轉發的基本機制。這也是Runtime的強大之處,通過它,我們可以為程序增加很多動態的行為,雖 然我們在實際開發中很少直接使用這些機制(如直接調用objc_msgSend),但了解它們有助于我們更多地去了解底層的實現。其實在實際的編碼過程中,我們也可以靈活地使用這些機制,去實現一些特殊的功能,如hook操作等。

    Method Swizzling

    理解Method Swizzling是學習runtime機制的一個很好的機會。在此不多做整理,僅翻譯由Mattt Thompson發表于nshipster的Method Swizzling一文。

    Method Swizzling是改變一個selector的實際實現的技術。通過這一技術,我們可以在運行時通過修改類的分發表中selector對應的函數,來修改方法的實現。

    例如,我們想跟蹤在程序中每一個view controller展示給用戶的次數:當然,我們可以在每個view controller的viewDidAppear中添加跟蹤代碼;但是這太過麻煩,需要在每個view controller中寫重復的代碼。創建一個子類可能是一種實現方式,但需要同時創建UIViewController, UITableViewController, UINavigationController及其它UIKit中view controller的子類,這同樣會產生許多重復的代碼。

    這種情況下,我們就可以使用Method Swizzling,如在代碼所示:

    #import <objc/runtime.h>

@implementation UIViewController (Tracking)

  • (void)load {

      static dispatch_once_t onceToken;
    

    dispatch_once(&onceToken, ^{

      Class class = [self class];         
      // When swizzling a class method, use the following:
                  // Class class = object_getClass((id)self);
    
      SEL originalSelector = @selector(viewWillAppear:);
                  SEL swizzledSelector = @selector(xxx_viewWillAppear:);
    
      Method originalMethod = class_getInstanceMethod(class, originalSelector);
                  Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);
    
      BOOL didAddMethod =
                      class_addMethod(class,
              originalSelector,
              method_getImplementation(swizzledMethod),
              method_getTypeEncoding(swizzledMethod));
    
      if (didAddMethod) {
                      class_replaceMethod(class,
              swizzledSelector,
              method_getImplementation(originalMethod),
              method_getTypeEncoding(originalMethod));
      } else {
          method_exchangeImplementations(originalMethod, swizzledMethod);
      }
    

    }); }

pragma mark - Method Swizzling

  • (void)xxx_viewWillAppear:(BOOL)animated {
      [self xxx_viewWillAppear:animated];
    
    NSLog(@"viewWillAppear: %@", self); }

@end</code></pre>

在這里,我們通過method swizzling修改了UIViewController的@selector(viewWillAppear:)對應的函數指針,使其實現指向了我們自定義的xxx_viewWillAppear的實現。這樣,當UIViewController及其子類的對象調用viewWillAppear時,都會打印一條日志信息。

上面的例子很好地展示了使用method swizzling來一個類中注入一些我們新的操作。當然,還有許多場景可以使用method swizzling,在此不多舉例。在此我們說說使用method swizzling需要注意的一些問題:

Swizzling應該總是在+load中執行

在Objective-C中,運行時會自動調用每個類的兩個方法。+load會在類初始加載時調用,+initialize會在第一次調用類的類方法或實例方法之前被調用。這兩個方法是可選的,且只有在實現了它們時才會被調用。由于method swizzling會影響到類的全局狀態,因此要盡量避免在并發處理中出現競爭的情況。+load能保證在類的初始化過程中被加載,并保證這種改變應用級別的行為的一致性。相比之下,+initialize在其執行時不提供這種保證—事實上,如果在應用中沒為給這個類發送消息,則它可能永遠不會被調用。

Swizzling應該總是在dispatch_once中執行

與上面相同,因為swizzling會改變全局狀態,所以我們需要在運行時采取一些預防措施。原子性就是這樣一種措施,它確保代碼只被執行一次,不管有多少個線程。GCD的dispatch_once可以確保這種行為,我們應該將其作為method swizzling的最佳實踐。

選擇器、方法與實現

在Objective-C中,選擇器(selector)、方法(method)和實現(implementation)是運行時中一個特殊點,雖然在一般情況下,這些術語更多的是用在消息發送的過程描述中。

以下是Objective-C Runtime Reference中的對這幾個術語一些描述:

  • Selector(typedef struct objc_selector *SEL):用于在運行時中表示一個方法的名稱。一個方法選擇器是一個C字符串,它是在Objective-C運行時被注冊的。選擇器由編譯器生成,并且在類被加載時由運行時自動做映射操作。

  • Method(typedef struct objc_method *Method):在類定義中表示方法的類型

  • Implementation(typedef id (*IMP)(id, SEL, …)):這是一個指針類型,指向方法實現函數的開始位置。這個函數使用為當前CPU架構實現的標準C調用規范。每一個參數是指向對象自身的指針(self),第二個參數是方法選擇器。然后是方法的實際參數。

理解這幾個術語之間的關系最好的方式是:一個類維護一個運行時可接收的消息分發表;分發表中的每個入口是一個方法(Method),其中key是一個特定名稱,即選擇器(SEL),其對應一個實現(IMP),即指向底層C函數的指針。

為了swizzle一個方法,我們可以在分發表中將一個方法的現有的選擇器映射到不同的實現,而將該選擇器對應的原始實現關聯到一個新的選擇器中。

調用_cmd

我們回過頭來看看前面新的方法的實現代碼:

- (void)xxx_viewWillAppear:(BOOL)animated {
    [self xxx_viewWillAppear:animated];
    NSLog(@"viewWillAppear: %@", NSStringFromClass([self class]));
}

咋看上去是會導致無限循環的。但令人驚奇的是,并沒有出現這種情況。在swizzling的過程中,方法中的[self xxx_viewWillAppear:animated]已經被重新指定到UIViewController類的-viewWillAppear:中。在這種情況下,不會產生無限循環。不過如果我們調用的是[self viewWillAppear:animated],則會產生無限循環,因為這個方法的實現在運行時已經被重新指定為xxx_viewWillAppear:了。

注意事項

Swizzling通常被稱作是一種黑魔法,容易產生不可預知的行為和無法預見的后果。雖然它不是最安全的,但如果遵從以下幾點預防措施的話,還是比較安全的:

  • 總是調用方法的原始實現(除非有更好的理由不這么做):API提供了一個輸入與輸出約定,但其內部實現是一個黑盒。Swizzle一個方法而不調用原始實現可能會打破私有狀態底層操作,從而影響到程序的其它部分。

  • 避免沖突:給自定義的分類方法加前綴,從而使其與所依賴的代碼庫不會存在命名沖突。

  • 明白是怎么回事:簡單地拷貝粘貼swizzle代碼而不理解它是如何工作的,不僅危險,而且會浪費學習Objective-C運行時的機會。閱讀Objective-C Runtime Reference和查看<objc/runtime.h>頭文件以了解事件是如何發生的。

  • 小心操作:無論我們對Foundation, UIKit或其它內建框架執行Swizzle操作抱有多大信心,需要知道在下一版本中許多事可能會不一樣。

協議與分類

Objective-C中的分類允許我們通過給一個類添加方法來擴充它(但是通過category不能添加新的實例變量),并且我們不需要訪問類中的代碼就可以做到。

Objective-C中的協議是普遍存在的接口定義方式,即在一個類中通過@protocol定義接口,在另外類中實現接口,這種接口定義方式也成為“delegation”模式,@protocol聲明了可以唄其他任何方法類實現的方法,協議僅僅是定義一個接口,而由其他的類去負責實現。

在本章中,我們來看看runtime對分類與協議的支持。

基礎數據類型

Category

Category是表示一個指向分類的結構體的指針,其定義如下:

typedef struct objc_category *Category;

struct objc_category { char category_name OBJC2_UNAVAILABLE; // 分類名 char class_name OBJC2_UNAVAILABLE; // 分類所屬的類名 struct objc_method_list instance_methods OBJC2_UNAVAILABLE; // 實例方法列表 struct objc_method_list class_methods OBJC2_UNAVAILABLE; // 類方法列表 struct objc_protocol_list *protocols OBJC2_UNAVAILABLE; // 分類所實現的協議列表 }</code></pre>

這個結構體主要包含了分類定義的實例方法與類方法,其中instance_methods列表是objc_class中方法列表的一個子集,而class_methods列表是元類方法列表的一個子集。

Protocol

Protocol的定義如下:

typedef struct objc_object Protocol;

我們可以看到,Protocol其中實就是一個對象結構體。

操作函數

Runtime并沒有在<objc/runtime.h>頭文件中提供針對分類的操作函數。因為這些分類中的信息都包含在objc_class中,我們可以通過針對objc_class的操作函數來獲取分類的信息。如下例所示:

@interface RuntimeCategoryClass : NSObject

  • (void)method1; @end

@interface RuntimeCategoryClass (Category)

  • (void)method2; @end

@implementation RuntimeCategoryClass

  • (void)method1 {

}

@end

@implementation RuntimeCategoryClass (Category)

  • (void)method2 {

}

@end

pragma mark -

NSLog(@"測試objc_class中的方法列表是否包含分類中的方法"); unsigned int outCount = 0; Method *methodList = class_copyMethodList(RuntimeCategoryClass.class, &outCount);

for (int i = 0; i < outCount; i++) { Method method = methodList[i];

const char *name = sel_getName(method_getName(method));

NSLog(@"RuntimeCategoryClass's method: %s", name);

if (strcmp(name, sel_getName(@selector(method2)))) {
    NSLog(@"分類方法method2在objc_class的方法列表中");
}

}</code></pre>

其輸出是:

2014-11-08 10:36:39.213 [561:151847] 測試objc_class中的方法列表是否包含分類中的方法
2014-11-08 10:36:39.215 [561:151847] RuntimeCategoryClass's method: method2
2014-11-08 10:36:39.215 [561:151847] RuntimeCategoryClass's method: method1
2014-11-08 10:36:39.215 [561:151847] 分類方法method2在objc_class的方法列表中

而對于Protocol,runtime提供了一系列函數來對其進行操作,這些函數包括:

// 返回指定的協議
Protocol  objc_getProtocol ( const char name );

// 獲取運行時所知道的所有協議的數組 Protocol * objc_copyProtocolList ( unsigned int outCount );

// 創建新的協議實例 Protocol objc_allocateProtocol ( const char name );

// 在運行時中注冊新創建的協議 void objc_registerProtocol ( Protocol *proto );

// 為協議添加方法 void protocol_addMethodDescription ( Protocol proto, SEL name, const char types, BOOL isRequiredMethod, BOOL isInstanceMethod );

// 添加一個已注冊的協議到協議中 void protocol_addProtocol ( Protocol proto, Protocol addition );

// 為協議添加屬性 void protocol_addProperty ( Protocol proto, const char name, const objc_property_attribute_t *attributes, unsigned int attributeCount, BOOL isRequiredProperty, BOOL isInstanceProperty );

// 返回協議名 const char protocol_getName ( Protocol p );

// 測試兩個協議是否相等 BOOL protocol_isEqual ( Protocol proto, Protocol other );

// 獲取協議中指定條件的方法的方法描述數組 struct objc_method_description protocol_copyMethodDescriptionList ( Protocol p, BOOL isRequiredMethod, BOOL isInstanceMethod, unsigned int *outCount );

// 獲取協議中指定方法的方法描述 struct objc_method_description protocol_getMethodDescription ( Protocol *p, SEL aSel, BOOL isRequiredMethod, BOOL isInstanceMethod );

// 獲取協議中的屬性列表 objc_property_t protocol_copyPropertyList ( Protocol proto, unsigned int *outCount );

// 獲取協議的指定屬性 objc_property_t protocol_getProperty ( Protocol proto, const char name, BOOL isRequiredProperty, BOOL isInstanceProperty );

// 獲取協議采用的協議 Protocol * protocol_copyProtocolList ( Protocol proto, unsigned int *outCount );

// 查看協議是否采用了另一個協議 BOOL protocol_conformsToProtocol ( Protocol proto, Protocol other );</code></pre>

  • objc_getProtocol函數,需要注意的是如果僅僅是聲明了一個協議,而未在任何類中實現這個協議,則該函數返回的是nil。

  • objc_copyProtocolList函數,獲取到的數組需要使用free來釋放

  • objc_allocateProtocol函數,如果同名的協議已經存在,則返回nil

  • objc_registerProtocol函數,創建一個新的協議后,必須調用該函數以在運行時中注冊新的協議。協議注冊后便可以使用,但不能再做修改,即注冊完后不能再向協議添加方法或協議

需要強調的是,協議一旦注冊后就不可再修改,即無法再通過調用protocol_addMethodDescription、protocol_addProtocol和protocol_addProperty往協議中添加方法等。

小結

Runtime并沒有提供過多的函數來處理分類。對于協議,我們可以動態地創建協議,并向其添加方法、屬性及繼承的協議,并在運行時動態地獲取這些信息。

拾遺

前面幾篇基本介紹了runtime中的大部分功能,包括對類與對象、成員變量與屬性、方法與消息、分類與協議的處理。runtime大部分的功能都是圍繞這幾點來實現的。

本章的內容并不算重點,主要針對前文中對Objective-C Runtime Reference內容遺漏的地方做些補充。當然這并不能包含所有的內容。runtime還有許多內容,需要讀者去研究發現。

super

在Objective-C中,如果我們需要在類的方法中調用父類的方法時,通常都會用到super,如下所示:

@interface MyViewController: UIViewController

@end

@implementation MyViewController

  • (void)viewDidLoad { [super viewDidLoad];

    // do something ... }

@end</code></pre>

如何使用super我們都知道。現在的問題是,它是如何工作的呢?

首先我們需要知道的是super與self不同。self是類的一個隱藏參數,每個方法的實現的第一個參數即為self。而super并不是隱藏參數,它實際上只是一個”編譯器標示符”,它負責告訴編譯器,當調用viewDidLoad方法時,去調用父類的方法,而不是本類中的方法。而它實際上與self指向的是相同的消息接收者。為了理解這一點,我們先來看看super的定義:

struct objc_super { id receiver; Class superClass; };

這個結構體有兩個成員:

  • receiver:即消息的實際接收者

  • superClass:指針當前類的父類

當我們使用super來接收消息時,編譯器會生成一個objc_super結構體。就上面的例子而言,這個結構體的receiver就是MyViewController對象,與self相同;superClass指向MyViewController的父類UIViewController。

接下來,發送消息時,不是調用objc_msgSend函數,而是調用objc_msgSendSuper函數,其聲明如下:

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

該函數第一個參數即為前面生成的objc_super結構體,第二個參數是方法的selector。該函數實際的操作是:從objc_super結構體指向的superClass的方法列表開始查找viewDidLoad的selector,找到后以objc->receiver去調用這個selector,而此時的操作流程就是如下方式了

objc_msgSend(objc_super->receiver, @selector(viewDidLoad))

由于objc_super->receiver就是self本身,所以該方法實際與下面這個調用是相同的:

objc_msgSend(self, @selector(viewDidLoad))

為了便于理解,我們看以下實例:

@interface MyClass : NSObject

@end

@implementation MyClass

  • (void)test { NSLog(@"self class: %@", self.class); NSLog(@"super class: %@", super.class); }

@end</code></pre>

調用MyClass的test方法后,其輸出是:

2014-11-08 15:55:03.256 [824:209297] self class: MyClass
2014-11-08 15:55:03.256 [824:209297] super class: MyClass

從上例中可以看到,兩者的輸出都是MyClass。大家可以自行用上面介紹的內容來梳理一下。

庫相關操作

庫相關的操作主要是用于獲取由系統提供的庫相關的信息,主要包含以下函數:

// 獲取所有加載的Objective-C框架和動態庫的名稱
const char * objc_copyImageNames ( unsigned int outCount );

// 獲取指定類所在動態庫 const char * class_getImageName ( Class cls );

// 獲取指定庫或框架中所有類的類名 const char * objc_copyClassNamesForImage ( const char image, unsigned int *outCount );</code></pre>

通過這幾個函數,我們可以了解到某個類所有的庫,以及某個庫中包含哪些類。如下代碼所示:

NSLog(@"獲取指定類所在動態庫");

NSLog(@"UIView's Framework: %s", class_getImageName(NSClassFromString(@"UIView")));

NSLog(@"獲取指定庫或框架中所有類的類名"); const char ** classes = objc_copyClassNamesForImage(class_getImageName(NSClassFromString(@"UIView")), &outCount); for (int i = 0; i < outCount; i++) { NSLog(@"class name: %s", classes[i]); }</code></pre>

其輸出結果如下:

2014-11-08 12:57:32.689 [747:184013] 獲取指定類所在動態庫
2014-11-08 12:57:32.690 [747:184013] UIView's Framework: /System/Library/Frameworks/UIKit.framework/UIKit
2014-11-08 12:57:32.690 [747:184013] 獲取指定庫或框架中所有類的類名
2014-11-08 12:57:32.691 [747:184013] class name: UIKeyboardPredictiveSettings
2014-11-08 12:57:32.691 [747:184013] class name: _UIPickerViewTopFrame
2014-11-08 12:57:32.691 [747:184013] class name: _UIOnePartImageView
2014-11-08 12:57:32.692 [747:184013] class name: _UIPickerViewSelectionBar
2014-11-08 12:57:32.692 [747:184013] class name: _UIPickerWheelView
2014-11-08 12:57:32.692 [747:184013] class name: _UIPickerViewTestParameters

塊操作

我們都知道block給我們帶到極大的方便,蘋果也不斷提供一些使用block的新的API。同時,蘋果在runtime中也提供了一些函數來支持針對block的操作,這些函數包括:

// 創建一個指針函數的指針,該函數調用時會調用特定的block
IMP imp_implementationWithBlock ( id block );

// 返回與IMP(使用imp_implementationWithBlock創建的)相關的block id imp_getBlock ( IMP anImp );

// 解除block與IMP(使用imp_implementationWithBlock創建的)的關聯關系,并釋放block的拷貝 BOOL imp_removeBlock ( IMP anImp );</code></pre>

  • imp_implementationWithBlock函數:參數block的簽名必須是method_return_type ^(id self, method_args …)形式的。該方法能讓我們使用block作為IMP。如下代碼所示:
@interface MyRuntimeBlock : NSObject

@end

@implementation MyRuntimeBlock

@end

// 測試代碼 IMP imp = imp_implementationWithBlock(^(id obj, NSString *str) { NSLog(@"%@", str); });

class_addMethod(MyRuntimeBlock.class, @selector(testBlock:), imp, "v@:@");

MyRuntimeBlock runtime = [[MyRuntimeBlock alloc] init]; [runtime performSelector:@selector(testBlock:) withObject:@"hello world!"];</code></pre>

輸出結果是

2014-11-09 14:03:19.779 [1172:395446] hello world!

弱引用操作

// 加載弱引用指針引用的對象并返回
id objc_loadWeak ( id location );

// 存儲weak變量的新值 id objc_storeWeak ( id *location, id obj );</code></pre>

  • objc_loadWeak函數:該函數加載一個弱指針引用的對象,并在對其做retain和autoreleasing操作后返回它。這樣,對象就可以在調用者使用它時保持足夠長的生命周期。該函數典型的用法是在任何有使用weak變量的表達式中使用。</p> </li>

  • objc_storeWeak函數:該函數的典型用法是用于__weak變量做為賦值對象時。

  • </ul>

    這兩個函數的具體實施在此不舉例,有興趣的小伙伴可以參考《Objective-C高級編程:iOS與OS X多線程和內存管理》中對__weak實現的介紹。

    宏定義

    在runtime中,還定義了一些宏定義供我們使用,有些值我們會經常用到,如表示BOOL值的YES/NO;而有些值不常用,如OBJC_ROOT_CLASS。在此我們做一個簡單的介紹。

    布爾值

    #define YES  (BOOL)1

    define NO (BOOL)0</code></pre>

    這兩個宏定義定義了表示布爾值的常量,需要注意的是YES的值是1,而不是非0值。

    空值

    #define nil  __DARWIN_NULL
    #define Nil  __DARWIN_NULL

    其中nil用于空的實例對象,而Nil用于空類對象。

    分發函數原型

    #define OBJC_OLD_DISPATCH_PROTOTYPES  1

    該宏指明分發函數是否必須轉換為合適的函數指針類型。當值為0時,必須進行轉換

    Objective-C根類

    #define OBJC_ROOT_CLASS

    如果我們定義了一個Objective-C根類,則編譯器會報錯,指明我們定義的類沒有指定一個基類。這種情況下,我們就可以使用這個宏定義來避過這個編譯錯誤。該宏在iOS 7.0后可用。

    其實在NSObject的聲明中,我們就可以看到這個宏的身影,如下所示:

    __OSX_AVAILABLE_STARTING(__MAC_10_0, __IPHONE_2_0)
    OBJC_ROOT_CLASS
    OBJC_EXPORT
    @interface NSObject <NSObject> {
        Class isa  OBJC_ISA_AVAILABILITY;
    }

    我們可以參考這種方式來定義我們自己的根類。

    局部變量存儲時長

    #define NS_VALID_UNTIL_END_OF_SCOPE

    該宏表明存儲在某些局部變量中的值在優化時不應該被編譯器強制釋放。

    我們將局部變量標記為id類型或者是指向ObjC對象類型的指針,以便存儲在這些局部變量中的值在優化時不會被編譯器強制釋放。相反,這些值會在變量再次被賦值之前或者局部變量的作用域結束之前都會被保存。

    關聯對象行為

    enum {
       OBJC_ASSOCIATION_ASSIGN  = 0,
       OBJC_ASSOCIATION_RETAIN_NONATOMIC  = 1,
       OBJC_ASSOCIATION_COPY_NONATOMIC  = 3,
       OBJC_ASSOCIATION_RETAIN  = 01401,
       OBJC_ASSOCIATION_COPY  = 01403
    };

    這幾個值在前面已介紹過,在此不再重復。

    總結

    至此,本系列對runtime的整理已完結。當然這只是對runtime的一些基礎知識的歸納,力圖起個拋磚引玉的作用。還有許多關于runtime有意思東西還需要讀者自己去探索發現。

     

    來自:http://www.jianshu.com/p/6b905584f536

     

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