Runtime最全总结
本系列详细讲解Runtime知识点,由于运行时的内容较多,所以将内容拆分成以下几个方面,可以自行选择想要查看的部分
- OC运行时机制Runtime(一):从isa指针开始初步结识Runtime
- OC运行时机制Runtime(二):探索Runtime的消息转发机制和分类Category
- OC运行时机制Runtime(三):关联对象Associated Object和分类Category
- OC运行时机制Runtime(四):尝试使用黑魔法 Method Swizzling
本文主要分析Runtime的消息机制,探究OC类和对象发送消息的原理。
理解 objc_msgSend 的作用
调用方法是面向对象语言常用的功能,用Objective-C的术语来说这叫“传递消息”,消息有“名称(name)”和“选择子(selector)”,而Runtime赋予了OC动态语言的特性,用一串伪代码简单介绍下静态语言和动态语言调用方法的区别。
if(bool){ function1(); }
else{ function2(); }
//函数地址硬编码在指令中,这是函数调用方式
void (*fnc)();
if(bool){ fnc = function1; }
else{ fnc = function2; }
fnc();
//需要的函数在运行时才能确定,动态绑定
并非编译时就会确定要调用的方法,而是交由运行时决定,这是消息机制的特点,OC基本调用方法为
id returnValue = [someObject messageName:parameter];
在这里someObject
叫做接受者(receiver)
,messageName
叫做选择子(selector)
,选择子和参数一同组成一条消息,编译器将其转为C语言函数调用,函数原型为void objc_msgSend(id self,SEL cmd, ...)
,这里函数的参数个数可变,那么上面的例子会变成以下这段代码
id returnValue = objc_msgSend(someObject, @selector(messageName:),parameter);
下面还以上一篇的Father和Son为例子,讲解一下消息传递的函数方法,在main.m中输入以下代码。
Son * son = [[Son alloc] init];
接下来用clang将其转为.cpp文件clang -rewrite-objc main.m
,代码如下:
Son * son = ((Son *(*)(id, SEL))(void *)objc_msgSend)((id)((Son *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("Son"), sel_registerName("alloc")), sel_registerName("init"));
这里将类型等简化为以下代码
Son * son = (objc_msgSend)((objc_msgSend)(objc_getClass("Son"), sel_registerName("alloc")), sel_registerName("init"));
解释下这段代码,首先使用objc_getClass获取当前类对象,然后通过objc_msgSend,向这个类对象发送消息,消息名为alloc,完成实例对象分配内存,再次调用objc_msgSend向这个实例对象发送消息,消息名为init,表示实例对象的初始化。
根据上一篇文章 OC运行时机制Runtime(一):从isa指针开始初步结识Runtime 我们了解了OC方法的继承体系
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;
/* Use `Class` instead of `struct objc_class *` */
每个方法从所属类的“方法列表”中查找,找不到则沿继承体系向上查找,如果还是找不到,执行“完整的消息转发机制”
,这里有个问题,如果每次都执行同一个方法都要查找一次,那么这个耗时还是比较长的,所以每次找到方法后会将方法存入“快速映射表”
中以便下次快速查找。“快速映射表”
结构体代码如下
struct objc_cache {
unsigned int mask /* total = mask + 1 */ OBJC2_UNAVAILABLE;
unsigned int occupied OBJC2_UNAVAILABLE;
Method buckets[1] OBJC2_UNAVAILABLE;
};
那么这个cache
到底有什么作用,OC许多类方法多到几十种,但是只有10%-20%的方法会经常调用,可能产生的调用次数超过80%,所以缓存的作用就体现出来了,首次调用方法从缓存中查找如果找不到再从方法列表中查找,极大的提升了查询效率。
这里已经看到了在Runtime的作用下,objc_msgSend
函数完成了面向对象消息传递向面向过程函数调用的转化,这也是消息机制的基础,那么就存在一个问题了,如果一条消息发送失败,runtime会如何处理呢?
消息转发机制:unrecognized selector出现后的三次机会
首先看一个最典型的案例
id string = @"hello";
[string addObject:@"world"];
这里用id类型声明了一个变量string,这时string默认为NSString类型,那我们调用NSMutableArray的方法,就会报错,[__NSCFConstantString addObject:]: unrecognized selector sent to instance 0x100001040
,我们知道oc方法继承体系是通过isa指针向上查找,直到NSObject依然找不到方法,就会调用doesNotRecognizeSelector:
方法,报unrecognized selector
的错误,当然这不是结束,oc在每个消息发送失败后会给出我们三次机会来解决这条消息,这就是消息转发机制
。
消息转发分为两大阶段,第一阶段看接受者是否能动态添加方法,这叫做动态方法解析
,如果运行期第一阶段已经被系统执行完毕,那么执行第二阶段分为两小步,先看其他有没有对象能处理这条消息,如果有备援的接收者
,将消息转发给这个对象,如果没有,则会启动完整的消息转发机制
,将所有信息封装到 NSInvocation
对象中,给最后一次解决这个问题的机会。
下面举一个例子,首先如果一个属性执行了@dynamic property;
,那么如果外界调用这个属性的setter或getter都会crash
这里用动态方法解析的方式解决一下这个问题
+ (BOOL)resolveInstanceMethod:(SEL)selector; //当遇到无法解读的实例方法时调用这个方法
+ (BOOL)resolveClassMethod:(SEL)selector; //当遇到无法解读的类方法时调用这个方法
Son * son = [Son new];
son.name = @"Tom";
NSLog(@"%@", son.name);
void dynamicNameSetter (id self,SEL _cmd, id value) {
Son *son = (Son *)self;
son.resolveName = (NSString *)value;
}
id dynamicNameGetter (id self,SEL _cmd) {
Son *son = (Son *)self;
return son.resolveName;
}
@implementation Son
@dynamic name;
+ (BOOL)resolveInstanceMethod:(SEL)sel {
if (sel == @selector(setName:)) {
class_addMethod(self, sel, (IMP)dynamicNameSetter, "v@:@");
return true;
}else if(sel == @selector(name)) {
class_addMethod(self, sel, (IMP)dynamicNameGetter, "@@:");
return true;
}
return [super resolveClassMethod:sel];
};
这里看到,当我们调用son的setter方法,因为实现了@dynamic;
关键字,所以首先消息发送失败,走到消息转发的第一步resolveInstanceMethod
,通过添加方法的方式,将值绑定到resolveName上,实现setter和getter方法。
那么如果上一步没有操作,会走到第二步
- (id)forwardingTargetForSelector:(SEL)selector;
//如果找到接收者,返回那个对象,否则返回nil,这一步转发的消息不能进行操作,可以通过完整的消息转发机制操作。
@implementation Son
- (id)forwardingTargetForSelector:(SEL)selector {
return [[Daughter alloc] init];
}
@end
@implementation Daughter
- (void)substitute {
NSLog(@"I can realize");
}
@end
这里外部调用
Son * son = [Son new];
[son performSelector:@selector(substitute)];
虽然son找不到substitute方法,但是它的备援接收者daughter可以,所以不会报错,并有成功打印。
如果以上操作都没有做,会执行最后一步完整的消息转发机制
。
- (void)forwardInvocation:(NSInvocation*)invocation;
//封装了NSInvocation对象,将target,selector,和parameters全部封装其中
总结
ObjC是一门动态语言,以消息机制
方式代替函数调用方式,运行时
才会决定消息的发送成功与失败,即使发送失败也有一套消息转发机制来避免crash,总共分为动态方法解析
,备援接收者
,完整的消息转发
三步,如果全部没有操作才会抛出异常。
后续
到这里分析了基础的两个部分,分别是运行时结构和消息机制,感兴趣的朋友们可以移步下一篇文章 OC运行时机制Runtime(三):关联对象Associated Object和分类Category,如果觉得本文对您有些作用,请在下方点个赞再走哈~