类与对象基础数据结构
Class
Objective-C类是由Class类型来表示的,它实际上是一个指向objc_class结构体的指针。它的定义如下:1
typedef struct objc_class *Class;
查看objc/runtime.h中objc_class结构体的定义如下:
struct objc_class {
Class isa OBJC_ISA_AVAILABILITY;
#if !__OBJC2__
Class super_class OBJC2_UNAVAILABLE; // 父类
const char *name OBJC2_UNAVAILABLE; // 类名
long version OBJC2_UNAVAILABLE; // 类的版本信息,默认为0
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;
这个定义中,下面几个字段是我们感兴趣的
isa:需要注意的是在Objective-C中,所有的类自身也是一个对象,这个对象的Class里面也有一个isa指针,它指向metaClass(元类),我们会在后面介绍它。
super_class:指向该类的父类,如果该类已经是最顶层的根类(如NSObject或NSProxy),则super_class为NULL。
cache:用于缓存最近使用的方法。一个接收者对象接收到一个消息时,它会根据isa指针去查找能够响应这个消息的对象。在实际使用中,这个对象只有一部分方法是常用的,很多方法其实很少用或者根本用不上。这种情况下,如果每次消息来时,我们都是methodLists中遍历一遍,性能势必很差。这时,cache就派上用场了。在我们每次调用过一个方法后,这个方法就会被缓存到cache列表中,下次调用的时候runtime就会优先去cache中查找,如果cache没有,才去methodLists中查找方法。这样,对于那些经常用到的方法的调用,但提高了调用的效率。
version:我们可以使用这个字段来提供类的版本信息。这对于对象的序列化非常有用,它可是让我们识别出不同类定义版本中实例变量布局的改变。
objc_object与id
objc_object
是表示一个类的实例的结构体,它的定义如下(objc/objc.h
):
struct objc_object {
Class isa OBJC_ISA_AVAILABILITY;
};
typedef struct objc_object *id;
@interface NSObject <NSObject> {
Class isa
}
objc_cache
上面提到了objc_class结构体中的cache字段,它用于缓存调用过的方法。这个字段是一个指向objc_cache结构体的指针,其定义如下:
struct objc_cache {
unsigned int mask /* total = mask + 1 */ OBJC2_UNAVAILABLE;
unsigned int occupied OBJC2_UNAVAILABLE;
Method buckets[1] OBJC2_UNAVAILABLE;
};
该结构体的字段描述如下:
mask:一个整数,指定分配的缓存bucket的总数。在方法查找过程中,Objective-C runtime使用这个字段来确定开始线性查找数组的索引位置。指向方法selector的指针与该字段做一个AND位操作(index = (mask & selector))。这可以作为一个简单的hash散列算法。
occupied:一个整数,指定实际占用的缓存bucket的总数。
buckets:指向Method数据结构指针的数组。这个数组可能包含不超过mask+1个元素。需要注意的是,指针可能是NULL,表示这个缓存bucket没有被占用,另外被占用的bucket可能是不连续的。这个数组可能会随着时间而增长。
在这篇文章”Obj-C Optimization: The faster objc_msgSend”2中看到了这样一段C版本的objc_msgSend的源码。
#include <objc/objc-runtime.h>
id c_objc_msgSend( struct objc_class /* ahem */ *self, SEL _cmd, ...)
{
struct objc_class *cls;
struct objc_cache *cache;
unsigned int hash;
struct objc_method *method;
unsigned int index;
if( self)
{
cls = self->isa;
cache = cls->cache;
hash = cache->mask;
index = (unsigned int) _cmd & hash;
do
{
method = cache->buckets[ index];
if( ! method)
goto recache;
index = (index + 1) & cache->mask;
}
while( method->method_name != _cmd);
return( (*method->method_imp)( (id) self, _cmd));
}
return( (id) self);
recache:
/* ... */
return( 0);
}
该源码中有一个do-while循环,这个循环就是在方法分发表里面查找method的过程。
元类(Meta Class)
当我们向一个对象发送消息时,runtime会在这个对象所属的这个类的方法列表中查找方法;而向一个类发送消息时,会在这个类的meta-class的方法列表中查找。
由于Subclass(meta)在横向上已经没有可以指向的对象了,所以它的isa指针统一指向纵向(继承关系)上的根meta class。而根meta class的isa则指向自己。
需要注意的是,Root class(class)的superclass指向是nil,Root class(meta)的superclass指向它的Root class (class)。
这里介绍几个runtime中的方法,还是看runtime源码:
Class object_getClass(id obj)
{
if (obj) return obj->getIsa();
else return Nil;
}
object_getClass()就是顺着isa的指向链找到对应的类。
+ (Class)class {
return self;
}
- (Class)class {
return object_getClass(self);
}
这是NSObject类里实例方法class与类方法class的实现,这里再强调一下:类方法是在meta class里的,类方法就是把自己返回,而实例方法中是返回实例isa的类,我们要验证这个isa的指向链的时候不能用这种方法,而要继续用object_getClass
方法,千万记住。
NSProxy
在OC的世界中,除了NSProxy类以外,所有的类都是NSObject的子类。在Foundation框架下,NSObject和NSProxy两个基类,定义了类层次结构中该类下方所有类的公共接口和行为。NSProxy是专门用于实现代理对象的类,这个类暂时本篇文章不提。这两个类都遵循了NSObject协议。在NSObject协议中,声明了所有OC对象的公共方法。
NSProxy类和NSObject同为OC里面的基类,但是NSProxy类是一种抽象的基类,无法直接实例化,可用于实现代理模式。它通过实现一组经过简化的方法,代替目标对象捕捉和处理所有的消息。NSProxy类也同样实现了NSObject的协议声明的方法,而且它有两个必须实现的方法。
- (void)forwardInvocation:(NSInvocation *)invocation;
- (nullable NSMethodSignature *)methodSignatureForSelector:(SEL)sel NS_SWIFT_UNAVAILABLE("NSInvocation and related APIs not available");
另外还需要说明的是,NSProxy类的子类必须声明并实现至少一个init方法,这样才能符合OC中创建和初始化对象的惯例。Foundation框架里面也含有多个NSProxy类的具体实现类。
方法调用流程
在Objective-C中,消息直到运行时才绑定到方法实现上。编译器会将消息表达式[receiver message]
转化为一个消息函数的调用,即objc_msgSend
。这个函数将消息接收者和方法名作为其基础参数,如以下所示:3
objc_msgSend(receiver, selector, arg1, arg2, ...)
当消息发送给一个对象时,objc_msgSend通过对象的isa指针获取到类的结构体,然后在方法分发表methodLists
里面查找方法的selector。如果没有找到selector,则通过objc_msgSend
结构体中的指向父类的指针找到其父类,并在父类的分发表里面查找方法的selector。依此,会一直沿着类的继承体系到达NSObject
类。一旦定位到selector,函数会就获取到了实现的入口点,并传入相应的参数来执行方法的具体实现。如果最后没有定位到selector,则会走消息转发流程。
为了加速消息的处理,运行时系统缓存使用过的selector及对应的方法的地址。
objc_msgSend有两个隐藏参数:
消息接收对象
方法的selector
这两个参数为方法的实现提供了调用者的信息。之所以说是隐藏的,是因为它们在定义方法的源代码中没有声明。它们是在编译期被插入实现代码的。
虽然这些参数没有显示声明,但在代码中仍然可以引用它们。我们可以使用self来引用接收者对象,使用_cmd来引用选择器。
消息转发流程4
动态方法解析
向当前对象的所属类发送resolveInstanceMethod:(针对实例方法)或resolveClassMethod(针对类方法)消息,检查是否动态向该类添加了方法。
/** 实例方法 */
+ (BOOL)resolveInstanceMethod:(SEL)sel;
/** 类方法 */
+ (BOOL)resolveClassMethod:(SEL)sel;
备援接受者
检查该对象是否实现了forwardingTargetForSelector:实例方法,将对象无法解读的某些选择子转交给其他对象来处理。如果对象实现了此方法,并返回一个非nil的结果,则这个对象会作为消息的新接收者。
/** 类方法 */
+ (id)forwardingTargetForSelector:(SEL)sel;
/** 实例方法 */
- (id)forwardingTargetForSelector:(SEL)sel;
完整消息转发
经过上述两步之后,如果还是无法处理选择子,则启动完整的消息转发机制。我们需要重写methodSignatureForSelector:
和forwardInvocation:
实例方法。runtime发送 methodSignatureForSelector:
消息获取选择子对应的方法签名,即参数与返回值的类型信息。runtime则根据方法签名创建描述该消息的NSInvocation
,以创建的NSInvocation
对象作为参数,向当前对象发送forwardInvocation:
消息。forwardInvocation:
方法定位能够响应封装在此NSInvocation
中的消息的对象。此NSInvocation
对象将会保留调用结果,运行时系统会提取这一结果并将其发送到消息的原始发送者。
此步骤会调用下列方法来转发消息:
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
- (void)forwardInvocation:(NSInvocation *)invocation
NSObject
的forwardInvocation:
方法实现只是简单调用了doesNotRecognizeSelector:
方法,它不会转发任何消息。这样,如果不在以上所述的三个步骤中处理未知消息,则会引发一个异常。
NSObject类方法load和initialize的区别8
` | +(void)load | +(void)initialize |
---|---|---|
执行时机 | 在程序运行后立即执行 | 在类的方法第一次被调时执行 |
若自身未定义,是否沿用父类的方法? | 否 | 是 |
分类中的定义 | 全都执行,但后于类中的方法 | 覆盖类中的方法,只执行一个 |
runtime调用
+(void)load
的时候,程序还没有建立其autorelease pool,所以那些会需要使用到autorelease pool的代码,都会出现异常。这一点是非常需要注意的,也就是说放在+(void)load
中的对象都应该是alloc
出来并且不能使用autorelease
来释放。
由于Objective-C Runtime是开源的,大家要想了解具体细节的话,可以参看苹果官方公布的源码9。
- Objective-C Runtime 运行时之一:类与对象 http://southpeak.github.io/2014/10/25/objective-c-runtime-1/ ↩
- Obj-C Optimization: The faster objc_msgSend http://www.mulle-kybernetik.com/artikel/Optimization/opti-9.html ↩
- Objective-C Runtime 运行时之三:方法与消息 http://southpeak.github.io/2014/11/03/objective-c-runtime-3/ ↩
- “Objective-C Runtime Programming Guide” https://developer.apple.com/library/content/documentation/Cocoa/Conceptual/ObjCRuntimeGuide/Introduction/Introduction.html#//apple_ref/doc/uid/TP40008048-CH1-SW1 ↩
- 击鼓传花之消息转发 http://www.jianshu.com/p/9f0184190920 ↩
- 消息发送与转发机制原理 http://yulingtianxia.com/blog/2016/06/15/Objective-C-Message-Sending-and-Forwarding ↩
- 神经病院Objective-C Runtime住院第二天——消息发送与转发 http://www.jianshu.com/p/4d619b097e20 ↩
- Objective C类方法load和initialize的区别 http://www.cnblogs.com/ider/archive/2012/09/29/objective_c_load_vs_initialize.html
+(void)load
在main()执行前加载,
+(void)initialize
在向class / instances of class / subclass / instances of subclass 发送第一个消息(除了+load, +initialize之外)时调用。但是很明显的不同在于,如果类自身的实现和分类(Category)都实现了这两个方法,只有最后一个分类的+(void)initialize
执行,类自身实现和其他分类中的该方法都被隐藏。而对于+(void)load
,类自身实现和分类中的该方法都会被执行,并且正如Apple的文档中介绍的顺序一样:先执行类自身的实现,再执行分类中的实现。 ↩ - objc4 https://opensource.apple.com/tarballs/objc4/ ↩