Runtime总结

目录

  • 简介
  • Runtime的作用
    • 通过 Objective-C源代码
    • 通过 Foundation 框架的 NSObject类定义的方法
    • 通过对 Runtime 库函数的直接调用
  • Runtime 基础数据结构
    • SEL
    • id
    • Class
    • Method
    • Ivar
    • IMP
    • Cache
    • objc_property_t
    • protocol_t
    • Category
  • 消息
    • objc_msgSend函数 消息发送步骤
    • 方法中的隐藏参数
    • 获取方法地址
  • 动态方法解析
  • 消息转发
    • 重定向 转发目标 备用接收者
    • 完整转发 转发调用 替换消息的方法
    • 转发与多继承
      • 替代者对象 使轻量级对象代表重量级对象
      • 转发与继承
  • 健壮的实例变量 (Non Fragile ivars)
  • 总结

简介

Runtime又叫运行时,是一套底层的C语言的API,其为iOS内部的核心之一,我们平时编写的OC代码,底层都是基于它来实现的。比如

[receiver message];
// 底层运行时会被编译器转化为:
objc_msgSend(receiver, selector)
// 如果其还有参数比如:
[receiver message:(id)arg...];
// 底层运行时会被编译器转化为:
objc_msgSend(receiver, selector, arg1, arg2, ...)

以上你可能看不出它的价值,但是我们需要了解的是Objective-C 是一门动态语言,它会将一些工作放在代码运行时才处理而并非编译时。也就是说,有很多类和成员变量在我们编译的时是不知道的,而在运行时,我们所编写的代码会转换成完整的确定的代码运行。

因此,编译器是不够的,我们还需要一个运行时系统(Runtime system)来处理编译后的代码。

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

Runtime 的作用

Objc 在三种层面上与 Runtime 系统进行交互:

  • 1.通过 Objective-C源代码
  • 2.通过 Foundation 框架的 NSObject类定义的方法
  • 3.通过对 Runtime 库函数的直接调用

通过 Objective-C 源代码

多数情况我们只需要编写 OC 代码即可,Runtime 系统自动在幕后搞定一切,还记得简介中如果我们调用方法,编译器会将 OC 代码转换成运行时代码,在运行时确定数据结构和函数。

通过 Foundation 框架的 NSObject 类定义的方法

Cocoa 程序中绝大部分类都是 NSObject 类的子类,所以都继承了 NSObject 的行为。(NSProxy类时个例外,它是个抽象超类)。

一些情况下,NSObject类仅仅定义了完成某件事情的模板,并没有提供所需的代码。例如-description方法,该方法返回类内容的字符串表示,该方法主要用来调试程序。NSObject 类并不知道子类的内容,所以它只是返回类的名字和对象的地址,NSObject 的子类可以重新实现。

还有一些NSObject的方法可以从Runtime系统中获取信息,允许对象进行自我检查。例如:

  • -class方法返回对象的类;
  • -isKindOfClass:-isMemberOfClass:方法检查对象是否存在于指定的类的继承体系中(是否是其子类或者父类或者当前类的成员变量);
    • isKindOfClass用来判断某个对象是否属于某个类,或者属于某个派生类
    • isMemberOfClass用来判断某个对象是否为当前类的实例
    • isMemberOfClass不能检测任何的类都是基于NSObject类这一事实,而isKindOfClass可以。
  • -respondsToSelector:检查对象能否响应指定的消息;
  • -conformsToProtocol:检车对象是否实现了指定协议类的方法;
  • -methodForSelector:返回指定方法实现的地址。

通过对 Runtime 库函数的直接调用

Runtime 系统是具有公共接口的动态共享库。头文件存放于/usr/include/objc目录下,这意味着我们使用时只需要引入objc/Runtime.h头文件即可。

许多函数可以让你使用纯C代码来实现来实现Objc中同样的功能。除非是写一些Objc中同样的功能。除非是写一些Objc与其他语言的桥接或者是底层的debug工作,你在写Objc代码时一般不会用到这些C语言函数。对于公共接口都有哪些,后面会讲到。我将会参考苹果官方的API文档。

Runtime 基础数据结构

要想全面了解 Runtime 机制,我们必须先了解 Runtime 的一些术语,他们都对应着数据结构。

SEL

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

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

其实它就是个映射到方法的C字符串,你可以用Objc编译器命令@selector()或者Runtime系统的sel_registerName函数来获得一个SEL类型的方法选择器。

不同类中相同名字的方法所对应的方法选择器是相同的,由于变量的类型不同,所以不会导致它们调用方法实现混乱。

id

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

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

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

以上定义,看到objc_object结构体包含一个isa指针,isa指向对象所属的类。

注意:
isa指针在代码运行时并不能总指向实例对象所属的类型,所以不能依靠它来确定类型,要想确定类型还是需要用对象的-class方法。

PS:KVO 的实现机理就是将被观察对象的 isa 指针指向一个中间类而不是真实类型。这是一种叫做 isa-swizzling 的技术,详见官方文档。

Class

Class 其实是一个指向objc_class 结构体的指针:

typedef struct objc_class *Class;

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

objc_class可以看到,一个运行时类中关联了它的父类指针,类名、成员变量、方法、缓存以及附属的协议。

// 成员变量列表
struct objc_ivar_list {
    int ivar_count                                           OBJC2_UNAVAILABLE;
#ifdef __LP64__
    int space                                                OBJC2_UNAVAILABLE;
#endif
    /* variable length structure */
    struct objc_ivar ivar_list[1]                            OBJC2_UNAVAILABLE;
}                                                            OBJC2_UNAVAILABLE;

// 方法列表
struct objc_method_list {
    struct objc_method_list *obsolete                        OBJC2_UNAVAILABLE;

    int method_count                                         OBJC2_UNAVAILABLE;
#ifdef __LP64__
    int space                                                OBJC2_UNAVAILABLE;
#endif
    /* variable length structure */
    struct objc_method method_list[1]                        OBJC2_UNAVAILABLE;
}

objc_ivar_list 结构体用来存储成员变量的列表,而 objc_ivar 则是存储了单个成员变量的信息;同理,objc_method_list 结构体存储着方法数组的列表,而单个方法的信息则由 objc_method 结构体存储。

我们再来看看objc-private.h文件中objc_class的源码:

struct objc_class : objc_object {
    // Class ISA;
    Class superclass;
    cache_t cache;             // formerly cache pointer and vtable
    class_data_bits_t bits;    // class_rw_t * plus custom rr/alloc flags
    class_rw_t *data() { 
        return bits.data();
    }
    ... 省略其他方法
}

objc_class继承于objc_object,也就是说一个ObjC类本身同时也是一个对象,为了处理类和对象的关系,runtime库创建了一种叫做元类(Meta Class)的东西,类对象所属类型就叫做元类,它用来表述类对象所具备的元数据。

我们所熟悉的类方法,就源自Meta Class,我们可以理解为类方法就类对象的实例方法。每个类仅有一个类对象,而每个类对象仅有一个与之相关的元类。

当你发出一个类似[NSObject alloc](类方法)的消息时,实际上,这个消息被发送给了一个类对象(Class Object),这个类对象必须是一个元类的实例,而这个元类同时也是一个根元类(Root Meta Class)的实例。所有元类的isa指针最终都指向根元类。

所以当[NSObject alloc]这条消息发送给类对象的时候,运行时代码 objc_msgSend() 会去它元类中查找能够响应消息的方法实现,如果找到了,就会对这个类对象执行方法调用。

Runtime总结_第1张图片
类结构图.png

上图实线是 super_class 指针,虚线时 isa 指针。而根元类的父类是 NSObject,isa指向了自己。而 NSObject 没有父类。

最后 objc_class 中还有一个 objc_cache ,缓存,它的作用很重要,后面会提到。

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

objc_method存储了方法名,方法类型和方法实现:

  • 方法名类型SEL
  • 方法类型method_types是个char指针,存储方法的参数类型和返回值类型
  • method_imp,指向了方法的实现,本质上是一个函数指针

Ivar

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
}

其中ivar_offset是基地址偏移字节,ivar_name是成员变量名称,ivar_type是成员变量类型。

IMP

IMP在objc.h中的定义是:

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

它就是一个函数指针,这是由编译器生成的。当你发起一个 ObjC 消息之后,最终它会执行的那段代码,就是由这个函数指针指定的。而 IMP 这个函数指针就指向了这个方法的实现。

如果得到了执行某个实例某个方法的入口,我们就可以绕开消息传递阶段,直接执行方法,这在后面Cache中会提到。

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

而一个确定的方法也只有唯一的一组id和SEL参数。

Cache

Cache定义如下:

typedef struct objc_cache *Cache

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

Cache为方法调用的性能进行优化,每当实例对象接收到一个消息时,它不会直接在isa指针指向的类的方法列表中遍历查找能够响应的方法,因为每次都要查找效率太低了,而是优先在Cache中查找。

Runtime 系统会把被调用的方法存到 Cache 中,如果一个方法被调用,那么它有可能今后还会被调用,下次查找的时候就会效率更高。就像计算机组成原理中 CPU 绕过主存先访问 Cache 一样。

objc_property_t

@property标记了类中的属性,它是一个指向property_t结构体的指针:

typedef struct property_t *objc_property_t;

struct property_t {
    const char *name;
    const char *attributes;
};

从上面Runtime的源代码可以看出,property_t有两个成员变量,其中name是属性名称,attributese是属性的@encode类型字符串;

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

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

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

const char *property_getName(objc_property_t property)

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

const char *property_getAttributes(objc_property_t property)

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

id LenderClass = objc_getClass("Lender");
unsigned int outCount, i;
objc_property_t *properties = class_copyPropertyList(LenderClass, &outCount);
for (i = 0; i < outCount; i++) {
    objc_property_t property = properties[i];
    fprintf(stdout, "%s %s\n", property_getName(property), property_getAttributes(property));
}

对比下class_copyIvarList函数,使用class_copyPropertyList函数只能获取类的属性,而不包含成员变量。但此时获取的属性名是不带下划线的。

protocol_t

typedef struct objc_object Protocol;

从上面可以看出Protocolobjc_object类型的结构体,我们再来看看Runtimeobjc-runtime-new.h中的源码:

struct protocol_t : objc_object {
    const char *mangledName;
    struct protocol_list_t *protocols;
    method_list_t *instanceMethods;
    method_list_t *classMethods;
    method_list_t *optionalInstanceMethods;
    method_list_t *optionalClassMethods;
    property_list_t *instanceProperties;
    uint32_t size;   // sizeof(protocol_t)
    uint32_t flags;
    // Fields below this point are not always present on disk.
    const char **_extendedMethodTypes;
    const char *_demangledName;
    property_list_t *_classProperties;

    ... 省略一些封装的便捷 get 方法
};

flags32位指针最后两位是给加载Mach-Ofix-up阶段使用的,前16位预留给Swift用的。

protocol主要内容其实是(可选)方法,其次就是继承其他protocol。Swift 还支持 protocol 多继承,所以需要 protocols 数组来做兼容。

Category

Category 为现有的类提供了拓展性,它是 category_t 结构体的指针。

typedef struct category_t *Category;

category_t 存储了类别中可以拓展的实例方法、类方法、协议、实例属性和类属性。类属性是 Objective-C 2016 年新增的特性,沾 Swift 的光。所以 category_t 中有些成员变量是为了兼容 Swift 的特性,Objective-C 暂没提供接口,仅做了底层数据结构上的兼容。

struct category_t {
    const char *name; //类的名字
    classref_t cls; //类
    struct method_list_t *instanceMethods; //category中所有给类添加的实例方法的列表
    struct method_list_t *classMethods; //category中所有添加的类方法的列表
    struct protocol_list_t *protocols; //category实现的所有协议的列表
    struct property_list_t *instanceProperties; //category中添加的所有属性
    // Fields below this point are not always present on disk.
    struct property_list_t *_classProperties; // 类属性

    method_list_t *methodsForMeta(bool isMeta) {
        if (isMeta) return classMethods;
        else return instanceMethods;
    }

    property_list_t *propertiesForMeta(bool isMeta, struct header_info *hi);
};

category_t的成员变量结构可以看出Category可以添加实例方法,类方法,实现协议和添加属性,却没有ivars,所以不能为类添加成员变量。

消息

一些Runtime术语讲完了,接下来就要说到消息了。体会苹果官方文档中的 messages aren’t bound to method implementations until Runtime。消息直到运行时才会与方法实现进行绑定。

objc_msgSend函数

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

  • 1.检测这个selector是不是要忽略的。比如Mac OS X开发,有了垃圾回收就不理会retainrelease这些函数了。
  • 2.检测这个target是不是nil对象。ObjC 的特性是允许对一个 nil 对象执行任何一个方法不会 Crash,因为会被忽略掉。
  • 3.如果上面两个都过了,那就开始查找这个类的IMP,先从cache里面找,完了找得到就跳到对应的函数去执行。
  • 4.如果cache找不到从类的methodLists方法列表中去找。
  • 5.如果在类的方法列表中找不到,则到super_class父类的方法列表中找,一直找,直到找到NSObject类为止。
  • 6.如果还找不到就要开始进入动态方法解析了,后面会提到。
Runtime总结_第2张图片
消息发送步骤.png

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

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

方法中的隐藏参数

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

objc_msgSend找到方法对应的实现时,它将直接调用该方法实现,并将消息中所有的参数都传递给方法实现,同时,它还将传递两个隐藏的参数:

  • 接收消息的对象(也就是self指向的内容)
  • 方法选择器(_cmd指向的内容)

之所以说它们是隐藏的,是因为在源代码方法的定义中并没有声明这两个参数。它们是在代码被编译时被插入实现的。尽管这些参数没有被明确声明,在源代码中我们仍然可以引用它们。
而当方法中的super关键字接收到消息时,编译器会创建一个objc_super结构体:

objc_msgSendSuper(void /* struct objc_super *super, SEL op, ... */ )

struct objc_super { id receiver; Class class; };

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

// 这句话并不能获取父类的类型,只能获取当前类的类型名
NSLog(@"%@", NSStringFromClass([super class]));

获取方法地址

IMP那节提到过可以避开消息绑定而直接获取方法的地址并调用方法。这种做法很少见,除非是需要持续大量重复调用某方法的极端情况,避开消息发送泛滥而直接调用该方法会更高效。

NSObject类中有个methodForSelector:实例方法,你可以用它来获取某个方法选择器对应的IMP,举个例子:

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

当方法被当做函数调用时,上节提到的两个隐藏参数就需要我们明确给出了。上面的例子调用了1000次函数,你可以试试直接给target发送1000次setFilled:消息会花多久。

PS:methodForSelector:方法是有Cocoa的Runtime系统提供的,而不是ObjC自身的特性。

动态方法解析

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

@dynamic propertyName;

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

void dynamicMethodIMP(id self, SEL _cmd) {
    // implementation ....
}
@implementation MyClass
+ (BOOL)resolveInstanceMethod:(SEL)aSEL
{
    if (aSEL == @selector(resolveThisMethodDynamically)) {
          class_addMethod([self class], aSEL, (IMP) dynamicMethodIMP, "v@:");
          return YES;
    }
    return [super resolveInstanceMethod:aSEL];
}
@end

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

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

那有如何用resolveClassMethod:解析类方法呢,我们可以将实例方法和类方法的动态方法解析对比下:

头文件:

#import 

@interface Student : NSObject
+ (void)learnClass:(NSString *) string;
- (void)goToSchool:(NSString *) name;
@end

.m文件:

#import "Student.h"
#import 

@implementation Student
+ (BOOL)resolveClassMethod:(SEL)sel {
    if (sel == @selector(learnClass:)) {
        class_addMethod(object_getClass(self), sel, class_getMethodImplementation(object_getClass(self), @selector(myClassMethod:)), "v@:");
        return YES;
    }
    return [class_getSuperclass(self) resolveClassMethod:sel];
}

+ (BOOL)resolveInstanceMethod:(SEL)aSEL
{
    if (aSEL == @selector(goToSchool:)) {
        class_addMethod([self class], aSEL, class_getMethodImplementation([self class], @selector(myInstanceMethod:)), "v@:");
        return YES;
    }
    return [super resolveInstanceMethod:aSEL];
}

+ (void)myClassMethod:(NSString *)string {
    NSLog(@"myClassMethod = %@", string);
}

- (void)myInstanceMethod:(NSString *)string {
    NSLog(@"myInstanceMethod = %@", string);
}
@end

需要深刻理解[self class]object_getClass(self)甚至object_getClass([self class])的关系,其实并不难,重点在与self的类型:

  • self为实例对象时,[self class]object_getClass(self)等价,因为前者会调用后者。object_getClass([self class])得到元类。
  • self为类对象时,[self class]返回值为自身,还是selfobject_getClass(self)object_getClass([self class])等价。

凡是涉及到类方法时,一定要弄清楚元类、selector、IMP等概念,这样才能做到举一反三,随机应变。

消息转发

Runtime总结_第3张图片
消息转发.png

重定向 转发目标 备用接收者

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

- (id)forwardingTargetForSelector:(SEL)aSelector
{
    if(aSelector == @selector(mysteriousMethod:)){
        return alternateObject;
    }
    return [super forwardingTargetForSelector:aSelector];
}

毕竟消息转发要耗费更多时间,抓住这次机会将消息重定向给别人是个不错的选择。如果此方法返回nilself,则会进入消息转发机制forwardInvocation:;否则将向返回的对象重新发送消息。

如果想替换类方法的接收者,需要重载+ (id)forwardingTargetForSelector:(SEL)aSelector方法,并返回类对象:

+ (id)forwardingTargetForSelector:(SEL)aSelector {
    if(aSelector == @selector(xxx)) {
        return NSClassFromString(@"Class name");
    }
    return [super forwardingTargetForSelector:aSelector];
}

完整转发 转发调用 替换消息的方法

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

/// 方法签名
+ (NSMethodSignature* ) methodSignatureForSelector:(SEL)aSelector {
    if (aSelector == @selector(walk)) {
        return [NSMethodSignature signatureWithObjCTypes:"v@:"];
    }
    return [super methodSignatureForSelector:aSelector];
}

+ (void) forwardInvocation:(NSInvocation *)anInvocation {

    //NSLog(@"%s", __func__);

   // 转发给别的对象
   // [anInvocation invokeWithTarget:[TZDog new]];

    /// 转发给别的对象,同时替换方法
    anInvocation.selector = @selector(run);
    //anInvocation.target = self;
    [anInvocation invokeWithTarget:[TZDog new]];
}

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

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

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

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

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

PS: 一个NSMethodSignature对象记录着某个方法的返回值类型信息以及参数类型信息。它用于合成一个方法调用者NSInvocation。参考文章NSMethodSignature

转发与多继承

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

Runtime总结_第4张图片
转发与多继承.png

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

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

替代者对象

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

转发与继承

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

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

结果是NO,尽管它能够接受negotiate消息而不报错,因为它靠转发消息给Diplomat类来响应消息。

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

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

Runtime总结_第5张图片
NonFragileIvars1.png

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

Runtime总结_第6张图片
NonFragileIvars2.png

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

Runtime总结_第7张图片
NonFragileIvars3.png

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

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

优化App的启动时间文件时有个步骤是通过fix-up修改偏移量来解决fragile base class。

总结

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

你可能感兴趣的:(Runtime总结)