NNPopObjc:在 Objective-C 上進(jìn)行面向協(xié)議的編程(下)

在上半部分主要介紹了 NNPopObjc 的使用烟逊,包括默認(rèn)協(xié)議擴(kuò)展、約束協(xié)議擴(kuò)展等牡直。本文 (下) 主要介紹 NNPopObjc 的實(shí)現(xiàn)思路和原理肩碟。

神奇的宏

NNPopObjc 中使用宏實(shí)現(xiàn)了關(guān)鍵字@nn_extension(...), @nn_where(...) 。但限于篇幅,這里我們不去詳細(xì)的講解這些宏是如何實(shí)現(xiàn)的游沿。對(duì)于本節(jié)我們會(huì)解釋元編程的概念及其編程思維饰抒。一旦對(duì)元編程及其編程思維有了一定的認(rèn)識(shí),那么再去分析 NNPopObjc 中的宏實(shí)現(xiàn)就會(huì)變的非常簡(jiǎn)單了诀黍。

元編程

宏編程也被稱為 C 語(yǔ)言系中的元編程袋坑,可以簡(jiǎn)單理解為代碼作為函數(shù)的輸入和輸出。在 NNPopObjc 中使用宏實(shí)現(xiàn)了關(guān)鍵字@nn_extension(...), @nn_where(...) 眯勾,為實(shí)現(xiàn)以上關(guān)鍵字枣宫, NNPopObjc 使用了大量的宏作為中間轉(zhuǎn)換。這些宏大多基于 metamacros.h 的實(shí)現(xiàn)或擴(kuò)展吃环。

元編程的思維

NNPopObjc 中使用了很多宏特性也颤。本節(jié)選擇具有代表性實(shí)現(xiàn)的宏函數(shù) nn_pop_if_less(A, B) 作為講解示例。

為了方便理解郁轻,這里我們對(duì) nn_pop_if_less(A, B) 的定義做一些簡(jiǎn)化翅娶,定義如下:

  • AB 取值范圍 [0..2]
  • 當(dāng) A < B 時(shí)輸出 A好唯,否則輸出 B

用元編程的思維去思考和實(shí)現(xiàn):

使用元編程去實(shí)現(xiàn)竭沫,那么結(jié)果的輸出應(yīng)該在編譯器階段完成的,也就是說骑篙,元編程中宏函數(shù)的結(jié)果不需要應(yīng)用程序的執(zhí)行蜕提。下面定義雖然也能實(shí)現(xiàn),但是真正的結(jié)果是由應(yīng)用程序執(zhí)行階段獲得的靶端,因此并不是我們期望的元編程實(shí)現(xiàn)贯溅。

#define nn_pop_if_less(A, B)    (A < B ? A : B)

用元編程的思維去實(shí)現(xiàn) nn_pop_if_less(A, B)

以下給出 nn_pop_if_less(A, B) 元編程的實(shí)現(xiàn):

#define nn_pop_if_less(A, B) \
        nn_pop_if_less_(A, B)(A)(B)
    
#define nn_pop_if_less_(A, B) \
        nn_pop_if_less_##A##_##B

#define nn_pop_if_less_0_0(A) nn_pop_expand_
#define nn_pop_if_less_0_1(A) A nn_pop_consume_
#define nn_pop_if_less_0_2(A) A nn_pop_consume_
#define nn_pop_if_less_1_0(A) nn_pop_expand_
#define nn_pop_if_less_1_1(A) nn_pop_expand_
#define nn_pop_if_less_1_2(A) A nn_pop_consume_
#define nn_pop_if_less_2_0(A) nn_pop_expand_
#define nn_pop_if_less_2_1(A) nn_pop_expand_
#define nn_pop_if_less_2_2(A) nn_pop_expand_

#define nn_pop_expand_(B)   B
#define nn_pop_consume_(B)

分析實(shí)現(xiàn)

這里我們通過兩個(gè)示例,使用類似數(shù)學(xué)推導(dǎo)的方式來分析 nn_pop_if_less(A, B) 的元編程實(shí)現(xiàn)躲查。 每一步的推導(dǎo)代表一次宏替換它浅。

示例 1 :
nn_pop_if_less(1, 2) = 1

  nn_pop_if_less(1, 2)
= nn_pop_if_less_(1, 2)(1)(2)
= nn_pop_if_less_##1##_##2(1)(2)
= nn_pop_if_less_1_2(1)(2)
= 1 nn_pop_consume_(2)
= 1

示例 2 :
nn_pop_if_less(1, 0) = 0

  nn_pop_if_less(1, 0)
= nn_pop_if_less_(1, 0)(1)(0)
= nn_pop_if_less_##1##_##0(1)(0)
= nn_pop_if_less_1_0(1)(0)
= nn_pop_expand_(0)
= 0

通過對(duì)上述兩個(gè)示例的推導(dǎo),可以看出宏函數(shù)的結(jié)果由編譯器分析獲得镣煮,獲得的結(jié)果不需要應(yīng)用程序的執(zhí)行姐霍,那么上述的實(shí)現(xiàn)也就是我們所期望的元編程實(shí)現(xiàn)。

優(yōu)化和改進(jìn)

在上面的實(shí)現(xiàn)中典唇,你也許會(huì)發(fā)現(xiàn)镊折,宏函數(shù)的比較最終由一系列的 nn_pop_if_less_A_B 擴(kuò)展提供。那介衔,如果參數(shù)的范圍是 [0..20] 或者更多那恨胚?排列組合后的結(jié)果不敢想象。針對(duì)這個(gè)問題炎咖,metamacros.h 提供了非常棒的實(shí)現(xiàn)方式赃泡。具體實(shí)現(xiàn)可參考宏函數(shù) metamacro_if_eq(A, B) 寒波。

可惜的是 metamacros.h 僅提供了 metamacro_if_eq(A, B) 用于判等的宏函數(shù),而未提供其他判斷條件的宏函數(shù)實(shí)現(xiàn)升熊。為了方便開發(fā)俄烁,作者在 NNPopObjc 中對(duì)其進(jìn)行了擴(kuò)展并提供了全條件判斷宏函數(shù)。

nn_pop_if_equal(A, B)
nn_pop_if_greater(A, B)
nn_pop_if_greater_or_equal(A, B)
nn_pop_if_less(A, B)
nn_pop_if_less_or_equal(A, B)

小結(jié)

本節(jié)介紹了一個(gè)非常重要的編程概念元編程级野。那么在理解了元編程之后页屠,對(duì)于 NNPopObjc 中關(guān)鍵字 @nn_extension(...), @nn_where(...) 的實(shí)現(xiàn)和理解就變的非常容易了。

最好的時(shí)機(jī)

凡是要對(duì)語(yǔ)言進(jìn)行擴(kuò)展的框架蓖柔,大多數(shù)逃不出運(yùn)行時(shí)的應(yīng)用辰企。同樣為了實(shí)現(xiàn)協(xié)議的擴(kuò)展, NNPopObjc 的注入實(shí)現(xiàn)也基于運(yùn)行時(shí)况鸣。

在定義協(xié)議擴(kuò)展時(shí)牢贸, NNPopObjc 會(huì)定義一個(gè)類作為協(xié)議擴(kuò)展實(shí)現(xiàn)的容器類,然后通過運(yùn)行時(shí)將容器類中的方法注入到遵守協(xié)議類中懒闷。

注入時(shí)機(jī)

使用程序注入,便會(huì)產(chǎn)生一個(gè)問題:在哪個(gè)時(shí)機(jī)進(jìn)行注入栈幸。本節(jié)我們將對(duì)這個(gè)問題進(jìn)行討論愤估。

動(dòng)態(tài)方法解析/消息轉(zhuǎn)發(fā)

在 main() 函數(shù)之后,動(dòng)態(tài)方法解析/消息轉(zhuǎn)發(fā)可以作為一個(gè)注入的時(shí)機(jī)速址。例如我們非常熟悉的在 Objective-C 中實(shí)現(xiàn)面向切面編程(AOP)的 Aspects玩焰,一段時(shí)間非常熱門的 iOS 動(dòng)態(tài)熱修復(fù)框架 JSPatch 。以上框架中的注入時(shí)機(jī)都選擇在了動(dòng)態(tài)方法解析/消息轉(zhuǎn)發(fā)芍锚。

NNPopObjc0.5.0 及以前的版本就采用了在動(dòng)態(tài)方法解析/消息轉(zhuǎn)發(fā)進(jìn)行注入昔园。如果選擇動(dòng)態(tài)方法解析/消息轉(zhuǎn)發(fā)作為 NNPopObjc 的注入時(shí)機(jī),那么程序就會(huì)有以下特點(diǎn):

  • 按需注入:由于在遵守協(xié)議類沒有協(xié)議的實(shí)際實(shí)現(xiàn)并炮,所以在調(diào)用未實(shí)現(xiàn)的協(xié)議方法時(shí)默刚,就會(huì)觸發(fā) 動(dòng)態(tài)方法解析/消息轉(zhuǎn)發(fā) 。在此時(shí)刻進(jìn)行注入逃魄,我們可以僅對(duì)需要的方法進(jìn)行注入荤西,因此可以避免全量注入而可能引起的性能問題。
  • 兼容性問題:期望對(duì)代碼無侵入伍俘,就必須要 hook 動(dòng)態(tài)方法解析/消息轉(zhuǎn)發(fā)期間的函數(shù)邪锌。雖然實(shí)現(xiàn)了對(duì)代碼的無侵入,但如果其他代碼也 hook 這些函數(shù)癌瘾,就可能會(huì)導(dǎo)致函數(shù)調(diào)用混亂觅丰。比如本節(jié)開頭提到的 AspectsJSPatch,兩個(gè)框架同時(shí)使用是就會(huì)存在兼容性問題妨退。

+ load()

在 main() 函數(shù)之前妇萄,+ load() 可以作為一個(gè)注入的時(shí)機(jī)蜕企。也常被廣大開發(fā)者做為方法交換,方法注入的選擇嚣伐。

使用在 + load() 中實(shí)現(xiàn)注入糖赔,程序就會(huì)有以下特點(diǎn):

  • 占用默認(rèn) + load() 方法:使用 + load() 方法作為注入時(shí)機(jī),一定會(huì)占用一個(gè)已有的 + load() 轩端。
    1. 協(xié)議擴(kuò)展類 + load():會(huì)導(dǎo)致開發(fā)者無法為擴(kuò)展類自定義 + load() 方法放典。
    2. 遵守協(xié)議類 + load():會(huì)導(dǎo)致嚴(yán)重的代碼侵入。
  • 性能問題:避免代碼入侵基茵,使用協(xié)議擴(kuò)展類 + load() 作為注入時(shí)機(jī)奋构。那么,在一個(gè)協(xié)議實(shí)現(xiàn)了多個(gè)擴(kuò)展的情況下拱层,為了實(shí)現(xiàn)注入弥臼,每個(gè)協(xié)議擴(kuò)展的 + load() 都需要遍歷一遍類列表。這樣無疑會(huì)增加 + load() 耗時(shí)根灯,影響應(yīng)用啟動(dòng)径缅。
  • 時(shí)序問題:由于 + load() 方法的調(diào)用順序是變化的,如果類或協(xié)議存在多個(gè)繼承關(guān)系就可能會(huì)導(dǎo)致注入結(jié)果與期望的不同烙肺。

__attribute__((constructor))

在 main() 函數(shù)之前纳猪,被 __attribute__((constructor)) 修飾的函數(shù)可以作為一個(gè)注入的時(shí)機(jī)。例如我們非常熟悉的在函數(shù) hook 框架 fishhook桃笙,阿里開源協(xié)程開發(fā)框架 coobjc 氏堤,都選擇了在 __attribute__((constructor)) 函數(shù)位置作為注入時(shí)機(jī)。

使用在 __attribute__((constructor)) 函數(shù)中實(shí)現(xiàn)注入搏明,程序就會(huì)有以下特點(diǎn):

  • 對(duì)遵守協(xié)議類 + load() 的影響:__attribute__((constructor)) 函數(shù)的執(zhí)行在所有 + load() 方法之后鼠锈,main() 函數(shù)之前。如果想在遵守協(xié)議類中對(duì)協(xié)議擴(kuò)展的方法進(jìn)行交換(MethodSwizz)是無法實(shí)現(xiàn)的星著,因?yàn)樵谧袷貐f(xié)議類的 + load() 中協(xié)議擴(kuò)展的方法還未被注入购笆,此時(shí)的方法實(shí)現(xiàn)并不存在。

初識(shí) __attribute__((constructor)) 函數(shù)

來自 GCC 上的一些描述:

constructor
destructor
constructor (priority)
destructor (priority)
The constructor attribute causes the function to be called automatically before execution enters main (). Similarly, the destructor attribute causes the function to be called automatically after main () completes or exit () is called. Functions with these attributes are useful for initializing data that is used implicitly during the execution of the program.
You may provide an optional integer priority to control the order in which constructor and destructor functions are run. A constructor with a smaller priority number runs before a constructor with a larger priority number; the opposite relationship holds for destructors. So, if you have a constructor that allocates a resource and a destructor that deallocates the same resource, both functions typically have the same priority. The priorities for constructor and destructor functions are the same as those specified for namespace-scope C++ objects (see C++ Attributes).

These attributes are not currently implemented for Objective-C.

嗯 …… 虚循,最后一句是針對(duì) GCC 的描述由桌,對(duì)于 LLVM 和 Clang 可以忽略。

取其中有用的內(nèi)容:

The constructor attribute causes the function to be called automatically before execution enters main ().

構(gòu)造屬性的函數(shù)會(huì)在進(jìn)入 main () 之前被自動(dòng)調(diào)用邮丰。

那函數(shù)到底是在什么位置被調(diào)用那行您?在 XCode 中 __attribute__((constructor)) 函數(shù)中增加斷點(diǎn),可以得到如下函數(shù)調(diào)用棧:

* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 2.1
  * frame #0: 0x00000001027cd83c NNPopObjc`popobjc::initializer(argc=1, argv=0x00007ffeed560da8, envp=0x00007ffeed560db8, apple=0x00007ffeed560fa0, vars=0x0000000102707170) at NNPopObjcInjection.mm:377:53
    frame #1: 0x00000001026c43a7 dyld_sim`ImageLoaderMachO::doModInitFunctions(ImageLoader::LinkContext const&) + 517
    frame #2: 0x00000001026c47b8 dyld_sim`ImageLoaderMachO::doInitialization(ImageLoader::LinkContext const&) + 40
    frame #3: 0x00000001026bf9a2 dyld_sim`ImageLoader::recursiveInitialization(ImageLoader::LinkContext const&, unsigned int, char const*, ImageLoader::InitializerTimingList&, ImageLoader::UninitedUpwards&) + 456
    frame #4: 0x00000001026bf90f dyld_sim`ImageLoader::recursiveInitialization(ImageLoader::LinkContext const&, unsigned int, char const*, ImageLoader::InitializerTimingList&, ImageLoader::UninitedUpwards&) + 309
    frame #5: 0x00000001026be7a6 dyld_sim`ImageLoader::processInitializers(ImageLoader::LinkContext const&, unsigned int, ImageLoader::InitializerTimingList&, ImageLoader::UninitedUpwards&) + 188
    frame #6: 0x00000001026be846 dyld_sim`ImageLoader::runInitializers(ImageLoader::LinkContext const&, ImageLoader::InitializerTimingList&) + 82
    frame #7: 0x00000001026b308c dyld_sim`dyld::initializeMainExecutable() + 199
    frame #8: 0x00000001026b70fc dyld_sim`dyld::_main(macho_header const*, unsigned long, int, char const**, char const**, char const**, unsigned long*) + 3831
    frame #9: 0x00000001026b21cd dyld_sim`start_sim + 122
    frame #10: 0x0000000105b028b7 dyld`dyld::useSimulatorDyld(int, macho_header const*, char const*, int, char const**, char const**, char const**, unsigned long*, unsigned long*) + 2308
    frame #11: 0x0000000105b00575 dyld`dyld::_main(macho_header const*, unsigned long, int, char const**, char const**, char const**, unsigned long*) + 818
    frame #12: 0x0000000105afb227 dyld`dyldbootstrap::start(dyld3::MachOLoaded const*, int, char const**, dyld3::MachOLoaded const*, unsigned long*) + 453
    frame #13: 0x0000000105afb025 dyld`_dyld_start + 37

其中 popobjc::initializer 是我們定義的 __attribute__((constructor)) 函數(shù)剪廉。
顯然 __attribute__((constructor)) 函數(shù)由 dyld 中的 ImageLoaderMachO::doModInitFunctions 調(diào)用娃循。dyld 是開源的,源碼可以在 dyld 下載斗蒋,NNPopObjc獲取線索小節(jié)中也有對(duì) dyld 源碼的參考捌斧。對(duì)于更多 Image 加載過程的相關(guān)內(nèi)容可以參考 dyld 源碼笛质。

小結(jié)

到此為止,本節(jié)介紹了常見的注入時(shí)機(jī)捞蚂。這里 NNPopObjc 在0.6.0及以后的版本中也選擇了 __attribute__((constructor)) 函數(shù)作為項(xiàng)目的注入時(shí)機(jī)妇押。

留下線索

要實(shí)現(xiàn)對(duì)遵守協(xié)議類的方法注入,就必須獲取以下信息:

  • 協(xié)議
  • 協(xié)議擴(kuò)展類(協(xié)議擴(kuò)展實(shí)現(xiàn)的容器類)
  • 遵守協(xié)議類

NNPopObjc 的實(shí)現(xiàn)中姓迅,協(xié)議協(xié)議擴(kuò)展類的信息在 @nn_extension 對(duì)協(xié)議進(jìn)行擴(kuò)展時(shí)被保存到了數(shù)據(jù)段(data segment)中敲霍, 之后在 __attribute__((constructor)) 函數(shù)注入時(shí),從數(shù)據(jù)段獲得協(xié)議協(xié)議擴(kuò)展類的信息丁存。

數(shù)據(jù)段/分段

數(shù)據(jù)段(data segment)通常是指用來存放程序中已初始化的全局變量的一塊內(nèi)存區(qū)域肩杈。數(shù)據(jù)段屬于靜態(tài)內(nèi)存分配 —— 百度百科。

分段(section)一個(gè)段包含多個(gè)分段解寝。

定義一個(gè)變量到指定的數(shù)據(jù)段分段中

struct duart a __attribute__ ((used, section ("__DATA", "DUART_A"))) = { 0 };
  • used:避免未被使用的段被編譯器優(yōu)化移除
  • section:描述變量保存的段描述
  • "__DATA":描述變量保存的段為數(shù)據(jù)段
  • "DUART_A":描述變量保存到數(shù)據(jù)段中名為 "DUART_A" 的分段

保存注入信息到數(shù)據(jù)段

@nn_extension(...) 的展開

下面是對(duì) NNCodeProtocol 實(shí)現(xiàn)一個(gè)協(xié)議擴(kuò)展

@nn_extension(NNCodeProtocol)

+ (void)sayHelloPop {
    DLog(@"+[%@ %s] code says hello pop", self, sel_getName(_cmd));
}

- (void)sayHelloPop {
    DLog(@"-[%@ %s] code says hello pop", [self class], sel_getName(_cmd));
}

@end

對(duì)上面的協(xié)議擴(kuò)展的宏定義部分進(jìn)行展開

@class NSObject;

static nn_pop_where_value_def w___NNPopObjc_NNCodeProtocol___NNCodeNameProtocol(Class self) {
    @autoreleasepool {}
    return ^nn_pop_where_value_def(__attribute__((objc_ownership(none))) Class self){
        if (self == ((void *)0)) {
            return nn_pop_where_value_unmatched;
            
        } BOOL
        is_match = (self = [NNCodeC class]);
        if (is_match == 0) {
            return nn_pop_where_value_unmatched;
        }
        return 0 ? nn_pop_where_value_matched_default : nn_pop_where_value_matched_constrained;
    }(self);
}

const nn_pop_extension_description_t s___NNPopObjc_NNCodeProtocol___NNCodeNameProtocol __attribute__((used, section("__DATA" "," "__nn_pop_objc__" ))) = {
    "NNCodeProtocol",
    "__NNPopObjc",
    "__NNPopObjc_NNCodeProtocol___NNCodeNameProtocol",
    w___NNPopObjc_NNCodeProtocol___NNCodeNameProtocol,
    1,
    {"NNCodeNameProtocol",},
};

@interface __NNPopObjc_NNCodeProtocol___NNCodeNameProtocol : NSObject < NNCodeProtocol ,NNCodeNameProtocol>

@end

@implementation __NNPopObjc_NNCodeProtocol___NNCodeNameProtocol

+ (void)sayHelloPop {
    printf("%s\n", [[NSString stringWithFormat:@"+[%@ %s] code says hello pop", self, sel_getName(_cmd)] UTF8String]);;
}

- (void)sayHelloPop {
    printf("%s\n", [[NSString stringWithFormat:@"-[%@ %s] code says hello pop", [self class], sel_getName(_cmd)] UTF8String]);;
}

@end

在展開中我們能夠發(fā)現(xiàn)以下代碼:

const nn_pop_extension_description_t s___NNPopObjc_NNCodeProtocol___NNCodeNameProtocol __attribute__((used, section("__DATA" "," "__nn_pop_objc__" ))) = {
    "NNCodeProtocol",
    "__NNPopObjc",
    "__NNPopObjc_NNCodeProtocol___NNCodeNameProtocol",
    w___NNPopObjc_NNCodeProtocol___NNCodeNameProtocol,
    1,
    {"NNCodeNameProtocol",},
};

在 NNCodeProtocol 的協(xié)議擴(kuò)展中程序?qū)⒁粋€(gè)名為 s___NNPopObjc_NNCodeProtocol___NNCodeNameProtocol 的結(jié)構(gòu)體變量保存到了名為 __nn_pop_objc__ 的數(shù)據(jù)段分段中扩然。在 NNPopObjc 中所有協(xié)議擴(kuò)展的數(shù)據(jù)都會(huì)保存在 __nn_pop_objc__ 分段中。

nn_pop_extension_description_t 結(jié)構(gòu)體

NNPopObjcnn_pop_extension_description_t 結(jié)構(gòu)體保存了所有協(xié)議擴(kuò)展注入時(shí)需要的信息聋伦。
下面是對(duì) nn_pop_extension_description_t 結(jié)構(gòu)體各字段的描述夫偶。

typedef struct {
    /// 協(xié)議名稱
    const char *protocol;
    /// 協(xié)議擴(kuò)展類前綴
    const char *prefix;
    /// 協(xié)議擴(kuò)展類名稱
    const char *clazz;
    /// 執(zhí)行 where 表達(dá)式的函數(shù)指針
    where_fp where_fp;
    /// 遵守協(xié)議類需要滿足的協(xié)議數(shù)量
    unsigned int confrom_protocols_count;
    /// 遵守協(xié)議類需要滿足的協(xié)議列表
    const char *confrom_protocols[20];
} nn_pop_extension_description_t;

小結(jié)

NNPopObjc 中協(xié)議擴(kuò)展注入的信息由結(jié)構(gòu)體 nn_pop_extension_description_t 描述,注入信息的結(jié)構(gòu)體變量被保存到了名為 __nn_pop_objc__ 的數(shù)據(jù)段分段中觉增。

獲取線索

只要在函數(shù)中讀取名為 __nn_pop_objc__ 的數(shù)據(jù)段分段兵拢,就能夠進(jìn)行協(xié)議擴(kuò)展注入了。

getsectbynamegetsectiondata

獲取分段數(shù)據(jù)可以通過 getsectbynamegetsectiondata 獲取抑片,但是在蘋果系統(tǒng)中由于 ASLR 卵佛,獲取數(shù)據(jù)段分段信息只能使用函數(shù) getsectiondata 杨赤。參考 crash-reading-bytes-from-getsectbyname敞斋。

mach_header

getsectiondata 接口定義

extern uint8_t *getsectiondata(
    const struct mach_header *mhp,
    const char *segname,
    const char *sectname,
    unsigned long *size);

這里需要一個(gè) mach_header 結(jié)構(gòu)體參數(shù)。mach_header 對(duì) 32 位和 64 為系統(tǒng)分別進(jìn)行了定義疾牲,但有效字段是一致的植捎。

以下是 32 mach_header 結(jié)構(gòu)的描述:

The 32-bit mach header appears at the very beginning of the object file for 32-bit architectures.

mach_header 保存在對(duì)象文件的頭部,mach_header 結(jié)構(gòu)體描述請(qǐng)參考頭文件 <mach/loader.h> 阳柔。

獲取 mach_header

通過 __attribute__((constructor)) 函數(shù)獲取

__attribute__((constructor)) 函數(shù)是一種函數(shù)回調(diào)焰枢,但是回調(diào)函數(shù)的格式缺并沒有給出。這里我們就要參考文中提到的 dyld 源碼舌剂。
最好的時(shí)機(jī) 章節(jié)中提到 __attribute__((constructor)) 函數(shù)最終由 dyld 中 ImageLoaderMachO::doModInitFunctions 方法調(diào)用济锄。

ImageLoaderMachO::doModInitFunctions 方法中我們會(huì)發(fā)現(xiàn)被調(diào)用的初始化函數(shù)為 Initializer 類型的函數(shù),Initializer 定義如下:

struct ProgramVars
{
    const void*     mh;
    int*            NXArgcPtr;
    const char***   NXArgvPtr;
    const char***   environPtr;
    const char**    __prognamePtr;
};
typedef void (*Initializer)(int argc, const char* argv[], const char* envp[], const char* apple[], const ProgramVars* vars);

ProgramVars 結(jié)構(gòu)體中 const void* mh; 即是我們需要的 mach_header 霍转。根據(jù) dyld 中定義的函數(shù)定義荐绝,實(shí)現(xiàn) __attribute__((constructor)) 函數(shù),如下:

typedef struct
#ifdef __LP64__
mach_header_64
#else
mach_header
#endif
nn_pop_mach_header;

struct ProgramVars {
    const void*        mh;
    int*            NXArgcPtr;
    const char***    NXArgvPtr;
    const char***    environPtr;
    const char**    __prognamePtr;
};

__attribute__((constructor)) void initializer(int argc,
                                              const char **argv,
                                              const char **envp,
                                              const char **apple,
                                              const ProgramVars* vars) {
    
    nn_pop_mach_header *mhp = (nn_pop_mach_header *)vars->mh;
    
    loadSection(mhp,
                nn_pop_metamacro_stringify(nn_pop_section_name),
                [](std::vector<ProtocolExtension *> protocolExtensions) {
        ......
    });
}

這樣我們就能夠在 __attribute__((constructor)) 函數(shù)中得到 mach_header 變量了避消。

但是低滩,這里需要注意的是召夹,__attribute__((constructor)) 函數(shù)的調(diào)用是所在 Mach-O 文件加載 doModInit 時(shí)調(diào)用。也就是說這里得到的 mach_header 是當(dāng)前加載的 Mach-O 文件的 mhp 恕沫。那么就可能會(huì)影響 NNPopObjc 中 __nn_pop_objc__ section 的加載:

  1. NNPopObjc 作為動(dòng)態(tài)庫(kù)集成:得到的 mach_header 為 NNPopObjc 動(dòng)態(tài)庫(kù)的 Mach-O 文件的 mhp监憎,只能加載 NNPopObjc 動(dòng)態(tài)庫(kù) Mach-O 的 __nn_pop_objc__ section 。
  2. NNPopObjc 作為靜態(tài)庫(kù)集成:
    • NNPopObjc 中包含 OC 對(duì)象:得到的 mach_header 為最終連接的 Mach-O 文件的 mhp婶溯,只能加載最終連接 Mach-O 的 __nn_pop_objc__ section 鲸阔。
    • NNPopObjc 中不包含 OC 對(duì)象:__attribute__((constructor)) 不會(huì)被調(diào)用。

通過 _dyld_register_func_for_add_image 獲取

通過 _dyld_register_func_for_add_image 注冊(cè)回調(diào)函數(shù)獲得 mach_header 爬虱。

/// Image loaded callback function.
/// @param mhp mhp
/// @param vmaddr_slide vmaddr_slide
void imageLoadedCallback(const struct mach_header *mhp, intptr_t vmaddr_slide) {
    
    nn_pop_mach_header *_mhp = (nn_pop_mach_header *)mhp;
    
    loadSection(mhp,
                nn_pop_metamacro_stringify(nn_pop_section_name),
                [](std::vector<ProtocolExtension *> protocolExtensions) {
        ......
    });
}

/// Initializer function is called by ImageLoaderMachO::doModInitFunctions at dyld project.
/// @note dyld project: https://opensource.apple.com/tarballs/dyld/
/// @note fix: The dynamic library section cannot be loaded when the protocol extensions
/// are implemented in a dynamic library.
__attribute__((constructor)) void initializer() {
    
    _dyld_register_func_for_add_image(imageLoadedCallback);
}

這里 imageLoadedCallback 回調(diào)函數(shù)會(huì)被調(diào)用多次隶债,每次回調(diào)中的 mhp 參數(shù)對(duì)應(yīng)一個(gè) Mach-O 文件 。

其他方式

關(guān)于 mach_header 的一些其它獲取方式可參考 <mach-o/dyld.h> 中相關(guān)函數(shù)跑筝。

小結(jié)

NNPopObjc 中使用 _dyld_register_func_for_add_image 注冊(cè)回調(diào)的方式依次獲取所有 mach_header 變量死讹,并通過 getsectiondata 函數(shù)嘗試讀取 mach_header 對(duì)應(yīng) Mach-O 文件保存在 __nn_pop_objc__ 數(shù)據(jù)段分段中用于注入的信息,最后進(jìn)行擴(kuò)展注入曲梗。

協(xié)議擴(kuò)展注入

關(guān)于協(xié)議擴(kuò)展注入這里就不做過多的介紹了赞警。基于類列表查找虏两,對(duì)與遵守協(xié)議且符合協(xié)議擴(kuò)展條件的類進(jìn)行方法注入即可愧旦。

結(jié)

NNPopObjc 為在 Objective-C 上進(jìn)行面向協(xié)議的編程提供了可能。在面向協(xié)議的編程中定罢,協(xié)議可以擁有自己的行為笤虫,使得程序減少類和繼承帶來的負(fù)面問題。讓程序更加靈活和便于維護(hù)祖凫。

致謝與參考

NNPopObjc 思路和實(shí)現(xiàn)離不開開源社區(qū)的力量琼蚯,在此由衷感謝!

以下是 NNPopObjc 實(shí)現(xiàn)中參考的項(xiàng)目及資料惠况。

項(xiàng)目

  • 相似項(xiàng)目

libextobjc
ProtocolKit

  • 其他項(xiàng)目

Aspects
JSPatch
fishhook
coobjc

文章


歡迎訪問我的GitHub博客

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末遭庶,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子稠屠,更是在濱河造成了極大的恐慌峦睡,老刑警劉巖,帶你破解...
    沈念sama閱讀 219,490評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件权埠,死亡現(xiàn)場(chǎng)離奇詭異榨了,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)攘蔽,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,581評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門龙屉,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人秩彤,你說我怎么就攤上這事叔扼∈驴蓿” “怎么了?”我有些...
    開封第一講書人閱讀 165,830評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵瓜富,是天一觀的道長(zhǎng)鳍咱。 經(jīng)常有香客問我,道長(zhǎng)与柑,這世上最難降的妖魔是什么谤辜? 我笑而不...
    開封第一講書人閱讀 58,957評(píng)論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮价捧,結(jié)果婚禮上丑念,老公的妹妹穿的比我還像新娘。我一直安慰自己结蟋,他們只是感情好脯倚,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,974評(píng)論 6 393
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著嵌屎,像睡著了一般推正。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上宝惰,一...
    開封第一講書人閱讀 51,754評(píng)論 1 307
  • 那天植榕,我揣著相機(jī)與錄音,去河邊找鬼尼夺。 笑死尊残,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的淤堵。 我是一名探鬼主播寝衫,決...
    沈念sama閱讀 40,464評(píng)論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼粘勒!你這毒婦竟也來了竞端?” 一聲冷哼從身側(cè)響起屎即,我...
    開封第一講書人閱讀 39,357評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤庙睡,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后技俐,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體乘陪,經(jīng)...
    沈念sama閱讀 45,847評(píng)論 1 317
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,995評(píng)論 3 338
  • 正文 我和宋清朗相戀三年雕擂,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了啡邑。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,137評(píng)論 1 351
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡井赌,死狀恐怖谤逼,靈堂內(nèi)的尸體忽然破棺而出贵扰,到底是詐尸還是另有隱情,我是刑警寧澤流部,帶...
    沈念sama閱讀 35,819評(píng)論 5 346
  • 正文 年R本政府宣布戚绕,位于F島的核電站,受9級(jí)特大地震影響枝冀,放射性物質(zhì)發(fā)生泄漏舞丛。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,482評(píng)論 3 331
  • 文/蒙蒙 一果漾、第九天 我趴在偏房一處隱蔽的房頂上張望球切。 院中可真熱鬧,春花似錦绒障、人聲如沸吨凑。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,023評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)怀骤。三九已至,卻和暖如春焕妙,著一層夾襖步出監(jiān)牢的瞬間蒋伦,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,149評(píng)論 1 272
  • 我被黑心中介騙來泰國(guó)打工焚鹊, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留痕届,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,409評(píng)論 3 373
  • 正文 我出身青樓末患,卻偏偏與公主長(zhǎng)得像研叫,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子璧针,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,086評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容