『libextobjc』Objctive-C 协议的默认实现

fgfm9310 2年前
   <p>继续阅读 <a href="/misc/goto?guid=4959548988611577419" rel="nofollow,noindex">libextobjc</a> 的源码,看到一个非常有趣的实现—— Objective-C 的 protocol 默认实现。当然,这不比 Swift 的 extension 默认实现,Objective-C 在这方面没有 Swift 强大,并不能完全的实现 POP,但是这不妨给我们提供一种思路。</p>    <p>首先,列举一下当面对这个问题时,都有哪些疑问:</p>    <ul>     <li>会用到方法注入,但是什么时候注入?</li>     <li>以什么形式获取默认实现的 SEL 与 IMP?</li>     <li>怎样减少性能开销?</li>    </ul>    <p>然后,我们来一起看看源码的实现思路( 因为更注重实现步骤和思想,所以在文章中不会出现大量的源码,大家最好自行对照源码进行阅读 )。</p>    <h2>用法</h2>    <pre>  <code class="language-objectivec">// MyProtocol.h    @protocol MyProtocol    @concrete  - (void)runTest;    @end          // MyProtocol.m  @concreteprotocol(MyProtocol)    - (void)runTest {      NSLog(@"%s", __FUNCTION__);  }    @end</code></pre>    <p>在 EXTConcreteProtocol.h 中能找到 concrete 与 concreteprotocol 这对宏。 concrete 很简单,也很巧妙,它就是用于修饰 protocol 方法的 optional 。用 concrete 宏的好处有两个:</p>    <ol start="">     <li>沿用 optional 的作用,防止遵循该 protocol 的类因为没实现代理方法,而报警告。</li>     <li>语义更加清晰,能直接表情这以下的方法是已经默认实现了的。</li>    </ol>    <p>然后来看看 concreteprotocol 的定义,首先, concreteprotocol 会将传入的 protocol name 与字符串 _ProtocolMethodContainer 拼接,即 MyProtocol_ProtocolMethodContainer 。因为源码不易阅读,我将它简化了一下(去掉注释与报错信息):</p>    <p><img src="https://simg.open-open.com/show/191fab8ced582725430c696c9dcb518a.png"></p>    <p>被框住的代码就是 concreteprotocol(MyProtocol) 展开的部分。这段代码能解答之前我们的两个疑问:</p>    <ol start="">     <li>什么时候注入:无论是在 +load 还是在被 __attribute__((constructor)) 修饰的函数中,至少能保证注入是发生在 main 函数之前的(关于 +load 与 __attribute__((constructor)) 的执行顺序,请参考我之前的文章: <a href="/misc/goto?guid=4959751846933388935" rel="nofollow,noindex"> 『Apple API』/ <em>/</em> attribute <em>/</em> </a> )。</li>     <li>以怎样的形式获取 SEL 与 IMP:这个宏直接为 protocol 扩展了一个容器类,所以默认实现的方法都是存在这个类中的,之后要进行注入,方法的 SEL 与 IMP 也应该是从这个容器类中进行获取。</li>    </ol>    <p>所以,根据调用顺序,我们接下来分成两步来分析整个实现。</p>    <h2>ext_addConcreteProtocol</h2>    <p>首先被调用的是 +load 中的 ext_addConcreteProtocol 函数。这个函数接收两个参数:当前的 protocol 对象,以及对应的容器类。实现中又调用了另外一个:</p>    <pre>  <code class="language-objectivec">BOOL ext_addConcreteProtocol (Protocol *protocol, Class containerClass) {      return ext_loadSpecialProtocol(protocol, ^(Class destinationClass){          ext_injectConcreteProtocol(protocol, containerClass, destinationClass);      });  }</code></pre>    <p>ext_loadSpecialProtocol 函数接收两个参数:当前 protocol,以及一个参数为 Class destinationClass 的 block。我们先不看这个 block 的具体实现,先来看看 ext_loadSpecialProtocol 都做了些什么。</p>    <h3>ext_loadSpecialProtocol</h3>    <p>同样,我简化了 ext_loadSpecialProtocol 的实现,代码大多用注释描述来代替:</p>    <pre>  <code class="language-objectivec">// protocol: 当前默认实现的 protocol  // void (^injectionBehavior)(Class destinationClass): 传入的 block    BOOL ext_loadSpecialProtocol (Protocol *protocol, void (^injectionBehavior)(Class destinationClass)) {      // 判断默认实现的 protocol 个数是否大于 SIZE_MAX      // specialProtocolCount 即默认实现的 protocol 的计数      if (specialProtocolCount == SIZE_MAX) {          return NO;      }        // specialProtocolCapacity 为数组总容量      if (specialProtocolCount >= specialProtocolCapacity) {          // 如果未超过 SIZE_MAX 则进行动态扩容          // 将动态扩容后的数组头指针交给 specialProtocols      }        // 将参数的 block copy 到堆,并赋值给 copiedBlock      ext_specialProtocolInjectionBlock copiedBlock = [injectionBehavior copy];        // 将 protocol, block, 和 NO 组装成 struct      // 然后将 struct 追加到数组中      specialProtocols[specialProtocolCount] = (EXTSpecialProtocol){          .protocol = protocol,          .injectionBlock = (__bridge_retained void *)copiedBlock,          .ready = NO      };        // 默认实现 protocol 的个数自增      ++specialProtocolCount;        // success!      return YES;  }</code></pre>    <p>所以,整个函数走下来,作用就是将 {protocol, block} 追加到数组中。</p>    <p>也就是说,在执行 __attribute__((constructor)) 修饰的方法以前,所有默认实现的 protocol,都会被加到这个数组中。</p>    <p><img src="https://simg.open-open.com/show/c9193a377e65f6f2c50e0de6c046c628.png"></p>    <p>接下来,我们来看 +load 之后做了什么。</p>    <h3>ext_loadConcreteProtocol</h3>    <p>同样, ext_loadConcreteProtocol 内部也调用了另一个函数:</p>    <pre>  <code class="language-objectivec">void ext_loadConcreteProtocol (Protocol *protocol) {      ext_specialProtocolReadyForInjection(protocol);  }</code></pre>    <p>整个函数的实现很简单,用于确保 +load 中加入数组的所有 protocol 都能找到:</p>    <pre>  <code class="language-objectivec">void ext_specialProtocolReadyForInjection (Protocol *protocol) {    // 循环遍历数组    for (size_t i = 0;i < specialProtocolCount;++i) {      // 如果数组的 protocol 是当前 protocl      if (specialProtocols[i].protocol == protocol) {        // 并且这个 protocol 还未被遍历过(也就是 ready 标识)        if (!specialProtocols[i].ready) {          // 则进行标记          specialProtocols[i].ready = YES;          // ready 标识计数自增          // !!! 当所有的 protocol 均 ready 之后          // 再调用 ext_injectSpecialProtocols          if (++specialProtocolsReady == specialProtocolCount)            ext_injectSpecialProtocols();        }          break;      }    }  }</code></pre>    <p>在 ext_specialProtocolReadyForInjection 的实现中, if (++specialProtocolsReady == specialProtocolCount) 这个判断比较有趣,它能回答我们的第三个问题(如何节省开销):</p>    <ol start="">     <li>+load 与 __attribute__((constructor)) 的优先级能使得所有 protocol 加入完成以后,再进行处理。</li>     <li>ready 计数 specialProtocolsReady 使得所有默认实现均判断无误后,再进行注入。</li>    </ol>    <p>到此,好像已经完事具备,马上就可以进行注入了。但苍天饶过谁,我们还有很重要的一个问题没有考虑。</p>    <h3>ext_injectSpecialProtocols</h3>    <p>优先级问题:如果 protocolA <ProtocolB> ,也就是 protocolA 遵循 protocolB ,那么谁的优先级更高呢?除此之外,如果遵循 protocol 的 class,自己也实现了默认方法呢?</p>    <p>这个问题,在 ext_injectSpecialProtocols 函数中能得到答案:</p>    <pre>  <code class="language-objectivec">static void ext_injectSpecialProtocols (void) {          qsort_b(specialProtocols, specialProtocolCount, sizeof(EXTSpecialProtocol), ^(const void *a, const void *b){      // 根据 a 是否 comform b,对整个数组进行排序 (protocol_conformsToProtocol)    });      // 通过 objc_getClassList 获得所有类列表        // 两个 for 循环嵌套    // 对类列表以及 protocol 列表进行遍历    // 如果 class comform protocol    // 则调用之前 struct 中的注入 block,进行注入    for (size_t i = 0;i < specialProtocolCount;++i) {      Protocol *protocol = specialProtocols[i].protocol;            for (unsigned classIndex = 0;classIndex < classCount;++classIndex) {        Class class = allClasses[classIndex];          if (!class_conformsToProtocol(class, protocol))            continue;          // 遵循 protocol 的 class 即为注入的目标 class        injectionBlock(class);      }    }  }</code></pre>    <p>所以,整个方法的任务也很清晰:</p>    <ol start="">     <li>对 protocol 进行优先级排序,给出具体注入的先后顺序,防止方法覆盖或无法注入。</li>     <li>获取全部 class 列表。</li>     <li>两层循环遍历,将 class 与其遵循的 protocol 进行匹配。</li>     <li>调用 struct 中的 block,并将目标 class 传出,进行注入。</li>    </ol>    <p>接下来,终于到了最后一步,来看看 block 中的注入方法的实现。</p>    <h3>ext_injectConcreteProtocol</h3>    <p>block 是在 +load 中就已经赋值了,而 block 的实现,就是直接调用了 ext_injectConcreteProtocol 函数:</p>    <pre>  <code class="language-objectivec">// 函数有三个参数  // protocol  // containerClass: 实现了 protocol 方法的 容器类  // class: 两层循环嵌套中,找到的要注入的目标 class  static void ext_injectConcreteProtocol (Protocol *protocol, Class containerClass, Class class) {         // 获取 容器类 中的实例方法列表    // 获取 容器类 meta class 中的类方法列表     // 循环注入实例方法      for (unsigned methodIndex = 0;methodIndex < imethodCount;++methodIndex) {          // 获取方法 SEL         // 获取方法 IMP         // 判断 目标类 是否存在该方法         // 进行注入      }     // 循环注入类方法同理  }</code></pre>    <p>到此,方法注入就已经全部完成了。</p>    <h2>总结</h2>    <p>面向过程的过完了整个源码,从头再来梳理一下:</p>    <ol start="">     <li>注入实现思路的重点在于,使用宏为 protocol 扩展了一个容器类。</li>     <li>容器类中,利用 +load 与 __attribute__((constructor)) 的特性,将注入流程分为了两个部分。</li>     <li>在 +load 中,将 protocol,执行注入的 block 打包成 struct,然后将 struct 装进数组。</li>     <li>当执行到 __attribute__((constructor)) 时,也就表示所有类的 +load 都已经执行过了,再对数组进行优先级排序。</li>     <li>排序完成后,两层循环嵌套,查找遵循了 protocol 的 class。</li>     <li>调用 block 执行注入。</li>    </ol>    <p> </p>    <p>来自:http://www.saitjr.com/ios/『libextobjc』objctive-c-协议的默认实现.html</p>    <p> </p>