『libextobjc』Objctive-C 協議的默認實現

fgfm9310 8年前發布 | 29K 次閱讀 iOS開發 移動開發

繼續閱讀 libextobjc 的源碼,看到一個非常有趣的實現—— Objective-C 的 protocol 默認實現。當然,這不比 Swift 的 extension 默認實現,Objective-C 在這方面沒有 Swift 強大,并不能完全的實現 POP,但是這不妨給我們提供一種思路。

首先,列舉一下當面對這個問題時,都有哪些疑問:

  • 會用到方法注入,但是什么時候注入?
  • 以什么形式獲取默認實現的 SEL 與 IMP?
  • 怎樣減少性能開銷?

然后,我們來一起看看源碼的實現思路( 因為更注重實現步驟和思想,所以在文章中不會出現大量的源碼,大家最好自行對照源碼進行閱讀 )。

用法

// MyProtocol.h

@protocol MyProtocol

@concrete
- (void)runTest;

@end


// MyProtocol.m
@concreteprotocol(MyProtocol)

- (void)runTest {
    NSLog(@"%s", __FUNCTION__);
}

@end

在 EXTConcreteProtocol.h 中能找到 concrete 與 concreteprotocol 這對宏。 concrete 很簡單,也很巧妙,它就是用于修飾 protocol 方法的 optional 。用 concrete 宏的好處有兩個:

  1. 沿用 optional 的作用,防止遵循該 protocol 的類因為沒實現代理方法,而報警告。
  2. 語義更加清晰,能直接表情這以下的方法是已經默認實現了的。

然后來看看 concreteprotocol 的定義,首先, concreteprotocol 會將傳入的 protocol name 與字符串 _ProtocolMethodContainer 拼接,即 MyProtocol_ProtocolMethodContainer 。因為源碼不易閱讀,我將它簡化了一下(去掉注釋與報錯信息):

被框住的代碼就是 concreteprotocol(MyProtocol) 展開的部分。這段代碼能解答之前我們的兩個疑問:

  1. 什么時候注入:無論是在 +load 還是在被 __attribute__((constructor)) 修飾的函數中,至少能保證注入是發生在 main 函數之前的(關于 +load 與 __attribute__((constructor)) 的執行順序,請參考我之前的文章: 『Apple API』/ / attribute / )。
  2. 以怎樣的形式獲取 SEL 與 IMP:這個宏直接為 protocol 擴展了一個容器類,所以默認實現的方法都是存在這個類中的,之后要進行注入,方法的 SEL 與 IMP 也應該是從這個容器類中進行獲取。

所以,根據調用順序,我們接下來分成兩步來分析整個實現。

ext_addConcreteProtocol

首先被調用的是 +load 中的 ext_addConcreteProtocol 函數。這個函數接收兩個參數:當前的 protocol 對象,以及對應的容器類。實現中又調用了另外一個:

BOOL ext_addConcreteProtocol (Protocol *protocol, Class containerClass) {
    return ext_loadSpecialProtocol(protocol, ^(Class destinationClass){
        ext_injectConcreteProtocol(protocol, containerClass, destinationClass);
    });
}

ext_loadSpecialProtocol 函數接收兩個參數:當前 protocol,以及一個參數為 Class destinationClass 的 block。我們先不看這個 block 的具體實現,先來看看 ext_loadSpecialProtocol 都做了些什么。

ext_loadSpecialProtocol

同樣,我簡化了 ext_loadSpecialProtocol 的實現,代碼大多用注釋描述來代替:

// 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;
}

所以,整個函數走下來,作用就是將 {protocol, block} 追加到數組中。

也就是說,在執行 __attribute__((constructor)) 修飾的方法以前,所有默認實現的 protocol,都會被加到這個數組中。

接下來,我們來看 +load 之后做了什么。

ext_loadConcreteProtocol

同樣, ext_loadConcreteProtocol 內部也調用了另一個函數:

void ext_loadConcreteProtocol (Protocol *protocol) {
    ext_specialProtocolReadyForInjection(protocol);
}

整個函數的實現很簡單,用于確保 +load 中加入數組的所有 protocol 都能找到:

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;
    }
  }
}

在 ext_specialProtocolReadyForInjection 的實現中, if (++specialProtocolsReady == specialProtocolCount) 這個判斷比較有趣,它能回答我們的第三個問題(如何節省開銷):

  1. +load 與 __attribute__((constructor)) 的優先級能使得所有 protocol 加入完成以后,再進行處理。
  2. ready 計數 specialProtocolsReady 使得所有默認實現均判斷無誤后,再進行注入。

到此,好像已經完事具備,馬上就可以進行注入了。但蒼天饒過誰,我們還有很重要的一個問題沒有考慮。

ext_injectSpecialProtocols

優先級問題:如果 protocolA <ProtocolB> ,也就是 protocolA 遵循 protocolB ,那么誰的優先級更高呢?除此之外,如果遵循 protocol 的 class,自己也實現了默認方法呢?

這個問題,在 ext_injectSpecialProtocols 函數中能得到答案:

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);
    }
  }
}

所以,整個方法的任務也很清晰:

  1. 對 protocol 進行優先級排序,給出具體注入的先后順序,防止方法覆蓋或無法注入。
  2. 獲取全部 class 列表。
  3. 兩層循環遍歷,將 class 與其遵循的 protocol 進行匹配。
  4. 調用 struct 中的 block,并將目標 class 傳出,進行注入。

接下來,終于到了最后一步,來看看 block 中的注入方法的實現。

ext_injectConcreteProtocol

block 是在 +load 中就已經賦值了,而 block 的實現,就是直接調用了 ext_injectConcreteProtocol 函數:

// 函數有三個參數
// 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
        // 判斷 目標類 是否存在該方法
        // 進行注入
    }
    // 循環注入類方法同理
}

到此,方法注入就已經全部完成了。

總結

面向過程的過完了整個源碼,從頭再來梳理一下:

  1. 注入實現思路的重點在于,使用宏為 protocol 擴展了一個容器類。
  2. 容器類中,利用 +load 與 __attribute__((constructor)) 的特性,將注入流程分為了兩個部分。
  3. 在 +load 中,將 protocol,執行注入的 block 打包成 struct,然后將 struct 裝進數組。
  4. 當執行到 __attribute__((constructor)) 時,也就表示所有類的 +load 都已經執行過了,再對數組進行優先級排序。
  5. 排序完成后,兩層循環嵌套,查找遵循了 protocol 的 class。
  6. 調用 block 執行注入。

 

來自:http://www.saitjr.com/ios/『libextobjc』objctive-c-協議的默認實現.html

 

 本文由用戶 fgfm9310 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!