重拾 ObjC 消息机制

消息机制是 Objective-C 语言的基础,也是它动态化的核心所在。笔者在阅读 objc 源码以后,对该语言的使用有了一些新的思考。ios

对象或者类在响应消息时,最多会经历 5 个过程:git

  1. 查找当前类的缓存
  2. 在当前类的方法列表查找
  3. 在父类缓存及方法列表查找
  4. 消息决议
  5. 消息转发

消息的响应过程实际上是根据选择子(sel)查找对应的函数实现(imp)的过程。github

查找当前类的缓存

发送消息的objc_msgSend函数会经过cache_getImp函数查找缓存,由于全部方法的执行,都会调用这两个函数,为了提升性能,这两个函数由汇编实现。数组

若是方法缓存没有命中,会在当前类的methods中查找。缓存

查找当前类的方法列表

当前类的方法列表底层是一个二维method_list_t数组,存储当前类以及全部 category 的方法。二维的方法列表是在 runtime 初始化的时候构建的,先添加主类的方法列表,而后依次添加 category 的,后添加的列表会放在数组的前面。函数

在查找时,会依次遍历二维数组中的每一个元素列表,而后在列表中使用二分查找,直到找到选择子(sel)对应的方法实现(imp)才终止。找到后使用log_and_fill_cache写入当前类的缓存。性能

若是这个过程没有找到,会进入父类查找。指针

在父类缓存及方法列表查找

在查找父类的方法列表前,会先查找父类的方法缓存,若是缓存没有命中才会遍历方法列表查找。查找过程和在当前类查找有点区别是,若是在缓存中找到消息转发的 imp _objc_msgForward_impcache,会中止查找,直接进入消息转发。code

若是在父类中找到对应的实现,会将该方法缓存到当前类中。对象

若是直到 NSObject 都没有找到方法对应的实现,会进入方法决议。

方法决议

方法的决议分为实例方法和类方法,由于二者的过程都类似,因此这里只讲实例方法。

方法决议时会触发resolveInstanceMethod:方法的调用,若是当前类实现该方法,并在该方法中使用 class_addMethod()动态的为参数sel关联实现(imp),那么在返回YES后会调用新关联的imp,并缓存。

若是在方法决议时,没有动态的关联实现,便会触发消息转发。

消息转发

消息的转发有两种形式forwardingTargetForSelectorforwardInvocation功能有所差异。

forwardingTargetForSelector:须要返回一个可以响应sel消息的对象。若是该对象没法响应传入的选择子会调用forwardInvocation:

forwardInvocation:调用前须要经过methodSignatureForSelector:方法提供方法签名。

forwardInvocation:接受一个NSInvocation参数,该对象包含当前选择子和对象。然而咱们彻底能够忽视这个参数作任何事情,由于只要这个方法实现,当前对象就不会再抛没法响应消息的异常了。

因此须要谨慎的重载这个方法,否者若是某个未知方法没有实现,却不会抛出异常,就没法察觉了。

最后若是没有实现消息转发,会在根类NSObject中调用doesNotRecognizedSelector抛异常。

总结

从响应消息的流程上来看,存在一些值得思考的地方。

应该尽可能减小 Category 的数量,由于 Category 会做为元素添加到二维指针数组,增长数组的长度,也就增长方法查找的时间消耗。

Category 的方法会先于主类被查找到,若是 Category 使用了主类的同名方法,主类的实现会被覆盖。

在方法决议时,会使用class_addMethod动态添加方法的实现,该方法会将新加的方法做为一个单一的 list 元素,添加到二维指针数组,一样会增长数组的长度。

同时,该函数在添加新方法后会冲刷(flush)方法缓存。其目的是为了防止先前存在同名的方法被调用过,被缓存,再次调用会命中缓存,而不会执行新添加的方法实现。

消息转发实际上是变相的实现了多态,将当前类的消息交给其余类处理,甚至忽略转发的消息而去作其余事情。

不过,到达消息转发须要经历前面的四步,也是一笔不小的开销,过多的依赖转发来响应消息会影响性能。

参考:

从源代码看 ObjC 中消息的发送

objc 源码

相关文章
相关标签/搜索