Objective-C runtime

本文转自:杨萧玉博客

本文详细整理了 CocoaRuntime 系统的知识,它使得 Objective-C 如虎添翼,具备了灵活的动态特性,使这门古老的语言焕发生机。主要内容如下:

  • 引言
  • 简介
  • 与Runtime交互
  • Runtime术语
  • 消息
  • 动态方法解析
  • 消息转发
  • 健壮的实例变量(Non Fragile ivars)
  • Objective-C Associated Objects
  • 8Method Swizzling
  • 总结

引言

曾经觉得Objective-C特别方便上手,面对着 Cocoa 中大量 API,只知道简单的查文档和调用。还记得初学 Objective-C 时把[receiver message]当成简单的方法调用,而无视了“发送消息”这句话的深刻含义。于是[receiver message]会被编译器转化为:

objc_msgSend(receiver, selector)

如果消息含有参数,则为:

objc_msgSend(receiver, selector, arg1, arg2, ...)
  • 如果消息的接收者能够找到对应的selector,那么就相当于直接执行了接收者这个对象的特定方法; 否则,消息要么被转发,或是临时向接收者动态添加这个selector 对应的实现内容,要么就干脆玩完崩溃掉。
  • 现在可以看出[receiver message]真的不是一个简简单单的方法调用。因为这只是在编译阶段确定了要向接收者发送message这条消息,而receive将要如何响应这条消息,那就要看运行时发生的情况来决定了。
  • Objective-CRuntime 铸就了它动态语言的特性,这些深层次的知识虽然平时写代码用的少一些,但是却是每个 Objective-C 程序员需要了解的。

简介

因为Objective-C是一门动态语言,所以它总是想办法把一些决定工作从编译连接推迟到运行时。也就是说只有编译器是不够的,还需要一个运行时系统 (runtime system) 来执行编译后的代码。这就是 Objective-C Runtime 系统存在的意义,它是整个Objc运行框架的一块基石。

Runtime其实有两个版本:“modern”和 “legacy”。我们现在用的 Objective-C 2.0 采用的是现行(Modern)版的Runtime系统,只能运行在 iOS 和 OS X 10.5 之后的64位程序中。而OS X较老的32位程序仍采用 Objective-C 1中的(早期)Legacy 版本的 Runtime 系统。这两个版本最大的区别在于当你更改一个类的实例变量的布局时,在早期版本中你需要重新编译它的子类,而现行版就不需要。

Runtime基本是C和汇编写的,可见苹果为了动态系统的高效而作出的努力。你可以在这里下到苹果维护的开源代码。苹果和GNU各自维护一个开源的Runtime版本,这两个版本之间都在努力的保持一致。

与Runtime交互

Objectie-C 从三种不同的层级上与 Runtime 系统进行交互,分别是通过 Objective-C 源代码,通过 Foundation 框架的 NSObject 类定义的方法,通过对 runtime 函数的直接调用。

Objective-C源代码

大部分情况下你就只管写你的Objc代码就行,Runtime 系统自动在幕后辛勤劳作着。还记得引言中举的例子吧,消息的执行会使用到一些编译器为实现动态语言特性而创建的数据结构和函数,Objective-C中的类、方法和协议等在 Runtime 中都由一些数据结构来定义,这些内容在后面会讲到。(比如objc_msgSend 函数及其参数列表中的 idSEL 都是啥)

NSObject的方法

Cocoa 中大多数类都继承于 NSObject 类,也就自然继承了它的方法。最特殊的例外是 NSProxy ,它是个抽象超类,它实现了一些消息转发有关的方法,可以通过继承它来实现一个其他类的替身类或是虚拟出一个不存在的类,说白了就是领导把自己展现给大家风光无限,但是把活儿都交给幕后小弟去干。
有的 NSObject 中的方法起到了抽象接口的作用,比如 description 方法需要你重载它并为你定义的类提供描述内容。NSObject 还有些方法能在运行时获得类的信息,并检查一些特性,比如 class 返回对象的类;isKindOfClass:isMemberOfClass: 则检查对象是否在指定的类继承体系中;respondsToSelector: 检查对象能否响应指定的消息;conformsToProtocol:
检查对象是否实现了指定协议类的方法;methodForSelector:则返回指定方法实现的地址。

Runtime的函数

Runtime 系统是一个由一系列函数和数据结构组成,具有公共接口的动态共享库。头文件存放于/usr/include/objc 目录下。许多函数允许你用纯C代码来重复实现 Objc 中同样的功能。虽然有一些方法构成了 NSObject 类的基础,但是你在写 Objc 代码时一般不会直接用到这些函数的,除非是写一些 Objc 与其他语言的桥接或是底层的debug工作。在Objective-C Runtime Reference中有对 Runtime 函数的详细文档。

Runtime术语

还记得引言中的 objc_msgSend:方法吧,它的真身是这样的:

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

下面将会逐渐展开介绍一些术语,其实它们都对应着数据结构。

SEL

objc_msgSend 函数第二个参数类型为SEL ,它是selector 在Objc中的表示类型(Swift中是Selector类)。selector 是方法选择器,可以理解为区分方法的 ID,而这个 ID 的数据结构是SEL:

typedef struct objc_selector *SEL;

其实它就是个映射到方法的C字符串,你可以用 Objc 编译器命令@selector()
或者 Runtime 系统的 sel_registerName函数来获得一个SEL 类型的方法选择器。
不同类中相同名字的方法所对应的方法选择器是相同的,即使方法名字相同而变量类型不同也会导致它们具有相同的方法选择器,于是 Objc 中方法命名有时会带上参数类型( NSNumber 一堆抽象工厂方法拿走不谢),Cocoa 中有好多长长的方法哦。

id

objc_msgSend 第一个参数类型为id,大家对它都不陌生,它是一个指向类实例的指针:

typedef struct objc_object *id;

objc_object 又是啥呢:

struct objc_object { Class isa; };

objc_object 结构体包含一个isa指针,根据isa指针就可以顺藤摸瓜找到对象所属的类。

Class

之所以说isa是指针是因为Class其实是一个指向objc_class结构体的指针:

typedef struct objc_class *Class;

objc_class就是我们摸到的那个瓜,里面的东西多着呢:

1    struct objc_class { 
2          Class isa OBJC_ISA_AVAILABILITY;
3    #if !__OBJC2__ 
4          Class super_class
5          const char *name
6          long version 
7          long info 
8          long instance_size 
9          struct objc_ivar_list *ivars
10         struct objc_method_list **methodLists
11         struct objc_cache *cache 
12         struct objc_protocol_list *protocols 
13    #endif         
14    } OBJC2_UNAVAILABLE;      

可以看到运行时一个类还关联了它的超类指针,类名,成员变量,方法,缓存,还有附属的协议。 其中objc_ivar_listobjc_method_list分别是成员变量列表和方法列表:

1    struct objc_ivar_list {
2        int ivar_count
3    #ifdef __LP64__
4        int space 
5    #endif
6        /* variable length structure */ 
7        struct objc_ivar ivar_list[1] 
8    } 
9    struct objc_method_list {
10       struct objc_method_list *obsolete
11       int method_count 
12   #ifdef __LP64__
13       int space
14   #endif
15       /* variable length structure */
16       struct objc_method method_list[1] 
17   }

如果你C语言不是特别好,可以直接理解为objc_ivar_list结构体存储objc_ivar
数组列表,而objc_ivar结构体存储了类的单个成员变量的信息;同理objc_method_list 结构体存储着objc_method数组列表,而objc_method结构体存储了类的某个方法的信息。

最后要提到的还有一个objc_cache,顾名思义它是缓存,它在objc_class的作用很重要,在后面会讲到。不知道你是否注意到了objc_class中也有一个isa对象,这是因为一个 ObjC 类本身同时也是一个对象,为了处理类和对象的关系,runtime 库创建了一种叫做元类 (Meta Class) 的东西,类对象所属类型就叫做元类,它用来表述类对象本身所具备的元数据。类方法就定义于此处,因为这些方法可以理解成类对象的实例方法每个类仅有一个类对象,而每个类对象仅有一个与之相关的元类。当你发出一个类似[NSObject alloc]的消息时,你事实上是把这个消息发给了一个类对象 (Class Object) ,这个类对象必须是一个元类的实例,而这个元类同时也是一个根元类 (root meta class) 的实例。

你会说 NSObject 的子类时,你的类就会指向NSObject 做为其超类。但是所有的元类最终都指向根元类为其超类。所有的元类的方法列表都有能够响应消息的类方法。所以当 [NSObject alloc] 这条消息发给类对象的时候,objc_msgSend()会去它的元类里面去查找能够响应消息的方法,如果找到了,然后对这个类对象执行方法调用。

struct

上图实线是 super_class 指针,虚线是isa指针。 有趣的是根元类的超类是NSObject,而isa指向了自己,而NSObject的超类为nil,也就是它没有超类。

Method

Method是一种代表类中的某个方法的类型。

typedef struct objc_method *Method;

objc_method在上面的方法列表中提到过,它存储了方法名,方法类型和方法实现:

1    struct objc_method { 
2        SEL method_name
3        char *method_types 
4        IMP method_imp
5    }

方法名类型为SEL,前面提到过相同名字的方法即使在不同类中定义,它们的方法选择器也相同。

方法类型method_types是个char指针,其实存储着方法的参数类型和返回值类型。
method_imp指向了方法的实现,本质上是一个函数指针,后面会详细讲到。

Ivar

Ivar是一种代表类中实例变量的类型。

typedef struct objc_ivar *Ivar;

objc_ivar在上面的成员变量列表中也提到过:

1    struct objc_ivar {
2        char *ivar_name 
3        char *ivar_type 
4        int ivar_offset 
5    #ifdef __LP64__
6        int space 
7    #endif
8    }

PS: OBJC2_UNAVAILABLE 之类的宏定义是苹果在 Objc 中对系统运行版本进行约束的黑魔法,有兴趣的可以查看源代码。

IMP

IMP在objc.h中的定义是:

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

它就是一个函数指针,这是由编译器生成的。当你发起一个 ObjC 消息之后,最终它会执行的那段代码,就是由这个函数指针指定的。而 IMP 这个函数指针就指向了这个方法的实现。既然得到了执行某个实例某个方法的入口,我们就可以绕开消息传递阶段,直接执行方法,这在后面会提到。

你会发现IMP指向的方法与objc_msgSend函数类型相同,参数都包含idSEL类型。每个方法名都对应一个SEL类型的方法选择器,而每个实例对象中 SEL对应的方法实现肯定是唯一的,通过一组idSEL参数就能确定唯一的方法实现地址;反之亦然。

Cache

runtime.h中Cache的定义如下:

typedef struct objc_cache *Cache

还记得之前objc_class结构体中有一个struct objc_cache *cache吧,它到底是缓存啥的呢,先看看objc_cache的实现:

1    struct objc_cache {
2        unsigned int mask /* total = mask + 1 */
3        unsigned int occupied 
4        Method buckets[1] 
5    };

Cache为方法调用的性能进行优化,通俗地讲,每当实例对象接收到一个消息时,它不会直接在isa指向的类的方法列表中遍历查找能够响应消息的方法,因为这样效率太低了,而是优先在Cache中查找。Runtime 系统会把被调用的方法存到Cache中(理论上讲一个方法如果被调用,那么它有可能今后还会被调用),下次查找的时候效率更高。这根计算机组成原理中学过的 CPU 绕过主存先访问Cache的道理挺像,而我猜苹果为提高Cache命中率应该也做了努力吧。

Property

@property标记了类中的属性,这个不必多说大家都很熟悉,它是一个指向objc_property结构体的指针:

1    typedef struct objc_property *Property;
2    typedef struct objc_property *objc_property_t;//这个更常用

可以通过class_copyPropertyListprotocol_copyPropertyList方法来获取类和协议中的属性:

1    objc_property_t *class_copyPropertyList(Class cls, unsigned int *outCount)
2    objc_property_t *protocol_copyPropertyList(Protocol *proto, unsigned int *outCount)

返回类型为指向指针的指针,哈哈,因为属性列表是个数组,每个元素内容都是一个objc_property_t指针,而这两个函数返回的值是指向这个数组的指针。
举个栗子,先声明一个类:

1    @interface Lender : NSObject {
2        float alone;
3    }
4    @property float alone;
5    @end

你可以用下面的代码获取属性列表:

1    id LenderClass = objc_getClass("Lender");
2    unsigned int outCount;
3    objc_property_t *properties = class_copyPropertyList(LenderClass, &outCount);

你可以用property_getName函数来查找属性名称:

const char *property_getName(objc_property_t property)

你可以用class_getPropertyprotocol_getProperty通过给出的名称来在类和协议中获取属性的引用:

1    objc_property_t class_getProperty(Class cls, const char *name)
2    objc_property_t protocol_getProperty(Protocol *proto, const char *name, BOOL isRequiredProperty, BOOL isInstanceProperty)

你可以用property_getAttributes函数来发掘属性的名称和@encode类型字符串:

const char *property_getAttributes(objc_property_t property)

把上面的代码放一起,你就能从一个类中获取它的属性啦:

1    id LenderClass = objc_getClass("Lender");
2    unsigned int outCount, i;
3    objc_property_t *properties = class_copyPropertyList(LenderClass, &outCount);
4    for (i = 0; i < outCount; i++) 
5    {
6        objc_property_t property = properties[i];
7        printf(stdout, "%s %s\n", property_getName(property), property_getAttributes(property));
8    }

消息

前面做了这么多铺垫,现在终于说到了消息了。Objc 中发送消息是用中括号([])把接收者和消息括起来,而直到运行时才会把消息与方法实现绑定。

objc_msgSend函数

在引言中已经对objc_msgSend进行了一点介绍,看起来像是objc_msgSend返回了数据,其实objc_msgSend从不返回数据而是你的方法被调用后返回了数据。下面详细叙述下消息发送步骤:

  1. 检测这个 selector 是不是要忽略的。比如 Mac OS X 开发,有了垃圾回收就不理会 retain,release 这些函数了。
  2. 检测这个 target 是不是 nil 对象。ObjC 的特性是允许对一个 nil 对象执行任何一个方法不会 Crash,因为会被忽略掉。
  3. 如果上面两个都过了,那就开始查找这个类的 IMP,先从 cache 里面找,完了找得到就跳到对应的函数去执行。
  4. 如果 cache 找不到就找一下方法分发表。
  5. 如果分发表找不到就到超类的分发表去找,一直找,直到找到NSObject 类为止。
  6. 如果还找不到就要开始进入动态方法解析了,后面会提到。

PS:这里说的分发表其实就是Class中的方法列表,它将方法选择器和方法实现地质联系起来。


其实编译器会根据情况在objc_msgSend, objc_msgSend_stret, objc_msgSendSuper, 或objc_msgSendSuper_stret 四个方法中选择一个来调用。如果消息是传递给超类,那么会调用名字带有”Super”的函数;如果消息返回值是数据结构而不是简单值时,那么会调用名字带有”stret”的函数。排列组合正好四个方法。

值得一提的是在 i386 平台处理返回类型为浮点数的消息时,需要用到objc_msgSend_fpret函数来进行处理,这是因为返回类型为浮点数的函数对应的 ABI(Application Binary Interface) 与返回整型的函数的 ABI 不兼容。此时objc_msgSend不再适用,于是objc_msgSend_fpret被派上用场,它会对浮点数寄存器做特殊处理。不过在 PPC 或 PPC64 平台是不需要麻烦它的。

PS:有木有发现这些函数的命名规律哦?带“Super”的是消息传递给超类;“stret”可分为“st”+“ret”两部分,分别代表“struct”和“return”;“fpret”就是“fp”+“ret”,分别代表“floating-point”和“return”。

方法中的隐藏参数

我们经常在方法中使用self关键字来引用实例本身,但从没有想过为什么self就能取到调用当前方法的对象吧。其实self的内容是在方法运行时被偷偷的动态传入的。

objc_msgSend找到方法对应的实现时,它将直接调用该方法实现,并将消息中所有的参数都传递给方法实现,同时,它还将传递两个隐藏的参数:– 接收消息的对象(也就是self指向的内容) – 方法选择器(_cmd指向的内容) 之所以说它们是隐藏的是因为在源代码方法的定义中并没有声明这两个参数。它们是在代码被编译时被插入实现中的。尽管这些参数没有被明确声明,在源代码中我们仍然可以引用它们。在下面的例子中,self引用了接收者对象,而_cmd引用了方法本身的选择器:

1    - strange
2    {
3        id target = getTheReceiver();
4        SEL method = getTheMethod();
5        if ( target == self || method == _cmd )
6            return nil;
7        return [target performSelector:method];
8    }

在这两个参数中,self 更有用。实际上,它是在方法实现中访问消息接收者对象的实例变量的途径。
而当使用[super XXXX] 调用时,会使用 objc_msgSendSuper 函数,看下 objc_msgSendSuper 的函数定义:

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

第一个参数是个objc_super的结构体,第二个参数还是类似上面的类方法的selector,先看下objc_super这个结构体是什么东西:

struct objc_super 
{
    id receiver;
    Class superClass;
};

可以看到这个结构体包含了两个成员,一个是 receiver,这个类似上面 objc_msgSend 的第一个参数 receiver,第二个成员是记录写 super 这个类的父类是什么,拿上面的代码为例,当编译器遇到 PersonMe 里setName:andAge 方法里的 [super setName:] 时,开始做这几个事:构建 objc_super 的结构体,此时这个结构体的第一个成员变量 receiver 就是 PersonMe* me,和 self 相同。而第二个成员变量 superClass 就是指类 Person,因为 PersonMe 的超类就是这个 Person。

调用 objc_msgSendSuper 的方法,将这个结构体和 setNamesel 传递过去。函数里面在做的事情类似这样:从 objc_super 结构体指向的 superClass的方法列表开始找 setNameselector,找到后再以 objc_super->receiver 去调用这个 selector,可能也会使用 objc_msgSend 这个函数,不过此时的第一个参数 theReceiver 就是 objc_super->receiver,第二个参数是从 objc_super->superClass 中找到的 selector.
例如:

NSLog(@"self ' class is %@", [self class]);
NSLog(@"super' class is %@", [super class]);

而当方法中的super关键字接收到消息时,编译器会创建一个objc_super结构体:

struct objc_super 
{
    id receiver; 
    Class class;  
};

这个结构体指明了消息应该被传递给特定超类的定义。但receiver仍然是self
本身,这点需要注意,因为当我们想通过[super class] 获取超类时,编译器只是将指向selfid指针和classSEL传递给了objc_msgSendSuper函数,因为只有在NSObject类找到class方法,然后class方法调用object_getClass(),接着调用objc_msgSend(objc_super->receiver, @selector(class)),传入的第一个参数是指向selfid指针,与调用[self class]相同,所以我们得到的永远都是self的类型。

获取方法地址

IMP那节提到过可以避开消息绑定而直接获取方法的地址并调用方法。这种做法很少用,除非是需要持续大量重复调用某方法的极端情况,避开消息发送泛滥而直接调用该方法会更高效。
NSObject类中有个methodForSelector:实例方法,你可以用它来获取某个方法选择器对应的IMP,举个栗子:

1    void (*setter)(id, SEL, BOOL);
2    int i;
3    setter = (void (*)(id, SEL, BOOL))
4    [target methodForSelector:@selector(setFilled:)];
5    for ( i = 0 ; i < 1000 ; i++ )
6        setter(targetList[i], @selector(setFilled:), YES);

当方法被当做函数调用时,上节提到的两个隐藏参数就需要我们明确给出了。上面的例子调用了1000次函数,你可以试试直接给target发送1000次setFilled:消息会花多久。
PS:methodForSelector:方法是由 Cocoa 的 Runtime 系统提供的,而不是 Objc 自身的特性。

动态方法解析

你可以动态地提供一个方法的实现。例如我们可以用@dynamic
关键字在类的实现文件中修饰一个属性:

@dynamic propertyName;

这表明我们会为这个属性动态提供存取方法,也就是说编译器不会再默认为我们生成setPropertyName:propertyName方法,而需要我们动态提供。我们可以通过分别重载resolveInstanceMethod:resolveClassMethod:方法分别添加实例方法实现和类方法实现。因为当 Runtime 系统在Cache和方法分发表中(包括超类)找不到要执行的方法时,Runtime会调用resolveInstanceMethod:resolveClassMethod:来给程序员一次动态添加方法实现的机会。我们需要用class_addMethod函数完成向特定类添加特定方法实现的操作:

1 void dynamicMethodIMP(id self, SEL _cmd)
2 { 
3     // implementation ....
4 }
5 @implementation 
6 + (BOOL)resolveInstanceMethod:(SEL)aSEL
7 {
8     if (aSEL == @selector(resolveThisMethodDynamically))
9     { 
10        class_addMethod([self class], aSEL, (IMP) dynamicMethodIMP, "v@:"); 
11        return YES;
12    } 
13    return [super resolveInstanceMethod:aSEL];
14 }
15 @end

上面的例子为resolveThisMethodDynamically方法添加了实现内容,也就是dynamicMethodIMP方法中的代码。其中 “v@: ” 表示返回值和参数,这个符号涉及 Type Encoding

PS:动态方法解析会在消息转发机制浸入前执行。如果 respondsToSelector:
instancesRespondToSelector: 方法被执行,动态方法解析器将会被首先给予一个提供该方法选择器对应的 IMP 的机会。如果你想让该方法选择器被传送到转发机制,那么就让resolveInstanceMethod: 返回NO。

消息转发

重定向

在消息转发机制执行前,Runtime 系统会再给我们一次偷梁换柱的机会,即通过重载- (id)forwardingTargetForSelector:(SEL)aSelector方法替换消息的接受者为其他对象:

1 - (id)forwardingTargetForSelector:(SEL)aSelector
2 {
3      if(aSelector == @selector(mysteriousMethod:))
4      { 
5          return alternateObject;
6      }
7      return [super forwardingTargetForSelector:aSelector];
8 }

毕竟消息转发要耗费更多时间,抓住这次机会将消息重定向给别人是个不错的选择,不过千万别返回self,因为那样会死循环。

转发

当动态方法解析不作处理返回NO 时,消息转发机制会被触发。在这时forwardInvocation: 方法会被执行,我们可以重写这个方法来定义我们的转发逻辑:

1 - (void)forwardInvocation:(NSInvocation *)anInvocation
2 { 
3      if ([someOtherObject respondsToSelector: [anInvocation selector]])     
4      {
5          [anInvocation invokeWithTarget:someOtherObject]; 
6      } else 
7      { 
8          [super forwardInvocation:anInvocation];
9      }
10 }

该消息的唯一参数是个NSInvocation 类型的对象——该对象封装了原始的消息和消息的参数。我们可以实现forwardInvocation: 方法来对不能处理的消息做一些默认的处理,也可以将消息转发给其他对象来处理,而不抛出错误。

这里需要注意的是参数anInvocation 是从哪的来的呢?其实在forwardInvocation: 消息发送前,Runtime系统会向对象发送methodSignatureForSelector: 消息,并取到返回的方法签名用于生成NSInvocation 对象。所以我们在重写forwardInvocation:的同时也要重写methodSignatureForSelector: 方法,否则会抛异常。

当一个对象由于没有相应的方法实现而无法响应某消息时,运行时系统将通过forwardInvocation:消息通知该对象。每个对象都从NSObject类中继承了forwardInvocation:方法。然而,NSObject中的方法实现只是简单地调用了doesNotRecognizeSelector:。通过实现我们自己的forwardInvocation:方法,我们可以在该方法实现中将消息转发给其它对象。

forwardInvocation:方法就像一个不能识别的消息的分发中心,将这些消息转发给不同接收对象。或者它也可以象一个运输站将所有的消息都发送给同一个接收对象。它可以将一个消息翻译成另外一个消息,或者简单的”吃掉“某些消息,因此没有响应也没有错误。forwardInvocation:方法也可以对不同的消息提供同样的响应,这一切都取决于方法的具体实现。该方法所提供是将不同的对象链接到消息链的能力。

注意: forwardInvocation:方法只有在消息接收对象中无法正常响应消息时才会被调用。 所以,如果我们希望一个对象将negotiate消息转发给其它对象,则这个对象不能有negotiate方法。否则,forwardInvocation:将不可能会被调用。

转发和多继承

转发和继承相似,可以用于为Objc编程添加一些多继承的效果。就像下图那样,一个对象把消息转发出去,就好似它把另一个对象中的方法借过来或是“继承”过来一样。



这使得不同继承体系分支下的两个类可以“继承”对方的方法,在上图中Warrior和Diplomat没有继承关系,但是Warrior将negotiate消息转发给了Diplomat后,就好似Diplomat是Warrior的超类一样。

消息转发弥补了 Objc 不支持多继承的性质,也避免了因为多继承导致单个类变得臃肿复杂。它将问题分解得很细,只针对想要借鉴的方法才转发,而且转发机制是透明的。

替代者对象(Surrogate Objects)

转发不仅能模拟多继承,也能使轻量级对象代表重量级对象。弱小的女人背后是强大的男人,毕竟女人遇到难题都把它们转发给男人来做了。这里有一些适用案例,可以参看官方文档

转发与继承

尽管转发很像继承,但是NSObject类不会将两者混淆。像respondsToSelector:isKindOfClass:这类方法只会考虑继承体系,不会考虑转发链。比如上图中一个Warrior对象如果被问到是否能响应negotiate
消息:

1 if ( [aWarrior respondsToSelector:@selector(negotiate)] ) 
2     ...

结果是NO,尽管它能够接受negotiate消息而不报错,因为它靠转发消息给Diplomat类来响应消息。
如果你为了某些意图偏要“弄虚作假”让别人以为Warrior继承到了Diplomat的negotiate方法,你得重新实现 respondsToSelector:isKindOfClass:来加入你的转发算法:

1 - (BOOL)respondsToSelector:(SEL)aSelector 
2 { 
3     if ( [super respondsToSelector:aSelector] ) 
4        return YES;
5     else {
6     /* 
       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. 
       */ 
7     }
8     return NO;
9 }

除了respondsToSelector:isKindOfClass:之外,instancesRespondToSelector:中也应该写一份转发算法。如果使用了协议,conformsToProtocol:同样也要加入到这一行列中。类似地,如果一个对象转发它接受的任何远程消息,它得给出一个methodSignatureForSelector:来返回准确的方法描述,这个方法会最终响应被转发的消息。比如一个对象能给它的替代者对象转发消息,它需要像下面这样实现methodSignatureForSelector:

1 - (NSMethodSignature*)methodSignatureForSelector:(SEL)selector
2 {
3     NSMethodSignature* signature = [super methodSignatureForSelector:selector]; 
4     if (!signature)
5     {
6         signature = [surrogate methodSignatureForSelector:selector]; 
7     } 
8     return signature;
9 }

健壮的实例变量(Non Fragile ivars)

在 Runtime 的现行版本中,最大的特点就是健壮的实例变量。当一个类被编译时,实例变量的布局也就形成了,它表明访问类的实例变量的位置。从对象头部开始,实例变量依次根据自己所占空间而产生位移:



上图左边是NSObject类的实例变量布局,右边是我们写的类的布局,也就是在超类后面加上我们自己类的实例变量,看起来不错。但试想如果那天苹果更新了NSObject 类,发布新版本的系统的话,那就悲剧了:



我们自定义的类被划了两道线,那是因为那块区域跟超类重叠了。唯有苹果将超类改为以前的布局才能拯救我们,但这样也导致它们不能再拓展它们的框架了,因为成员变量布局被死死地固定了。在脆弱的实例变量(Fragile ivars) 环境下我们需要重新编译继承自 Apple 的类来恢复兼容性。那么在健壮的实例变量下回发生什么呢?

在健壮的实例变量下编译器生成的实例变量布局跟以前一样,但是当 runtime 系统检测到与超类有部分重叠时它会调整你新添加的实例变量的位移,那样你在子类中新添加的成员就被保护起来了。

需要注意的是在健壮的实例变量下,不要使用sizeof(SomeClass),而是用class_getInstanceSize([SomeClass class])代替;也不要使用offsetof(SomeClass, SomeIvar),而要用ivar_getOffset(class_getInstanceVariable([SomeClass class], "SomeIvar"))来代替。

Objective-C Associated Objects

在 OS X 10.6 之后,Runtime系统让Objc支持向对象动态添加变量。涉及到的函数有以下三个:

1 void objc_setAssociatedObject ( id object, const void *key, id value, objc_AssociationPolicy policy );
2 id objc_getAssociatedObject ( id object, const void *key );
3 void objc_removeAssociatedObjects ( id object );

这些方法以键值对的形式动态地向对象添加、获取或删除关联值。其中关联政策是一组枚举常量:

1 enum 
2 {
3     OBJC_ASSOCIATION_ASSIGN = 0,         
4     OBJC_ASSOCIATION_RETAIN_NONATOMIC = 1,            
5     OBJC_ASSOCIATION_COPY_NONATOMIC = 3, 
6     OBJC_ASSOCIATION_RETAIN = 01401, 
7     OBJC_ASSOCIATION_COPY = 01403
8 };

这些常量对应着引用关联值的政策,也就是 Objc 内存管理的引用计数机制。

Method Swizzling

之前所说的消息转发虽然功能强大,但需要我们了解并且能更改对应类的源代码,因为我们需要实现自己的转发逻辑。当我们无法触碰到某个类的源代码,却想更改这个类某个方法的实现时,该怎么办呢?可能继承类并重写方法是一种想法,但是有时无法达到目的。这里介绍的是 Method Swizzling ,它通过重新映射方法对应的实现来达到“偷天换日”的目的。跟消息转发相比,Method Swizzling 的做法更为隐蔽,甚至有些冒险,也增大了debug的难度。

这里摘抄一个 NSHipster 的例子:

1 #import <objc/runtime.h> 
2 #import "ViewController.h"
3 @implementation UIViewController (Tracking)
4 + (void)load
5 {
6     static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ Class class = [self class];
7        // When swizzling a class method, use the following:
8        // Class class = object_getClass((id)self);
9        SEL originalSelector = @selector(viewWillAppear:);
10       SEL swizzledSelector = @selector(xxx_viewWillAppear:);
11       Method originalMethod = class_getInstanceMethod(class, originalSelector);
12       Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);
13       BOOL didAddMethod = class_addMethod(class, originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod));
14       if (didAddMethod)
15       {
16           class_replaceMethod(class, swizzledSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod));
17       }
18       else
19       {
20           method_exchangeImplementations(originalMethod, swizzledMethod);
21       }
22    });
23 }
24 #pragma mark - Method Swizzling 
25 - (void)xxx_viewWillAppear:(BOOL)animated
26 {
27    [self xxx_viewWillAppear:animated]; 
28    NSLog(@"viewWillAppear: %@", self);
29 }
30 @end

上面的代码通过添加一个Tracking 类别到UIViewController类中, UIViewController 类的viewWillAppear: 方法和Tracking 类别中xxx_viewWillAppear: 方法的实现相互调换。Swizzling 应该在+load 方法中实现,因为+load 是在一个类最开始加载时调用。dispatch_once 是GCD中的一个方法,它保证了代码块只执行一次,并让其为一个原子操作,线程安全是很重要的。

先用class_addMethodclass_replaceMethod函数将两个方法的实现进行调换,如果类中已经有了viewWillAppear:方法的实现,那么就调用method_exchangeImplementations 函数交换了两个方法的IMP,这是苹果提供给我们用于实现 Method Swizzling 的便捷方法。最后xxx_viewWillAppear:方法的定义看似是递归调用引发死循环,其实不会的。因为[self xxx_viewWillAppear:animated]消息会动态找到xxx_viewWillAppear:方法的实现,而它的实现已经被我们viewWillAppear:方法实现进行了互换,所以这段代码不仅不会死循环,如果你把[self xxx_viewWillAppear:animated]换成[self viewWillAppear:animated]反而会引发死循环。

看到有人说+load方法本身就是线程安全的,因为它在程序刚开始就被调用,很少会碰到并发问题,于是 StackOverFlow 上也有大神给出了另一个 Method Swizzling 的实现:

1 - (void)replacementReceiveMessage:(const struct BInstantMessage *)arg1
2 {
3     NSLog(@"arg1 is %@", arg1); [self replacementReceiveMessage:arg1];
4 }
5 + (void)load
6 {
7     SEL originalSelector = @selector(ReceiveMessage:);
8     SEL overrideSelector = @selector(replacementReceiveMessage:);
9     Method originalMethod = class_getInstanceMethod(self, originalSelector);
10    Method overrideMethod = class_getInstanceMethod(self, overrideSelector);
11    if (class_addMethod(self, originalSelector, method_getImplementation(overrideMethod), method_getTypeEncoding(overrideMethod)))
12    {
13        class_replaceMethod(self, overrideSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod));
14    }
15    else
16    {
17        method_exchangeImplementations(originalMethod, overrideMethod);
18    }
19 }

上面的代码同样要添加在某个类的类别中,相比第一个种实现,只是去掉了dispatch_once 部分。Method Swizzling 的确是一个值得深入研究的话题,Method Swizzling 的最佳实现是什么呢?小弟才疏学浅理解的不深刻,找了几篇不错的资源推荐给大家:

总结

我们之所以让自己的类继承NSObject 不仅仅因为苹果帮我们完成了复杂的内存分配问题,更是因为这使得我们能够用上 Runtime 系统带来的便利。可能我们平时写代码时可能很少会考虑一句简单的[receiver message] 背后发生了什么,而只是当做方法或函数调用。深入理解 Runtime 系统的细节更有利于我们利用消息机制写出功能更强大的代码,比如 Method Swizzling 等。
参考链接:

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,319评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,801评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,567评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,156评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,019评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,090评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,500评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,192评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,474评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,566评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,338评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,212评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,572评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,890评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,169评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,478评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,661评论 2 335

推荐阅读更多精彩内容