XiaoboTalk

Objective-C 的消息机制

C 函数如何执行

事实上,如果不理解 C 函数调用的时候内存发生了什么,就没办法理解 Objective-C 的消息转发。
notion image
传统的 C 函数,编译后,直接生成了对应的虚拟地址,写入到了二进制可执行文件;执行的时候,该地址直接指向栈中函数的入口,中间没有任何其他步骤。
选读内容: 现代计算机,为了追求更高效的运行效率,在硬件上引入了寄存器,寄存器和内存(RAM)都是易失性存储器。但是寄存器属于 SRAM(Static Random Access Memory),而内存属于 DRAM(Dynamic Random Access Memory),区别在于 DRAM 属于破坏性读出,即一次数据读取后,数据就会被彻底破坏,需要重新刷新该内存区域,一般刷新周期为 2ms。而 SRAM 则是非破坏性读出,所以不需要刷新,这让 SRAM 的读写速度远高于 DRAM,但是 SRAM 的成本会更高,内部电路会更复杂。所以计算机内部只有少量的高速 Cache 和 寄存器选用 SRAM 结构。内存则采用 DRAM 结构。

Objective-C 的消息机制

回到正题,相比 C 语言,Objective-C 语言则做了新的设计,称为消息机制,即给某个对象发个消息,发消息这个用词很微妙,因为发了消息,仅仅代表目标对象收到了某个消息,并不代币它会直接调用最终的目标函数;所以在 Objective-C 中叫 Method(方法),而不直接叫函数 (Function)。设计上,所有 Objective-C 的消息,都交给一个全局函数objc_msgSend去处理。该函数由 C 和部分汇编共同实现,主要的目的就是让消息经过多次流转,从而让使用者拥有更多,更灵活的方式处理该消息,这也就实现了语言的部分动态化。具体,可以看下图:
notion image
通过上图可以看到,对任意一个对象发任意消息,它在栈中第一个进入的都是objc_msgSend函数,该函数内部有关消息转发的逻辑主要有 4步,如上图所示。

编译器如何编译一个方法

首先来看编译器的作用,编译器干了什么,才真正体现了该语言的设计。编译器首先会做一次--rewrite-objc,用 C++ 重新翻译一遍 OC 语言,然后在这个过程中会偷偷的加了一个中间层–objc_msgSend。从函数调用栈上来看,objc_msgSend实现的功能,相当于用硬编码实现了一次间接寻址。对于一个看似普通的 OC method,会翻译出来一堆东西:
1、selector 选择器,底层是 objc_selector 结构体,它的类型实际上是一个char *字符串,内容是方法名+参数,然后用特定的书写格式记录。有了 selector 作为 Key,将来就可以根据这个 Key 在对象的内存结构中去寻找真正与之对应的 IMP
2、IMP 函数指针,它是真正最终被调用的函数指针(如果有具体的实现),编译器参考原 OC 方法,编译出一个 C++ 函数。IMP 就是该函数的地址,这里假设,OC 的方法是doSomethingWithParam:,那么翻译后的样子是下边这样的:
// 实现 doSomethingWithParam: 方法 void MyClass::doSomethingWithParam(NSString *param) { NSLog((NSString *)&__NSConstantStringImpl__var_folders...__, param); }
3、Method,实为一个 object_method 结构体
struct objc_method { SEL method_name; char *method_types; IMP method_imp; };
method 可以简单的理解为一对[key value]KeyselectorValue就是IMP。此外还有一个方法类型签名method_types,这个在将来能够用来解析该Method的参数和返回值具体是什么类型,它的值是一种约定好的特殊编码,例如"v@:ii",表示void (int, int),一个接收两个int型参数,没有返回值的方法。事实上每种编程语言为了方便编译器高效执行,都有自己的类型编码表,所以,你无需惧怕v@ii这些奇形怪状的编码,它们只是为了让编译器识别和解析。当然语言本身提供了通过类型解析出具体编码的 OC 方法(objcType)和语法糖(@encode),例如:
@encode(long) // 输出: 'l' NSNumber *foo = [NSNumber numberWithBool: YES]; [foo objCType] // 输出: 'c'
这些编码可以在苹果官方文档中查询。
4、编译出一条objc_msgSend函数调用语句,作为执行的入口:
objc_msgSend(myObject, @selector(doSomethingWithParam:), param);
所以,可以看到在编译器和msg_msgSend()的共同作用下,实现了 Objective-C 的消息机制,当然整个过程中调用了很多runtime的 C函数。文末我会放一个精简的代码示例,展示 OC (编译为)–> C++ 的前后对比。这里先集中注意力到消息机制的原理上。

objc_msgSend 中对消息机制的 4 步处理

notion image
1、 第一步是在类对象本身的继承体系中寻找与selector的实现,如果成功找到,就直接调用目标函数,消息机制处理完毕!这里涉及到 OC 对象模型,
Objective-C 对象模型
中会详细讨论。
2、 如果第一步中没有找到,objc_msgSend函数则会尝试调用resolveInstanceMethod方法,进行一次方法的动态解析,该方法的一般实现如下:
#import <objc/runtime.h> void dynamicMethodIMP(id self, SEL _cmd) { NSLog(@"Dynamic method implementation"); } @implementation MyClass // 1、或者给类对象实例添加方法 + (BOOL)resolveInstanceMethod:(SEL)sel { if (sel == @selector(testMethod)) { class_addMethod([self class], sel, (IMP)dynamicMethodIMP, "v@:"); return YES; } return [super resolveInstanceMethod:sel]; } // 2、或者给类添加方法 + (BOOL)resolveClassMethod:(SEL)sel { if (sel == @selector(printMessage)) { class_addMethod(object_getClass(self), sel, (IMP)dynamicMethodIMP, "v@:"); return YES; } return [super resolveClassMethod:sel]; } @end
方法返回BOOL,表示是否成功实现了动态解析;该方法的目的是让用户动态的给selector添加一个函数实现,从而调用它。这里的class_addMethodruntime库提供的C函数。另外这里有两个解析方法,resolveClassMethodresolveInstanceMethod,分别解析实例方法和类方法;由于实例方法在类对象中,类方法在元类对象中,所以这两个方法本身都是+方法。这样[self class]就可以表示类对象,object_getClass(self)表示元类对象。
如果该方法返回true,则直接执行函数,消息机制结束!
3、 如果 2 失败,则会发生消息转发forwardingTargetForSelector,这里才是真实意义上的消息转发,实现将消息转发给其他对象,具体使用:
@implementation MyClass - (void)foo { NSLog(@"MyClass foo"); } @end @interface MyForwardingClass : NSObject @end @implementation MyForwardingClass - (id)forwardingTargetForSelector:(SEL)aSelector { if (aSelector == @selector(foo)) { return [[MyClass alloc] init]; } return [super forwardingTargetForSelector:aSelector]; } @end
forwardingTargetForSelector需要返回一个接收消息的对象,然后消息会被再次转发到目标对象上,在目标对象上会重新从第一步开始执行消息处理流程。
4、 这里故事到了结尾,很遗憾,前边的各种机会都没成功,这里将进行NSInvocation打包,运行时把所有有关消息的数据都收集一遍,打包进NSInvocation对象。能走到这里,说明目前还没找到能处理该selector的目标对象,所以运行时无法自己拿到该selector对应的方法签名(前文提到如果有目标对象,则目标对象会编译出一个 objc_method 结构体,该结构体内部有selector, imp, method_type,其中method_type就是函数的签名或者叫编码,问题是到目前还没找到这个目标对象,所有impmethod_type都没有)。因此我们必须先实现一个methodSignatureForSelector方法,返回方法签名,然后在处理NSInvocation
@implementation MyForwardingClass - (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector { if (aSelector == @selector(foo:bar:)) { return [NSMethodSignature signatureWithObjCTypes:"v@:ii"]; // 返回方法签名 } return [super methodSignatureForSelector:aSelector]; } - (void)forwardInvocation:(NSInvocation *)anInvocation { MyClass *obj = [[MyClass alloc] init]; if ([obj respondsToSelector:[anInvocation selector]]) { // 这里我们可以对 anInvocation 做更灵活的调用,比如一次派发给多个对象,来响应该消息。 [anInvocation invokeWithTarget:obj]; } else { [super forwardInvocation:anInvocation]; } } @end

完整版本编译器翻译 OC –> C++

假设原 OC 代码:
// 定义一个类 @interface MyClass : NSObject - (void)doSomethingWithParam:(NSString *)param; @end @implementation MyClass - (void)doSomethingWithParam:(NSString *)param { NSLog(@"Received parameter: %@", param); } @end // 在 main 函数中调用 MyClass 的方法 int main(int argc, const char * argv[]) { @autoreleasepool { MyClass *myObject = [[MyClass alloc] init]; NSString *param = @"Hello World"; [myObject doSomethingWithParam:param]; } return 0; }
编译为 C++ 后:
// 定义一个类 struct objc_class { struct objc_class *isa; char *class_name; struct objc_method_list *method_list; }; struct objc_method { SEL method_name; char *method_types; IMP method_imp; }; struct objc_method_list { struct objc_method_list *next; int count; struct objc_method method_list[1]; }; class NSObject { public: virtual void release(); virtual void retain(); virtual id autorelease(); virtual id init(); static id alloc(); void dealloc(); }; class MyClass : public NSObject { public: void doSomethingWithParam(NSString *param); static void __doSomethingWithParam(MyClass *self, SEL _cmd, NSString *param); static Class class$MyClass; }; Class MyClass::class$MyClass = (Class)&OBJC_CLASS_$_MyClass; // 实现 doSomethingWithParam: 方法 void MyClass::doSomethingWithParam(NSString *param) { NSLog((NSString *)&__NSConstantStringImpl__var_folders...__, param); } void MyClass::__doSomethingWithParam(MyClass *self, SEL _cmd, NSString *param) { self->doSomethingWithParam(param); } // MyClass 的方法列表 static struct objc_method_list _OBJC_INSTANCE_METHODS_MyClass __attribute__ ((used, section ("__OBJC, __inst_meth")))= { 1, {{(SEL)"doSomethingWithParam:", "@24@0:8@16", (IMP)&MyClass::__doSomethingWithParam}} }; // 在 main 函数中调用 MyClass 的方法 int main(int argc, const char * argv[]) { NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init]; MyClass *myObject = ((MyClass *(*)(id, SEL))(void *)objc_msgSend)((id)((MyClass *(*)(id, SEL))(void *)objc_msgSend)(MyClass::class$MyClass, sel_registerName("alloc")), sel_registerName("init")); NSString *param = ((NSString *(*)(id, SEL))(void *)objc_msgSend)((id)((NSString *(*)(id, SEL))(void *)objc_msgSend)((id)((NSString *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("NSString"), sel_registerName("alloc")), sel_registerName("initWithUTF8String:"), "Hello World"); ((void (*)(id, SEL, id))(void *)objc_msgSend)((id)myObject, sel_registerName("doSomethingWithParam:"), (id)param); ((void (*)(id))(void *)objc_msgSend)((id)pool, sel_registerName("drain")); return 0; }
关键的代码在最终的main函数中: ...objc_msgSend)((id)myObject, sel_registerName("doSomethingWithParam:"), (id)param);