runtime
首先感谢下列关于runtime的文章的作者,本文章是基于这些巨人的肩膀:《Objective-C 中的类和对象》和《Objective-C 中的消息与消息转发》、《category的秘密》、《objc kvo简单探索》、《重识 Objective-C Runtime - Smalltalk 与 C 的融合》、《重识 Objective-C Runtime - 看透 Type 与 Value》
runtime,运行时,意思是“在运行的时候”。Object-C是一门动态语言:它总是尽可能在运行时做出决定,而不是在编译和链接的时候。所以这意味着Object-C不仅需要一个编译器,还需要一个系统来在运行时做决定,这个系统可以看作是Object-C的操作系统,Object-C是基于这个系统来工作的。我们将这个系统称为运行时系统,下面我们要说的runtime,指的就是这个运行时系统。
简而言之,Object-C背后需要一个运行时系统(runtime)来工作,它的动态特性,也是通过这个运行时系统实现的。
一般来说,我们不需要和runtime打交道,我们只需要编写和编译Object-C源代码,runtime就会自动在背后默默地为我们工作,你几乎感觉不到runtime的存在。其实我们可以从以下三个地方“看到”runtime:
-
runtime是一个有公开接口的动态库,由一些数据结构和函数的集合组成,这些数据结构和函数的声明头文件在/usr/include/objc中。只要你导入<objc/runtime.h>等头文件就能直接调用runtime中的函数;
-
NSObject的方法。NSObject的方法其实是在间接地调用runtime中的函数;
-
Object-C源代码。这个是看不见的:当编译Object-C源代码时,编译器最终会将Object-C“翻译”成C,“翻译”的规则就是由runtime规定的。
类和对象
类
我们知道,Object-C最终会被编译器“翻译”成C。其实,Object-C中类就是C中的结构体。在runtime的头文件<objc/runtime.h>中我们可以看到一个类是怎么定义的:
struct objc_class {
Class isa OBJC_ISA_AVAILABILITY;
#if !__OBJC2__
Class super_class OBJC2_UNAVAILABLE;
const char *name OBJC2_UNAVAILABLE;
long version OBJC2_UNAVAILABLE;
long info OBJC2_UNAVAILABLE;
long instance_size OBJC2_UNAVAILABLE;
struct objc_ivar_list *ivars OBJC2_UNAVAILABLE;
struct objc_method_list **methodLists OBJC2_UNAVAILABLE;
struct objc_cache *cache OBJC2_UNAVAILABLE;
struct objc_protocol_list *protocols OBJC2_UNAVAILABLE;
#endif
} OBJC2_UNAVAILABLE;
此外为了方便,我们可以使用Class来表示一个指向类的指针,Class的定义可以在头文件<objc/objc.h>中看到:
typedef struct objc_class *Class;
Object-C源代码经过编译后,会将Object-C中的类“翻译”成一个C结构体全局变量,这个结构体和objc_class的结构是一样的:成员ivars存放着该类的所有实例变量,成员methodLists存放着该类的所有方法,成员protocols存放着该类遵守的所有协议...也就是说,在运行时,一个类就是一个objc_class结构体全局变量。
对象
首先我们创建一个类,类名为SampleClass,它继承自NSObject,而且只有一个property:
@interface SampleClass : NSObject
@property (assign,nonatomic) NSInteger param;
@end
@implementation SampleClass
@end
然后我们可以这样创建一个对象:
SampleClass *obj = [[SampleClass alloc] init];
在这里简单说明一下alloc方法和init方法的作用:alloc方法会根据SampleClass类定义的大小,使用C中的calloc函数在在内存的堆区划出一片内存。alloc方法执行后,我们已经得到一个对象了,然后就可以调用init方法对对象执行一些初始化操作。
一个对象的大小是由它的类决定的,我们可以通过runtime的头文件<objc/runtime.h>中的一个函数来获取:
unsigned long size = class_getInstanceSize([SampleClass class]);//size == 16
在面向对象的语言中,例如C++:一个对象的大小等于它所有的成员变量的大小之和。其实Object-C也是一样的。我们的SampleClass中只有一个属性param,所以它只有一个实例变量_param,而且它的大小为8个字节,所以SampleClass的对象的大小应该是8个字节,但事实上SampleClass的实例变量的大小是16个字节。
那么,多出来的8个字节是哪里来的呢?在runtime的头文件<objc/objc.h>中我们可以看到一个对象的定义:
struct objc_object {
Class isa OBJC_ISA_AVAILABILITY;
};
runtime中规定,一个对象的首地址是一个isa指针。isa指针是Class类型的,它指向这个对象所属的类。上面那多出来的8个字节,是属于isa指针的。
那么究竟什么是对象?简单粗暴:对象就是根据它的类定义的大小在内存的堆区划出来的一片空间,而且这片空间的首地址是指向该对象所属的类的指针。
拓展:
Block其实也是一个结构体,它的第一个元素是一个isa指针,所以Block通常也可以看做一个对象。
值得了解的一个细节是,经过编译后,我们可以发现类名其实是objc_object的一个别名,以SampleClass为例:
typedef struct objc_object SampleClass;
另外,在runtime的头文件<objc/objc.h>中我们发现了神一般的id指针的定义:
typedef struct objc_object *id;
所以没什么魔法:无论id
还是SampleClass *
...任何一个指向对象的指针,其实都是struct objc_object *
。
访问实例变量
现在我们知道,对象就是堆中的一片内存空间。目前,我们除了知道对象的内存空间的首地址是isa指针,剩下的一无所知。那么我们如何访问对象中的实例变量呢?答案是获取实例变量在对象的内存空间中的位置,只要我们获取到实例变量的位置,我们就能访问这个实例变量了。
那么,我们如何获取到一个实例变量在对象内存空间中的位置呢?在对象的类中,记录着所有实例变量的信息。在一个类中是用objc_ivar这个结构体来定义一个实例变量的。在runtime的头文件<objc/runtime.h>中我们可以找到objc_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
}
为了方便在<objc/runtime.h>中还给objc_ivar *指针起了别名:
typedef struct objc_ivar *Ivar;
objc_ivar中除了一些实例变量的基本信息例如名字(ivar_name)、类型(ivar_type)之外,还有这个实例变量在对象的内存空间中,相对于首地址的偏移量(ivar_offset)。只要我们获取到实例变量的偏移量,就能在对象中访问这个实例变量了。
下面演示一下如何获取和修改SampleClass的对象的实例变量_param:
SampleClass *obj = [[SampleClass alloc] init];
NSLog(@"%ld",obj.param);//输出:0
//在SampleClass类中获取到目标实例变量"_param"
Ivar ivar = class_getInstanceVariable([obj class], "_param");
//获取到实例变量"_param"在对象的内存空间中的偏移量
int offset = (int)ivar_getOffset(ivar);
NSLog(@"%d",offset);//实例变量"_param"的偏移量是8
//通过偏移量找到"_param"的位置,并且将"_param"的值修改为9527
*(NSInteger *)( (char *)obj + offset ) = 9527;
NSLog(@"%ld",obj.param);//输出:9527
此外值得注意的是,在继承链条中,当前类不能获取到从父类继承过来的实例变量,要想获取到父类中声明的实例变量,只能切换到该父类才行。
//定义A、B两个类,B继承自A,两个类中都声明了一个属性
@interface A : NSObject
@property (assign,nonatomic) NSInteger param1;
@end
@implementation A
@end
@interface B : A
@property (assign,nonatomic) NSInteger param2;
@end
@implementation B
@end
//分别遍历A和B中的所有实例变量
unsigned int outCount;
Ivar *ivars = class_copyIvarList([A class], &outCount);//outCount == 1
for (int i = 0; i < outCount; i++) {
Ivar ivar = ivars[i];
NSLog(@"%s",ivar_getName(ivar));//_param1
}
ivars = class_copyIvarList([B class], &outCount);//outCount == 1
for (int i = 0; i < outCount; i++) {
Ivar ivar = ivars[i];
NSLog(@"%s",ivar_getName(ivar));//_param2
}
在上述例子中,类B的对象有两个属性,但是类B中只声明了一个属性,所以在遍历类B中的实例变量时,也只能获取到一个。
消息
动态绑定
现在我们已经从runtime的角度了解什么是类,什么是对象和如何访问对象中的实例变量了,那么在runtime中一个方法是如何被调用的呢?不急,先为我们的SampleClass添加一个方法:
@interface SampleClass : NSObject
@property (assign,nonatomic) NSInteger param;
- (void)foo:(int)aValue;
@end
@implementation SampleClass
- (void)foo:(int)aValue{
self.param += aValue;
NSLog(@"%ld",self.param);
}
@end
大家有没有想过,为什么我们可以在一个方法中访问到self?它到底是从哪儿来的?我们先来看一下将Object-C“翻译”成C时,一个类中定义的方法是怎么“翻译”的,如上面的foo:方法为例,它最终会被“翻译”成一个C全局函数:
void foo(id self,SEL _cmd,int aValue){
//...
}
真相大白:原来一个方法中还有两个隐藏的参数:一个是id类型的self,代表着调用这个方法的对象;另一个是SEL类型的_cmd,是一个方法选择器。
方法“翻译”成C全局函数后,为了以后能够找到和调用该函数,runtime使用了一个结构体objc_method来记录该函数,然后保存到类(objc_class)的methodLists中。在<objc/runtime.h>中我们可以看到objc_method的定义:
struct objc_method {
SEL method_name OBJC2_UNAVAILABLE;
char *method_types OBJC2_UNAVAILABLE;
IMP method_imp OBJC2_UNAVAILABLE;
} OBJC2_UNAVAILABLE;
其中主要的有SEL类型的method_name和IMP类型的method_imp,IMP是指向上述的C全局函数的函数指针,它的定义如下:
#if !OBJC_OLD_DISPATCH_PROTOTYPES
typedef void (*IMP)(void /* id, SEL, ... */ );
#else
typedef id (*IMP)(id, SEL, ...);
#endif
现在我们知道:一个类的方法会被“翻译”成一个C全局函数,并且会被封装成结构体objc_method保存在类中。下面就来看看我们平时调用一个方法时,到底发生了什么事情:
SampleClass *receiver = [[SampleClass alloc] init];
[receiver foo:9527];
像[receiver foo:9527];
这样调用一个方法我们早已烂熟于心,我们称之为“发消息”,表示向receiver发送名为foo:的消息。其实所有Object-C中的“发消息”最终都会被编译为runtime中objc_msgSend函数的调用:
objc_msgSend(receiver, @selector(foo:),9527);
objc_msgSend函数可以在<objc/message.h>中找到,它的定义为:
id objc_msgSend(id self, SEL op, ...)
在函数objc_msgSend中,第一个参数表示消息发送的对象self,第二个参数是一个方法选择器op,后面的省略号表示该消息需要的其他参数。objc_msgSend函数做的事情,就是使用方法选择器op,从对象self所属的类中找到该消息的函数实现(IMP),然后调用它。
发送一个消息的过程中,最关键的无疑是通过方法选择器(SEL)从对象的类中找到方法的C函数指针(IMP),我们称这个过程为动态绑定。因为动态绑定机制,方法的调用会比直接的函数调用慢一点,为了加快速度,苹果还引入了缓存机制,寻找IMP的过程大概是这样的:先从类的缓存中找,然后从类的methodLists中找,如果都找不到,就去父类中找,如果都找不到,就会用系统函数指针_objc_msgForward代替IMP,_objc_msgForward会触发消息转发机制(后面会说),如何我们没有做消息转发功能,就会奔溃。动态绑定的过程如下:
[图片上传失败...(image-8e2fe5-1677916458017)]
类方法
我们是可以给一个类发送消息的:
[SampleClass alloc];
这里面又是什么黑科技呢?我们知道类是一个全局变量,再看一下objc_class的结构会发现,它的第一个元素是一个isa指针,也是一坨起始地值是一个isa指针的内存空间,根据对象的定义,类其实也可以看做是一个对象!那么,给一个类发送消息的原理跟给一个对象发送消息的原理是完全一样的。
所以:
[SampleClass alloc];
最终最终会被转化为:
objc_msgSend( (id)([SampleClass class]),sel_registerName("alloc") );
我们已经知道类也是一个对象,那么,类对象的类是什么呢?是元类(meta-class)。元类也是对象,元类对象的类是根元类,根元类也是对象,但为了停止无休止的循环,根元类对象的类是自己。假设我们有NyanCat : Cat : NSObject 这样一个继承树,画出图来就是这样子的:
[图片上传失败...(image-4bfcf-1677916458017)]
消息转发
当我们向一个对象发送一个不存在的消息时,会发生奔溃,但在在奔溃前,NSObject中提供了三个尝试添加或者转发该消息的方法,可以避免奔溃。
我们以SampleClass为例,向它的对象发送一个不存在的方法:
SampleClass *sender = [[SampleClass alloc] init];
[sender performSelector:sel_registerName("rock")];
首先,runtime会触发resolveInstanceMethod:
/resolveClassMethod:
方法,该允许用户在该方法中动态为该Class添加该消息的实现:
@interface SampleClass : NSObject
@end
@implementation SampleClass
void rock(id self,SEL _cmd){
printf("rock!!!\n");
}
+ (BOOL)resolveInstanceMethod:(SEL)sel{
if (sel == @selector(rock)) {
//动态地添加foo的方法实现
class_addMethod([self class], sel, (IMP)rock, "v@:");
return YES;
}
return [super resolveInstanceMethod:sel];
}
@end
如果没有使用上述方法解决问题,runtime会触发forwardingTargetForSelector:
方法,尝试将该消息转发给另一个对象:
@interface SampleClass : NSObject
@end
@implementation SampleClass
- (id)forwardingTargetForSelector:(SEL)aSelector{
if (aSelector == @selector(rock)) {
//将消息转发给另一个可以对象
FooMaster *master = [FooMaster new];
return master;
}
return [super forwardingTargetForSelector:aSelector];
}
@end
如果没有使用上述方法解决问题,runtime会触发methodSignatureForSelector:
方法,尝试获得一个方法签名。如果获取不到,则直接调用doesNotRecognizeSelector
抛出异常。如何可以获取到,会继续触发forwardInvocation:
方法,将上一步获取到的方法签名包装成Invocation
传入,然后就可以进行消息的转发了:
@interface SampleClass : NSObject
@end
@implementation SampleClass
- (void)forwardInvocation:(NSInvocation *)anInvocation{
if (anInvocation.selector == @selector(rock)) {
FooMaster *master = [FooMaster new];
[anInvocation invokeWithTarget:master];
}
else{
[super forwardInvocation:anInvocation];
}
}
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{
if (aSelector == @selector(rock)) {
NSMethodSignature *signature = [NSMethodSignature signatureWithObjCTypes:"v@:"];
return signature;
}
return [super methodSignatureForSelector:aSelector];
}
@end
如果没有使用上述任意一种方法来实现或者转发一个不存在的消息,那么就会奔溃。
类型编码
为了辅助runtime,编译器会将一个方法的返回值类型和参数类型编码成一个字符串,然后和方法选择器(SEL)绑定起来。例如上面的演示代码中的class_addMethod([self class], sel, (IMP)rock, "v@:");
和NSMethodSignature *signature = [NSMethodSignature signatureWithObjCTypes:"v@:"];
,其中的v@:
字符串就是一个类型编码,它记录着void rock(id self,SEL _cmd)
这个函数的返回值类型和参数类型:v
表示返回值是void
,@
表示第一个参数是id
类型的,:
表示第二个参数是SEL
类型的。
可以使用@encode()
来获取一个类型用什么字符来表示,它的使用类似于sizeof()
:
char *buf1 = @encode(int);
Runtime的应用
实现KVO
当对象A的属性name被对象B监听后,runtime会构造一个新的类,这个类继承自对象A的类,命名为NSKVONotifying_classNameOfA,并重写属性name的setter方法:
- (void)setName:(NSString *)name{
[self willChangeValueForKey:@"name"];
[super setName:name];
[self didChangeValueForKey:@"name];
}
willChangeValueForKey
和didChangeValueForKey
都可能会(会不会取决于监听的配置)触发对象B调用observeValueForKeyPath:ofObject:change:context:
方法,前者传递的是name的旧值,后者传递的是name的新值。
新的类构造好后,对象A变成新的类的实例。
动态创建一个新的类
setter、getter函数:
void setName(id self,SEL _cmd,NSString *name){
NSString *aux = [name copy];
Ivar ivar = class_getInstanceVariable([self class], "_name");
object_setIvar(self, ivar, aux);
}
NSString *name(id self,SEL _cmd){
Ivar ivar = class_getInstanceVariable([self class], "_name");
return object_getIvar(self, ivar);
}
使用runtime创建一个新类:
//创建一个新类,类名为CooSample
Class cls = objc_allocateClassPair([NSObject class], "CooSample", 0);
if (cls == NULL) {
NSLog(@"create new class failure");
return;
}
//为该类添加实例变量。
//只能在objc_allocateClassPair函数之后和objc_registerClassPair函数之前为一个类添加实例变量
class_addIvar(cls, "_name", sizeof(NSString *), 0, "@");
//正式注册类
objc_registerClassPair(cls);
//添加属性“name”
objc_property_attribute_t type = { "T", "@\"NSString\"" };
objc_property_attribute_t ownership = { "C", "" }; // C = copy
objc_property_attribute_t backingivar = { "V", "_name" };
objc_property_attribute_t attrs[] = {type,ownership,backingivar};
class_addProperty(cls, "name", attrs, 3);
//添加setter、getter方法
class_addMethod(cls, @selector(setName:), (IMP)setName, "v@:@");
class_addMethod(cls, @selector(name), (IMP)name, "v@:");
使用新创建的类:
Class cls = objc_getClass("CooSample");
id obj = [[cls alloc] init];
[obj setName:@"tom"];
NSLog(@"%@",[obj name]);
使用分类添加属性
下面的代码是一个NSObject的分类,它为NSObject添加了一个NSString类型的属性magicValue:
分类头文件:
@interface NSObject (addMagicValue)
@property (copy,nonatomic) NSString *magicValue;
@end
分类实现文件:
@implementation NSObject (addMagicValue)
- (void)setMagicValue:(NSString *)magicValue{
objc_setAssociatedObject(self, "magic_value", magicValue, OBJC_ASSOCIATION_COPY_NONATOMIC);
}
- (NSString *)magicValue{
return objc_getAssociatedObject(self, "magic_value");
}
@end
objc_setAssociatedObject有四个参数:第一个是要设置AssociatedObject的对象;第二个是一个key值,后面取回AssociatedObject时需要用到;第三个是AssociatedObject;第四个是关联的策略,是一个枚举值,详情可看源代码注释。
super
我们知道self是类的一个隐藏参数,每个方法的实现的第一个参数即为self。但是super并不是隐藏参数,它实际上只是一个”编译器标示符”,它负责告诉编译器,当调用方法时,去调用父类的方法,而不是本类中的方法。
所以使用super调用的方法,方法的调用者依然是self。
//下面的输出是一样的
NSLog(@"%@", NSStringFromClass([self class]));
NSLog(@"%@", NSStringFromClass([super class]));
isKindOfClass 与 isMemberOfClass
isKindOfClass
实例方法返回一个布尔值,反映该对象是否是该类的实例,或者是任何继承该类的类的实例。
isMemberOfClass
实例方法返回一个布尔值,反映该对象是否是该类的实例。
class
+ class
返回类对象。比如[CooPerson class]
返回的是CooPerson
类。
- class
返回该对象的类的类对象。比如:
CooPerson *tom = [CooPerson alloc] init];
Class cls = [tom class];//CooPerson类
load
+ load
是NSObject
的一个类方法,当一个类或者分类被添加到runtime后被调用;你可以实现这个方法在loading的时候执行一些类相关的特性。
另外:
-
一个类的的所有父类的
load
方法执行完毕后才会执行它自己的load
方法; -
一个分类所属的类的
load
方法执行完毕后才会执行它自己的load
方法。
分类的实现
分类也是一个结构体:
struct objc_category {
char * _Nonnull category_name OBJC2_UNAVAILABLE;
char * _Nonnull class_name OBJC2_UNAVAILABLE;
struct objc_method_list * _Nullable instance_methods OBJC2_UNAVAILABLE;
struct objc_method_list * _Nullable class_methods OBJC2_UNAVAILABLE;
struct objc_protocol_list * _Nullable protocols OBJC2_UNAVAILABLE;
}
分类是在runtime进行加载的,加载过程:
-
把分类的实例方法、协议添加到类的实例对象中原本存储的实例方法、协议列表 的前面 ;
-
把分类的类方法添加到类的元类上。
如此,保证了分类方法优先调用,注意,不是覆盖,而是共同存在在实例方法列表中,只是分类在前而已。
网友评论