iOS Runtime 介绍和使用

3,799 阅读17分钟
  1. Runtime 简介
  2. Runtime 消息机制和相关函数(imp、objc_msg、isa、objc_class、objc_cache、objc_method、objc_category、ivar)
  3. Runtime 三次转发流程
  4. Runtime 应用
  5. Runtime 面试题

1. Runtime 简介

OC 是动态语言,它不仅需要一个编译器,也需要一个运行时系统来执行编译好的代码(动态得创建类和对象、进行消息传递和转发)。
Runtime 是 OC 面向对象和动态机制的基石,可以从系统层面解决一些设计或技术问题。它基本是用 C 和汇编写的,属于1个 C 语言库,包含了很多底层的 C 语言 API,如跟类、成员变量、方法相关的API。它的核心是消息传递(Messaging)。

运行时系统的版本和平台

在早期版本中,如果您改变类中实例变量的布局,您必须重新编译该类的所有子类。
在现行版本中,如果您改变类中实例变量的布局,您无需重新编译该类的任何子类。

和运行时系统的交互

通过 OC 源代码。
Foundation 框架的 NSObject 类定义的方法。
通过运行时系统的函数

头文件<objc/runtime.h><objc/message.h>

动态绑定

动态绑定将调用方法的确定推迟到运行时。在编译时,方法的调用并不和代码绑定在一起,只有在消实发送出来之后,才确定被调用的代码。通过动态类型和动态绑定技术,您的代码每次执行都可以得到不同的结果。运行时因此负责确定消息的接收者和被调用的方法。

运行时的消息分发机制为动态绑定提供支持。当您向一个动态类型确定了的对象发送消息时,运行环境系统会通过接收者的 isa 指针定位对象的类,并以此为起点确定被调用的方法,方法和消息是动态绑定的。

动态加载

OC 程序可以在运行时链接和载入新的类和范畴类。新载入的类和在程序启动时载入的类并没有区别。

动态加载可以用在很多地方。例如,系统配置中的模块就是被动态加载的。

在 Cocoa 环境中,动态加载一般被用来对应用程序进行定制。您的程序可以在运行时加载其他程序员编写的模块——和 Interface Build 载入定制的调色板以及系统配置程序载入定制的模块的类似。 这些模块通过您许可的方式扩展了您的程序,而您无需自己来定义或者实现。您提供了框架,而其它的程序员提供了实现。

NSProxy

Cocoa 中大多数类都继承于 NSObject 类,也就自然继承了它的方法。最特殊的例外是 NSProxy,它是个抽象超类,它实现了一些消息转发有关的方法,可以通过继承它来实现一个其他类的替身类或是虚拟出一个不存在的类。

类型编码

为了和运行时系统协作,编译器将方法的返回类型和参数类型都编码成一个字符串,并且和方法选标关联在一起。这些编码在别的上下文环境中同样有用,所以您可以直接使用@encode()编译指令来得到具体的编码。

给定一个类型, @encode()将返回该类型的编码字符串。类型可以是基本类型例如整形、指针、结构体或者联合体,也可以是一个类,就和 C 语言中的 sizeof()操作符的参数一样,可以是任何类型。

2. Runtime 消息机制 和 相关定义

Runtime 详细消息发送步骤:

  1. 检测这个 selector 是不是要忽略的。
  2. 检测这个 target 是不是 nil 对象。OC 的特性是允许对一个 nil 对象执行任何一个方法不会 crash ,因为会被忽略掉。
  3. 查找这个类的 IMP,从 cache 里找,找到就跳到对应的函数去执行。
  4. 如果 cache 找不到,消息函数首先根据该对象的 isa 指针找到该对象对应的类的方法表.
  5. 如果找不到,将继续从父类中寻找,直到 NSObject 类。
  6. 如果还找不到,进入动态方法解析。
  7. 如果消息转发都失败了,则执行 doesNotRecognizeSelector: 方法报 unrecognized selector 错。

在面向对象编程中,一般称作方法和消息动态绑定的过程。

为了加快消息的处理过程,运行时系统通常会将使用过的方法选标和方法实现的地址放入缓存中。每个类都有一个独立的缓存,同时包括继承的方法和在该类中定义的方法。

  • 举例:
    一个对象的方法像这样 [obj eat],编译器转成消息发送 objc_msgSend(obj, eat),Runtime 时执行的流程是这样的:
    1. 通过 obj 的 isa 指针找到它的 class。
    2. 在 class 的 method list 找 eat。
    3. 如果 class 中没找到 eat,继续往它的 superclass 中找,一旦找到 eat 这个函数,就去执行它的实现IMP

消息传递用到的一些概念:

IMP、objc_msg
实例:objc_object
类对象:objc_class
元类:Meta Class
Method:objc_method
SEL:objc_selector
类缓存:objc_cache
Category:objc_category

IMP

就是指向最终实现程序的内存地址的指针,类型是 typedef void (*IMP)(void /* id, SEL, ... */ );

它就是一个函数指针,是由编译器生成的。发一个 OC 消息最终会执行哪段代码,是由这个函数指针指定的,因为 IMP 指针指向了这个方法的实现。

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

objc_msg

在 OC 中,消息是直到运行的时候才和方法实现绑定的。编译器会把一个消息表达式 [receiver message] 转换成一个对消息函数 objc_msgSend 的调用。objc_msgSend(receiver, selector) 函数有两个主要参数消息接收者、消息对应的方法名即方法选标),同时接收消息中的任意数量的参数 objc_msgSend(receiver, selector, arg1, arg2, ...)

注意:编译器将自动插入调用该消息函数的代码。您无须在代码中显示调用该消息函数。

消息机制的关键在于编译器为类和对象生成的结构。每个类的结构中至少包括两个基本元素:

  1. 指向父类的指针。
  2. 类的方法表。方法表将方法选标和该类的方法实现的地址关联起来。
id objc_msgSend ( id self, SEL op, ... ) 解析

id:objc_msgSend 第一个参数类型为 id,是一个指向类实例的指针。类型是 typedef struct objc_object *id;
SEL(objc_selector):objc_msgSend 第二个参数类型为 SEL,它是 selector 在 OC 中的表示类型(Swift 中是 Selector 类)。类型是 @property SEL selector;typedef struct objc_selector *SEL;。selector 是方法选择器,可以理解为区分方法的 ID,而这个 ID 的数据结构是 SEL。可以看到 selector 是 SEL 的一个实例。

注意:写 C 代码的时候,经常会用到函数重载(函数名相同,参数不同),但这在 OC 中是行不通的,因为 selector 只记了 method 的 name,没有参数,所以没法区分不同的 method。

使用隐藏的参数

当 objc_msgSend 找到方法对应的实现时,它将直接调用该方法实现,并将消息中所有的参数都传递给方法实现,同时,它还将传递两个隐藏的参数(接收消息的对象、方法选标)。这些参数帮助方法实现获得了消息表达式的信息。它们被认为是 "隐藏" 的是因为它们并没有在定义方法的源代码中声明,而是在代码编译时是插入方法的实现中的。

尽管这些参数没有被显示声明,但在源代码中仍然可以引用它们(就像可以引用消息接收者对象的实例变量一样)。在方法中可以通过 self 来引用消息接收者对象,通过选标_cmd 来引用方法本身。在下面 例子中,_cmd 指的是 strange 方法,self 指的收到 strange 消息的对象。

- strange {
    id target = getTheReceiver(); 
    SEL method = getTheMethod();
    if ( target == self || method == _cmd )
        return nil;
    return [target performSelector:method];
}

在这两个参数中,self 更有用一些。实际上,它是在方法实现中访问消息接收者对象的实例变量的途径。

isa

当新的对象被创建时,其内存同时被分配实例变量也同时被初始化。对象的第一个实例变量是一个指向该对象的类结构的指针,叫做 isa。通过该指针,对象可以访问它对应的类以及相应的父类(从而找到上面的方法)。
在 OC 中,任何类的定义都是对象。类和类的实例(对象)没有任何本质上的区别。任何对象都有 isa 指针。

isa 在 NSObject 中被声明为一个指针,MAC OS 系统是 64 位的,所以指针也就是64 的。即占 8 字节空间。
isa 起始地址和对象的起始地址值是一样的。对象指针加 8 才是自己声明的实例变量的地址,前边 8 个字节是 isa 指针。

注意: 尽管严格来说这并不是 OC 语言的一部分,但是在 OC 运行时系统中对象需要有 isa 指针。对象和结构体 struct objc_object(在 objc/objc.h 中定义) 必须一致。然而,您很少需要创建您自己的根对象,因为从 NSObject 或者 NSProxy 继承的对象都自动包括 isa 变量。

实例(objc_object)

objc_object 结构体包含一个 isa 指针,类型为 isa_t 联合体。根据 isa 指向对象所属的类。isa 这里还涉及到 tagged pointer 等概念。因为 isa_t 使用 union 实现,所以可能表示多种形态,既可以当成是指针,也可以存储标志位置。

struct objc_object {
private:
    isa_t isa;
public:
    // ISA() assumes this is NOT a tagged pointer object
    Class ISA();
    // getIsa() allows this to be a tagged pointer object
    Class getIsa();
    ... 此处省略其他方法声明
}

注意: isa 指针不总是指向实例对象所属的类,不能依靠它来确定类型,而是应该用 class 方法来确定实例对象的类。因为 KVO 的实现机理就是将被观察对象的 isa 指针指向一个中间类而不是真实的类,这是一种叫做 isa-swizzling 的技术。

isa 指针优化 - tagged pointer

苹果引入 Tagged Pointer,不但减少了64位机器下程序的内存占用,还提高了运行效率,完美地解决了小内存对象在存储和访问效率上的问题。在 WWDC2013 的《Session 404 Advanced in Objective-C》视频中,看到苹果对于 Tagged Pointer 特点的介绍:

  1. Tagged Pointer专门用来存储小的对象,例如NSNumber和NSDate
  2. Tagged Pointer指针的值不再是地址了,而是真正的值。
    实际上它不再是一个对象了,它只是一个披着对象皮的普通变量而已。
    它的内存并不存储在堆中,也不需要malloc和free。
  3. 在内存读取上有着3倍的效率,创建时比以前快106倍。

那么如果一个数超过了 Tagged Pointer 所能表示的范围,系统会怎么处理?
如果一个数超出了 Tagged Pointer 所能表示的范围,系统会自动采用分配成对象,可以根据指针的最后4位是否为0来区分。

objc_class

OC 类是由 Class 类型来表示的,它实际上是一个指向 objc_class 结构体的指针。类型是 typedef struct objc_class *Class;

objc/runtime.hobjc_class 结构体的定义如下:

struct objc_class {
    Class _Nonnull isa  OBJC_ISA_AVAILABILITY;

#if !__OBJC2__
    Class _Nullable super_class                         OBJC2_UNAVAILABLE; // 父类的指针
    const char * _Nonnull name                          OBJC2_UNAVAILABLE; // 类的名字
    long version                                        OBJC2_UNAVAILABLE; // 版本
    long info                                           OBJC2_UNAVAILABLE; 
    long instance_size                                  OBJC2_UNAVAILABLE; // 实例大小
    struct objc_ivar_list * _Nullable ivars             OBJC2_UNAVAILABLE; // 实例变量列表
    struct objc_method_list * _Nullable * _Nullable methodLists OBJC2_UNAVAILABLE; // 方法列表
    struct objc_cache * _Nonnull cache                  OBJC2_UNAVAILABLE; // 缓存
    struct objc_protocol_list * _Nullable protocols     OBJC2_UNAVAILABLE; // 遵守的协议列表
#endif

} OBJC2_UNAVAILABLE;

对象在内存中的排布可以看成一个结构体,该结构体的大小并不能动态变化,所以无法在运行时动态给对象增加成员变量。相对的,对象的方法定义都保存在类的可变区域中。如下图所示为 Class 的描述信息,其中 methodList 为可访问类中定义的方法的指针的指针,通过修改该指针所指向的指针的值,我们可以实现为类动态增加方法实现。

objc_class 继承于 objc_object,OC 类本身也是对象,我们称之为类对象,类对象也是一个 struct objc_class 的结构体。

元类(Meta Class)

类的 isa 指针一个 objc_class 结构体,就是他的元类。每个类对象仅有一个与之相关的元类。类对象的类方法存在于元类的 methodLists 中,你也可以理解为这些方法是类对象的实例方法,元类中保存了创建类对象以及类方法所需的所有信息。

  1. 类的 isa 指针指向他的元类。
  2. 元类的 isa 指针根源类。
  3. 根元类 isa 指针指向自己,形成一个回路。
  4. 元类的 super_class 指针指向它原本的类的 super_class 的元类。但是根元类的 super_class 指向 NSObject。
  5. NSObject 的 super_class 为 nil,也就是它没有超类。

objc_method

typedef struct objc_method *Method;
struct objc_method {
    SEL method_name                         OBJC2_UNAVAILABLE;
    char *method_types                      OBJC2_UNAVAILABLE;
    IMP method_imp                          OBJC2_UNAVAILABLE;
}

SEL method_name:方法名,相同名字的方法即使在不同类中定义,它们的方法选择器也相同。
char *method_types:方法类型,是个 char 指针,其实存储着方法的参数类型和返回值类型。
IMP method_imp:方法实现,本质上是一个函数指针。

在 Runtime 中,Method 通过 selector 和 IMP 两个属性,实现了快速查询方法及实现,相对提高了性能,又保持了灵活性。

objc_cache

objc_cache 是存在 objc_class 结构体中的。objc_cache 为方法调用进行性能优化。每个消息都遍历一次 isa 指向的类的方法列表效率太低,于是 Runtime 把被调用的方法存到 cache 中( method_name 作为 key ,method_imp 作为 value),下次查找会先在 cache 中找,效率更高。

cache_t 中 _buckets 、_mask 和 _occupied:

struct cache_t {
    struct bucket_t *_buckets;
    mask_t _mask;
    mask_t _occupied;
    ... 省略其他方法
}

bucket_t 中存储了 指针 与 IMP 的键值对:

struct bucket_t {
private:
    cache_key_t _key;
    IMP _imp;

public:
    inline cache_key_t key() const { return _key; }
    inline IMP imp() const { return (IMP)_imp; }
    inline void setKey(cache_key_t newKey) { _key = newKey; }
    inline void setImp(IMP newImp) { _imp = newImp; }

    void set(cache_key_t newKey, IMP newImp);
};

objc_category

Category 为现有的类提供拓展性,category_t 是一个指向分类的结构体的指针。定义是typedef struct category_t *Category;

struct category_t { 
    const char *name; 
    classref_t cls; 
    struct method_list_t *instanceMethods; 
    struct method_list_t *classMethods;
    struct protocol_list_t *protocols;
    struct property_list_t *instanceProperties;
};
name:是指 class_name 而不是 category_name。
cls:要扩展的类对象,编译期间是不会定义的,而是在Runtime阶段通过name对 应到对应的类对象。
instanceMethods:category中所有给类添加的实例方法的列表。
classMethods:category中所有添加的类方法的列表。
protocols:category实现的所有协议的列表。
instanceProperties:表示Category里所有的properties,这就是我们可以通过objc_setAssociatedObject和objc_getAssociatedObject增加实例变量的原因,不过这个和一般的实例变量是不一样的。

从上边 category_t 的结构体中可以看出,分类中可以添加实例方法,类方法,甚至可以实现协议,添加属性,不可以添加成员变量。

Ivar

Ivar(typedef struct ivar_t *Ivar;) 代表类中实例变量的类型。

struct ivar_t {
    int32_t *offset;
    const char *name;
    const char *type;
    // alignment is sometimes -1; use alignment() instead
    uint32_t alignment_raw;
    uint32_t size;

    uint32_t alignment() const {
        if (alignment_raw == ~(uint32_t)0) return 1U << WORD_SHIFT;
        return 1 << alignment_raw;
    }
};

class_copyIvarList 函数获取的不仅有实例变量,还有属性。但会在原本的属性名前加上一个下划线。

objc_property_t

@property 标记了类中的属性,它是一个指向 objc_property(typedef struct property_t *objc_property_t;) 结构体的指针:

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

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

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

class_copyIvarList 和 class_copyPropertyList 对比:

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

- (void)runtimeGetIvarList {
    id RuntimeExploreInfo = objc_getClass("RuntimeExploreInfo");
    unsigned int numIvars = 0;
    Ivar *ivars = class_copyIvarList(RuntimeExploreInfo, &numIvars);
    for(int i = 0; i < numIvars; i++) {
        Ivar thisIvar = ivars[i];
        const char *type = ivar_getTypeEncoding(thisIvar);
        NSString *stringType =  [NSString stringWithCString:type encoding:NSUTF8StringEncoding];
        if (![stringType hasPrefix:@"@"]) {
            continue;
        }
        fprintf(stdout, "runtimeGetIvarList---%s\n", ivar_getName(thisIvar));
    }
}

打印结果:

3. 消息转发

发送消息会先搜索类对象的方法列表,如果找不到则会沿着继承树向上搜索直到根部 NSObject,如果还是找不到并且消息转发都失败了就回执行 doesNotRecognizeSelector: 方法报 unrecognized selector 错。

消息转发流程:(第3步也可以说成是消息转发里面的一个步骤,这里拿出来说)

  1. 动态方法解析: +resolveInstanceMethod:, +resolveClassMethod:
  2. 消息转发: forwardingTargetForSelector
  3. 方法签名: methodSignatureForSelector:, forwardInvocation:

动态方法解析

OC 运行时会调用 +resolveInstanceMethod: 或者 +resolveClassMethod:,让你有机会提供一个函数实现。如果你添加了函数并返回YES, 那运行时系统就会重新启动一次消息发送的过程。

    - (void)viewDidLoad {
        [super viewDidLoad];
        // Do any additional setup after loading the view, typically from a nib.
        //执行foo函数
        [self performSelector:@selector(foo:)];
    }
    
    + (BOOL)resolveInstanceMethod:(SEL)sel {
        if (sel == @selector(foo:)) {//如果是执行foo函数,就动态解析,指定新的IMP
            class_addMethod([self class], sel, (IMP)fooMethod, "v@:");
            return YES;
        }
        return [super resolveInstanceMethod:sel];
    }
    
    void fooMethod(id obj, SEL _cmd) {
        NSLog(@"Doing foo");//新的foo函数
    }

如果 resolve 方法返回 NO,运行时就会移到下一步 forwardingTargetForSelector。

消息转发

通过 -forwardingTargetForSelector: 你可以把这个消息转发给其他对象处理。
如通过 forwardingTargetForSelector 把 Controller 把方法转发给了 RuntimeExploreInfo 去执行。
Controller:

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.
    [self performSelector:@selector(runtimeMessageTest)];
}

+ (BOOL)resolveInstanceMethod:(SEL)sel {
    return YES; // 返回YES,进入下一步转发
}

- (id)forwardingTargetForSelector:(SEL)aSelector {
    if (aSelector == @selector(runtimeMessageTest)) {
        return [RuntimeExploreInfo new]; // 返回RuntimeExploreInfo对象,让RuntimeExploreInfon对象接收这个消息
    }
    return [super forwardingTargetForSelector:aSelector];
}

RuntimeExploreInfo`:

#import "RuntimeExploreInfo.h"

@implementation RuntimeExploreInfo

- (void)runtimeMessageTest {
    NSLog(@"runtimeMessageTest---");
}

@end

注意:在类不能处理某个 selector 的情况下,如果类重载了该函数,并使用 class_addMethod 添加了相应的 selector ,并返回 YES,那么后面forwardingTargetForSelector 就不会被调用。如果在该函数中没有添加相应的 selector,那么不管返回什么,后面都会继续调用 forwardingTargetForSelector。如果在 forwardingTargetForSelector并未返回能接受该 selector 的对象,那么 resolveInstanceMethod 会再次被触发,这一次,如果仍然不添加 selector,程序就会报异常。

方法签名

上面两步都失败后,进去完整的消息转发机制。首先它会发送 -methodSignatureForSelector: 消息获得函数的参数和返回值类型。如果 -methodSignatureForSelector: 返回 nil ,Runtime 会发出 -doesNotRecognizeSelector: 消息,程序这时也就挂掉了。如果返回了一个函数签名,Runtime 就会创建一个 NSInvocation 对象并发送 -forwardInvocation: 消息给目标对象。

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.

    [self performSelector:@selector(runtimeMessageTest)];
}

+ (BOOL)resolveInstanceMethod:(SEL)sel {
    return YES; // 返回YES,进入下一步转发
}

- (id)forwardingTargetForSelector:(SEL)aSelector {
    return nil; // 返回nil,进入下一步转发
}

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
    if ([NSStringFromSelector(aSelector) isEqualToString:@"runtimeMessageTest"]) {
        return [NSMethodSignature signatureWithObjCTypes:"v@:"]; // 签名,进入forwardInvocation
    }
    return [super methodSignatureForSelector:aSelector];
}

- (void)forwardInvocation:(NSInvocation *)anInvocation {
    SEL sel = anInvocation.selector;

    RuntimeExploreInfo *p = [RuntimeExploreInfo new];
    if([p respondsToSelector:sel]) {
        [anInvocation invokeWithTarget:p];
    }
}

我们实现了完整的转发。通过签名 Runtime 生成了一个 anInvocation 对象发送给 forwardInvocation,我们在 forwardInvocation 方法里面让 RuntimeExploreInfo 对象去执行了 runtimeMessageTest 函数。

要转发消息给其它对象,forwardInvocation: 方法所必须做的有:

  1. 决定将消息转发给谁。
  2. 将消息和原来的参数一块转发出去。

消息可以通过 invokeWithTarget:方法来转发:

- (void)forwardInvocation:(NSInvocation *)anInvocation {
    if ([someOtherObject respondsToSelector: [anInvocation selector]])
        [anInvocation invokeWithTarget:someOtherObject];
    else
        [super forwardInvocation:anInvocation];
}

转发消息后的返回值将返回给原来的消息发送者。您可以将返回任何类型的返回值,包括: id,结构体,浮点数等。

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

Runtime方式方法交换的风险

  1. 需要在 +load 方法中进行方法交换。因为如果在其他时候进行方法交换,难以保证另外一个线程中不会同时调用被交换的方法,从而导致程序不能按预期执行。
  2. 被交换的方法必须是当前类的方法,不能是父类的方法,直接把父类的实现拷贝过来不会起作用。父类的方法必须在调用的时候使用,而不是方法交换时使用。
  3. 交换的方法如果依赖了 cmd,那么交换后,如果 cmd 发生了变化,就会出现各种奇怪问题,而且这些问题还很难排查。特别是交换了系统方法,你无法保证系统方法内部是否依赖了 cmd。
  4. 方法交换命名冲突。如果出现冲突,可能会导致方法交换失败。

其他

消息转发和多重继承

消息转发很象继承,并且可以用来在Objective-C程序中模拟多重继承。如下图所示, 一个对象通过转发来响应消息,看起来就象该对象从别的类那借来了或者”继承“了方法实现一样。

在上图中,Warrior 类的一个对象实例将 negotiate 消息转发给 Diplomat 类的一个实例。看起来,Warrior 类似乎和 Diplomat 类一样, 响应 negotiate 消息,并且行为和 Diplomat 一样(尽管实际上是 Diplomat 类响应了该消息)。 转发消息的对象看起来有两个继承体系分支——自己的和响应消息的对象的。在上面的例子中,Warrior 看起来同时继承自 Diplomat 和自己的父类。 消息转发提供了多重继承的很多特性。然而,两者有很大的不同:多重继承是将不同的行为封装到单个的对象中,有可能导致庞大的,复杂的对象。而消息转发是将问题分解到更小的对象中,但是又以一种对消息发送对象来说完全透明的方式将这些对象联系起来。

消息代理对象

消息转发不仅和继承很象,它也使得以一个轻量级的对象(消息代理对象)代表更多的对象进行消息处理成为可能。
代理类负责将消息转 发给远程消息接收对象的管理细节,保证消息参数的传输等等。但是消息类没有进一步的复制远程对象的 功能,它只是将远程对象映射到一个本地地址上,从而能够接收其它应用程序的消息。 同时也存在着其它类型的消息代理对象。例如,假设您有个对象需要操作大量的数据——它可能需要创建 一个复杂的图片或者需要从磁盘上读一个文件的内容。创建一个这样的对象是很费时的,您可能希望能推 迟它的创建时间——直到它真正需要时,或者系统资源空闲时。同时,您又希望至少有一个预留的对象和 程序中其它对象交互。 在这种情况下,你可以为该对象创建一个轻量的代理对象。该代理对象可以有一些自己的功能,例如响应 数据查询消息,但是它主要的功能是代表某个对象,当时间到来时,将消息转发给被代表的对象。当代理 对象的 forwardInvocation:方法收到需要转发给被代表的对象的消息时,代理对象会保证所代表的 对象已经存在,否则就创建它。所有发到被代表的对象的消息都要经过代理对象,对程序来说,代理对象 和被代表的对象是一样的。

消息转发和类继承

尽管消息转发很“象”继承,但它不是继承。例如在 NSObject 类中,方法 respondsToSelector: 和 isKindOfClass:只会出现在继承链中,而不是消息转发链中。 例如,如果向一个 Warrior 类的对象 询问它能否响应 negotiate 消息,

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

返回值是NO,尽管该对象能够接收和响应negotiate。

大部分情况下,NO 是正确的响应。但不是所有时候都是的。例如,如果您使用消息转发来创建一个代理 对象以扩展某个类的能力,这儿的消息转发必须和继承一样,尽可能的对用户透明。如果您希望您的代理 对象看起来就象是继承自它代表的对象一样,您需要重新实现 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; }

除了 respondsToSelector:和 isKindOfClass:外,instancesRespondToSelector: 方法也必须重新实现。如果您使用的是协议类,需要重新实现的还有 conformsToProtocol:方法。 类似地,如果对象需要转发远程消息,则 methodSignatureForSelector:方法必须能够返回实际 响应消息的方法的描述。例如,如果对象需要将消息转发给它所代表的对象,您可能需要如下的 methodSignatureForSelector:实现:

4. Runtime 应用

  1. 关联对象 - 给分类增加属性
  2. 方法魔法 - 方法添加和替换
  3. KVO 实现
  4. 实现 NSCoding 的自动归档和自动解档
  5. 实现字典和模型的自动转换(MJExtension、YYModel)
  6. 用于封装框架(想怎么改就怎么改)

关联对象 - 给分类增加属性

RuntimeExploreInfo+RuntimeAddProperty.h 添加了 phoneNum 属性

#import "RuntimeExploreInfo+RuntimeAddProperty.h"
#import "objc/runtime.h"

@implementation RuntimeExploreInfo (RuntimeAddProperty)

static char kPhoneNumKey;

- (void)setPhoneNum:(NSString *)phoneNum {
    objc_setAssociatedObject(self, &kPhoneNumKey, phoneNum, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

- (id)phoneNum {
    return objc_getAssociatedObject(self, &kPhoneNumKey);
}

@end
- (void)runtimeAddProperty {
    RuntimeExploreInfo *test = [RuntimeExploreInfo new];
    test.phoneNum = @"12342424242";
    NSLog(@"RuntimeAddProperty---%@", test.phoneNum);
}

方法魔法 - 方法添加和替换

添加方法

/**
 class_addMethod(Class  _Nullable __unsafe_unretained cls, SEL  _Nonnull name, IMP  _Nonnull imp, const char * _Nullable types)
 cls 被添加方法的类
 name 添加的方法的名称的SEL
 imp 方法的实现。该函数必须至少要有两个参数,self,_cmd
 类型编码
 */
class_addMethod([self class], sel, (IMP)fooMethod, "v@:");

替换方法
class_replaceMethod 替换类方法的定义
method_exchangeImplementations 交换两个方法的实现
method_setImplementation 设置一个方法的实现
注意:class_replaceMethod 试图替换一个不存在的方法时候,会调用 class_addMethod 为该类增加一个新方法

+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        Class class = [self class];
        SEL originalSelector = @selector(viewDidLoad);
        SEL swizzledSelector = @selector(runtimeReplaceViewDidLoad);

        Method originalMethod = class_getInstanceMethod(class, originalSelector);
        Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);

        //judge the method named  swizzledMethod is already existed.
        BOOL didAddMethod = class_addMethod(class, originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod));
        // if swizzledMethod is already existed.
        if (didAddMethod) {
            class_replaceMethod(class, swizzledSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod));
        }else {
            method_exchangeImplementations(originalMethod, swizzledMethod);
        }
    });
}

- (void)runtimeReplaceViewDidLoad {
    NSLog(@"替换的方法");
    //[self runtimeReplaceViewDidLoad];
}

swizzling应该只在 +load 中执行一次( dispatch_once )完成。在 Objective-C 的运行时中,每个类有两个方法都会自动调用。+load 是在一个类被初始装载时调用,+initialize 是在应用第一次调用该类的类方法或实例方法前调用的。两个方法都是可选的,并且只有在方法被实现的情况下才会被调用。

KVO实现

当观察对象A时,KVO 机制动态创建一个 NSKVONotifying_A 的类,该类继承自对象A的本类。且 KVO 在 NSKVONotifying_A 中重写观察属性的 setter 方法,在 setter 方法之前和之后,通知所有观察对象属性值的更改情况。

NSKVONotifying_A 类剖析

NSLog(@"self->isa:%@",self->isa);  
NSLog(@"self class:%@",[self class]);  

在建立KVO监听前,打印结果为:

self->isa:A
self class:A

在建立KVO监听之后,打印结果为:

self->isa:NSKVONotifying_A
self class:A

KVO 的键值观察通知依赖于 NSObject 的两个方法: willChangeValueForKey:didChangeValueForKey: ,在存取数值的前后分别调用这 2 个方法,之后 observeValueForKey:ofObject:change:context: 也会被调用。

KVO 为子类的观察者属性重写调用存取方法的工作原理在代码中相当于:

- (void)setName:(NSString *)newName { 
      [self willChangeValueForKey:@"name"];    //KVO 在调用存取方法之前总调用
      [super setValue:newName forKey:@"name"]; //调用父类的存取方法 
      [self didChangeValueForKey:@"name"];     //KVO 在调用存取方法之后总调用
}

实现NSCoding的自动归档和自动解档

原理描述:用 runtime 提供的函数遍历 Model 自身所有属性,并对属性进行 encodedecode 操作。

核心方法:在Model的基类中重写方法:

    - (id)initWithCoder:(NSCoder *)aDecoder {
        if (self = [super init]) {
            unsigned int outCount;
            Ivar * ivars = class_copyIvarList([self class], &outCount);
            for (int i = 0; i < outCount; i ++) {
                Ivar ivar = ivars[i];
                NSString * key = [NSString stringWithUTF8String:ivar_getName(ivar)];
                [self setValue:[aDecoder decodeObjectForKey:key] forKey:key];
            }
        }
        return self;
    }
    
    - (void)encodeWithCoder:(NSCoder *)aCoder {
        unsigned int outCount;
        Ivar * ivars = class_copyIvarList([self class], &outCount);
        for (int i = 0; i < outCount; i ++) {
            Ivar ivar = ivars[i];
            NSString * key = [NSString stringWithUTF8String:ivar_getName(ivar)];
            [aCoder encodeObject:[self valueForKey:key] forKey:key];
        }
    }

实现字典和模型的自动转换

原理描述:用runtime提供的函数遍历Model自身所有属性,如果属性在json中有对应的值,则将其赋值。
核心方法:在NSObject的分类中添加方法

    - (instancetype)initWithDict:(NSDictionary *)dict {
    
        if (self = [self init]) {
            //(1)获取类的属性及属性对应的类型
            NSMutableArray * keys = [NSMutableArray array];
            NSMutableArray * attributes = [NSMutableArray array];
    
            unsigned int outCount;
            objc_property_t * properties = class_copyPropertyList([self class], &outCount);
            for (int i = 0; i < outCount; i ++) {
                objc_property_t property = properties[i];
                //通过property_getName函数获得属性的名字
                NSString * propertyName = [NSString stringWithCString:property_getName(property) encoding:NSUTF8StringEncoding];
                [keys addObject:propertyName];
                //通过property_getAttributes函数可以获得属性的名字和@encode编码
                NSString * propertyAttribute = [NSString stringWithCString:property_getAttributes(property) encoding:NSUTF8StringEncoding];
                [attributes addObject:propertyAttribute];
            }
            //立即释放properties指向的内存
            free(properties);
    
            //(2)根据类型给属性赋值
            for (NSString * key in keys) {
                if ([dict valueForKey:key] == nil) continue;
                [self setValue:[dict valueForKey:key] forKey:key];
            }
        }
        return self;
    
    }

5. Runtime 面试题

  • Self & Super

    @implementation Son : Father
    - (id)init
    {
        self = [super init];
        if (self)
        {
            NSLog(@"%@", NSStringFromClass([self class]));
            NSLog(@"%@", NSStringFromClass([super class]));
        }
        return self;
    }
    @end
    

    答案:都输出 Son
    解惑:这个题目主要是考察关于 objc 中对 selfsuper 的理解。

    self 是类的隐藏参数,指向当前调用方法的这个类的实例。而 super 是一个 Magic Keyword, 它本质是一个编译器标示符,和 self 是指向的同一个消息接受者。上面的例子不管调用 [self class] 还是 [super class] ,接受消息的对象都是当前 Son *xxx 这个对象。而不同的是,super 是告诉编译器,调用 class 这个方法时,要去父类的方法,而不是本类里的。

    当使用 self 调用方法时,会从当前类的方法列表中开始找,如果没有,就从父类中再找;
    而当使用 super 时,则从父类的方法列表中开始找。然后调用父类的这个方法。

    当调用 [self class] 时,实际先调用的是 objc_msgSend 函数,第一个参数是 Son 当前的这个实例,然后在 Son 这个类里面去找 - (Class)class 这个方法,没有,去父类 Father 里找,也没有,最后在 NSObject 类中发现这个方法。而 - (Class)class 的实现就是返回 self 的类别,故上述输出结果为 Son

    当调用 [super class] 时,会转换成 objc_msgSendSuper 函数。第一步先构造 objc_super 结构体,结构体第一个成员就是 self 。第二个成员是 (id)class_getSuperclass(objc_getClass(“Son”)) , 实际该函数输出结果为 Father。第二步是去 Father 这个类里去找 - (Class)class ,没有,然后去 NSObject 类去找,找到了。最后内部是使用 objc_msgSend(objc_super->receiver, @selector(class)) 去调用,此时已经和 [self class] 调用相同了,故上述输出结果仍然返回 Son

  • Object & Class & Meta Clas

    @interface Sark : NSObject
    @end
    @implementation Sark
    @end
    int main(int argc, const char * argv[]) {
        @autoreleasepool {
            BOOL res1 = [(id)[NSObject class] isKindOfClass:[NSObject class]];
            BOOL res2 = [(id)[NSObject class] isMemberOfClass:[NSObject class]];
            BOOL res3 = [(id)[Sark class] isKindOfClass:[Sark class]];
            BOOL res4 = [(id)[Sark class] isMemberOfClass:[Sark class]];
            NSLog(@"%d %d %d %d", res1, res2, res3, res4);
        }
        return 0;
    }
    

    答案: 1 0 0 0
    我们看到在 Objective-C 的设计哲学中,一切都是对象。Class在设计中本身也是一个对象。而这个 Class 对象的对应的类,我们叫它 Meta Class 。即 Class 结构体中的 isa 指向的就是它的 Meta Class
    Meta Class 理解为 一个 Class 对象的 Class 。简单的说:
    当我们发送一个消息给一个 NSObject 对象时,这条消息会在对象的类的方法列表里查找;
    当我们发送一个消息给一个类时,这条消息会在类的 Meta Class 的方法列表里查找

  • 消息 和 Category

    @interface NSObject (Sark)
    + (void)foo;
    @end
    @implementation NSObject (Sark)
    - (void)foo
    {
        NSLog(@"IMP: -[NSObject(Sark) foo]");
    }
    @end
    int main(int argc, const char * argv[]) {
        @autoreleasepool {
            [NSObject foo];
            [[NSObject new] foo];
        }
        return 0;
    }
    

    答案:
    IMP: -[NSObject(Sark) foo]
    IMP: -[NSObject(Sark) foo]
    解释:

    1. objc runtime 加载完后,NSObjectSark Category 被加载。而 NSObjectSark Category 的头文件 + (void)foo 并没有实质参与到工作中,只是给编译器进行静态检查,所有我们编译上述代码会出现警告,提示我们没有实现 + (void)foo 方法。而在代码编译中,它已经被注释掉了。
    2. 实际被加入到 Classmethod list 的方法是 - (void)foo ,它是一个实例方法,所以加入到当前类对象 NSObject 的方法列表中,而不是 NSObject Meta class 的方法列表中。
    3. 当执行 [NSObject foo] 时,我们看下整个 objc_msgSend 的过程:
    • objc_msgSend 第一个参数是 (id)objc_getClass("NSObject") ,获得 NSObject Class 的对象。
    • 类方法在 Meta Class 的方法列表中找,我们在 load Category 方法时加入的是 - (void)foo 实例方法,所以并不在 NSOBject Meta Class 的方法列表中
    • 继续往 super class 中找,NSObject Meta Classsuper classNSObject 本身。所以,这个时候我们能够找到 - (void)foo 这个方法。
      所以正常输出结果。
    1. 当执行 [[NSObject new] foo] ,我们看下整个 objc_msgSend 的过程:
      [NSObject new] 生成一个 NSObject 对象。直接在该对象的类( NSObject )的方法列表里找。能够找到,所以正常输出结果。
  • 成员变量与属性

    @interface Sark : NSObject
    @property (nonatomic, copy) NSString *name;
    @end
    @implementation Sark
    - (void)speak
    {
        NSLog(@"my name is %@", self.name);
    }
    @end
    @interface Test : NSObject
    @end
    @implementation Test
    - (instancetype)init
    {
        self = [super init];
        if (self) {
            id cls = [Sark class];
            void *obj = &cls;
            [(__bridge id)obj speak];
        }
        return self;
    }
    @end
    int main(int argc, const char * argv[]) {
        @autoreleasepool {
            [[Test alloc] init];
        }
        return 0;
    }
    

    答案: my name is

  • 能否向编译后得到的类中增加实例变量?能否向运行时创建的类中增加实例变量?为什么? 不能向编译后得到的类中增加实例变量;
    能向运行时创建的类中增加实例变量;
    解释:
    因为编译后的累已经注册在 runtime中,类结构体中 objc_ivar_list 实例变量的链表和 instance_size 实例变量的内存大小已经确定,同时 runtime 会调用 class_setIvarLayout 或 class_setWeakIvarLayout 来处理 strong weak 引用。所以不能向存在的类中添加实例变量;
    运行时创建的类是可以添加实例变量,调用clas_addIvar 函数。但是得在调用 objc_allocateClassPair 之后,objc_registerClassPair 之前,原因同上。

 

更多实用详见 Demo Runtime文件夹下

 

参考文章

Objective-C Runtime
刨根问底Objective-C Runtime