我们知道C语言是静态类语言, 静态类语言是指在编译阶段就已经确定了变量的数据类型, 函数地址等。Objective-C是动态语言, 动态语言的特点是:在编译阶段并不知道变量的具体类型, 也不知道在调用时,真正调用的函数(IMP)是哪个函数, 只有在运行时才会检查变量的数据类型, 以及根据函数名去查找对应的函数实现(IMP)。 简述:在程序没运行时,我们并不能确认调用一个方法具体会发生什么。动态语言的特点: 将确定性的(比如:数据类型,函数实现)从编译阶段,推迟到运行时阶段的这种方式, 优点: 让语言变得更加灵活,我们可以在程序运行的时候,动态去修改一个方法的实现。缺点: 由于在运行时才进行方法IMP查找, 类型的判断等, 它在性能上是要稍低于静态语言,静态语言可以直接通过寻址调用函数(执行效率高)。而让Objective-C拥有这个超能力的核心源自于:Runtime(运行时机制)。Runtime实际是一个底层库,这个库在运行时创建对象,检查对象,修改类和对象的方法。这里我整理了一个图,是涉及到的一些核心知识点,接下来会逐步讲解一下。
在Objective-c编程语言, 方法的调用方式是[receiver selector];的形式,receiver是消息的调用者, selector是要调用的函数, 它运行的本质的全过程是:1. 编译阶段: [receiver selector];方法被gcc编译器转换为:- 不带参数: 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;
上面是详细介绍了一下方法的调用, 接下来通过Runtime的API,逐一介绍一下,涉及到一些专有名词及定义:打开runtime库源码的 objc/objc.h中:关于Object(对象)的定义,Object(对象)被定义为objc_object结构体,其数据结构如下:/// 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(对象)
比如:我们进行方法调用时[receiver selector];,它会通过isa指针去找对应的objc_object结构体,并在objc_object结构体的cache列表或methodLists中找到要调用的方法并执行。打开runtime库源码的 objc/runtime.h中:我们知道Class(类)定义为指向objc_class结构体的指针, objc_class的结构体的数据结构如下: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; // 类的版本信息,默认为 0 long 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指针指向的就是元类, 每个类对象犹且仅有一个与之相关的元类。
总结: 方法消息机制的基本原理:
通过上面介绍了实力对象(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 |
|---|
c
| A char |
i
| An int |
s
| A short |
l
| A long l is treated as a 32-bit quantity on 64-bit programs.
|
q
| A long long |
C
| An unsigned char |
I
| An unsigned int |
S
| An unsigned short |
L
| An unsigned long |
Q
| An unsigned long long |
f
| A float |
d
| A double |
B
| A C++ bool or a C99 _Bool |
v
| A void |
*
| A character string (char *) |
@
| An object (whether statically typed or typed id) |
#
| A class object (Class) |
:
| A method selector (SEL) |
[array type] | An array |
{name=type...} | A structure |
(name=type...) | A union |
bnum
| A bit field of num bits |
^type
| 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的动态解析会更方便。