深入研究Runtime(2) - 发送消息Messaging - 简书
ywkm2020
8年前
<h2>消息机制</h2> <p>在Objective-C中,最重要的就是消息发送机制。本文从两个方面深入学习该机制:</p> <ol> <li>消息表达式如何转换为调用<code>objc_msgSend</code>函数 && 如何通过<code>方法名</code>发送消息?</li> <li>如何利用<code>objc_msgSend</code>函数 && 必要时如何避免动态绑定?</li> </ol> <h2>objc_msgSend</h2> <p>在Objective-C语言里,消息<code>直到运行时</code>才会绑定到具体的方法实现。编译器把<code>消息表达式</code></p> <blockquote> <p>[receiver message]</p> </blockquote> <p>转化为调用消息发送函数<code>objc_msgSend</code>函数。该函数有两个主要的参数:</p> <ul> <li>receiver:消息接收者</li> <li>selector:方法选择器</li> </ul> <blockquote> <p>objc_msgSend(receiver, selector)</p> </blockquote> <p>如果消息表达式有参数,则转化如下:</p> <blockquote> <p>objc_msgSend(receiver, selector, arg1, arg2, ...)</p> </blockquote> <p><strong><em>动态绑定就是在消息发送函数<code>objc_msgSend</code>里实现的</em></strong></p> <ol> <li>根据<code>receiver的类</code>找到<code>selector</code>指向的方法的实现</li> <li>调用找到的方法实现,把消息<code>接收对象</code>( 实际上传递的是对象数据的指针 )和方法的<code>相关参数</code>传递进去</li> <li>将执行方法实现后的返回值返回出去</li> </ol> <p><em>其实当一个<code>对象</code>被创建、分配好内存、初始化实例变量时,第一个被创建的变量就是指向<code>类结构体</code>的指针( isa 指针 )</em></p> <blockquote> <p>note:一个对象与在运行时系统上工作,isa 指针是必须有的。定义一个对象的结构体,必须与<code>struct objc_object { Class isa; }</code>相符合( 在objc/objc.h中定义 )。但是我们很少去定义这个结构体,因为<code>NSObject</code>和<code>NSProxy</code>中的 <code>alloc</code> 和 <code>allocWithZone:</code> 方法会调用 <code>class_createInstance</code> 生成 <code>objc_object</code></p> </blockquote> <p><strong>如下整个消息发送的流程框架:实现了上面(1.)步骤</strong></p> <p><img src="https://simg.open-open.com/show/59733cc488eef1606fcaf085e16061b9.png" alt="深入研究Runtime(2) - 发送消息Messaging - 简书" width="1224" height="1182"></p> <p>Messaging.png</p> <ul> <li> <p>在上面内容中已经知道,调用<code>发送消息函数</code>时,已经获得<code>对象数据的指针</code>。通过该指针,我们可以找到对象内存地址,从而<code>找到isa指针</code>。由上图可知,获得isa指针后,我们可以遍历相关的类对象。每个类对象中都包含了一个<code>方法列表</code>,配合已知<code>receiver的类</code>,则可以定位到要调用的方法( 当前类对象方法列表没有,则寻找父类的,直到找到或者到NSObject为止 )。</p> </li> <li> <p>上述整个过程就是<code>动态绑定</code>的过程( OOP编程 )</p> </li> </ul> <blockquote> <p>补充:</p> <ul> <li><code>对象方法</code>存储在<code>类对象</code>的方法列表当中,而<code>类方法</code>存储在<code>元类( metaclass )对象</code>的方法列表当中</li> <li>其实为了加快整个消息发送的过程,运行时系统<code>缓存</code>了我们曾经使用的方法名和方法地址( 如方法列表中的 selector ... address )。每个类的缓存是分开的,包括重写了的父类方法。在查找方法列表之前,会先查找缓存。如果方法已经缓存,发送消息只是比直接调用函数慢一点。</li> </ul> </blockquote> <h2>使用隐藏参数</h2> <p>由上面内容可以,<code>objc_msgSend</code>函数实现中,当找到方法的实现后,会执行该实现,并传入相关的对象和参数。而相关对象就是:</p> <ol> <li>消息接收的对象 ( self )</li> <li>方法的动态指针 ( _cmd )</li> </ol> <blockquote> <p>note:为什么叫隐藏参数?因为在方法声明实现时,在参数列表中,我们没有明显的写出来</p> </blockquote> <p>如下( strange方法动态绑定大概实现猜测 ):</p> <pre> <code> - strange { id target = getTheReceiver(); SEL method = getTheMethod(); if ( target == self || method == _cmd ) return nil; return [target performSelector:method]; // 根据方法名调用方法 }</code></pre> <h2>获取方法的地址</h2> <p>想要避免动态绑定,唯一方法就是获得获得方法地址,然后想函数一样调用它。一般这种情况很少用,例如想<code>大量重复调用</code>方法,而你又减少动态绑定的资源开销。</p> <ul> <li>使用 <code>NSObject</code> 的 <code>methodForSelector:</code> 方法可以根据方法名获得方法的地址</li> </ul> <p><em>例子: <code>setFilled: (BOOL)</code>重复调用</em></p> <pre> <code>// - (IMP) methodForSelector:(SEL)aSelector 返回的是类型是IMP // id (*IMP)(id, SEL, ...):方法实现对应的C语言函数的函数指针 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);</code></pre> <ul> <li>定义<code>IMP</code>函数指针时,隐藏参数不能省略,方法参数需必须传入。 <ul> <li>id:消息接收对象</li> <li>SEL:方法选择器</li> <li>BOOL:<code>setFilled:</code>的参数</li> </ul> </li> </ul> <blockquote> <p>其实如果不是大量的循环发送消息,这种资源的浪费是很少的,所以没必要去主动去避免动态绑定。</p> </blockquote> <p><br> </p> <p>文/老谭是谁(简书作者)<br> 来源:http://www.jianshu.com/p/b2b1e4d04b81<br> </p>