Objective-C:消息机制

Page content

OC的消息发送石沉大海,就容易崩溃

消息发送

Objective-C 中 的方法调用,其本质是消息的发送,在编译时程序不查找要执行的函数,必须等到真正运行时,程序才查找要执行的函数。

为这一特性提供支撑的,就是消息发送机制。

在OC中调用方法,以如下形式:

~~~objective-c
#import <Foundation/Foundation.h>
#import "Obj.h"
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Obj * obj = [[Obj alloc] init];
        [obj doSomething];
    }
    return 0;
}
~~~

通过对象名 obj 调用方法 doSomething,我们将main.m编译链接处理一下: 打开终端,进入main.m所在文件夹,输入clang编译命令:clang -rewrite-objc main.m,查看编译生成的main文件(拖拽到最下方,查看main函数):

~~~objective-c
// - (void)doSomething;
/* @end */
int main(int argc, const char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 
        Obj * obj = ((Obj *(*)(id, SEL))(void *)objc_msgSend)((id)((Obj *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("Obj"), sel_registerName("alloc")), sel_registerName("init"));
        ((void (*)(id, SEL))(void *)objc_msgSend)((id)obj, sel_registerName("doSomething"));
    }
    return 0;
}
static struct IMAGE_INFO { unsigned version; unsigned flag; } _OBJC_IMAGE_INFO = { 0, 2 };
~~~

可知,OC的方法调用,会在编译链接后,被处理为:

~~~objective-c
objc_msgSend(obj,@selector(doSomething));
~~~

我们可以直接使用底层的C函数来进行方法调用:

  1. 打开编译器Build Settings 将消息发送代码检查Enable Strict Checking of objc_msgSend Calls选项置为NO;
  2. 在Obj类中声明方法:- (void)doSomething:(NSString *)str;
  3. 导入头文件#import ,调用:
~~~objective-c
objc_msgSend(obj, @selector(doSomething:), @"some obj");
~~~

打印结果:doSomething with str : some obj

对于继承自NSObject的obj对象(不特指obj):

~~~objective-c
@interface NSObject  {
    Class isa  OBJC_ISA_AVAILABILITY;
}
~~~

存在一个Class类型的isa指针,该指针的类型包含的信息就是这篇文章的重点

类结构与isa指针

isa指针的Class类型结构:

~~~objective-c
typedef struct objc_class *Class;
struct objc_class {
  Class isa;           // 实例的isa指向类对象,类对象的isa指向元类
  Class super_class ;  // 指向其父类
  const char *name ;   // 类名
  long version ;       // 类的版本信息,初始化默认为0,可以通过runtime函数class_setVersion和class_getVersion进行修改、读取
  long info;           // 一些标识信息,如CLS_CLASS (0x1L) 表示该类为普通 class ,其中包含对象方法和成员变量;CLS_META (0x2L) 表示该类为 metaclass,其中包含类方法;
  long instance_size ; // 该类的实例变量大小(包括从父类继承下来的实例变量);
  struct objc_ivar_list *ivars;           // 用于存储每个成员变量的地址
  struct objc_method_list **methodLists ; // 与 info 的一些标志位有关,如CLS_CLASS (0x1L),则存储对象方法,如CLS_META (0x2L),则存储类方法;
  struct objc_cache *cache;               // 指向最近使用的方法的指针,一种优化,用于提升效率;调用过的方法存入缓存列表,下次调用先找缓存
  struct objc_protocol_list *protocols;   // 存储该类遵守的协议
}
~~~

对象的isa指针指向类,类中的isa指针指向元类,元类中的指针指向NSObject,这也是NSObject类是所有类的根类的原因。

在该结构中,我们可以看到ivars 和methodLists字段。对于普通类,ivars中存储成员变量的信息,methodLists中存储对象方法信息;

对于静态类,ivars中存储静态成员变量的信息,methodLists中存储类方法信息。

消息发送的过程

我们回到运行时对方法调用的处理中:

objc_msgSend(obj,@selector(doSomething));其中@selector() 是SEL类型的方法选择器,SEL本身是一个int类型的地址,存放方法名字,每一个方法对应一个SEL,SEL是根据方法名字生成的,这也决定了OC中的方法不可重名。

SEL的主要作用是快速通过方法名doSomething查找对应方法的函数指针,继而进行调用该函数。

总结消息发送的过程:    

  1. [obj doSomething]; 在编译时被处理为objc_msgSend(obj,@selector(doSomething));
  2. 对于 objc_msgSend 函数,先通过obj的isa指针找到其class,在Class中查找函数列表methodLists;
  3. 根据 SEL 在 methodLists 中找到 imp 指针(函数指针),执行函数;
  4. 没有找到对应的imp指针,则根据 class 中的 super_class 指针,去父类中查找,继而上述到元类,根类,若根类中无此函数信息,则抛出 unrecognized selector sent to xxx 的异常,如果找到该函数指针,则执行。
  5. 在 isa ——> methodLists 之间,存有缓存优化,即有 cache 的实现,根据 isa 在 Class 中先去 cache 通过 SEL 查找对应的 method,若 cache 中未找到,再去 methodLists 中查找,若找到,则将 method 加入到 cache 中,节约了消息发送的时间成本。(猜测 cache 中 method 列表是以SEL为key通过hash表来存储的,这样能提高函数查找速度)。

以上,就是OC的消息发送特性,它是Runtime的重要组成,决定了OC区别于C的动态特性。

附:

  1. 事件监听本质发送消息.但是发送消息是OC的特性,如果Swift中将一个函数声明称private,那么该函数不会被添加到方法列表中,如果在private前面加上@objc,那么该方法依然会被添加到方法列表中。
  2. 重写父类的方法,并没有覆盖掉父类的方法,只是在当前类对象中找到了这个方法后就不再去父类中找。
  3. 调用已经重写过的方法的父类的实现,使用super这个编译器标识,其作用是在运行时跳过在当前的类对象中寻找方法的过程。  

消息发送过程中的拦截调用

在消息发送过程中,如果没有找到method,再抛出异常之前,运行时还会进一步处理,这不处理即:转向拦截调用。 拦截调用,通过重写NSObject提供的四个方法来进行处理:

~~~objective-c
+ (BOOL)resolveClassMethod:(SEL)sel;
~~~

当调用一个不存在的类方法时,会调用该方法,返回值默认为NO,重写为:加上自己的处理然后返回YES;

~~~objective-c
+ (BOOL)resolveInstanceMethod:(SEL)sel;
~~~

该方法同上,区别在于处理的是实例方法;

~~~objective-c
- (id)forwardingTargetForSelector:(SEL)aSelector;
~~~

该方法将所调用的未找到的method,重定向到另一个声明了该method的类,需要返回拥有该方法的实例;

~~~objective-c
- (void)forwardInvocation:(NSInvocation *)anInvocation;
~~~

该方法将所调用的未找到的method,打包成NSInvocation(即anInvocation),在该方法中进行自定义处理后,调用invokeWithTarget:方法,让另一个实例来触发;

通过这四个方法,我们有机会对“未找到消息接受者”导致闪退进行处理,Runtime给我们提供了机会,也意味着“最后通牒”。