iOS中的HOOK技術(shù)

一、fishhook

1、介紹

fishhook是facebook出品的一個(gè)開(kāi)源庫(kù)。利用mach-o文件加載原理,通過(guò)rebind_symbols函數(shù)修改__DATA Segment的符號(hào)指針指向,來(lái)動(dòng)態(tài)的Hook C函數(shù)。

2、主要信息

2.1、結(jié)構(gòu)體

struct rebinding {
  const char *name;   //函數(shù)名稱
  void *replacement;  //新的函數(shù)地址
  void **replaced;   //保存原始函數(shù)地址變量的指針(通常要存儲(chǔ)下來(lái),在替換后的方法里調(diào)用)
};

2.2、主要接口

  /*
  交換方法
  arg1: 存放rebinding結(jié)構(gòu)體的數(shù)組
  arg2: 數(shù)組的長(zhǎng)度
  */
int rebind_symbols(struct rebinding rebindings[], size_t rebindings_nel);

3、實(shí)現(xiàn)

3.1、hook OC函數(shù)

#import "ViewController.h"
#import "fishhook.h"

@interface ViewController ()

@end

@implementation ViewController
- (void)viewDidLoad {
  [super viewDidLoad];

  //這里必須要先加載一次要交換的函數(shù),否則符號(hào)表里面不會(huì)出現(xiàn)要交換的函數(shù)的地址
  NSLog(@"我是純正的NSLog函數(shù)");

  //定義rebinding結(jié)構(gòu)體
  struct rebinding manager;
  //要交換的函數(shù)的名稱
  manager.name = "NSLog";
  //新的函數(shù)地址
  manager.replacement = new_NSLog;
  //保存原始函數(shù)地址變量的指針(存儲(chǔ)下來(lái),在替換后的方法里調(diào)用)
  manager.replaced = (void *)&old_NSLog;

  //定義數(shù)組
  struct rebinding rebs[] = {manager};

  /*
  交換方法
  arg1: 存放rebinding結(jié)構(gòu)體的數(shù)組
  arg2: 數(shù)組的長(zhǎng)度
  */
  rebind_symbols(rebs, 1);
}

//函數(shù)指針,用來(lái)保存原始的函數(shù)地址
static void (* old_NSLog)(NSString *format, ...);

//新的NSLog函數(shù)
void new_NSLog(NSString *format, ...) {
  //再調(diào)用原來(lái)的
  old_NSLog(@"勾上了!");
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
  NSLog(@"點(diǎn)擊屏幕");
}
@end

結(jié)果

OC.png

3.2、hook C函數(shù)

#import "ViewController.h"
#import "fishhook.h"

@interface ViewController ()

@end

@implementation ViewController
- (void)viewDidLoad {
  [super viewDidLoad];

  //這里必須要先加載一次要交換的函數(shù),否則符號(hào)表里面不會(huì)出現(xiàn)要交換的函數(shù)的地址
  printf("我是純正的printf函數(shù)\n");

  //定義rebinding結(jié)構(gòu)體
  struct rebinding manager;
  //要交換函數(shù)的名稱
  manager.name = "printf";
  //新的函數(shù)地址
  manager.replacement = new_printf;
  //保存原始函數(shù)地址變量的指針(存儲(chǔ)下來(lái),在替換后的方法里調(diào)用)
  manager.replaced = (void *)&old_printf;

  //定義數(shù)組
  struct rebinding rebs[] = {manager};

 /*
  交換方法
  arg1: 存放rebinding結(jié)構(gòu)體的數(shù)組
  arg2: 數(shù)組的長(zhǎng)度
  */
  rebind_symbols(rebs, 1);
}

//函數(shù)指針,用來(lái)保存原始的函數(shù)地址
static int (* old_printf)(const char *, ...);

//新的printf函數(shù)
void new_printf(const char * name, ...) {
  //再調(diào)用原來(lái)的
  old_printf("勾上了!\n");
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
  printf("點(diǎn)擊屏幕");
}
@end
C.png

二、Method Swizzle

1、介紹

Method Swizzle是利用OC的Runtime特性,動(dòng)態(tài)改變SEL(方法編號(hào))和IMP(方法實(shí)現(xiàn))的對(duì)應(yīng)關(guān)系,達(dá)到OC方法調(diào)用流程改變的目的。主要用于OC方法調(diào)換或往系統(tǒng)方法注入代碼。

2、Runtime 術(shù)語(yǔ)的數(shù)據(jù)結(jié)構(gòu)

SEL

它是selector在 Objc 中的表示(Swift 中是 Selector 類(lèi))。selector 是方法選擇器,其實(shí)作用就和名字一樣,日常生活中,我們通過(guò)人名辨別誰(shuí)是誰(shuí),注意 Objc 在相同的類(lèi)中不會(huì)有命名相同的兩個(gè)方法。selector 對(duì)方法名進(jìn)行包裝,以便找到對(duì)應(yīng)的方法實(shí)現(xiàn)。它的數(shù)據(jù)結(jié)構(gòu)是:

typedef struct objc_selector *SEL;

我們可以看出它是個(gè)映射到方法的 C 字符串,你可以通過(guò) Objc 編譯器器命令@selector() 或者 Runtime 系統(tǒng)的 sel_registerName 函數(shù)來(lái)獲取一個(gè) SEL 類(lèi)型的方法選擇器。

注意:
不同類(lèi)中相同名字的方法所對(duì)應(yīng)的selector是相同的,由于變量的類(lèi)型不同,所以不會(huì)導(dǎo)致它們調(diào)用方法實(shí)現(xiàn)混亂。

id

id 是一個(gè)參數(shù)類(lèi)型,它是指向某個(gè)類(lèi)的實(shí)例的指針。定義如下:

typedef struct objc_object *id;
struct objc_object { Class isa; };

以上定義,看到 objc_object 結(jié)構(gòu)體包含一個(gè) isa 指針,根據(jù) isa 指針就可以找到對(duì)象所屬的類(lèi)。

注意:
isa 指針在代碼運(yùn)行時(shí)并不總指向?qū)嵗龑?duì)象所屬的類(lèi)型,所以不能依靠它來(lái)確定類(lèi)型,要想確定類(lèi)型還是需要用對(duì)象的 -class 方法。(如:KVO 的實(shí)現(xiàn)機(jī)理就是將被觀察對(duì)象的 isa 指針指向一個(gè)中間類(lèi)而不是真實(shí)類(lèi)型)

Class

typedef struct objc_class *Class;

Class 其實(shí)是指向 objc_class 結(jié)構(gòu)體的指針。objc_class 的數(shù)據(jù)結(jié)構(gòu)如下:

struct objc_class {
    Class isa  OBJC_ISA_AVAILABILITY;   //類(lèi)的isa指針,指向其所屬的元類(lèi)(meta)

#if !__OBJC2__
    Class super_class  //父類(lèi)指針                       OBJC2_UNAVAILABLE;
    const char *name   //類(lèi)名                           OBJC2_UNAVAILABLE;
    long version  //是類(lèi)的版本信息                       OBJC2_UNAVAILABLE;
    long info  //是類(lèi)的詳情                             OBJC2_UNAVAILABLE;
    long instance_size  //該類(lèi)的實(shí)例對(duì)象的大小            OBJC2_UNAVAILABLE;
    struct objc_ivar_list *ivars  //成員變量列表         OBJC2_UNAVAILABLE;
    struct objc_method_list **methodLists  //方法列表    OBJC2_UNAVAILABLE;
    struct objc_cache *cache  //緩存                    OBJC2_UNAVAILABLE;
    struct objc_protocol_list *protocols  //協(xié)議列表     OBJC2_UNAVAILABLE;
#endif

} OBJC2_UNAVAILABLE;
  • ·Class 也有一個(gè) isa 指針,指向其所屬的元類(lèi)(meta)。
  • ·super_class:指向其超類(lèi)。
  • ·name:是類(lèi)名。
  • ·version:是類(lèi)的版本信息。
  • ·info:是類(lèi)的詳情。
  • ·instance_size:是該類(lèi)的實(shí)例對(duì)象的大小。
  • ·ivars:指向該類(lèi)的成員變量列表。
  • ·methodLists:指向該類(lèi)的實(shí)例方法列表,它將方法選擇器和方法實(shí)現(xiàn)地址聯(lián)系起來(lái)。methodLists 是指向 ·objc_method_list 指針的指針,也就是說(shuō)可以動(dòng)態(tài)修改 *methodLists 的值來(lái)添加成員方法,這也是 Category 實(shí)現(xiàn)的原理,同樣解釋了 Category 不能添加屬性的原因。
  • ·cache:Runtime 系統(tǒng)會(huì)把被調(diào)用的方法存到 cache 中(理論上講一個(gè)方法如果被調(diào)用,那么它有可能今后還會(huì)被調(diào)用),下次查找的時(shí)候效率更高。
  • ·protocols:指向該類(lèi)的協(xié)議列表。

其中 objc_ivar_listobjc_method_list 分別是成員變量列表和方法列表:

//成員變量列表
struct objc_ivar_list {
    int ivar_count                                     OBJC2_UNAVAILABLE;
#ifdef __LP64__
    int space                                          OBJC2_UNAVAILABLE;
#endif
    /* variable length structure */
    struct objc_ivar ivar_list[1]                      OBJC2_UNAVAILABLE;
}                                                      OBJC2_UNAVAILABLE;
//方法列表
struct objc_method_list {
    struct objc_method_list *obsolete                  OBJC2_UNAVAILABLE;

    int method_count                                   OBJC2_UNAVAILABLE;
#ifdef __LP64__
    int space                                          OBJC2_UNAVAILABLE;
#endif
    /* variable length structure */
    struct objc_method method_list[1]                  OBJC2_UNAVAILABLE;
}

由此可見(jiàn),我們可以動(dòng)態(tài)修改 *methodList 的值來(lái)添加成員方法,這也是 Category 實(shí)現(xiàn)的原理,同樣解釋了 Category 不能添加屬性的原因。

objc_ivar_list 結(jié)構(gòu)體用來(lái)存儲(chǔ)成員變量的列表,而 objc_ivar 則是存儲(chǔ)了單個(gè)成員變量的信息;同理,objc_method_list 結(jié)構(gòu)體存儲(chǔ)著方法數(shù)組的列表,而單個(gè)方法的信息則由 objc_method 結(jié)構(gòu)體存儲(chǔ)。

值得注意的時(shí),objc_class 中也有一個(gè) isa 指針,這說(shuō)明 Objc 類(lèi)本身也是一個(gè)對(duì)象。為了處理類(lèi)和對(duì)象的關(guān)系,Runtime 庫(kù)創(chuàng)建了一種叫做 Meta Class(元類(lèi)) 的東西,類(lèi)對(duì)象所屬的類(lèi)就叫做元類(lèi)。Meta Class 表述了類(lèi)對(duì)象本身所具備的元數(shù)據(jù)。

isa.png

Method

Method 代表類(lèi)中某個(gè)方法的類(lèi)型

typedef struct objc_method *Method;

struct objc_method {
    SEL method_name                                    OBJC2_UNAVAILABLE;
    char *method_types                                 OBJC2_UNAVAILABLE;
    IMP method_imp                                     OBJC2_UNAVAILABLE;
}

objc_method 存儲(chǔ)了方法名,方法類(lèi)型和方法實(shí)現(xiàn):

  • 方法名,類(lèi)型為 SEL
  • 方法類(lèi)型 method_types 是個(gè) char 指針,存儲(chǔ)方法的參數(shù)類(lèi)型和返回值類(lèi)型
  • method_imp 指向了方法的實(shí)現(xiàn),本質(zhì)是一個(gè)函數(shù)指針

Ivar

Ivar 是表示成員變量的類(lèi)型。

typedef struct objc_ivar *Ivar;

struct objc_ivar {
    char *ivar_name                                    OBJC2_UNAVAILABLE;
    char *ivar_type                                    OBJC2_UNAVAILABLE;
    int ivar_offset                                    OBJC2_UNAVAILABLE;
#ifdef __LP64__
    int space                                           OBJC2_UNAVAILABLE;
#endif
}

其中 ivar_offset 是基地址偏移字節(jié)

IMP

IMP在objc.h中的定義是:

typedef id (*IMP)(id, SEL, ...);

它就是一個(gè)函數(shù)指針,這是由編譯器生成的。當(dāng)你發(fā)起一個(gè) ObjC 消息之后,最終它會(huì)執(zhí)行的那段代碼,就是由這個(gè)函數(shù)指針指定的。而 IMP 這個(gè)函數(shù)指針就指向了這個(gè)方法的實(shí)現(xiàn)。

你會(huì)發(fā)現(xiàn) IMP 指向的方法與 objc_msgSend 函數(shù)類(lèi)型相同,參數(shù)都包含 idSEL 類(lèi)型。每個(gè)方法名都對(duì)應(yīng)一個(gè) SEL 類(lèi)型的方法選擇器,而每個(gè)實(shí)例對(duì)象中的 SEL 對(duì)應(yīng)的方法實(shí)現(xiàn)肯定是唯一的,通過(guò)一組 idSEL參數(shù)就能確定唯一的方法實(shí)現(xiàn)地址。

Cache

Cache 定義如下:

typedef struct objc_cache *Cache

struct objc_cache {
    unsigned int mask /* total = mask + 1 */           OBJC2_UNAVAILABLE;
    unsigned int occupied                              OBJC2_UNAVAILABLE;
    Method buckets[1]                                  OBJC2_UNAVAILABLE;
};

Cache 為方法調(diào)用的性能進(jìn)行優(yōu)化,每當(dāng)實(shí)例對(duì)象接收到一個(gè)消息時(shí),它不會(huì)直接在 isa 指針指向的類(lèi)的方法列表中遍歷查找能夠響應(yīng)的方法,因?yàn)槊看味家檎倚侍土耍莾?yōu)先在 Cache 中查找。

Runtime 系統(tǒng)會(huì)把被調(diào)用的方法存到 Cache 中,如果一個(gè)方法被調(diào)用,那么它有可能今后還會(huì)被調(diào)用,下次查找的時(shí)候就會(huì)效率更高。就像計(jì)算機(jī)組成原理中 CPU 繞過(guò)主存先訪問(wèn) Cache 一樣。

3、方法交換(Method Swizzling)

下面代碼對(duì)UIViewController中的viewDidLoad方法進(jìn)行交換

1、UIViewController+swizzling.m文件

#import "UIViewController+swizzling.h"
#import <objc/runtime.h>

@implementation UIViewController (swizzling)

//load類(lèi)方法(當(dāng)某個(gè)類(lèi)的代碼被讀到內(nèi)存后調(diào)用)
+ (void)load
{
    /*
     SEL: 它就是個(gè)映射到方法的C字符串,可以用Objective-C編譯器命令@selector()或者Runtime系統(tǒng)的sel_registerName函數(shù)來(lái)獲得一個(gè)SEL類(lèi)型的方法選擇器。
     
     數(shù)據(jù)結(jié)構(gòu)是:
     typedef struct objc_selector *SEL;
     */
    SEL origSel = @selector(viewDidLoad);
    SEL swizSel = @selector(swiz_viewDidLoad);
  
    [UIViewController swizzleMethods:[self class] originalSelector:origSel swizzledSelector:swizSel];
}

//交換兩個(gè)方法的實(shí)現(xiàn)
+ (void)swizzleMethods:(Class)class originalSelector:(SEL)origSel swizzledSelector:(SEL)swizSel
{
    /*
     Method 代表類(lèi)中某個(gè)方法的類(lèi)型
     
     typedef struct objc_method *Method;
     
     struct objc_method {
     SEL method_name                                    OBJC2_UNAVAILABLE;
     char *method_types                                 OBJC2_UNAVAILABLE;
     IMP method_imp                                     OBJC2_UNAVAILABLE;
     }
     objc_method 存儲(chǔ)了方法名,方法類(lèi)型和方法實(shí)現(xiàn):
     
     方法名類(lèi)型為 SEL
     方法類(lèi)型 method_types 是個(gè) char 指針,存儲(chǔ)方法的參數(shù)類(lèi)型和返回值類(lèi)型
     method_imp 指向了方法的實(shí)現(xiàn),本質(zhì)是一個(gè)函數(shù)指針
     
     
     class_getInstanceMethod(class, origSel);
     返回class類(lèi)的origSel方法。
    */
    Method origMethod = class_getInstanceMethod(class, origSel);
    Method swizMethod = class_getInstanceMethod(class, swizSel);
    
    /*
     周全起見(jiàn),有兩種情況要考慮一下
     第一種情況:要交換的方法并沒(méi)有在目標(biāo)類(lèi)中實(shí)現(xiàn),而是在其父類(lèi)中實(shí)現(xiàn)了, 這時(shí)使用class_getInstanceMethod函數(shù)獲取到的originalSelector指向的就是父類(lèi)的方法
     第二種情況:要交換的方法已經(jīng)存在于目標(biāo)類(lèi)中

     
      class_addMethod(class, origSel, method_getImplementation(swizMethod), method_getTypeEncoding(swizMethod));
      往class這個(gè)類(lèi),添加方法origSel,新增方法的實(shí)現(xiàn)地址為method_getImplementation(swizMethod),參數(shù)及返回值類(lèi)型為method_getTypeEncoding(swizMethod)
      添加成功返回YES,失敗返回NO(例如,該類(lèi)已經(jīng)包含一個(gè)同名的方法實(shí)現(xiàn))
     
      method_getImplementation(swizMethod)  返回swizMethod方法實(shí)現(xiàn)
      method_getTypeEncoding(swizMethod)  返回描述swizMethod方法參數(shù)和返回值類(lèi)型的字符串
     */
    //判斷是情況一還是情況二
    BOOL didAddMethod = class_addMethod(class, origSel, method_getImplementation(swizMethod), method_getTypeEncoding(swizMethod));
  
    if (didAddMethod)
    {
        /*
          第一種情況:要交換的方法已經(jīng)在父類(lèi)中實(shí)現(xiàn)了,則替換父類(lèi)方法
          使用replaceMethod來(lái)替換給定類(lèi)的方法實(shí)現(xiàn)(將origMethod方法替換成swizSel方法)
         */
        class_replaceMethod(class, swizSel, method_getImplementation(origMethod), method_getTypeEncoding(origMethod));
    }
    else
    {
        /*
          第二種情況:要交換的方法已經(jīng)存在于目標(biāo)類(lèi)中
          通過(guò)method_exchangeImplementations來(lái)交換方法
         */
        method_exchangeImplementations(origMethod, swizMethod);
    }
}

//自己實(shí)現(xiàn)的用于交換的方法
- (void)swiz_viewDidLoad
{
    NSLog(@"調(diào)用了swiz_viewDidLoad方法");
    
    /*
       需要注入的代碼寫(xiě)在此處
     */
    
    //執(zhí)行這句的時(shí)候跳轉(zhuǎn)到viewDidLoad方法中
    [self swiz_viewDidLoad];
}
@end

2、ViewController.m文件

#import "ViewController.h"
#import "UIViewController+swizzling.h"

@interface ViewController ()

@end

@implementation ViewController

- (void)viewDidLoad
{
    //執(zhí)行這句的時(shí)候跳到swiz_viewDidLoad方法中
    [super viewDidLoad];
  
    NSLog(@"調(diào)用了viewDidLoad方法");
}
@end

當(dāng)執(zhí)行[super viewDidLoad]時(shí),跳轉(zhuǎn)到swiz_viewDidLoad方法中。當(dāng)執(zhí)行到[self swiz_viewDidLoad]時(shí),再跳轉(zhuǎn)到viewDidLoad方法中。因此,先打印“調(diào)用了swiz_viewDidLoad方法”,再打印“調(diào)用了viewDidLoad方法

OK.png

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容