iOS的消息机制

一、OC语言的特性

首先,想要了解iOS的消息发送机制,我们需要先理解OC这门语言。
相较于静态语言而言,动态语言是指程序在运行时可以改变其结构:新的函数可以被引进,已有的函数可以被删除等在结构上的变化。比如众所周知的ECMAScript(JavaScript)便是一个动态语言。除此之外如Ruby、Python等也都属于动态语言,而C、C++等语言则不属于动态语言。

OC作为一门动态语言,其很多行为是在程序运行的时候确定的,其具有很多的动态的特性,基本的有以下三个方面:

  • 动态类型(Dynamic typing)
  • 动态绑定(Dynamic binding)
  • 动态加载(Dynamic loading)
1、动态类型。实际上静态类型因为其固定性和可预知性而使用得更加广泛。静态类型是强类型,而动态类型属于弱类型。程序执行时才决定接收者或真实类型,才能确定其真实所属的类。

例如:id 数据类型
id 通用的对象类型,可以存储任意类型的对象,id后面没有号,它本身就是个指针类似于void ,但只可以指向对象类型

静态类型与动态类型:

  • 编译期检查与运行时检查
  • 静态类型在编译期就能检查出错误
  • 静态类型声明代码可读性好
  • 动态类型只有在运行时才能发现错误

补充:
语言的强、弱类型:语言有无类型、强类型和弱类型三种。无类型的不做任何检查,甚至不区分指令和数据;弱类型的检查很弱,仅能区分指令和数据;强类型的严格在编译期进行检查。强类型语言在没有强制类型转化前,不允许两种不同类型的变量相互操作。

2、动态绑定。让代码在运行时判断需要调用什么方法,而不是在编译时。与其他面向对象语言一样,方法调用和代码并没有在编译时连接在一起,而是在消息发送时才进行连接,也就是执行时决定调用哪个方法。

动态绑定所做的,即是在实例所属类确定后,将某些属性和相应的方法绑定到实例上。
例如:我们在.h文件中定义的方法,我们在.m文件中并未实现,但在编译阶段,并不会报错,只有在执行的时候,才会去验证其是否实现,若实现了,在通过@selector()方法获取其入口地址,也就是进行绑定。

3、动态加载:根据需求加载所需要的资源。让程序在运行时添加代码模块以及其他资源,用户可以根据需要加载一些可执行代码和资源,而不是在启动时就加载所有组件。可执行代码中可以含有和程序运行时整合的新类。

例如,iOS不同机型的图片适配。这点很容易理解,对于iOS开发来说,基本就是根据不同的机型做适配。最经典的例子就是在Retina设备上加载@2x的图片,而在老一些的普通屏设备上加载原图。

BOOL类型
bool是C语言的布尔类型,有true和false,BOOL是Objective C 语言的布尔类型,有YES和NO,因为OC可以跟C混编,所以bool和BOOL可以同时出现在代码中

BOOL深入解析:
typedef signed char BOOL;
BOOL类型有两个值YES,NO。YES=1,NO=0。
说明:objective-c 中的BOOL实际上是一种对带符号的字符类型(signed char)的类型定义(typedef),它使用8位的存储空间。通过#define指令把YES定义为1,NO定义为0。

 Class类:表示一个类名,class被创建后,我们可以把class来当成对象的类。
 Class cla1 = [类名 class]
 Class cla2 = [对象 class]
 Class cla3 = NSClassFromString(@"类名");
SEL 类成员方法的指针:
可以理解 @selector()就是取类方法的编号,他的行为基本可以等同C语言中的函数指针,只不过C语言中,可以把函数名直接赋给一个函数指针,这样只能做一个@selector语法来取。它的结果是一个SEL类型。这个类型本质是类方法的编号(函数地址)
 
 1>类里面的方法都是被转换成SEL变量进行存储的。
 2>放类声明一个对象,对象调用方法的时候,系统会被这个方法转换成SEL,然后拿这个SEL到类方法中去匹配。
 3>我们可以自己手动把方法转换成SEL,然后用这个SEL去查找方法(performSelector)。
    -isMemberOfClass:
    判断是否是这个类的实例
    -isKindOfClass:
    判断是否是这个类或者这个类的子类的实例
    -respondsToSelector:
    判读实例是否有这样方法
    +instancesRespondToSelector:
    判断类是否有这个方法。此方法是类方法。

二、Runtime(运行时)

通过上面的叙述,我们知道,Object C把编译时的行为延后到执行阶段,如上面说的,在执行阶段,确定动态类型的真实类属性,确定一个实例的所属的属性和方法等等,这些仅仅依靠编译器是不够的,我们还需要一个运行时的系统(Runtime system)来处理编译后的代码。

1、什么是Runtime

RunTime简称运行时。就是系统在运行时候的一些机制,其中最主要的是消息机制。对于C语言,函数的调用在编译的时候会决定的。编译完成之后直接顺序执行,无任何二义性。OC的函数调用机制为消息发送,属于动态调用过程。在编译的时候并不能决定真正调用哪个函数(事实证明,在编译阶段,OC可以调用任何函数,即使这个函数并未实现,只要申明过就不会报错。而C语言在编译阶段就会报错)。只有在真正运行的时候才会根据函数的名称找到对应的函数来调用。

2、Runtime的版本

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

3、Runtime的作用

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 版本,这两个版本之间都在努力保持一致。

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

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

Objective-C 源代码:

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

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

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

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

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

-class方法返回对象的类;

-isKindOfClass: 和-isMemberOfClass: 方法检查对象是否存在于指定的类的继承体系中(是否是其子类或者父类或者当前类的成员变量);

-respondsToSelector: 检查对象能否响应指定的消息;

-conformsToProtocol:检查对象是否实现了指定协议类的方法;

-methodForSelector: 返回指定方法实现的地址。

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

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

例如:

object_getClass(id _Nullable obj);//获取实例对象所属的类
object_setClass(id _Nullable obj, Class _Nonnull cls);//给一个实例对象更新所属的类
object_getIvar(id _Nullable obj, Ivar _Nonnull ivar);//获取实例对象的成员变量

许多函数可以让你使用纯C 代码来实现Objc 中同样的功能。除非是写一些Objc 与其他语言的桥接或是底层的debug 工作,你在写Objc 代码时一般不会用到这些C 语言函数。

三、Runtime术语的数据结构(底层支撑)

由上面所说,我们知道我们所写的Object C代码,最终都会由Runtime系统转化为运行时代码,为了进一步了解Runtime的运行时机制或消息机制,我们有必要先了解一下Runtime的一些术语和对应的数据结构是如何定义的。

1.objc(实例对象)和id(范型实例)

id 是一个参数类型,它是指向某个类的实例的指针。定义如下:

typedef struct objc_object *id;

objc的定义如下:

struct objc_object { Class isa; };

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

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

例如:在一个对象实例通过KVO添加观察者监听事,该对象实例的isa会被指向一个对象实例类的派生类(通过object_setClass(id _Nullable obj, Class _Nonnull cls),实现)。
点击查看研究KVO的小示例
https://github.com/lwc1990/KVOStudyDemo

2、Class(类)
typedef struct objc_class *Class;

Class 其实是指向objc_class 结构体的指针。objc_class的数据结构如下:

struct objc_class {

       Class isa; // 指向metaclass

       Class super_class ; // 指向其父类

       const char *name ; // 类名

       long version ; // 类的版本信息,初始化默认为0,可以通过runtime函数class_setVersion和class_getVersion进行修改、读取

       long info; // 一些标识信息,如CLS_CLASS (0x1L) 表示该类为普通class ,其中包含对象方法和成员变量;CLS_META (0x2L) 表示该类为metaclass,其中包含类方法;

       long instance_size ; // 该类的实例变量大小(包括从父类继承下来的实例变量);

       struct objc_ivar_list *ivars; // 用于存储每个成员变量的地址

       struct objc_method_list **methodLists ; // 与info 的一些标志位有关,如CLS_CLASS (0x1L),则存储对象方法,如CLS_META (0x2L),则存储类方法;

       struct objc_cache *cache; // 指向最近使用的方法的指针,用于提升效率;

       struct objc_protocol_list *protocols; // 存储该类遵守的协议

 }

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

其中objc_ivar_list 和objc_method_list 分别是成员变量列表和方法列表:

 // 成员变量列表

 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;

}

由此可见,我们可以动态修改*methodList 的值来添加成员方法,这也是Category 实现的原理,同样解释了Category 不能添加属性的原因。

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

3、类和对象的继承层次关系

isa:objec_object(对象)中isa指针指向的类结构称为class(也就是该对象所属的类),其中存放着普通成员变量与对象方法 (“-”开头的方法);然而此处isa指针指向的类结构称为metaclass,其中存放着static类型的成员变量与static类型的方法 (“+”开头的方法)。

super_class: 指向该类的父类的指针,如果该类是根类(如NSObject或NSProxy),那么super_class就为NULL。

所有的metaclass中isa指针都是指向根metaclass,而根metaclass则指向自身。根metaclass是通过继承根类产生的,与根class结构体成员一致,不同的是根metaclass的isa指针指向自身。

在调用类或实例的方法时,是如何找到方法的实现的?

1)、当我们调用某个对象的对象方法时,它会首先在自身isa指针指向的类(class)methodLists中查找该方法,如果找不到则会通过class的super_class指针找到其父类,然后从其methodLists中查找该方法,如果仍然找不到,则继续通过 super_class向上一级父类结构体中查找,直至根class;

2)、当我们调用某个类方法时,它会首先通过自己的isa指针找到metaclass,并从其methodLists中查找该类方法,如果找不到则会通过metaclass的super_class指针找到父类的metaclass结构体,然后从methodLists中查找该方法,如果仍然找不到,则继续通过super_class向上一级父类结构体中查 找,直至根metaclass;

继承层次关系图如下:


类与对象的继承层次图

值得注意的是,objc_class 中也有一个isa 指针,这说明Objc 类本身也是一个对象。为了处理类和对象的关系,Runtime 库创建了一种叫做Meta Class(元类) 的东西,类对象所属的类就叫做元类。Meta Class 表述了类对象本身所具备的元数据。

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

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

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

4、category中为什么不能添加实例变量

在Objective-C提供的runtime函数中,确实有一个class_addIvar()函数用于给类添加成员变量,但是阅读过苹果的官方文档的人应该会看到:

This function may only be called after objc_allocateClassPair and before objc_registerClassPair. Adding an instance variable to an existing class is not supported.

大概的意思说,这个函数只能在“构建一个类的过程中”调用。当编译你的类的时候,编译器生成了一个实例变量内存布局(ivar layout),来告诉运行时去那里访问你的类的实例变量们,一旦完成类定义,就不能再添加成员变量了。经过编译的类在程序启动后就被runtime加载,没有机会调用addIvar。程序在运行时动态构建的类需要在调用objc_registerClassPair之后才可以被使用,同样没有机会再添加成员变量。
测试代码:

#import "ViewController.h"
#import "MyClass.h"
#import <objc/runtime.h>
@interface ViewController ()
@end

@implementation ViewController
void otherMethod(id self,SEL _cmd){
    NSLog(@"新添加的方法");
}
- (void)viewDidLoad {
    [super viewDidLoad];
    [self dynamicAddIvarPropertyMethod];
    [self test];
}
-(void)dynamicAddIvarPropertyMethod{
    //添加Ivar
    class_addIvar([MyClass class],"newIvar",sizeof(id),log2(sizeof(id)), "@");
    //添加属性
    objc_property_attribute_t type = {"T","@\"NSString\""};
    objc_property_attribute_t ownerShip = {"c",""};
    objc_property_attribute_t attrs[] = {type,ownerShip};
    class_addProperty([MyClass class], "age",attrs, 2);
    //添加方法
    class_addMethod([MyClass class],@selector(otherMethod),(IMP)otherMethod,"v@");
}
//测试相应的添加是否成功
-(void)test{
    //首先调用动态添加的方法
    [[MyClass new] performSelector:NSSelectorFromString(@"otherMethod") withObject:nil];
    unsigned int nums;//用于记录成员变量、属性或方法的个数
    //取成员变量
    Ivar *vars = class_copyIvarList([MyClass class],&nums);
    NSString *ivarkey = @"";
    for (int i = 0;i < nums;i++) {
        Ivar ivar = vars[I];
        ivarkey = [NSString stringWithUTF8String:ivar_getName(ivar)];
        NSLog(@"variable name:%@",ivarkey);
    }
    printf("-----------成员变量打印结束---------");
    //取成员属性
    NSString *propertyKey = @"";
    objc_property_t *properties = class_copyPropertyList([MyClass class],&nums);
    for (int i = 0;i < nums;i++) {
        objc_property_t property = properties[I];
        propertyKey = [NSString stringWithUTF8String:property_getName(property)];
        NSLog(@"property name:%@",propertyKey);
    }
    printf("-----------成员属性打印结束--------");
    //取方法
    Method *methods = class_copyMethodList([MyClass class],&nums);
    for (int i = 0;i < nums;i++) {
        Method method = methods[I];
        SEL selector = method_getName(method);
        NSString *methodName = NSStringFromSelector(selector);
        NSLog(@"method name:%@",methodName);
    }
}

运行结果如下:

运行结果

点击下载完整代码:
从运行结果中看出,你不能为一个类动态的添加成员变量,可以给类动态增加方法和属性。

因为方法和属性并不“属于”类实例,而成员变量“属于”类实例。我们所说的“类实例”概念,指的是一块内存区域,包含了isa指针和所有的成员变量。所以假如允许动态修改类成员变量布局,已经创建出的类实例就不符合类定义了,变成了无效对象。但方法定义是在objc_class中管理的,不管如何增删类方法,都不影响类实例的内存布局,已经创建出的类实例仍然可正常使用。

5、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 指向了方法的实现,本质是一个函数指针

6、SEL

它是selector在Objc 中的表示。selector 是方法选择器,其实作用就和名字一样,日常生活中,我们通过人名辨别谁是谁,注意Objc 在相同的类中不会有命名相同的两个方法。selector 对方法名进行包装,以便找到对应的方法实现。它的数据结构是:

typedef struct objc_selector *SEL;

我们可以看出它是个映射到方法的C 字符串,你可以通过Objc 编译器器命令@selector() 或者Runtime 系统的sel_registerName 函数来获取一个SEL 类型的方法选择器。通过方法器来获取方法的实现地址的过程运用了hashMap的算法。

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

7、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 是基地址偏移字节

8、IMP

IMP在objc.h中的定义是:

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

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

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

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

9、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 一样。

10、Property
typedef struct objc_property *Property;

typedef struct objc_property *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 指针
示例如:

 @interface Person : NSObject
 /** 姓名*/
@property (strong, nonatomic) NSString *name;
 /** age */
@property (assign, nonatomic) int age;
/** weight */
@property (assign, nonatomic) double weight;
@end

以上是一个Person 类,有3个属性。让我们用上述方法获取类的运行时属性。

unsigned int outCount = 0;

objc_property_t *properties = class_copyPropertyList([Person class], &outCount);

 NSLog(@"%d", outCount);

 for (NSInteger i = 0; i < outCount; i++) {

  NSString *name = @(property_getName(properties[i]));

  NSString *attributes = @(property_getAttributes(properties[i]));

  NSLog(@"%@--------%@", name, attributes);

}

property_getName 用来查找属性的名称,返回c 字符串。property_getAttributes 函数挖掘属性的真实名称和@encode 类型,返回c 字符串。

objc_property_t class_getProperty(Class cls, const char *name)

objc_property_t protocol_getProperty(Protocol *proto, const char *name, BOOL isRequiredProperty, BOOL isInstanceProperty)

class_getProperty 和protocol_getProperty 通过给出属性名在类和协议中获得属性的引用。

四、消息和消息发送的步骤

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

通过终端命令clang -rewrite-objc xxx.m可以看到xxx.m编译后的xxx.cpp(C++文件),比对.m文件和.cpp文件,你会发现方括号形式的方法调用基于返回类型的不同被编译器转化成objc_msgSend系列函数中的某一个)的调用。

通过clang方法也可以分析block的实现,传送门:谈Objective-C block的实现iOS中block实现的探究

这里要清楚一点,objc_msgSend 方法看清来好像返回了数据,其实objc_msgSend 从不返回数据,而是你的方法在运行时实现被调用后才会返回数据。

// OC形式:
[receiver messageWithArgs:arg1 and:arg2 …]; 
// C语言函数及参数说明:
// ◈ receiver => 消息接收者,类型为id,通过其isa指针找到指定类的结构
// ◈ selector => 方法选择器,类型为SEL,用于在类结构的方法分发表中搜索指定名字的方法实现/地址
objc_msgSend(receiver, selector, arg1, arg2, ...)

有代码可以看出,在方法的实现中始终有两个隐藏参数。
方法中的隐藏参数
疑问:
我们经常用到关键字 self ,但是 self 是如何获取当前方法的对象呢?
其实,这也是 Runtime 系统的作用,self 是在方法运行时被动态传入的。
当 objc_msgSend 找到方法对应实现时,它将直接调用该方法实现,并将消息中所有参数都传递给方法实现,同时,它还将传递两个隐藏参数:

  • 接受消息的对象reciver(self 所指向的内容,当前方法的对象指针)
  • 方法选择器selector(_cmd 指向的内容,当前方法的 SEL 指针)

因为在源代码方法的定义中,我们并没有发现这两个参数的声明。它们是在代码被编译时被插入方法实现中的。尽管这些参数没有被明确声明,在源代码中我们仍然可以引用它们。
这两个参数中, self更实用。它是在方法实现中访问消息接收者对象的实例变量的途径。

这时我们可能会想到另一个关键字 super ,实际上 super 关键字接收到消息时,编译器会创建一个 objc_super 结构体:

注意:

  • 在实例方法中,self表示对象;在类方法中,self表示元类对象(即类)。
  • super关键字实际上会被转化成一个objc_super类型的结构体,其值为{self, self.superclass}。
struct objc_super { id receiver; Class class; };

这个结构体指明了消息应该被传递给特定的父类。 receiver 仍然是 self 本身,当我们想通过 [super class] 获取父类时,编译器其实是将指向 self 的 id 指针和 class 的 SEL 传递给了 objc_msgSendSuper 函数。只有在 NSObject 类中才能找到 class 方法,然后 class 方法底层被转换为 object_getClass(), 接着底层编译器将代码转换为 objc_msgSend(objc_super->receiver, @selector(class)),传入的第一个参数是指向 self 的 id 指针,与调用 [self class] 相同,所以我们得到的永远都是 self 的类型。因此你会发现:

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

获取方法地址
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: 消息会花多久。

虽然可以更高效的调用方法,但是这种做法很少用,除非时需要持续大量重复调用某个方法的情况,才会选择使用以免消息发送泛滥。

注意:
me t ho d F o rSelector:方法是由Runtime系统提供的,而不是Objc自身的特性

下面详细叙述消息发送的步骤(如下图):


消息发送步骤图.png

具体步骤如下:

1).首先检测这个selector是不是要忽略。比如 Mac OS X 开发,有了垃圾回收就不理会 retain,release 这些函数。

2).检测这个selector的 target 是不是nil,Objc 允许我们对一个 nil 对象执行任何方法不会 Crash,因为运行时会被忽略掉。

3).如果上面两步都通过了,那么就开始查找这个类的实现IMP,先从 cache 里查找,如果找到了就运行对应的函数去执行相应的代码。

4).如果 cache中 找不到就通过isa指针找类的方法列表中是否有对应的方法。

5).如果类的方法列表中找不到就通过superClass到父类的方法列表中查找,一直找到 NSObject 类为止。

6).如果还找不到,就要开始进入动态方法解析->重定向->消息转发了(后面会讲到)。

如果第六步的拦截调用方法都未实现,程序就会调用doNotRecognizeSelector方法崩溃。

在消息的传递中,编译器会根据情况在 objc_msgSend , objc_msgSend_stret , objc_msgSendSuper , objc_msgSendSuper_stret 这四个方法中选择一个调用。如果消息是传递给父类,那么会调用名字带有 Super 的函数,如果消息返回值是数据结构而不是简单值时,会调用名字带有 stret 的函数。

五、消息转发及步骤(动态解析 ->重定向->转发)

拦截调用如下图所示:

消息拦截转发图

在一个函数找不到时,runtime提供了三种方式去补救:

1)、调用resolveInstanceMethod给机会让类添加这个实现这个函数(动态解析)

2)、调用forwardingTargetForSelector让别的对象去执行这个函数(fast forwarding)

3)、调用forwardInvocation(函数执行器)灵活的将目标函数以其他形式执行(normal forwarding)。
如果都不中,调用doesNotRecognizeSelector抛出异常。

步骤一、动态解析

通过实现resolveInstanceMethodresolveClassMethod:方法,我们有机会为该未知消息(SEL)新增一个“处理方法”(IMP)。

  • 这意味着在消息转发前,你有机会通过class_addMethod给类动态添加一些方法;
  • 实际上返回值YES/NO无关紧要,只要你在resovle过程中新增过方法,就会触发class_getMethodImplementation,其作用相当于重新启动一次消息发送过程。

1.你可以动态提供一个方法实现。如果我们使用关键字 @dynamic 在类的实现文件中修饰一个属性,表明我们会为这个属性动态提供存取方法,编译器不会再默认为我们生成这个属性的 setter 和 getter 方法了,需要我们自己提供。

@dynamic propertyName;

这时,我们可以通过分别重载resolveInstanceMethod:和resolveClassMethod:方法添加实例方法实现和类方法实现。

2.当 Runtime 系统在 Cache 和类的方法列表(包括父类)中找不到要执行的方法时,Runtime 会调用 resolveInstanceMethod: 或 resolveClassMethod: 来给我们一次动态添加方法实现的机会。我们需要用 class_addMethod 函数完成向特定类添加特定方法实现的操作:

#import "MyClass.h"
#import <objc/runtime.h>
//添加要动态添加的类方法的实现
void dynamicClassMethodIMP(id self,SEL _cmd){
    NSLog(@"拦截到未实现的类方法,并为之添加实现");
}
//添加要动态添加的实例化方法的实现
void dynamicInstanceMethodIMP(id self,SEL _cmd){
    NSLog(@"拦截到未实现的实例化方法,并为之添加实现");
}
/*
 * 在NSObject中有动态解析方法,我们所有的类(除NSPoxy之外)都继承于NSObject,因此,我们可以所有的类中都可以重载动态解析方法
 */
@implementation MyClass
//重载类方法的动态解析方法
+(BOOL)resolveClassMethod:(SEL)sel{
    if (sel == @selector(unknownClassMethod)) {
        //objc_getMetaClass("MyClass"),根据类于对象的继承关系,给类添加类方法这里需要找MyClass的原类
        class_addMethod(objc_getMetaClass("MyClass"),sel,(IMP)dynamicClassMethodIMP,"v@:");//"v@:"是动态的添加方法的返回值和参数类型,这里是void。
        return YES;
    }
    return [super resolveClassMethod:sel];
}
//重载实例方法的动态解析方法
+(BOOL)resolveInstanceMethod:(SEL)sel{
    if (sel == @selector(unknownInstanceMethod)) {
        class_addMethod([self class],sel,(IMP)dynamicInstanceMethodIMP,"v@:");
        return YES;
    }

    return [super resolveInstanceMethod:sel];
}
@end

各种类型标示的含义见:Type Encoding
查看完整代码:https://github.com/lwc1990/DynamicResolveStudyDemo.git

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

步骤二、重定向(指定备用能处理相关事件的接收者)

从第二步开始,真正进入消息转发阶段。

在消息转发机制执行前,Runtime系统允许我们替换消息的接受者(执行者)为其他对象。

通过实现-forwardingTargetForSelector: 方法将消息(SEL)直接转发给另一个对象(备用接收者),也就是在另一个对象上重启消息发送过程。

#import "MyClass.h"
/*
 * -(id)forwardingTargetForSelector:(SEL)aSelector;是NSObject的方法,
 * 除NSPoxy类之外,所有的类继承于NSObject,也就是任何类的实例都可以重载这个方法,实现消息的快速转发。
 */
@implementation MyClass
//重载快速转发方法,把消息转发给能处理的对象
-(id)forwardingTargetForSelector:(SEL)aSelector{
    if (aSelector == @selector(unknownInstanceMethod)) {
        return [NSClassFromString(@"MyFastForwardingClass") new];
    }
    return [super forwardingTargetForSelector:aSelector];
}
@end

点击查看完整代码:https://github.com/lwc1990/FastForwardingDemo
如果返回nil或self,则会进入消息完整转发阶段。

步骤三、完整消息转发

当快速转发不做处理时,则会出发消息转发机制,在消息完整转发阶段,-forwardInvocation:方法会被执行,我们可以重写这个方法来自定义我们的转发逻辑。
代码示例:

#import "ViewController.h"
//消息转发后的消息执行者
@interface TestObj : NSObject
//转发方法的声明
-(void)testMethod;
@end
@implementation TestObj
//转发方法的实现
-(void) testMethod
{
    NSLog(@"%s",__func__);
}
@end
@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    //    [self selfTestMethod]; 结果:[ViewController selfTestMethod]
//    [self performSelector:@selector(testMethod)];结果:[TestObj testMethod]
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
    [self performSelector:NSSelectorFromString(@"otherMethod")];
#pragma clang diagnostic pop
}
/*
 * 当动态解析和快速转发都未能处理时,该方法就会被执行,如果该方法,也未能处理,Runtime系统就会调用doNotRecognizeSelector:进而崩溃。
 * 参数anInvocation 类型是NSInvocation,这个类里,封装了小执行的方法SEL,和消息原定的接受者,以及参数、返回值等信息;
 * 因此,我们可以修改其中的消息接受者也就是执行者,或者要执行方法,实现消息的完整转发,防止系统崩溃。
 * 这里的anInvocation对象是要经过-(NSMethodSignature *)methodSignatureForSelector:方法生成的签名和原消息打包来生成的,
 * 因此,我们要实现消息的完整转发,是需要该方法与下面的方法协同实现。
 */
-(void)forwardInvocation:(NSInvocation *)anInvocation{
    SEL sel = anInvocation.selector;
    if ([self respondsToSelector:sel]){
        //已实现的直接消息转发
        [anInvocation invoke];
    }else if ([[[TestObj alloc]init] respondsToSelector:anInvocation.selector]){
        //未实现的用其他实现的Target转发
        [anInvocation invokeWithTarget:[[TestObj alloc]init]];
    }else{
        //都未实现,用预定义的方法方法转发,防止崩溃
        SEL unKnownSel = anInvocation.selector;
        anInvocation.selector = @selector(unKnownSelector:);
        [anInvocation setArgument:unKnownSel atIndex:0];
        anInvocation.target = self;
        [anInvocation invoke];
    }
}
/*
 * 传递过来的消息,需要经过该方法进行签名,该方法生成的签名和原消息一起打包到一个NSInvocation对象中, 以供上面的消息转发方法进行入参。
* 方法签名,类似于C++编译时的函数签名,里面包含方法的参数个数,方法返回值信息大多信息是值读的;可以通过NSObject的methodSignatureForSelector:方法获取实例化对象,它主要是协同NSInvocation实现消息转发 ,要保证NSInvocation对象的中的Selector与该方法返回的方法签名中的Seletor一致
 */
-(NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{
    //这个方法返回的实例是协同上面的方法实现消息转发的
    NSMethodSignature *signature = [super methodSignatureForSelector:aSelector];
    if (!signature){
        if ([[[TestObj alloc]init] respondsToSelector:aSelector]){
            signature = [[[TestObj alloc]init] methodSignatureForSelector:aSelector];
        }else{
            NSString *str = [NSString stringWithFormat:@"%@ hasn't implementation method %@",NSStringFromClass([self class]),NSStringFromSelector(aSelector)];
            NSAssert(!DEBUG,str);
            signature = [self methodSignatureForSelector:@selector(unKnownSelector:)];
        }
    }
    return signature;
}
-(void)unKnownSelector:(SEL)aSelector{
    NSLog(@"%@ hasn't implementation method %@",NSStringFromClass([self class]),NSStringFromSelector(aSelector));
}
-(void)selfTestMethod{
    NSLog(@"%s",__func__);
}
- (void)didReceiveMemoryWarning {
    [super didReceiveMemoryWarning];
}
@end

点击下载完整代码:https://github.com/lwc1990/DemoForInvocation

完整转发的详细过程
1.通过实现-methodSignatureForSelector:提供方法签名(提供参数和返回值的类型)

  • 方法签名,类似于C++编译时的函数签名,里面包含方法的参数个数,方法返回值信息,大多信息是只读的;可以通过NSObject的methodSignatureForSelector:方法获取实例化对象,它主要是协同NSInvocation实现消息转发 ,要保证NSInvocation对象的中的Selector与该方法返回的方法签名中的Seletor一致.

2. 生成的签名将和原始消息一起打包到一个NSInvocation对象中

  • 通过操作NSInvocation对象的target、selector属性可以方便地转发,甚至转发给另一个对象的另一个需要不同参数的SEL也是可以的;

3. 实现-forwardInvocation: 方法

  • 通过调用 -invoke方法重新启动一个消息发送过程。
  • 不调用invoke,系统会吞掉这个消息(不做任何处理)

forwardInvocation: 方法就是一个不能识别消息的分发中心,将这些不能识别的消息转发给不同的接收对象,或者转发给同一个对象,再或者将消息翻译成另外的消息,亦或者简单的“吃掉”某些消息,因此没有响应也不会报错。这一切都取决于方法的具体实现。

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

六、消息的转发功能模拟了多继承功能

1.消息转发如何模拟实现多继承
我们都知道,Objec不具有多继承的特性,但由于Objec有消息转发的机制,一个对象把消息转发出去,就好像它把另一个类中的方法“继承”过来一样,因此我们可以利用消息转发的机制为Objc编程添加一些多继承的效果。
如下图所示:

不同继承分支之间的方法调用

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

消息转发弥补了 Objc 不支持多继承的性质,也避免了因为多继承导致单个类变得臃肿复杂

3.消息转发与继承的区别

虽然转发可以实现继承的功能,但是 NSObject 还是比较严谨的,像 respondsToSelector: 和 isKindOfClass: 这类方法只会考虑继承体系,不会考虑转发链。

如果上图中的 Warrior 对象被问到是否能响应 negotiate消息:

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

回答当然是 NO, 尽管它能接受 negotiate 消息而不报错,因为它靠转发消息给 Diplomat 类响应消息。

如果你就是想要让别人以为 Warrior 继承到了 Diplomat 的 negotiate 方法,你得重新实现 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:,这个方法会最终响应被转发的消息。从而生成一个确定的 NSInvocation 对象描述消息和消息参数。这个方法最终响应被转发的消息。它需要像下面这样实现:

- (NSMethodSignature*)methodSignatureForSelector:(SEL)selector
{
    NSMethodSignature* signature = [super methodSignatureForSelector:selector];
    if (!signature) {
       signature = [surrogate methodSignatureForSelector:selector];
    }
    return signature;
}

以下方法只考虑类继承体系(不含转发链);如需要对象表现得和继承一样,重写它们并把转发算法包括进来:

  • -respondsToSelector: & +instancesRespondToSelector:
  • -isKindOfClass: & -isMemberOfClass:
  • -conformsToProtocol:

4.转发与多继承的区别
转发模拟了继承,所以可以用来为Objc程序提供类似多继承的功能。转发和多继承的区别如下:

  • 多继承是将许多功能combine到一个对象中;
  • 转发则将功能分解到多个对象,并一种对消息发送者透明的方式将它们关联起来;
七、使用场景总结

点击查看:
使用场景总结一

八、示例Demo下载

使用黑魔法对数组越界预处理:https://github.com/lwc1990/RunTimeForSwizzlingDemo
使用黑魔法处理对Button频繁点击的问题:https://github.com/lwc1990/UIButtonRunTimeDemo
结合消息完整转发和NSPoxy处理NSTimer循环引用的问题:https://github.com/lwc1990/MessageForward_NSProxyDemo/tree/master/WeakProxyDemo
运用运行时实现自己的通知中心:https://github.com/lwc1990/CustomNotification
运用运行时研究KVO:https://github.com/lwc1990/KVOStudyDemo

结语:我个人认为,运行时是一个很好的研究系统提供的各种方法或设计模式实现的工具,除个别场景之外,在复杂的项目中可被运用的方式很少。
但是,我们无法否认的是,对其理解和运用,这种炫技后的满足感!

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

推荐阅读更多精彩内容