iOS-运行时-runtime

Objective-C 扩展了 C 语言,并加入了面向对象特性和 Smalltalk 式的消息传递机制。而这个扩展的核心是一个用 C 和 编译语言 写的 Runtime 库。它是 Objective-C 面向对象和动态机制的基石。

Objective-C 是一个动态语言,这意味着它不仅需要一个编译器,也需要一个运行时系统来动态得创建类和对象、进行消息传递和转发。理解 Objective-C 的 Runtime 机制可以帮我们更好的了解这个语言,适当的时候还能对语言进行扩展,从系统层面解决项目中的一些设计或技术问题。了解 Runtime ,要先了解它的核心 - 消息传递 (Messaging)。

消息传递机制

Alan Kay 曾多次强调 Smalltalk 的核心不是面向对象,面向对象只是the lesser ideas,消息传递才是the big idea

在很多语言,比如 C ,调用一个方法其实就是跳到内存中的某一点并开始执行一段代码。没有任何动态的特性,因为这在编译时就决定好了。而在 Objective-C 中,[object foo]语法并不会立即执行 foo 这个方法的代码。它是在运行时给 object 发送一条叫 foo 的消息。这个消息,也许会由 object 来处理,也许会被转发给另一个对象,或者不予理睬假装没收到这个消息。多条不同的消息也可以对应同一个方法实现。这些都是在程序运行的时候决定的。

事实上,在编译时你写的 Objective-C 函数调用的语法都会被翻译成一个 C 的函数调用 -objc_msgSend()。比如,下面两行代码就是等价的:

[array insertObject:foo atIndex:5];

objc_msgSend(array,@selector(insertObject:atIndex:),foo,5);

消息传递的关键藏于objc_object中的 isa 指针和objc_class中的 class dispatch table。调度表。

在Objective-C中,类、对象和方法都是一个C的结构体,从objc/objc.h,头文件中。

isa指针:是一个Class 类型的指针. 每个实例对象有个isa的指针,他指向对象的类,而Class里也有个isa的指针, 指向meteClass(元类)。元类保存了类方法的列表。当类方法被调用时,先会从本身查找类方法的实现,如果没有,元类会向他父类查找该方法。同时注意的是:元类(meteClass)也是类,它也是对象。元类也有isa指针,它的isa指针最终指向的是一个根元类(root meteClass).根元类的isa指针指向本身,这样形成了一个封闭的内循环。


struct objc_object {

Class isa  OBJC_ISA_AVAILABILITY;

};

struct  objc_class {

Class isa  OBJC_ISA_AVAILABILITY;

#if!__OBJC2__Class super_class;

constchar*name;

long version;

long info;

long instance_size;

struct objc_ivar_list *ivars;变量列表

**struct objc_method_list **methodLists**;方法列表

**struct objc_cache *cache**;

struct objc_protocol_list *protocols;

#endif};

struct objc_method_list{

struct objc_method_list *obsolete;

int method_count;

#ifdef __LP64__

int space;

#endif

/* variable length structure */

struct objc_method method_list[1];

};

struct objc_method {

SEL method_name;

char*method_types;/* a string representing argument/return types */

IMP method_imp;

};

objc_method_list 本质是一个有objc_method 元素的可变长度数组。一个 objc_method机构体中有函数名,也就是SEL,有表示函数类型的字符串,以及函数的实现IMP。

IMP:IMP是”implementation”的缩写,它是objetive-C 方法(method)实现代码块的地址,可像C函数一样直接调用。通常情况下我们是通过[object method:parameter]或objc_msgSend()的方式向对象发送消息,然后Objective-C运行时(Objective-C runtime)寻找匹配此消息的IMP,然后调用它;但有些时候我们希望获取到IMP进行直接调用。

由于Method的内部结构不可见,所以不能通过method->method_name的方式访问其内部属性,只能Objective-C运行时提供的函数获取。

SEL method_getName(Method method);

IMP method_getImplementation(Method method);

const char * ivar_getTypeEncoding(Ivar ivar);

从这些定义中可以看出发送一条消息也就是objc_msgSend做了什么,例如:objc_msgsend(obj,foo):

1、首先,通过obj的isa找到它的class;

2、在class的method list 中找到 foo;

3、如果class中没有foo,继续往它的superClass中找;

4、一旦找到foo函数,就去执行他的IMP。

但是这种实现又个问题就是效率低,一个class中往往只有20%的函数会经常被调用,可能占总调用次数的80%。每个消息都要遍历一边,objc_method_list并不合理。如果把经常被调用的函数缓存下来,那就可以大大提升函数查询的效率。这就是objc_class 中另一个重要成员objc_cache做的事情。在找到foo之后,把method_name作为key,method_IMP作为value给存起来。当再次收到foo消息时候,可以直接在cache中找到,避免去遍历objc_method_list;

动态方法解析和转发

在上面的例子中,如果foo没有找到会发生什么?通常情况下,程序会在运行时crash,并抛出,unrecognized selector sent to ..异常,但是在抛出异常前,Object-c的运行时会给你三次拯救程序的机会:

1、Method resolution

2、Fast forwarding

3、Normal forwarding

Method resolution

首先Object-C运行时会调用 +resolveInstanceMehtod:或者+resolveClassMethod:,让你有机会提供一个函数的实现。如果你添加了函数并返回YES,那么运行时就会重新启动一次消息发送的过程。还是以foo为例,你可以这么实现:

void fooMethod(idobj, SEL _cmd){

NSLog(@"Doing foo");

}

+ (BOOL)resolveInstanceMethod:(SEL)aSEL{

if(aSEL ==@selector(foo:)){

class_addMethod([self class], aSEL, (IMP)fooMethod,"v@:");

returnYES;

}

return [super resolveInstanceMethod];

}

PS:iOS4.3 加入很多新的runtime方法,主要都是以imp为前缀的方法,比如:imp_implementationWithBlock() 用block 快速创建一个imp。上面的例子可以重写成:

IMP fooIMP = imp_implementationWithBlack( ^(id _self)){

NSLog(@"Doing foo");

});

class_addMehtod([self class],aSEL,fooIMP,"v@:");

Core Data 有用到这个方法。NSManageObjects 中的properties 的getter和setter 就是在运行时动态添加的。

Message Forwarding

如果resolve方法饭回NO,运行时就会移到下一步:消息转发(Message Forwarding)

FastForwarding

如果目标对象实现了 -fowardingTargetForSelector:,Runtime这时候就会调用这个方法,给你一个这个消息转发给其他对象的机会。

- (id)forwardingTargetForSelector:(SEL)aSelector{

if(aSelector ==@selector(foo:)){

return alternateObject;

}

return [super forwardingTargetForSelector:aSelector];

}

只要这个方法返回的不是nil和self,整个消息发送的过程就会被重启,当然发送的对象会变成你返回的那个对象。否则,就会继续Normal Forwarding。这里叫Fast,只是为了区别下一步转发机制。因为这一步不会创建任何心的对象,但下一步转发会创建一个NSInvocatioin对象,所以相对更快一些。

Normal Forwarding

这一步是 Runtime 最后一次给你挽救的机会。首先它会发送

- methodSignatureForSelector:消息获得函数的参数和返回值类型。如果返回 nil,Runtime 则会发出

- doesNotRecognizeSelector:消息,程序这时也就挂掉了。如果返回了一个函数签名,Runtime就会创建一个NSInvocation对象并发送 -fowardInvocation:消息给目标对象。

NSInvocation 实际上就是对一个消息的描述,包括selector 以及参数等信息。所以你可以在- forwardInvocation:里修改传进来的NSInvocation对象

-(void)forwardInvocation:(NSInvocation *)invocation{

SEL sel = invocation.selector;

if (alternateObject respondsToSelector:sel){

[invocation invokeWithTarget:alternateObject]

}else{

[self doesNotRecognizeSelector:sel];

}

}

总结

Objective-C 中给一个对象发送消息会经过以下几个步骤

1、在对象类的dispatch table中尝试找到该消息。如果找到了,跳到相应的函数IMP去执行实现代码;

2、如果没有找到,Runtime 会发送+resolveInstanceMethod:或者+resolveClassMethod:尝试去 resolve 这个消息

3、如果 resolve 方法返回 NO,Runtime 就发送-forwardingTargetForSelector:允许你把这个消息转发给另一个对象。

4、如果没有新的目标对象返回, Runtime 就会发送-methodSignatureForSelector:和-forwardInvocation:消息。你可以发送-invokeWithTarget:消息来手动转发消息或者发送-doesNotRecognizeSelector:抛出异常

参考了:http://tech.glowing.com/cn/objective-c-runtime/

你可能感兴趣的:(iOS-运行时-runtime)