引言
曾经觉得Objc特别方便上手,面对着Cocoa中大量API,只知道简单的查文档和调用。还记得当初学OC 时把[receiver message]
当成简单的方法调用,而无视了“发送消息”的深刻含义。其实[receiver message]
会被编译器转化为:
objc_msgSend(receiver, selector)
如果消息含有参数,则为:
objc_msgSend(receiver, selector, arg1, arg2,...)
如果消息能够找到对应的selector
,那么就相当于直接执行力接收者这个对象的特定方法;否则,消息要么被转发,或是临时向接收者动态添加这个selector
对应的实现内容,要么就干脆玩完崩溃掉。
现在可以看出[recevier message]
真的不是一个简简单单的方法调用。因为这只是在编译阶段确定了要向接收者发送message
这条消息,而receiver
将要如何响应这条消息,那就要看运行时发生的情况来决定了。
OC是一门动态语言,所以它总是想办法把一些决定从工作编译连接推迟到运行时,也就是说只有编译器是不够的,还需要一个运行时系统(runtime system)来执行编译后的代码。这就是Objective-C Runtime系统存在的意义,它是整个Objc运行框架的一块基石
与Runtime交互
Objc从三种不同层级上与Runtime系统进行交互,分别是通过OC源代码,通过Foundation框架的NSObject类定义的方法,通过runtime函数直接调用
Objective-C源代码
大部分情况下你就只管写你的OC代码就行了,runtime系统自动在幕后辛勤劳作着。
还记得引言的粒子吧,消息的执行会使用到一些编译器为实现动态语言特性儿创建的数据结构和函数,Objc中的类,方法和协议等在runtime中都有一些数据结构来定义,这些内容会在后面讲到。
NSObject的方法
Cocoa中大多数类都继承于NSObject类,也就自然继承了它的方法。最特殊的例外是NSProxy,它是一个抽象超类,它实现了一些消息转发有关的方法,可以通过继承它来实现一个其他类的替身类或者虚拟出一个不存在的类,说白了就是领导把自己展现给大家风光无限,但是把活儿都交给幕后小弟去干。
有的NSObject中的方法起到了抽象接口的作用,比如description发方法需要你重载它并为你定义的类提供描述内容。NSObject还有些方法能在运行时获得类的信息,并检查一些特性,比如class返回对象的类;isKindOfClass:和isMemberOfClass:则检测对象在制定的类集成体系中;respondsToSelector:检查对象能否响应制定的消息;conformToProtocol:检查对象是否实现了制定协议类的方法;methodForSelector:则返回制定方法实现的地址。
Runtime的函数
Runtime系统是一个由一系列函数和数据结构组成,具有公共接口的动态共享库。头文件存放于 /usr/include/objc
目录下。许多函数允许你用纯C代码来重复实现Objc中同样的功能。虽然有一些方法构成了NSObject类的基础,但是你在写Objc代码时一般不会直接用到这些函数的,除非是写一些Objc与其它语言的桥接或者是底层的debug工作。
Runtime基础数据结构
还记得引言中的objc_msgSend:
方法把,它的真身是这样的
id objc_msgSend(id self, SEL op, ...);
下面将会逐渐展开介绍一些属于,其实他们都对应着数据结构。
SEL
objc_msgSend
函数第二个参数类型为SEL,它是selector
在Objc中的表示类型(Swift中是Selector
类)。selector是方法选择器,可以理解为区分方法的ID ,而这个ID的数据结构是SEL:
typedef struct objc_selector *SEL;;
其实它就是个映射到方法的C字符串,你可以使用Objc编译器命令@selector()或者Runtime系统的sel_registerName函数来获取一个SEL类型的方法选择器。
不同类中相同名字的方法所对应的选择器是相同的,即使方法名字相同而变量类型不同也会导致他们具有形同的方法选择器,于是Objc中方法命名有时会带上参数类型(NSNumber 一对抽象工厂方法)。
id
objc_msgSend
第一个参数类型是id,它是一个指向类实例的指针:
typrdef struct objc_object *id
那么objc_object又是啥呢
typedef struct objc_class *Class;
/// Represents an instance of a class.
struct objc_object {
Class _Nonnull isa OBJC_ISA_AVAILABILITY;
};
struct objc_class {
Class _Nonnull isa OBJC_ISA_AVAILABILITY;
#if !__OBJC2__
Class _Nullable super_class OBJC2_UNAVAILABLE;
const char * _Nonnull name OBJC2_UNAVAILABLE;
long version OBJC2_UNAVAILABLE;
long info OBJC2_UNAVAILABLE;
long instance_size OBJC2_UNAVAILABLE;
struct objc_ivar_list * _Nullable ivars OBJC2_UNAVAILABLE;
struct objc_method_list * _Nullable * _Nullable methodLists OBJC2_UNAVAILABLE;
struct objc_cache * _Nonnull cache OBJC2_UNAVAILABLE;
struct objc_protocol_list * _Nullable protocols OBJC2_UNAVAILABLE;
#endif
} OBJC2_UNAVAILABLE;
objc_object结构体包含一个isa指针,类型为Class
Class
Class其实是一个指向objc_class结构体的指针,objc_class继承与objc_object,也就是说一个objc类本身同时也是一个对象,为了处理类和对象的关系,runtime库创建了一种叫做元类(Meta Class)的东西,类对象所属类型就叫做元类,它用来表述类对象本身所具备的元数据。类方法就定义于此处,因为这些方法可以理解成类队形的实例方法。每个类仅有一个类对象,而每个类对象仅有一个与之相关的元类。当你发出一个类似于[NSObject alloc]
的消息时,你事实上是把这个消息发送给了一个类对象(Class Object),这个类对象必须是一个元类的实例,而这个元类同时也是一个根元类(root mate class)的实例。所有的元类最终都指向根元类为其超类。所有的元类的方法列表都有能够响应消息的类方法。所以当[NSObject alloc]
这条消息发给类对象时候,objc_msgSend()
会去它的元类里面去查找能够响应消息的方法,如果找到了,然后对这个类对象执行方法调用。
上图实线是superclass指针,虚线是isa指针。有趣的是根元类的超类是NSObject,而isa指向了自己,而NSObject的超类为nil,也就是它没有超类。
可以看到运行时一个类还联系了它的超类指针,类名,成员变量,方法,缓存,还有附属的协议。
objc_cache
struct objc_cache {
unsigned int mask /* total = mask + 1 */ OBJC2_UNAVAILABLE;
unsigned int occupied OBJC2_UNAVAILABLE;
Method _Nullable buckets[1] OBJC2_UNAVAILABLE;
};
cache为方法调用的性能进行优化,通俗的讲,每当实例对象接收到一个消息时候,它不会直接在isa指向的类方法的方法列表中遍历查找能够响应消息的方法,效率太低了,而是有限在cache中查找。Runtime系统会把被调用的方法存在cache中,下次查找的时候效率更高。
Category
Category为现有的类提供了扩展性,它是objc_category结构体指针。
typedef struct objc_category *Category;
objc_category存储了类别中可以扩展的实例方法,类方法,协议,实例属性和类属性。
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;
}
并没有存放属性列表,所以category中不能添加属性
在APP启动加载镜像文件时,会在 _read_images 函数间接调用到 attachCategories 函数,完成向类中添加 Category 的工作。原理就是向 class_rw_t 中的 method_array_t, property_array_t, protocol_array_t 数组中分别添加 method_list_t, property_list_t, protocol_list_t 指针。之前讲过 xxx_array_t 可以存储对应 xxx_list_t 的指针数组。
Method
Mthod是一种代表类中的某个方法的类型
typedef struct objc_method *Method;
struct objc_method {
SEL _Nonnull method_name OBJC2_UNAVAILABLE;
char * _Nullable method_types OBJC2_UNAVAILABLE;
IMP _Nonnull method_imp OBJC2_UNAVAILABLE;
}
objc_method存储了方法名,方法类型和方法实现
- 方法名类型为SEL,前面提到过相同的方法名即使在不同类中定义,他们的方法选择器也是相同
- 方法类型types是个char指针,其实存储着方法的参数类型和返回值类型。
- imp指向了方法的实现,本质上是一个函数指针
Ivar
Ivar是一种表示类中实例变量的类型
typedef struct objc_ivar *Ivar;
struct objc_ivar {
char * _Nullable ivar_name OBJC2_UNAVAILABLE;
char * _Nullable ivar_type OBJC2_UNAVAILABLE;
int ivar_offset OBJC2_UNAVAILABLE;
#ifdef __LP64__
int space OBJC2_UNAVAILABLE;
#endif
}
objc_ivar可以根据实例查找其在类中的名字,也就是“反射”;
NSInteger numIvars = 0;
Ivar *ivars = class_copyIvarList([self class], &numIvars);
for (int i = 0; i < numIvars; i ++) {
Ivar ivar = ivars[i];
const char *name = ivar_getName(ivar);
const char *type = ivar_getTypeEncoding(ivar);
NSString *typeString = [NSString stringWithCString:type encoding:NSUTF8StringEncoding];
NSString *nameString = [NSString stringWithUTF8String:name];
NSLog(@"type === %@, name === %@", typeString, nameString);
}
class_copyIvarList函数获取的不仅有实例变量,还有属性。但是会在原本的属性前面加上一个下划线。
objc_property_t
@property标记了类中的属性,它是一个指向objc_property结构体的指针:
typedef struct objc_property *objc_property_t;
可以通过class_copyPropertyList和protocol_copyPropertyList方法来获取类和协议中的属性:
OBJC_EXPORT objc_property_t _Nonnull * _Nullable
class_copyPropertyList(Class _Nullable cls, unsigned int * _Nullable outCount)
OBJC_EXPORT objc_property_t _Nonnull * _Nullable
protocol_copyPropertyList(Protocol * _Nonnull proto,
unsigned int * _Nullable outCount)
返回值类型为指向指针的指针,因为属性列表是个数组,每个元素内容都是一个objc_property_t指针,而这两个函数返回的值是指向这个数组的指针。
unsigned int number = 0
objc_property_t *propertys = class_copyPropertyList([self class], &number);
for (int i = 0; i < number; i ++) {
objc_property_t property = propertys[i];
const char *name = property_getName(property);
const char *type = property_getAttributes(property);
NSString *nameStr = [NSString stringWithUTF8String:name];
NSString *typeStr = [NSString stringWithUTF8String:type];
NSLog(@"name === %@ type === %@", nameStr, typeStr);
}
IMP
typedef void (*IMP)(void /* id, SEL, ... */ );
IMP它是一个函数指针,是由编译器生成的,当你发起一个OC对象的之后,最终会执行的那段代码,就是由这个函数指定的。而IMP这个函数指针就指向了这个方法的实现。既然得到了执行某个实例方法的入口,我们就可以绕开消息传递阶段,直接执行方法。
IMP指向的方法与objc_msgSend函数类型相同,参数都包含了id和SEL类型,每个方法名都对应一个SEL类型的方法选择器,而每个实例对象中的SEL对应的方法名可定是唯一的,通过一组id和SEL就能确定唯一的方法实现,反之亦然。
消息
OC中发送消息使用[]把接收者和消息括起来,而直到运行时才会把消息和方法实现绑定。
objc_msgSend函数
OBJC_EXPORT void
objc_msgSend(void /* id self, SEL op, ... */ )
OBJC_AVAILABLE(10.0, 2.0, 9.0, 1.0, 2.0);
objc_msgSend从不返回数据,而是你的方法调用后返回了数据,消息发送步骤
1、检测这个selector是不是需要忽略的。比如Mac OS X开发,有了垃圾回收就不理会retain,release这些函数了。
2、检测这个target是不是nil对象。OC的特性是允许对一个人nil对象执行任何方法不会crash,因为会被忽略掉。
3、如果上面两个都过了,那就开始查找这个类的IMP,先从cache里面找,完了找到后就去跳到对应的函数去执行。
4、如果cache找不到就找一下方法分发表
5、如果分发表找不到就到超类的分发表去找,一直找,直到找到NSObject类位置。
6、如果还找不到就要开始进入动态方法解析了,后面会提到。
PS:这里面说的分发表其实就是Class中的方法列表,它将方法选择器和和方法实现地址联系起来。
其实编译器会根据情况在objc_msgSend、objc_msgSend_stret、objc_msgSendSuper、或者objc_msgSendSuper_stret四个方法中选择一个来调用。如果消息是传递给超类,那么会第阿勇名字带有super的函数;如果消息返回返回值是数据结构而不是简单值时,那么会调用名字带有‘stret’函数。
带‘super’的消息传递给超类,‘stret’可分为‘st’+‘ret’两部分,分别代表着‘struct’和‘return’;‘fpret’就是‘fp’+‘ret’分别代表着‘floating-point’和‘retain’。
方法中的隐藏函数
我们经常在方法中使用self关键字来引用实例本身,但是没有想过为什么self技能第阿勇当前方法对象。其实self的内容是在方法运行时偷偷的被动传入的。
当objc_msgSend找到方法对应的实现时,它将直接调用该方法,并将消息中所有的参数都传递给方法实现,同时它还将传递领个隐藏参数:
- 接收消息的对象(也就是self指向的内容)
- 方法选择器(_cmd指向的内容)
之所以说他们是隐藏的是因为在源代码的定义中并没有声明这两个参数。它们是在代码被编译时插入实现的。尽管这些参数没有被明确声明,在源代码中我们仍然可以引用它们,在虾米恩的例子中,self引用了接收者对象,而_cmd引用了方法本身的选择器:
- strange
{
id target = getTheReceiver();
SEL method = getTheMethod();
if ( target == self || method == _cmd )
return nil;
return [target performSelector:method];
}
当方法中super关键字接收到消息时,编译器会创建一个objc_super结构体
struct objc_super {
/// Specifies an instance of a class.
__unsafe_unretained _Nonnull id receiver;
/// Specifies the particular superclass of the instance to message.
#if !defined(__cplusplus) && !__OBJC2__
/* For compatibility with old objc-runtime.h header */
__unsafe_unretained _Nonnull Class class;
#else
__unsafe_unretained _Nonnull Class super_class;
#endif
/* super_class is the first class to search */
};
这个结构体指明了消息应该被传递给特定超类的定义。但receiver仍然是self本身,这点需要注意,因为我们想通过[super class]获取超类时,编译器只是将指向self的id指针和class的SEL传递给了objc_msgSendSuper函数,因为只有在NSObject类中才能找到class方法调用object_getClass(),接着调用objc_msgSend(objc_super->receiver, @selector(class)),传入的第一个参数是指向self的id指针,与调用[self class]相同,所以我们得到的永远都是self类型。
获取方法地址
在IMP提到过可以避开消息绑定而直接获取方法地址并调用方法。这种做法很少用,除非是需要持续大量重复调用某方法的极端情况,避免消息发送泛滥而直接调用该方法会更高效。
NSObject类中有个methodForSelector:实例方法,你可以用它来获取某个方法选择器对应的IMP,
FatherClass *father = [[FatherClass alloc] init];
IMP methodIMP = [father methodForSelector:@selector(isSELEqual)];
动态方法解析
你可动态的提供一个方法的实现,例如我们用@dynamic关键字在类的实现文件中修饰一个属性,这表明我们会为这个属性动态提供存取方法,也就是说编译器不会再默认为我们生成set和get方法,而需要我们动态提供。我们可以分别重载resolveInstanceMethod:和resolveClassMethod:分别添加实例方法实现和类方法实现。以为当Runtime系统在Cache和方法分发表中(包括超类)找不到要执行的方法时候,Runtime会调用resolveInstanceMethod:或resolveClassMethod:来给程序员一次动态添加方法实现的机会,我们需要用class_addMethod函数完成向特定类添加特定方法实现的操作:
void dynamicMethodIMP(id self, SEL _cmd) {
// implementation ....
}
@implementation MyClass
+ (BOOL)resolveInstanceMethod:(SEL)aSEL
{
if (aSEL == @selector(resolveThisMethodDynamically)) {
class_addMethod([self class], aSEL, (IMP) dynamicMethodIMP, "v@:");
return YES;
}
return [super resolveInstanceMethod:aSEL];
}
@end
上面的例子为resolveThisMethodDynamically动态添加了实现内容,也就是dynamicMethodIMP方法中的代码。其中“v@:”表示返回值和参数。
动态方法解析会在消息转发机制浸入之前。如果resolveInstanceMethod:或resolveClassMethod:方法被执行,动态方法解析器将会被首先基于一个提供该方法选择器对应的IMP机会。如果你想让该方法选择器被传送到转发机制,那么久让resolveInstanceMethod:返回NO。
实例对象、类对象、元类和isa指针
前面说过类对象所属的类型就叫做元类,[father class]和 [FatherClass class]都是获取其类对象,object_getClass(father)
object_getClass([FatherClass class])获取的是元类
object_getClass([father class])获取的是元类
FatherClass *father = [[FatherClass alloc] init]
NSLog(@"%d", [father class] == [FatherClass class]);//输出1
NSLog(@"%d", [father class] == object_getClass(father));//输出1
NSLog(@"%d", class_isMetaClass([FatherClass class]));//输出0
NSLog(@"%d", class_isMetaClass(object_getClass([FatherClass class])));//输出1
NSLog(@"%d",object_getClass(father) == object_getClass([father class]));//输出0
NSLog(@"%d", class_isMetaClass(object_getClass([father class])));//输出1
需要深刻理解[self class]与object_getClass(self)甚至object_getClass([self class])的关系,重点在于self的类型
1、当self为实例对象时,[self class]与object_getClass(self)等价,因为前者会调用后者。object_getClass([self class])得到元类。
2、当self为类对象时,[self class]返回值为自身,还是self。object_getClass(self) 与 object_getClass([self class]) 等价,返回元类。
消息转发
重定向
在消息转发机制执行之前,Runtime会给我们一次偷梁换柱的机会,即通过重载- (id)forwardingTargetForSelector:(SEL)aSelector方法替换消息的接受者为其它对象
- (id)forwardingTargetForSelector:(SEL)aSelector {
if (aSelector == @selector(transmitSonClass:)) {
return [[SonClass alloc] init];
}
return [super forwardingTargetForSelector:aSelector];
}
如果此方法返回nil或者self,就会进入消息转发机制forwardInvocation:,否则将向返回对象重新发送消息。
如果想要替换类方法,需要覆写 + (id)forwardingTargetForSelector:(SEL)aSelector 方法,并返回类对象:
转发
当动态方法解析不做处理返回NO时候,消息转发机制会被触发,这时候forwardInvocation:方法会被执行,我们可以重写这个方法来定义我们的转发逻辑
- (void)forwardInvocation:(NSInvocation *)anInvocation
{
if ([someOtherObject respondsToSelector:
[anInvocation selector]])
[anInvocation invokeWithTarget:someOtherObject];
else
[super forwardInvocation:anInvocation];
}
该消息的唯一参数就是NSInvocation类型的对象,该类型的对象封装了原始的消息和消息的参数,我们可以实现forwardInvocation:来对不能处理的消息进行默认处理,也可以将消息转发给其它对象而不会抛出错误。
这里需要注意的是参数anInvocation是从哪里来的?其实在forwardInvocation:消息发送前,Runtime系统会向对象发送methodSignatureForSelector:消息,并取到返回的方法签名用于生成NSInvocation对象。所以我们在重写forwardInvocation:的同事也要重写methodSignatureForSelector:方法,否则会抛出异常。
当一个对象由于没有响应的方法实现而无法响应某消息时,运行时系统将通过forwardInvocation:消息通知该对象。每个对象都是从NSObject类中继承了forwardInvocation:方法,然而,NSObject中的方法实现只是简单地调用了doesNotRecognizeSelector:。通过实现我们自己的forwardInvocation:方法,我们可以在该方法中降消息转发给其它对象。
forwardInvocation:方法就行一个不能识别的消息的分发中心,将这些消息转发给不同接收对象。或者他们也可以像一个运输站将所有消息都发送给同一个接受对象。它可以将消息翻译成另外一个消息,或者简单的吃掉某些消息,因此没有响应也没有错误。forwardInvocation:方法也可以对不同的消息提供同样的响应,这一起都取决于方法的具体实现。该方法所提供是将不同的对象链接到消息链的能力。
转发和多继承
转发和继承相似,可以用于为OC编程添加一些多继承效果。一个对象把消息转发出去,就好似它把另一个对象中的方法借过来或是“集成”过来一样。这使得不同继承体系分支下的两个类可以“继承”对方的方法,在上图中Warrior和Diplomat没有继承关系,但是Warrior将negotiate消息转发给Diplomat后,就好像Diplomat是Warrior的超类一样。
版权:http://yulingtianxia.com/blog/2014/11/05/objective-c-runtime/