参考文章:
1、Objctive-C Runtime
2、梧雨北辰
3、jackyshan
4、人仙儿a
目录
- Runtime介绍
- Runtime消息传递
- Runtime消息转发
- Runtime之多继承的实现思路
Runtime介绍
因为Objc是一门动态语言,所以它总是想办法把一些决定工作从编译连接推迟到运行时。也就是说只有编译器是不够的,还需要一个运行时系统 (runtime system) 来执行编译后的代码。这就是 Objective-C Runtime 系统存在的意义,Runtime是一套底层纯C语言API。OC代码最终都会被编译器转化为运行时代码,通过消息机制决定函数调用方式,这也是OC作为动态语言使用的基础。
RunTime简称运行时。OC就是运行时机制,其中最主要的是消息机制。对于C语言,函数的调用在编译的时候会决定调用哪个函数。对于OC的函数,属于动态调用过程,在编译的时候并不能决定真正调用哪个函数,只有在真正运行的时候才会根据函数的名称找到对应的函数来调用
Runtime基本是用 C 和汇编写的,可见苹果为了动态系统的高效而作出的努力。你可以在这里下到苹果维护的开源代码。苹果和GNU各自维护一个开源的 runtime 版本,这两个版本之间都在努力的保持一致。
高级编程语言想要成为可执行文件需要先编译为汇编语言再汇编为机器语言,机器语言也是计算机能够识别的唯一语言,但是OC并不能直接编译为汇编语言,而是要先转写为纯C语言再进行编译和汇编的操作,从OC到C语言的过渡就是由runtime来实现的。然而我们使用OC进行面向对象开发,而C语言更多的是面向过程开发,这就需要将面向对象的类转变为面向过程的结构体。
Runtime消息传递
当一个对象调用一个方法的时候,类似 [objc customMethod]
,编译器会转换成消息发送objc_msgSend(objc, customMethod)
。具体传递过程如下:
- 首先,通过obj的isa指针找到它的 class ;
- 在 class 的 method list 中查找
customMethod
; - 如果 class 中没到
customMethod
,继续往它的 superclass 中找 ; - 一旦找到
customMethod
这个函数,就去执行它的实现IMP 。
objec_msgSend
的方法定义如下:
OBJC_EXPORT id objc_msgSend(id self, SEL op, ...)
消息传递过程中各种函数定义
1、class是一个指向objc_class结构体的指针,即在Runtime中:
typedef struct objc_class *Class;
下面是Runtime中对objc_class结构体的具体定义:
//usr/include/objc/runtime.h
struct objc_class {
Class _Nonnull isa OBJC_ISA_AVAILABILITY;
#if !OBJC2
Class Nullable super_class OBJC2UNAVAILABLE;
const char * Nonnull name OBJC2UNAVAILABLE;
long version OBJC2_UNAVAILABLE;
long info OBJC2_UNAVAILABLE;
long instance_size OBJC2_UNAVAILABLE;
struct objc_ivar_list * Nullable ivars OBJC2UNAVAILABLE;
struct objc_method_list * Nullable * _Nullable methodLists OBJC2UNAVAILABLE;
struct objc_cache * Nonnull cache OBJC2UNAVAILABLE;
struct objc_protocol_list * Nullable protocols OBJC2UNAVAILABLE;
#endif
} OBJC2_UNAVAILABLE;
结构体参数说明如下:
- isa指针:我们会发现objc_class和objc_object同样是结构体,而且都拥有一个isa指针。我们很容易理解objc_object的isa指针指向对象的定义,那么objc_class的指针是怎么回事呢?
其实,在Runtime中Objc类本身同时也是一个对象。Runtime把类对象所属类型就叫做元类,用于描述类对象本身所具有的特征,最常见的类方法就被定义于此,所以objc_class中的isa指针指向的是元类,每个类仅有一个类对象,而每个类对象仅有一个与之相关的元类。 - super_class指针:super_class指针指向objc_class类所继承的父类,但是如果当前类已经是最顶层的类(如NSProxy),则super_class指针为NULL
- cache:为了优化性能,objc_class中的cache结构体用于记录每次使用类或者实例对象调用的方法。这样每次响应消息的时候,Runtime系统会优先在cache中寻找响应方法,相比直接在类的方法列表中遍历查找,效率更高。
- ivars:ivars用于存放所有的成员变量和属性信息,属性的存取方法都存放在methodLists中。
- methodLists:methodLists用于存放对象的所有成员方法。
通过这个结构体的命名我们不难发现,这个结构体保存的就是类包含的信息。这也说明了类对象其实就是一个结构体。这个结构体存放的数据称为元数据(metadata)。
2、实例(objc_object)
//对象
struct objc_object {
Class isa OBJC_ISA_AVAILABILITY;
};
/// A pointer to an instance of a class.
typedef struct objc_object *id;
我们都知道 id 在OC中是表示一个任意类型的类实例,从这里也可以看出,OC中的对象虽然没有明显的使用指针,但是在OC代码被编译转化为C之后,每个OC对象其实都是拥有一个isa的指针的。
类对象中的元数据存储的都是如何创建一个实例的相关信息,那么类对象和类方法应该从哪里创建呢?
就是从isa指针指向的结构体创建,类对象的isa指针指向的我们称之为元类(metaclass),
元类中保存了创建类对象以及类方法所需的所有信息,因此整个结构应该如下图所示:
补充说明: objc_class 继承于 objc_object,也就是说一个 ObjC 类本身同时也是一个对象,为了处理类和对象的关系,runtime 库创建了一种叫做元类 (Meta Class) 的东西,类对象所属类型就叫做元类,它用来表述类对象本身所具备的元数据。类方法就定义于此处,因为这些方法可以理解成类对象的实例方法。每个类仅有一个类对象,而每个类对象仅有一个与之相关的元类。当你发出一个类似 [NSObject alloc] 的消息时,你事实上是把这个消息发给了一个类对象 (Class Object) ,这个类对象必须是一个元类的实例,而这个元类同时也是一个根元类 (root meta class) 的实例。所有的元类最终都指向根元类为其超类。所有的元类的方法列表都有能够响应消息的类方法。所以当 [NSObject alloc] 这条消息发给类对象的时候,objc_msgSend() 会去它的元类里面去查找能够响应消息的方法,如果找到了,然后对这个类对象执行方法调用。
3、元类(Meta Class)
元类(Meta Class)是一个类对象的类。
通过上图我们可以看出整个体系构成了一个自闭环,struct objc_object
结构体实例它的isa指针指向类对象,类对象的isa指针指向了元类,super_class指针指向了父类的类对象,而元类的super_class指针指向了父类的元类,那元类的isa指针又指向了自己。
在上面我们提到,所有的类自身也是一个对象,我们可以向这个对象发送消息(即调用类方法)。
为了调用类方法,这个类的isa指针必须指向一个包含这些类方法的一个objc_class结构体。这就引出了meta-class的概念,元类中保存了创建类对象以及类方法所需的所有信息。
任何NSObject继承体系下的meta-class都使用NSObject的meta-class作为自己的所属类,而基类的meta-class的isa指针是指向它自己。
4、Method(objc_method)
Method表示某个方法的类型,即在Runtime中:
/// An opaque type that represents a method in a class definition.
typedef struct objc_method *Method;
我们可以在objct_class定义中看到methodLists,其中的元素就是Method,下面是Runtime中objc_method结构体的具体定义:
struct objc_method {
SEL Nonnull method_name OBJC2UNAVAILABLE;
char * Nullable method_types OBJC2UNAVAILABLE;
IMP Nonnull method_imp OBJC2UNAVAILABLE;
}
参数说明:
- method_name:方法名类型SEL
- method_types: 一个char指针,指向存储方法的参数类型和返回值类型
- method_imp:本质上是一个指针,指向方法的实现
在这个结构体重,我们已经看到了SEL
和IMP
,说明SEL
和IMP
其实都是Method
的属性。
5、Ivar
Ivar代表类中实例变量的类型,是一个指向ojbcet_ivar的结构体的指针,即在Runtime中:
/// An opaque type that represents an instance variable.
typedef struct objc_ivar *Ivar;
下面是Runtime中对objc_ivar结构体的具体定义:
struct objc_ivar {
char * Nullable ivar_name OBJC2UNAVAILABLE;
char * Nullable ivar_type OBJC2UNAVAILABLE;
int ivar_offset OBJC2_UNAVAILABLE;
#ifdef LP64
int space OBJC2_UNAVAILABLE;
#endif
}
关于Ivar的深度解释,请看这篇文章
6、SEL
SEL是一个指向objc_selector结构体的指针,即在Runtime中:
/// An opaque type that represents a method selector.
typedef struct objc_selector *SEL;
objc_msgSend函数第二个参数类型为SEL,它是selector在Objective-C中的表示类型(Swift中是Selector类)。selector是方法选择器,可以理解为区分方法的 ID,而这个 ID 的数据结构是SEL:
@property SEL selector;
可以看到selector是SEL的一个实例。
A method selector is a C string that has been registered (or “mapped“) with the Objective-C runtime. Selectors generated by the compiler are automatically mapped by the runtime when the class is loaded.
其实selector就是个映射到方法的C字符串,你可以用 Objective-C 编译器命令@selector()或者 Runtime 系统的sel_registerName函数来获得一个 SEL 类型的方法选择器。
selector既然是一个string,我觉得应该是类似className+method的组合,命名规则有两条:
- 同一个类,selector不能重复
- 不同的类,selector可以重复
这也带来了一个弊端,我们在写C代码的时候,经常会用到函数重载,就是函数名相同,参数不同,但是这在Objective-C中是行不通的,因为selector只记了method的name,没有参数,所以没法区分不同的method。
注意:
- 不同类中相同名字的方法对应的方法选择器是相同的。
- 即使是同一个类中,方法名相同而变量类型不同也会导致它们具有相同的方法选择器。
通常我们获取SEL有三种方法:
- OC中,使用@selector(“方法名字符串”)
- OC中,使用NSSelectorFromString(“方法名字符串”)
- Runtime方法,使用sel_registerName(“方法名字符串”)
7、IMP
IMP是一个函数指针,它在Runtime中的定义如下:
/// A pointer to the function of a method implementation.
typedef void (IMP)(void / id, SEL, ... */ );
IMP就是指向最终实现程序的内存地址的指针。
当OC发起消息后,最终执行的代码是由IMP指针决定的。利用这个特性,我们可以对代码进行优化:当需要大量重复调用方法的时候,我们可以绕开消息绑定而直接利用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);
注意:这里需要注意的就是函数指针的前两个参数必须是id和SEL。
8、Category(objc_category)
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增加实例变量的原因,不过这个和一般的实例变量是不一样的。
消息转发
通过前文的理解,我们知道OC的方法被编译之后对应的函数如下:
id _Nullable objc_msgSend(id _Nullable self, SEL _Nonnull op, ...)
这是我们常见的形式,其实还有如下几种:
- objc_msgSend_stret
- objc_msgSendSuper
- objc_msgSendSuper_stret
如果消息传递给超类就使用带有super的方法,如果返回值是结构体而不是简单值就使用带有stret的值。
运行时阶段的消息发送的详细步骤如下:
- 检测selector 是不是需要忽略的。比如 Mac OS X 开发,有了垃圾回收就不理会retain,release 这些函数了。
- 检测target 是不是nil 对象。ObjC 的特性是允许对一个 nil对象执行任何一个方法不会 Crash,因为会被忽略掉。
- 如果上面两个都过了,那就开始查找这个类的 IMP,先从 cache 里面找,若可以找得到就跳到对应的函数去执行。
- 如果在cache里找不到就找一下方法列表methodLists。
- 如果methodLists找不到,就到超类的方法列表里寻找,一直找,直到找到NSObject类为止。
- 如果还找不到,Runtime就提供了如下三种方法来处理:动态方法解析、消息接受者重定向、消息重定向,这三种方法的调用关系如下图:
动态方法解析(Dynamic Method Resolution)
所谓动态解析,我们可以理解为通过cache和方法列表没有找到方法时,Runtime为我们提供一次动态添加方法实现的机会,主要使用到有三个方法:
//OC方法:
//类方法未找到时调起,可于此添加类方法实现
+ (BOOL)resolveClassMethod:(SEL)sel
//实例方法未找到时调起,可于此添加实例方法实现
+ (BOOL)resolveInstanceMethod:(SEL)sel
//Runtime方法:
/**
运行时方法:向指定类中添加特定方法实现的操作
@param cls 被添加方法的类
@param name selector方法名
@param imp 指向实现方法的函数指针
@param types imp函数实现的返回值与参数类型
@return 添加方法是否成功
*/
BOOL class_addMethod(Class _Nullable cls,
SEL _Nonnull name,
IMP _Nonnull imp,
const char * _Nullable types)
使用实例解析:
@interface RuntimeTestManager : NSObject
+ (void)test01;
- (void)test02;
@end
#import "RuntimeTestManager.h"
#import
@implementation RuntimeTestManager
+ (BOOL)resolveClassMethod:(SEL)sel {
if (sel == @selector(test01)) {
class_addMethod(object_getClass(self), sel, class_getMethodImplementation(object_getClass(self), @selector(test01_st)), "v@");
return true; ///如果返回false,则会走消息接收者重定向,详情见下面的消息接收者重定向
}
return [class_getSuperclass(self) resolveClassMethod:sel];
}
+ (BOOL)resolveInstanceMethod:(SEL)sel {
if (sel == @selector(test02)) {
class_addMethod([self class], sel, class_getMethodImplementation([self class], @selector(test02_st)), "v@");
return true; ///如果返回false,则会走消息接收者重定向,详情见下面的消息接收者重定向
}
return [super resolveInstanceMethod:sel];
}
+ (void)test01_st {
NSLog(@"+++++++++++");
}
- (void)test02_st {
NSLog(@"-----------");
}
@end
调用以及执行结果
///调用
RuntimeTestManager *runtime = [[RuntimeTestManager alloc] init];
[RuntimeTestManager test01];
[runtime test02];
///执行结果
2018-11-27 14:31:43.340386+0800 Runtime[7423:2700369] +++++++++++
2018-11-27 14:31:43.340422+0800 Runtime[7423:2700369] -----------
注意
- 我们注意到class_addMethod方法中的特殊参数“v@”,具体可参考这里
- 成功使用动态方法解析还有个前提,那就是我们必须存在可以处理消息的方法,比如上述代码中的
test01_st
与test02_st
消息接收者重定向
如果上文中的动态方法解析的两个方法resolveInstanceMethod :
和resolveClassMethod:
返回false,消息发送机制就进入了消息转发(Forwarding)的阶段。我们可以使用Runtime通过下面的方法替换消息接收者的为其他对象,从而保证程序的继续执行。
///重定向类方法的消息接收者,返回一个类
+ (id)forwardingTargetForSelector:(SEL)aSelector
///重定向实例方法的消息接受者,返回一个实例对象
- (id)forwardingTargetForSelector:(SEL)aSelector
实例介绍:
#import "ViewController.h"
#import "RuntimeTestManager.h"
#import
@interface ViewController ()
@property(nonatomic, strong) RuntimeTestManager *runtimeManager;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
[ViewController performSelector:@selector(test01) withObject:nil];
RuntimeTestManager *runtime = [[RuntimeTestManager alloc] init];
self.runtimeManager = runtime;
[self performSelector:@selector(test02) withObject:nil];
}
///重定向类方法的消息接收者,返回一个类
+ (id)forwardingTargetForSelector:(SEL)aSelector {
if (aSelector == @selector(test01)) {
return [RuntimeTestManager class];
}
return [super forwardingTargetForSelector:aSelector];
}
///重定向实例方法的消息接受者,返回一个实例对象
- (id)forwardingTargetForSelector:(SEL)aSelector {
if (aSelector == @selector(test02)) {
return self.runtimeManager;
}
return [super forwardingTargetForSelector:aSelector];
}
@end
注意
-
RuntimeTestManager
就是一个普通的类,声明并实现一个实例方法和类方法。 - 动态方法解析阶段返回NO时,我们可以通过
forwardingTargetForSelector
可以修改消息的接收者,该方法返回参数是一个对象,如果这个对象是非nil,非self,系统会将运行的消息转发给这个对象执行。否则,继续查找其他流程。
执行结果
2018-11-27 14:58:47.122145+0800 Runtime[7427:2708367] +++++++++++
2018-11-27 14:58:47.122187+0800 Runtime[7427:2708367] -----------
消息重定向
如果在上一步还不能处理未知消息,则唯一能做的就是启用完整的消息转发机制了。
首先它会发送-methodSignatureForSelector:
消息获得函数的参数和返回值类型。如果-methodSignatureForSelector:
返回nil ,Runtime则会发出 -doesNotRecognizeSelector:
消息,程序这时也就挂掉了。如果返回了一个函数签名,Runtime就会创建一个NSInvocation 对象并发送-forwardInvocation:
消息给目标对象。
实例介绍:
/////需要从这个方法中获取的信息来创建NSInvocation对象,因此我们必须重写这个方法,为给定的selector提供一个合适的方法签名。
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
if (aSelector == @selector(test01)) {
return [NSMethodSignature signatureWithObjCTypes:"v@:"];
}
return [super methodSignatureForSelector:aSelector];
}
- (void)forwardInvocation:(NSInvocation *)anInvocation {
SEL sel = anInvocation.selector;
RuntimeTestManager *runtimeManager = [[RuntimeTestManager alloc] init];
if ([runtimeManager respondsToSelector:sel]) {
///若可以响应,则将消息转发给其他对象处理
[anInvocation invokeWithTarget:runtimeManager];
} else {
///若仍然无法响应,则报错:找不到响应方法
[self doesNotRecognizeSelector:sel];
}
}
调用方法以及执行顺序
///调用方法
[self performSelector:@selector(test01) withObject:nil];
///执行结果
2018-11-27 15:30:21.859604+0800 Runtime[7465:2722857] +++++++++++
总结:
从以上的代码中就可以看出,forwardingTargetForSelector仅支持一个对象的返回,也就是说消息只能被转发给一个对象,而forwardInvocation可以将消息同时转发给任意多个对象,这就是两者的最大区别。
虽然理论上可以重载doesNotRecognizeSelector函数实现保证不抛出异常(不调用super实现),但是苹果文档着重提出“一定不能让这个函数就这么结束掉,必须抛出异常”。(If you override this method, you must call super or raise an invalidArgumentException exception at the end of your implementation. In other words, this method must not return normally; it must always result in an exception being thrown.)
forwardInvocation甚至能够修改消息的内容,用于实现更加强大的功能。