OC Runtime

Objective-C

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

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

  要了解runtime,就让我们先来了解它的核心-消息传递机制

消息传递(Messaging)

  Smalltalk 的核心不是面向对象,面向对象只是 the lesser ideas,消息传递才是 the big idea。

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

  事实上,在编译时你写的OC函数调用的语法都会被翻译成一个C的函数调用-objc_msgSend().比如

  [array insertObject:foo atIndex:5];

  等价于

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

  消息传递的关键在于objc_object中的isa指针和objc_class中的class dispatch table

objc_objectobjc_class 以及 Ojbc_method

  在 Objective-C 中,类、对象和方法都是一个 C 的结构体,从 objc/objc.h 头文件中,我们可以找到他们的定义:

  

  

  

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

  从这些定义中可以看出,发送一条消息也就是objc_msgSend做了什么事。以objc_msgSend(obj, foo)这个例子来说

    1.首先通过obc的isa指针找到它的class;

    2.在class的method list找foo;

    3.如果class中没找到foo,继续往它的superclass中找;

    4.一旦找到foo这个函数,就去执行它的实现IMP

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

动态方法解析和转发

  如果foo没有找到会发生什么?通常情况下,程序会在运行时挂掉,并抛出unrecoginzed selector sent to... 的一场。但在异常抛出前,Objective-c的运行时会给你三次拯救程序的机会

  1.method resolution

  2. fast forwarding

  3.normal forwrading

Method Resolution

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

  core data 有用到这个方法,NSManagedObjects中properties的getter和setter就是在运行时动态添加的

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

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

  IMP fooIMP = imp_implementationWithBlock(^(id _self) {

    NSLog(@"Doing foo");

  });

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

Fast forwarding

  如果目标对象实现了-forwardingTargetForSelector: , runtime这是就会调用这个方法,给你把这个消息转发给其他对象的机会

  

只要这个方法返回的不是nil和self,整个消息发送的过程就会被重启,当然发送对象会变成你返回的那个对象。否则就会继续Normal Fowarding。

  这里叫Fast,只是为了区别下一步的转发机制,因为这一步不会创建任何新的对象,但下一步转发会转件一个NSInvocation对象,所以相对更快点。

Normal forwarding

  这一步是runtime最后一次给你挽救的机会。首先它会发送-methodSignatureForSelector:消息获得函数的参数和返回值类型。如果-methodSignatureForSelector:返回nil,runtime则会发出doesNotRecognizeSelector:消息,程序这时也就挂掉了,如果返回一个函数签名,runtime就会创建一个NSInvocation对象,并发送-forwardInvocation:消息给目标对象

NSInvocation实际上就是对一个消息的描述,包括selector以及参数等消息,所以你可以在-forwardInvocation:里修改传进来的NSInvocation对象,然后发送-invokeWithTarget:消息给它,穿进去一个新的目标

 

Cocoa里有很多地方都利用到了消息传递机制来对语言进行扩展,如PROXIES,NSUndomanager跟Responder Chain。NSProxy 就是专门用来作为代理转发消息的;NSUndoManager 截取一个消息之后再发送;而 Responder Chain 保证一个消息转发给合适的响应者。

总结

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

  1. 在对象类的 dispatch table 中尝试找到该消息。如果找到了,跳到相应的函数IMP去执行实现代码; 
  2. 如果没有找到,Runtime 会发送 +resolveInstanceMethod: 或者 +resolveClassMethod: 尝试去 resolve 这个消息; 
  3. 如果 resolve 方法返回 NO,Runtime 就发送 -forwardingTargetForSelector: 允许你把这个消息转发给另一个对象; 
  4. 如果没有新的目标对象返回, Runtime 就会发送 -methodSignatureForSelector:和 -forwardInvocation: 消息。你可以发送 -invokeWithTarget: 消息来手动转发消息或者发送 -doesNotRecognizeSelector: 抛出异常。

利用 Objective-C 的 runtime 特性,我们可以自己来对语言进行扩展,解决项目开发中的一些设计和技术问题。

posted on 2017-02-07 15:05  liandog  阅读(153)  评论(0)    收藏  举报