Objective-C:消息机制
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函数来进行方法调用:
- 打开编译器Build Settings 将消息发送代码检查Enable Strict Checking of objc_msgSend Calls选项置为NO;
- 在Obj类中声明方法:- (void)doSomething:(NSString *)str;
- 导入头文件#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查找对应方法的函数指针,继而进行调用该函数。
总结消息发送的过程:
- [obj doSomething]; 在编译时被处理为objc_msgSend(obj,@selector(doSomething));
- 对于 objc_msgSend 函数,先通过obj的isa指针找到其class,在Class中查找函数列表methodLists;
- 根据 SEL 在 methodLists 中找到 imp 指针(函数指针),执行函数;
- 没有找到对应的imp指针,则根据 class 中的 super_class 指针,去父类中查找,继而上述到元类,根类,若根类中无此函数信息,则抛出 unrecognized selector sent to xxx 的异常,如果找到该函数指针,则执行。
- 在 isa ——> methodLists 之间,存有缓存优化,即有 cache 的实现,根据 isa 在 Class 中先去 cache 通过 SEL 查找对应的 method,若 cache 中未找到,再去 methodLists 中查找,若找到,则将 method 加入到 cache 中,节约了消息发送的时间成本。(猜测 cache 中 method 列表是以SEL为key通过hash表来存储的,这样能提高函数查找速度)。
以上,就是OC的消息发送特性,它是Runtime的重要组成,决定了OC区别于C的动态特性。
附:
- 事件监听本质发送消息.但是发送消息是OC的特性,如果Swift中将一个函数声明称private,那么该函数不会被添加到方法列表中,如果在private前面加上@objc,那么该方法依然会被添加到方法列表中。
- 重写父类的方法,并没有覆盖掉父类的方法,只是在当前类对象中找到了这个方法后就不再去父类中找。
- 调用已经重写过的方法的父类的实现,使用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给我们提供了机会,也意味着“最后通牒”。