【问题标题】:Objective C message dispatch mechanismObjective C 消息分发机制
【发布时间】:2010-11-02 04:46:39
【问题描述】:

我只是在玩 Objective C(编写玩具 iPhone 应用程序),我对用于发送消息的底层机制感到好奇。我对 C++ 中的虚拟函数通常是如何实现的以及相对于静态或非虚拟方法调用的成本有很好的了解,但是我没有任何 Obj-C 的背景知识来了解消息是如何发送的。浏览了一下,我发现this 宽松基准,它提到 IMP 缓存消息比虚拟函数调用更快,而虚拟函数调用又比标准消息发送更快。

我并不想优化任何东西,只是更深入地了解消息是如何发送的。

  • Obj-C 消息是如何分派的?
  • 实例方法指针如何被缓存,您(通常)能否通过阅读代码来判断消息是否会被缓存?
  • 类方法与 C 函数(或 C++ 中的静态类方法)本质上是否相同,还是它们有更多的东西?

我知道其中一些问题可能与“实现相关”,但只有一种实现才是真正重要的。

【问题讨论】:

    标签: objective-c performance


    【解决方案1】:

    Obj-C 消息是如何分派的?

    Objective-C 消息是使用运行时的objc_msgSend() 函数分派的。如Apple docs 所示,该函数至少需要 2 个参数:

    1. 接收对象
    2. 消息的选择器
    3. [正在发送的消息的可变参数列表。]

    类的实例有一个isa 指针,它是指向它们的类对象的指针。每个对象中方法的选择器存储在类对象中的一个“表”中,objc_msgSend()函数跟随isa指向类对象的指针,找到这个表,并检查方法是否在类的表。如果找不到,它会在类的超类的表中查找该方法。如果没有找到,它会沿着对象树继续向上,直到找到方法或到达根对象 (NSObject)。此时抛出异常。

    实例方法指针是如何被缓存的,您(通常)能否通过阅读代码来判断消息是否会被缓存?

    来自 Apple 在 Messaging 上的 Objective-C 运行时指南:

    为了加快消息传递过程,运行时系统会在使用方法时缓存方法的选择器和地址。每个类都有一个单独的缓存,它可以包含继承方法以及类中定义的方法的选择器。在搜索调度表之前,消息传递例程首先检查接收对象类的缓存(理论上,曾经使用过的方法可能会再次使用)。如果方法选择器在缓存中,消息传递只比函数调用慢一点。一旦一个程序运行了足够长的时间来“预热”它的缓存,它发送的几乎所有消息都会找到一个缓存方法。缓存会随着程序运行而动态增长以容纳新消息。

    如前所述,缓存在程序运行后开始发生,并且在程序运行足够长的时间后,大多数方法调用都会通过缓存的方法运行。正如它所说的,缓存是在使用方法时发生的,所以消息只有在使用时才会被缓存。

    类方法与 C 函数(或 C++ 中的静态类方法)本质上是否相同,还是它们有更多的东西?

    类对象以与类实例类似的方式处理方法调度。每个类对象都有一个对象,该对象存储自己的 class 方法,在一个名为 metaclass 的对象中。类对象有自己的isa 指向它的元类对象的指针,而元类对象又具有超元类对象,它可以从中继承类对象。方法派发到类方法是这样的:

    1. 调度系统遵循类对象的isa指向元类对象的指针
    2. 在元类对象的方法表中搜索类方法。
    3. 如果未找到,则继续搜索元类对象的超类,并继续搜索。
    4. 此过程一直重复,直到找到方法,或者直到它到达根元类,并引发异常。

    【讨论】:

    • 很好的解释。我发现了解动态调度系统以及选择器如何参与也很好地解释了为什么方法名称必须是唯一的,而 Objective-C 不像 Java 和 C++ 那样支持重载方法。因为选择器被缓存为一个整数,任何具有相同名称的方法都会映射到同一个选择器。无论好坏,它确实迫使程序员更多地考虑正确命名方法。
    • 特殊的 'forward' 和 'performv' 方法呢?什么时候叫他们?
    【解决方案2】:

    如果有人想深入了解,我还在我的博客上写了一个关于 x86_64 上的 objc_msgSend() 的指令指南:

    https://friday.com/bbum/objc_msgsend-part-1-the-road-map/

    【讨论】:

    • 有趣的阅读。 “请注意,选择器是一个 C 字符串,但选择器也是唯一的,因此允许缓存查找简单地比较字符串地址。”我在一个进程中得到了这个,但是这是如何跨模块边界完成的?
    • @DwayneRobinson dyld 执行了一个“修复”阶段,作为解析和规范化所有此类常见数据的可执行加载。我不记得细节了,坦率地说,自从我上次看以来,它们可能已经改变了。运行时和 dyld 的源代码可在此处获得 opensource.apple.com。有趣的读物。
    【解决方案3】:

    调度机制

    用于在方法被调用时查找必要的可执行代码(发送消息)

    • 内联
    • Static(Direct)(C, Java final, C++ default, Swift static, final) - 编译器在编译时知道必要的方法实现。
    • Dynamic - 基于witness table(虚拟表,调度表),它引入了多态性
      • Table, V-Table(C++ virtual, Java default, Swift default) - 每个对象都有一个类的引用,该类有一个包含所有方法地址的表(super, overrides, new)。 SIL 包含 vtablewitness_table
      • Message(Objective-C, Swift dynamic) - 每个 object 都有一个对 class 的引用(isa),其中包含对 superclass 的引用strong> 和调度表(仅包含已实现的方法(开销和新方法))并且不包含来自 super 的方法。如果在当前调度表中没有找到方法,则继续在超类的调度表中搜索。此过程通过缓存进行了优化。 SIL 包含 volatile Objective-C 消息调度

    例如

    class A {
        func foo1() {}
        func foo2() {}
    }
    class B: A {
        override func foo2() {}
        func foo3() {}
    }
    

    Objective-C obc_msgSend

    id obc_msgSend(id self, SEL op, ...)
    // self - object which receive a message
    // op - selector of method
    //... - arguments
    
    

    如果没有找到给定选择器的方法实现,您会看到下一个错误

    unrecognized selector sent to instance
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2012-03-25
      • 1970-01-01
      • 2010-09-14
      • 1970-01-01
      • 1970-01-01
      • 2012-05-11
      • 2013-08-19
      • 1970-01-01
      相关资源
      最近更新 更多