iOS中的HOOK技術(shù)

一、fishhook

1、介紹

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

2、主要信息

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

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

2.2、主要接口

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

3、實現(xiàn)

3.1、hook OC函數(shù)

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

@interface ViewController ()

@end

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

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

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

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

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

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

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

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
  NSLog(@"點擊屏幕");
}
@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ù),否則符號表里面不會出現(xiàn)要交換的函數(shù)的地址
  printf("我是純正的printf函數(shù)\n");

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

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

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

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

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

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

二、Method Swizzle

1、介紹

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

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

SEL

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

typedef struct objc_selector *SEL;

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

注意:
不同類中相同名字的方法所對應的selector是相同的,由于變量的類型不同,所以不會導致它們調(diào)用方法實現(xiàn)混亂。

id

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

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

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

注意:
isa 指針在代碼運行時并不總指向?qū)嵗龑ο笏鶎俚念愋?,所以不能依靠它來確定類型,要想確定類型還是需要用對象的 -class 方法。(如:KVO 的實現(xiàn)機理就是將被觀察對象的 isa 指針指向一個中間類而不是真實類型)

Class

typedef struct objc_class *Class;

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

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

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

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

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

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

isa.png

Method

Method 代表類中某個方法的類型

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 存儲了方法名,方法類型和方法實現(xiàn):

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

Ivar

Ivar 是表示成員變量的類型。

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, ...);

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

你會發(fā)現(xiàn) IMP 指向的方法與 objc_msgSend 函數(shù)類型相同,參數(shù)都包含 idSEL 類型。每個方法名都對應一個 SEL 類型的方法選擇器,而每個實例對象中的 SEL 對應的方法實現(xiàn)肯定是唯一的,通過一組 idSEL參數(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)用的性能進行優(yōu)化,每當實例對象接收到一個消息時,它不會直接在 isa 指針指向的類的方法列表中遍歷查找能夠響應的方法,因為每次都要查找效率太低了,而是優(yōu)先在 Cache 中查找。

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

3、方法交換(Method Swizzling)

下面代碼對UIViewController中的viewDidLoad方法進行交換

1、UIViewController+swizzling.m文件

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

@implementation UIViewController (swizzling)

//load類方法(當某個類的代碼被讀到內(nèi)存后調(diào)用)
+ (void)load
{
    /*
     SEL: 它就是個映射到方法的C字符串,可以用Objective-C編譯器命令@selector()或者Runtime系統(tǒng)的sel_registerName函數(shù)來獲得一個SEL類型的方法選擇器。
     
     數(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];
}

//交換兩個方法的實現(xiàn)
+ (void)swizzleMethods:(Class)class originalSelector:(SEL)origSel swizzledSelector:(SEL)swizSel
{
    /*
     Method 代表類中某個方法的類型
     
     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 存儲了方法名,方法類型和方法實現(xiàn):
     
     方法名類型為 SEL
     方法類型 method_types 是個 char 指針,存儲方法的參數(shù)類型和返回值類型
     method_imp 指向了方法的實現(xiàn),本質(zhì)是一個函數(shù)指針
     
     
     class_getInstanceMethod(class, origSel);
     返回class類的origSel方法。
    */
    Method origMethod = class_getInstanceMethod(class, origSel);
    Method swizMethod = class_getInstanceMethod(class, swizSel);
    
    /*
     周全起見,有兩種情況要考慮一下
     第一種情況:要交換的方法并沒有在目標類中實現(xiàn),而是在其父類中實現(xiàn)了, 這時使用class_getInstanceMethod函數(shù)獲取到的originalSelector指向的就是父類的方法
     第二種情況:要交換的方法已經(jīng)存在于目標類中

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

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

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

OK.png

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

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