【Objective-C Runtime】类和对象的数据结构和消息传递机制

类与对象基础数据结构

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的方法列表中查找。

Class & 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

NSObjectforwardInvocation:方法实现只是简单调用了doesNotRecognizeSelector:方法,它不会转发任何消息。这样,如果不在以上所述的三个步骤中处理未知消息,则会引发一个异常。

完整的流程图如下:567

Objective-C消息转发完整流程图

NSObject类方法load和initialize的区别8

`+(void)load+(void)initialize
执行时机在程序运行后立即执行在类的方法第一次被调时执行
若自身未定义,是否沿用父类的方法?
分类中的定义全都执行,但后于类中的方法覆盖类中的方法,只执行一个

runtime调用+(void)load的时候,程序还没有建立其autorelease pool,所以那些会需要使用到autorelease pool的代码,都会出现异常。这一点是非常需要注意的,也就是说放在+(void)load中的对象都应该是alloc出来并且不能使用autorelease来释放。

由于Objective-C Runtime是开源的,大家要想了解具体细节的话,可以参看苹果官方公布的源码9


  1. Objective-C Runtime 运行时之一:类与对象 http://southpeak.github.io/2014/10/25/objective-c-runtime-1/
  2. Obj-C Optimization: The faster objc_msgSend http://www.mulle-kybernetik.com/artikel/Optimization/opti-9.html
  3. Objective-C Runtime 运行时之三:方法与消息 http://southpeak.github.io/2014/11/03/objective-c-runtime-3/
  4. “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
  5. 击鼓传花之消息转发 http://www.jianshu.com/p/9f0184190920
  6. 消息发送与转发机制原理 http://yulingtianxia.com/blog/2016/06/15/Objective-C-Message-Sending-and-Forwarding
  7. 神经病院Objective-C Runtime住院第二天——消息发送与转发 http://www.jianshu.com/p/4d619b097e20
  8. 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的文档中介绍的顺序一样:先执行类自身的实现,再执行分类中的实现。
  9. objc4 https://opensource.apple.com/tarballs/objc4/
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值