Objective-c runtime及消息机制

来源:互联网 发布:感动一个人 知乎 编辑:程序博客网 时间:2024/06/05 08:23

一、代码的生命周期阶段:
主要包含以下阶段:edit time、compile time、distribution time、installation time、link time、load time、run time。
edit time: 这个阶段从创建代码开始持续到bug fix、refactoring,最后形成源代码。
compile time: 源代码被转换为机器代码。
distribution time: 传输代码拷贝给用户的过程。
installation time: 用户安装发布的程序的过程。
link time: 指连接所有机器代码、包括第三方代码的过程。分为静态连接和动态连接。静态连接由编译器完成,在程序执行之前;动态连接由操作系统完成,可以在程序执行之前,也可以在程序执行过程中。
load time: 指操作系统把可执行代码从硬盘中,加载到内存的过程,以便执行。
run time: 指中央处理单元执行机器代码指令的过程。

二、静态语言与动态语言
Objective-c是动态语言, 但什么是动态语言呢?如果让你(设计)实现一个静态计算机语言、动态语言,该怎么做?
下面通过面向过程、面向对象、动态语言在编译时和运行时的操作来辨析动态语言:

1、面向过程的语言,比如C
实现C语言编译器,只需按照语法规则实现一个LALR语法分析器,把C代码解析为前端,中间经过优化、后端根据系统和环境生成机器代码。编译器最基础的功能是把C源代码中的函数名,转换成一个相对内存地址,把调用函数的语句转换为jmp跳转指令,程序在运行时阶段,调用语句可以直接跳转到对应的函数实现地址。Everything is per-determined。

2. 面向对象的语言,比如C++
C++在C的基础上增加了类的部分。在编写编译器时,需要绕一个弯,在严格的C编译器上增加一层类处理的机制:把函数限制在它的class环境里,每次调用一个函数,先找到它的对象、函数类型、返回值、参数参数等,确定后再jmp跳转到函数实现。
编译完成后,运行时阶段,函数调用可以直接跳转到实现代码。
虽然同样一个函数调用会根据参数和类的环境有不同的代码实现,但是这种跳转机制编译完成就已经确定,运行阶段直接跳转到代码实现,故称C++是static language。

3. 动态语言
希望在运行时阶段,动态类型(Dynamic typing)、动态绑定(Dynamic binding)和动态加载(Dynamic loading)。在运行时,动态的确定某个函数的实现;在运行时,动态识别某个对象的类型;在运行时,动态的加载某些资源或代码块。

因此,需要把上面哪个类的实现部分完全抽象出来,做成一套在运行在运行阶段的完整的检测环境。这样,在编译阶段,编译器只对语言进行最基本的检查报错,包括词法分析、语法分析等,然后解析为机器语言。而在运行时阶段,会具体对类型进行检查,对某些对象进行动态的类型识别、动态的确定方法的实现、动态的加载资源及代码块。
这个在运行时阶段的检测环境,在Objective-c中称为runtime。
Runtime is everything between your each function call.
上述抽象出来的在运行时阶段的完整的检测环境,称之为runtime library;
runtime libraty指,由LLVM在程序运行时阶段使用的,以实现编程语言内置功能的特定程序库。

三、Objective-c runtime
Objective-c是一种动态性语言,而runtime system是Objective-c语言的核心。
Objective-c中的runtime system指一个动态共享库(a dynamic shared library),该lib中包含一组functions和data structures的公开接口。

1、Objective-c程序和runtime之间的交互有三种级别:
(1)、Objective-c源代码:在编译器编译源代码时,编译器将Objective-c源代码解析为实现动态特征的数据结构和函数,C代码。该数据结构可以捕获class、category、protocol、object、selectors、instance variable templates等定义中的所有信息,也就是objc.h和runtime.h中声明的数据结构:

typedef struct objc_method *Method;      代表一个类定义中的方法typedef struct objc_ivar *Ivar;      代表一个实例变量typedef struct objc_category *Category;     代表一个categorytypedef struct objc_property *objc_property_t;   代表一个objective-c声明的property/// Represents an instance of a class.代表一个class实例,注意:该结构体声明中未定义相应的实例变量列表及实例方法列表,实际上只是隐藏起来而已。struct objc_object {                Class isa ;};typedef struct objc_object *id;    指向一个class实例的指针typedef struct objc_class *Class;     代表一个objective-c classtypedef struct objc_selector *SEL;   代表一个method selectortypedef void (*IMP)(void /* id, SEL, ... */ );  代表一个指向某方法实现的函数的指针    OBJC1中,class的表示如下:typedef struct objc_class *Class;struct objc_class {  Class isa;                // 指向metaclass  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; // 存储该类遵守的协议}

(2)、使用NSObject的某些方法查询runtime

- (BOOL)isKindOfClass:(Class)aClass;- (BOOL)isMemberOfClass:(Class)aClass;- (BOOL)conformsToProtocol:(Protocol *)aProtocol;- (BOOL)respondsToSelector:(SEL)aSelector;+ (BOOL)instancesRespondToSelector:(SEL)aSelector;+ (BOOL)conformsToProtocol:(Protocol *)protocol;+ (BOOL)isSubclassOfClass:(Class)aClass;- (IMP)methodForSelector:(SEL)aSelector;+ (IMP)instanceMethodForSelector:(SEL)aSelector;

注:NSObject中可用于和runtime交互的方法,它们的实现基础是动态库函数。
比如:

- (IMP)methodForSelector:(SEL)aSelector;+ (IMP)instanceMethodForSelector:(SEL)aSelector;- (void)doesNotRecognizeSelector:(SEL)aSelector;

(3)、直接调用runtime.h中声明的动态库函数,示例:

- (void)logMethodsAndProperties {    unsigned int count=1;    //获取属性列表    objc_property_t *propertyList = class_copyPropertyList([self class], &count);    for (unsigned int i=0; i<count; i++) {        const char *propertyName = property_getName(propertyList[i]);        MPLog(@"property---->%@", [NSString stringWithUTF8String:propertyName]);    }    //获取方法列表    Method *methodList = class_copyMethodList([self class], &count);    for (unsigned int i=0; i<count; i++) {        Method method = methodList[i];        MPLog(@"method---->%@", NSStringFromSelector(method_getName(method)));    }    //获取成员变量列表    Ivar *ivarList = class_copyIvarList([self class], &count);    for (unsigned int i=0; i<count; i++) {        Ivar myIvar = ivarList[i];        const char *ivarName = ivar_getName(myIvar);        MPLog(@"Ivar---->%@", [NSString stringWithUTF8String:ivarName]);    }    //获取协议列表    __unsafe_unretained Protocol **protocolList = class_copyProtocolList([self class], &count);    for (unsigned int i=0; i<count; i++) {        Protocol *myProtocal = protocolList[i];        const char *protocolName = protocol_getName(myProtocal);        MPLog(@"protocol---->%@", [NSString stringWithUTF8String:protocolName]);    }}

2、objective-c的消息机制
Runtime最主要的是消息机制。对于C语言,函数的调用在编译的时候会决定调用哪个函数实现,编译完成之后直接顺序执行,无任何二义性。
OC的函数调用称为消息发送,属于动态调用。在编译的时候并没有绑定每个方法的实现函数,只有在运行时才会根据方法的名称找到对应的实现函数来调用,这个过程,称为动态绑定。
实现dynamic blinding的关键在于编译阶段编译器为每个class和object生成的C数据结构。
如上节所述,每个class结构体均持有该class的实例方法和类方法dispatch list,甚至频繁使用的cache分发表。表中的每个条目是一个key-value对:key指的是method的selector,value是method的实现函数的地址,即IMP。
(1)、下面解析消息机制:
[receiver messageWithParameter1:arg1 parameter2:arg2];
编译器将代码[receiver messageWithParameter1:arg1 parameter2:arg2];转化为objc_msgSend(receiver, @selector(messageWithParameter1:parameter2:),arg1,agr2);,
1. 在class结构体的cache中,查找SEL对应的IMP,若找到,执行并返回;
2. 在class结构体中的methodLists中,查找SEL对应的IMP,若找到,执行并返回;
3. 在父类class结构中的cache和methodList中依次查找IMP,若找到,执行并返回;
4. 沿着继承链向,依次查找cache和methodlist,直到根类,若找到,执行并返回;
5. 调用resolveInstanceMethod:或resolveClassMethod:,若返回YES,则重新开始查找,这一次对象会响应这个选择器,因为一般情况下已经调用过class_addMethod:动态的添加了IMP。
6. 调用forwardingTargetForSelector:,若返回值非nil,那就把消息转发给返回的对象。这里不要返回self,否则会形成死循环。
7. 调用methodSignatureForSelector:,若返回值非nil,创建一个NSInvocation并传给forwardInvocation:。
8. 调用doesNotRecognizeSelector:,默认的实现是抛出异常。
9. 程序crash。
先看下第5步,resolveInstanceMethod:和resolveClassMethod:在运行时提供实现,这通常是@dynamic合成属性的地方。就是需要自己实现属性的getter和setter方法,通过resolveInstanceMethod:方法来把setter方法和getter方法和属性绑定。
若第5步返回NO,系统接着会首先尝试一次调用forwardingTargetForSelector:,看其能否返回一个对象,若有对象返回,就转发给返回的对象。若快速转发返回nil,接下来就进行普通的转发,调用methodSignatureForSelector:进行普通的转发。

(2)、消息动态拦截
默认情况下,NSObject子类并没有实现这二个动态拦截,在找不到IMP导致crash之前,runtime system自动调用对象的动态拦截,可以通过重写动态拦截来动态添加IMP,以避免程序crash。
+ (BOOL)resolveClassMethod:(SEL)sel;
(BOOL)resolveInstanceMethod:(SEL)sel;
第一个方法处理一个没有实现的类方法。
第二个方法处理一个没有实现的实例方法。
objective-c中的方法其实就是一个带有至少两个参数(self和_cmd)的普通的C函数。因此,可以为某个需要拦截的SEL,动态的提供一个IMP。IMP可以是C函数,也可以是Objective-c方法,但必须提供IMP。
例如:

@interface DynamicResolutionTest : NSObject- (void)noIMPInstanceMethodWithArg:(NSString*)string;+ (void)noIMPClassMethodWithArg:(NSString*)string;@end#include <objc/runtime.h>/* _cmd在Objective-C的方法中表示当前方法的selector,正如同self表示当前方法调用的对象实例。 */void addInstanceIMP(id self, SEL _cmd, NSString* string){    MPLog(@"add instance IMP to %@", NSStringFromSelector(_cmd));}void addClassIMP(id self, SEL _cmd, NSString* string){    MPLog(@"add class IMP to %@", NSStringFromSelector(_cmd));}#pragma clang diagnostic push#pragma clang diagnostic ignored  "-Wincomplete-implementation"@implementation DynamicResolutionTest#pragma clang diagnostic pop//方法拦截+ (BOOL)resolveInstanceMethod:(SEL)name  {    if (name == @selector(noIMPInstanceMethodWithArg:)) {        class_addMethod([self class],name,(IMP)addInstanceIMP,"v@:@");        return YES;    }    return [super resolveInstanceMethod:name];}+(BOOL)resolveClassMethod:(SEL)sel {   Class metaClass = objc_getMetaClass(class_getName(self));    if (sel == @selector(noIMPClassMethodWithArg:)) {        class_addMethod(metaClass, sel, (IMP)addClassIMP , "v@:@");        return YES;    }    return [super resolveClassMethod:sel];}@end

(3)、消息转发forsward
场景:假定,当前对象对某消息的实现代码实际上存在于另一个类中,向当前对象发送该消息,有两种处理方式:
<1>、当前对象提供该消息的虚实现,实际实现代码在另一个对象中,在虚实现中调用另一个对象的实际实现,例如:

- (id)forwardedInstanceMethodWithArg:(NSString*)string {    SomeOtherTarget *target = [[SomeOtherTarget alloc] init];     if ([target respondsToSelector: @selector(forwardedInstanceMethodWithArg:)]) {         [target performSelector:@selector(forwardedInstanceMethodWithArg withObject:string)];     } }

<2>、在forwardingTargetForSelector:中,根据respondsToSelector:SEL,转发给另外对象:

- (id)forwardingTargetForSelector:(SEL)aSelector {    if ([someOtherObject respondsToSelector: aSelector]) {        return someOtherObject;    }   return [super forwardingTargetForSelector: aSelector];}

向某对象发送一条未处理的消息,在程序报错之前,runtime system会自动的给该对象发送一条forwardingTargetForSelector:消息,可以在forwardingTargetForSelector:中将错误消息转发给另一个对象来处理,但不能swallow message。否则报错。
注:forwardingTargetForSelector:只支持转发实例方法。

<3>、methodSignatureForSelector:结合forwardInvocation:提供最后一次机会处理SEL的IMP。
注意:必须重写methodSignatureForSelector:并返回有效signature,runtime system才会自动调用forwardInvocation:;若methodSignatureForSelector:返回nil,则crash。
methodSignatureForSelector提供selector的@encode签名(注1),forwardInvocation:提供转发。
forwardInvocation:可以做为一个调度中心,把当前对象未实现的消息分发给不同的receiver;也可以做为一个中转站,把所有未实现的消息转发给同一个目标;forwardInvocation:可以把多条未实现的消息,用同一个默认response处理,也可以简单的不处理,即swallow message。
注1:runtime system中,objective-c方法的参数和返回类型都用@encode指令描述,比如v==void、@==a object、:==selector、*==c string、^==pointer。
详情参见:
https://developer.apple.com/library/content/documentation/Cocoa/Conceptual/ObjCRuntimeGuide/Articles/ocrtTypeEncodings.html#//apple_ref/doc/uid/TP40008048-CH100-SW1
注:invocation中封闭了消息的参数:第一个self,第二个_cmd,第三及随后是正常参数。

//动态添加getter、setter@interface DynamicResolutionTest : NSObject {    //动态添加getter、setter    NSMutableDictionary *book;}//声明了两个setter/getter@property (strong) NSString *title;@property (strong) NSString *author;@end//自定义signature- (NSMethodSignature *)methodSignatureForSelector:(SEL)selector {    NSString *sel = NSStringFromSelector(selector);    if ([sel rangeOfString:@"set"].location == 0) {        //动态造一个 setter函数        return [NSMethodSignature signatureWithObjCTypes:"v@:@"];    } else {        //动态造一个 getter函数        return [NSMethodSignature signatureWithObjCTypes:"@@:"];    }}//寻找父类或别的对象中,对该SEL的signature- (NSMethodSignature*)methodSignatureForSelector:(SEL)selector{    NSMethodSignature* signature = [super methodSignatureForSelector:selector];    if (!signature) {       signature = [surrogate methodSignatureForSelector:selector];    }    return signature;}//swallow消息(void)forwardInvocation:(NSInvocation *)invocation {    //针对signature什么也不做,即swallow,程序不会crash。}//提供默认处理- (void)forwardInvocation:(NSInvocation *)invocation {    //拿到函数名    NSString *key = NSStringFromSelector([invocation selector]);    if ([key rangeOfString:@"set"].location == 0) {        //setter函数形如 setXXX: 拆掉 set和冒号        key = [[key substringWithRange:NSMakeRange(3, [key length]-4)] lowercaseString];        NSString *obj;        //从参数列表中找到值        [invocation getArgument:&obj atIndex:2];        [book setObject:obj forKey:key];    } else {        //getter函数就相对简单了,直接把函数名做 key就好了。        NSString *obj = [book objectForKey:key];        [invocation setReturnValue:&obj];    }}//转发给别的对象处理消息- (void)forwardInvocation:(NSInvocation *)anInvocation {     if ([someOtherObject respondsToSelector:[anInvocation selector]])         [anInvocation invokeWithTarget:someOtherObject];     else         [super forwardInvocation:anInvocation]; }

//此函数通常是不需要重载的,但是在动态实现了查找过程后,需要重载此函数让对外接口查找动态实现函数的时候返回YES,保证对外接口的行为统一。

- (BOOL) respondsToSelector:(SEL)aSelector {    if (@selector(setTitle:) == aSelector ||        @selector(title) == aSelector ||        @selector(setAuthor:) == aSelector ||        @selector(author) == aSelector)    {        return YES;    }   return [super respondsToSelector: aSelector];}

(4)、最后一步,抛出异常;
(void)doesNotRecognizeSelector:(SEL)aSelector

总结:
在一个函数找不到时,Objective-C提供了三种方式去补救:
(1)、调用resolveInstanceMethod给个机会让类添加这个实现这个函数
(2)、调用forwardingTargetForSelector让别的对象去执行这个函数
(3)、调用methodSignatureForSelector(函数符号制造器)和forwardInvocation(函数执行器)灵活的将目标函数以其他形式执行。
如果都不中,调用doesNotRecognizeSelector抛出异常。
消息机制中,SEL查找IMP,转到消息拦截后的过程,参考:
http://www.cnblogs.com/biosli/p/NSObject_inherit_2.html

3、规避动态绑定
当某个方法被执行很多次时,可以使用methodForSelector:方法来规避动态绑定以节省时间。
如下例:

void (*setter)(id, SEL, BOOL);int i;setter = (void (*)(id, SEL, BOOL))[target methodForSelector:@selector(setFilled:)];for ( i = 0 ; i < 1000 ; i++ )    setter(targetList[i], @selector(setFilled:), YES);

methodForSelector:方法返回的是某个方法的实现函数的指针。注意:返回的函数指针必须显式的转换为自己声明的函数指针类型。使用函数指针调用实现函数时,传入的前两个参数分别为self、@selector。

4、动态加载dynamic loading
尼玛,IOS终于可以添加第三方插件了。Objective-c程序在运行时,可以动态的load和link第三方的framwork。在objective-c代码中预先写好条件,请求用户安装某framwork,若用户安装完毕,动态link的framwork和自己代码中添加的静态库使用方法一样,则执行自定义使用framwork的功能模块;若用户不安装,则不执行。
动态添加库两种方法:
(1)、objc/objc-load.h 文件中定义的objc_loadModules函数
(2)、NSBundle中的load()方法,参见https://developer.apple.com/documentation/foundation/bundle/1415927-load
5、runtime system中的property
编译器遇到property声明时,也会和方法声明一样,使用描述性编译指令来编码,参见:https://developer.apple.com/library/content/documentation/Cocoa/Conceptual/ObjCRuntimeGuide/Articles/ocrtPropertyIntrospection.html#//apple_ref/doc/uid/TP40008048-CH101-SW24

原创粉丝点击