
不带参数: objc_msgSend(receiver, selector)
带参数:objc_msgSend(receiver, selector, org1, org2, ...)
通过receiver的isa指针找到receiver的Class; 在Class中的cache(方法缓存)列表中查找对应的IMP(方法实现); 若cache列表中没有找到IMP,会继续在Class的method list中查找对应的selector,若找到则将它添加到cache中,并返回selector; 若Class中也没有找到目标selector,则继续向它的父类superClass中继续查找(递归向上查找); 当找到对应的selector,则直接执行receiver的selector方法IMP; 若最终没找到对应的selector,消息进入转发机制(后续再介绍此过程),若转发还无实现,则会抛出crash;
/// Represents an instance of a class.struct objc_object {Class _Nonnull isa; // objc_object 结构体的实例指针};/// A pointer to an instance of a class.typedef struct objc_object *id;
从代码中可以知道objc_object结构体只有一个isa指针,一个Object(对象)
typedef struct objc_class *Class;struct objc_class {Class _Nonnull isa; // objc_class 结构体的实例指针#if!__OBJC2__Class _Nullable super_class; // 指向父类的指针const char * _Nonnull name; // 类的名字long version; // 类的版本信息,默认为 0long info; // 类的信息,供运行期使用的一些位标识long instance_size; // 该类的实例变量大小;struct objc_ivar_list * _Nullable ivars; // 该类的实例变量列表struct objc_method_list * _Nullable * _Nullable methodLists; // 方法定义的列表struct objc_cache * _Nonnull cache; // 方法缓存struct objc_protocol_list * _Nullable protocols; // 遵守的协议列表#endif};
在objc_class结构体中,可以了解到一些关键信息: 例如:第一个成员变量isa, isa指针是objc_class的实例指针,isa指针中保存的是当前所属类的结构体的实例指针, 简述:Class类的本质其实就是一个对象,我们称为类对象。 super_class指向父类的指针, ivars是所有实例变量,methodLists是所有方法的定义,cache就是上面提到的cache列表, protocols就是遵守的协议列表。 objc_class结构体存放的数据也称为元数据(meta-data)
我们已经了解到 对象(objc_object结构体)的isa指针,指向的是类对象(objc_class结构体), 那么类对象(objc_class结构体)的isa指向的是类对象的元类(Meta Class)。
Meta Class(元类)是一个类对象所属的类, 一个对象所属的类叫做类对象, 而一个类对象所属的类叫做元类。 Runtime中将类对象所属的类型就叫做Meta Class(元类),作用:用于描述类对象本身所具有的特征信息, 而在元类的methodlists中,保存了类的方法链表,也就是类方法,并且类对象中的isa指针指向的就是元类, 每个类对象犹且仅有一个与之相关的元类。
总结: 方法消息机制的基本原理:
实例方法的查找过程: 通过对象的isa指针找到对应的Class(类), 在Class(类)的cache或method list(方法列表)中找到对应的selector。
类方法的查找过程: 通过类对象的isa指针找到对应的Meta Class(元类), 在Meta Class(元类)的method list(方法列表)中找到对应的selector。
通过上面介绍了实力对象(Object), 类(Class), Meta Class(元类)的基本概念,可以通过一个图来表示他们之间的关系结构:

例如: 子类Student的实例对象的isa指向的对应的Student类对象,而Student类的isa指针指向的Student的元类,所有的元类最后都指向类NSObject元类, NSObject元类的也指向了它自己,它也被称为根元类。
整个方法的调用过程的源码,可以结合Runtime源码,例如方法的查找过程,我整理的结构图,可以对照参考着了解一下。

在objc_class结构体中的methodLists(方法列表)中存放的元素就是方法(Method)。
在objc/runtime.h文件中, 表示方法(Method)的objc_method结构体的数据结构:
/// An opaque type that represents a method in a class definition./// 代表类定义中一个方法的不透明类型typedef struct objc_method *Method;struct objc_method {SEL _Nonnull method_name; // 方法名char * _Nullable method_types; // 方法类型IMP _Nonnull method_imp; // 方法实现};
该结构体中包含了方法名、方法类型、方法实现。
1. SEL _Nonnull method_name
其中SEL的定义是:
/// An opaque type that represents a methodselector.typedef struct objc_selector *SEL;
SEL是一个指向objc_selector结构体的指针, 实际我们在代码开发中,也知道SEL只是一个保存方法名的字符串。 例如:
SEL sel1 = @selector(test);NSLog(@"%s", sel1); // 输出:test
2. char * _Nullable method_types; 方法类型
方法类型method_types是个字符串,用于存储方法的参数类型和返回值类型。
3. IMP _Nonnull method_imp; 方法实现
关于IMP的代码定义:
/// A pointer to the function of a method implementation.#if!OBJC_OLD_DISPATCH_PROTOTYPEStypedef void (*IMP)(void/* id, SEL, ... */ );#elsetypedef id _Nullable (*IMP)(id _Nonnull, SEL _Nonnull, ...);#endif
IMP的实质就是一个函数指针,它指向的就是方法的实现, IMP是用来查找函数地址,并执行函数。
在上面已经介绍了方法调用时的查找过程, 如果方法并没有找到IMP,那么objective-c并没有直接抛异常, 而是又给了我们一次挽救的机会,通过Runtime机制, 该消息被转发或者临时向receiver添加一个selector的实现方法, 那么就可以实现方法调用, 而且Runtime的消息转发过程还提供了3次挽救的方式, 若还没实现则直接抛Crash。
具体来讲:
当调用方法IMP找不到时, Runtime提供了消息的动态解析,消息接受者重定向, 消息重定向(方法签名)这三步处理消息。 调用的流程图如下:

Runtime在方法进入消息转发环节时,会首先调用
+resolveInstanceMethod:或者 +resolveClassMethod:
可以通过重写这2个方法来添加函数的实现(一个是重写实例方法,一个是重写类方法),并返回YES即可。
- (void)viewDidLoad {[super viewDidLoad];// 这里调用一个不存在的方法[self performSelector:@selector(test)];}// 重写 resolveInstanceMethod:方法的实现+ (BOOL)resolveInstanceMethod:(SEL)sel {if (sel == @selector(test)) {class_addMethod([self class], sel, (IMP)testMethod, "v@:");return YES;}return [super resolveInstanceMethod:sel];}void testMethod(id obj, SEL _cmd) {NSLog(@"testMethod");}
这个实例中通过实现+resolveInstanceMethod:方法,并在方法体中,通过class_addMethod的方法动态添加了test方法对应的IMP是(testMethod)这个方法,也就是说,当调用test方法时,Runtime会自动通过消息转发的形式,调用到testMethod方法。
关于上面代码中class_addMethod的方法最后一个参数是表示参数的类型,具体的写法,可以参考下面Type Encoding列表:
Code | Meaning |
|---|---|
| A |
| An |
| A |
| A
|
| A |
| An |
| An |
| An |
| An |
| An |
| A |
| A |
| A C++ |
| A |
| A character string ( |
| An object (whether statically typed or typed |
| A class object ( |
| A method selector ( |
[array type] | An array |
{name=type...} | A structure |
(name=type...) | A union |
| A bit field of num bits |
| A pointer to type |
| An unknown type (among other things, this code is used for function pointers) |
消息接受者重定向
如上图所示, 如果类中并没有实现+resolveInstanceMethod: 或者 +resolveClassMethod:方法时, Runtime会进入下一步, 消息接受者重定向-----若当前对象实现了-forwardingTargetForSelector:, Runtime会调用这个方法,允许我们将消息的接受者转发给其它对象。
- (void)viewDidLoad {[super viewDidLoad];// 这里调用一个不存在的方法[self performSelector:@selector(test)];}// 这里没有实现具体的逻辑+ (BOOL)resolveInstanceMethod:(SEL)sel {return YES;}// 消息接受者重定向- (id)forwardingTargetForSelector:(SEL)aSelector {if (aSelector == @selector(test)) {return [[Student alloc] init];// 返回 test 对象,让 Student类的实例对象接收并处理这个消息}return [super forwardingTargetForSelector:aSelector];}
我们需要创建一个Student类来实现具体的IMP的执行,细节不再逐一赘述。若代码中并没有实现这个方法,则进入到下一步: 消息重定向流程(方法签名方案)。
消息接受者重定向(方法签名)
若上面2步均未实现,Runtime还给了最后一次的保护机会,可以通过实现-methodSignatureForSelector:方法获取函数的参数和返回值类型。
一般做iOS的Crash防护的, 通常都会在此方法在Release环境来实现,避免Crash。
-methodSignatureForSelector:方法返回的是一个NSMethodSignature对象(函数签名),Runtime会创建一个NSInvocation对象,并通过-forwardInvocation:消息通知给当前对象,实现最后一次IMP的机会。
-methodSignatureForSelector:方法若返回nil, Runtime则会抛出-doesNotRecognizeSelector:消息,直接让App发生crash。
这里使用的关键方法包含:
// 获取函数的参数和返回值类型并返回方法签名- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector;// 重定向- (void)forwardInvocation:(NSInvocation *)anInvocation;
完整的代码如下:
#import "ViewController.h"#include"objc/runtime.h"@interfaceStudent : NSObject- (void)test;@end@implementationStudent- (void)test {NSLog(@"test");}@end@interfaceViewController ()@end@implementationViewController- (void)viewDidLoad {[super viewDidLoad];// 执行 test 函数[self performSelector:@selector(test)];}+ (BOOL)resolveInstanceMethod:(SEL)sel {return YES;}- (id)forwardingTargetForSelector:(SEL)aSelector {return nil;}// 获取函数的参数和返回值类型并返回方法签名- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {if ([NSStringFromSelector(aSelector) isEqualToString:@"test"]) {return [NSMethodSignature signatureWithObjCTypes:"v@:"];}return [super methodSignatureForSelector:aSelector];}// 消息重定向- (void)forwardInvocation:(NSInvocation *)anInvocation {SEL sel = anInvocation.selector; // 从 anInvocation 中获取消息Student *p = [[Student alloc] init];if([p respondsToSelector:sel]) { // 判断 Student 是否实现有sel方法[anInvocation invokeWithTarget:p]; // 若实现直接调用方法} else {[self doesNotRecognizeSelector:sel]; // 若仍然无法响应,则报错:找不到响应方法}}@end
通过Runtime除了上面做Crash防护场景之外,还可以应用到无痕埋点的技术。
无痕埋点(Click事件)
例如: 对App内所有的Button的Click进行监听并上报, 我们知道UIButton父类是UIControl, 所以创建一个UIControl的分类, 并在+load方法中,进行方法交换, 这里对方法sendAction:to:forEvent:进行操作。
@implementationUIControl (Tracking)+ (void)load {Method method1 = class_getInstanceMethod(self, @selector(sendAction:to:forEvent:));Method method2 = class_getInstanceMethod(self, @selector(new_sendAction:to:forEvent:));method_exchangeImplementations(method1, method2);}- (void)new_sendAction:(SEL)action to:(id)target forEvent:(UIEvent *)event {NSLog(@"%@-%@-%@", self, target, NSStringFromSelector(action));// 处理相关的埋点逻辑的功能// 这里需要调用原来的实现[self new_sendAction:action to:target forEvent:event];}@end
总结:
通过本小结详细介绍Runtime的一些功能点,尤其是类的底层结构struct的内容,以及方法的调用链和转发逻辑等, 我们就可以做一些特殊的功能逻辑,例如: 可以遍历类的所有成员变量,实现方法的交换,无痕埋点的功能,还有Crash防护(安全气垫的功能), 当然可以基于Runtime,实现紧急的线上问题修复(这里可以通过将OC语言先编译为AST语法树, 动态解析AST,再通过runtime反向解析,并实现这个过程), 这部分的逻辑可以先了解一下yacc/lex的知识点,实现AST的动态解析会更方便。
夜雨聆风