淺談 iOS swizzle

簡單使用

簡介

Method Swizzle的本質是在運行時交換方法實現(IMP),通常是在原有的方法中,插入本身的業務需求。html

原理

Objective-C的消息機制:在 Objective-C 中調用一個方法, 其實是在底層經過 objc_msgSend()發送一個消息。 而查找消息的惟一依據是selector的方法名。ios

[obj doSomething]; /// => objc_msgSend(obj,@selector(doSomething))
複製代碼

每個OC實例對象都保存有isa指針和實例變量,其中isa指針所屬類,類維護一個運行時可接收的方法列表(MethodLists); 方法列表(MethodLists)中保存selector & IMP的映射關係。在運行時,經過selecter找到匹配的IMP,從而找到的具體的實現函數。git

開發中能夠利用Objective-C的動態特性,在運行時替換selector對應的方法實現(IMP),達到給hook的目的。下圖是利用 Method Swizzle 來替換selector對應IMP後的方法列表示意圖。github

例子

在description() 以前打印「description 被 Swizzle 了」這樣的日誌。編程

@implementation NSObject (Swizzle)
+ (void)load{
    //調換IMP
    Method originalMethod = class_getInstanceMethod([NSObject class], @selector(description));
    Method newMethod = class_getInstanceMethod([NSObject class], @selector(replace_description));
    method_exchangeImplementations(originalMethod, newMethod);
}
- (void)replace_description{
    NSLog(@"description 被 Swizzle 了");
    [self replace_description];
}
@end
複製代碼

使用swizzle時,咱們應該注意哪些問題呢?bash

問題一:繼承問題

若是 originalMethod 是其父類實現的,那麼直接 method_exchangeImplementations 是把父類中的 originalMethod 給替換了,致使該父類以及其餘子類調用的 originalMethod 也會被替換函數

解決: 經過 class_addMethod 判斷 method 是否是屬於本類本身實現的?post

  1. class_addMethod 返回 YES -> addMethod 成功,class中不存在 method,也就是存在父類中。addMethod以後,當前class也就存在method 了(覆蓋了父類的方法)
  2. class_addMethod 返回 NO -> addMethod 失敗,class中存在 method,說明當前方法屬於當前class
  3. 判斷以後,再執行 exchange

代碼:ui

@implementation Model (Swizzle)
+ (void)load {
    Class class = [self class];
    SEL originalSelector = @selector(hhh);
    SEL swizzledSelector = @selector(new_hhh);
    Method originalMethod = class_getInstanceMethod(class, originalSelector);
    Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);
    // 添加 originalSelector->swizzle method 到 class
    BOOL success = class_addMethod(class, originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod));
    if (success) { // 說明originalSelector在父類中
        class_replaceMethod(class, swizzledSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod));
    } else { // 說明originalSelector在當前類中
        method_exchangeImplementations(originalMethod, swizzledMethod);
    }
}
@end
複製代碼

問題二:方法的參數會被改變

若是 originalMethod 中使用了 _cmd參數,可能形成bugspa

@interface IncorrectSwizzleClass : NSObject
- (void) swizzleExample;
- (void) originalMethod;
@end
@implementation IncorrectSwizzleClass
- (void)swizzleExample {
    Method m1 = class_getInstanceMethod([self class], @selector(originalMethod));
    Method m2 = class_getInstanceMethod([self class], @selector(replaceImp));
    method_exchangeImplementations(m1, m2);
}
- (void)originalMethod {
    NSLog(@"方法名爲 originalMethod,其 _cmd 的值爲:%@",[NSString stringWithFormat:@"*** -%@", NSStringFromSelector(_cmd)]);
}
- (void)replaceImp {
    /*
     * 添加本身的邏輯:好比添加log
     */
    [self replaceImp];
}
@end

- (void)incorrect {
    NSLog(@"#################### incorrect #######################");
    IncorrectSwizzleClass* example2 = [[IncorrectSwizzleClass alloc] init];
    NSLog(@"## swizzle 以前,調用 originalMethod 的打印信息:");
    [example2 originalMethod];
    [example2 swizzleExample];
    NSLog(@"## swizzle 以後,調用 originalMethod 的打印信息:");
    [example2 originalMethod];
}
複製代碼

打印結果:

分析: 執行 OC方法時,默認會傳遞兩個參數(self & _cmd) [self replaceImp]; /// 會被編譯器變成 objc_msgSend(self, @selector(replaceImp)),方法的第二個參數是 @「replaceImp」,故 originalMethod 中打印的是 replaceImp。

解決:C方法+ method_setImplementation 的方式

@interface CorrectSwizzleClass : NSObject
- (void) swizzleExample;
- (void) originalMethod;
@end
static IMP __original_Method_Imp;
void replaceImp(id self, SEL _cmd) {
    /*
     * 添加本身的邏輯:好比添加 log
     */
    ((int(*)(id,SEL))__original_Method_Imp)(self, _cmd);
}
@implementation CorrectSwizzleClass
- (void)swizzleExample {
    Method m = class_getInstanceMethod([self class],@selector(originalMethod));
    /// method_setImplementation:return The previous implementation of the method
    __original_Method_Imp = method_setImplementation(m,(IMP)replaceImp);
}
- (void)originalMethod {
    NSLog(@"方法名爲 originalMethod,其 _cmd 的值爲:%@",[NSString stringWithFormat:@"*** -%@", NSStringFromSelector(_cmd)]);
}
@end

- (void)correct {
    NSLog(@"#################### correct #######################");
    CorrectSwizzleClass* example = [[CorrectSwizzleClass alloc] init];
    NSLog(@"## swizzle 以前,調用 originalMethod 的打印信息:");
    [example originalMethod];
    [example swizzleExample];
    NSLog(@"## swizzle 以後,調用 originalMethod 的打印信息:");
    [example originalMethod];
}
複製代碼

打印結果:

問題三:如何作到對象級別的 swizzle?

只對某個對象進行 swizzle,不影響其餘對象

方案:

  1. 類自己支持。能夠標記一下,在執行方法時,判斷是否存在標記來判斷是否執行swizzle 以後的方法。能夠參考:第三方庫 DZNEmptyDataSet(統一空白頁)
  2. 動態生成一個當前對象所屬類的子類,並將當前對象與子類關聯。這樣的話,swizzle的都是其子類的方法,不會影響父類。能夠參考:第三方庫 Aspects

聊一下Aspects

Aspects屬於AOP編程的庫,源碼總數不超過1000行,對外就暴露了兩個方法。 使用方式:能夠hook 類方法、對象實例方法,還有三種執行位置:before、insert、after

@interface NSObject (Aspects)
+ (id<AspectToken>)aspect_hookSelector:(SEL)selector
                           withOptions:(AspectOptions)options
                            usingBlock:(id)block
                                 error:(NSError **)error;

- (id<AspectToken>)aspect_hookSelector:(SEL)selector
                           withOptions:(AspectOptions)options
                            usingBlock:(id)block
                                 error:(NSError **)error;
@end
複製代碼

例子:

/**
 *  事件攔截
 *  攔截UIViewController的viewDidLoad方法
 */
[UIViewController aspect_hookSelector:@selector(viewDidLoad) withOptions:AspectPositionAfter 
usingBlock:^(id<AspectInfo> aspectInfo)
 {
     /**
      *  添加咱們要執行的代碼,因爲withOptions 是 AspectPositionAfter。
      *  因此每一個控制器的 viewDidLoad 觸發都會執行下面的方法
      */
     [self doSomethings];
 } error:NULL];
- (void)doSomethings {
    //TODO: 好比日誌輸出、統計代碼
    NSLog(@"------");
}
複製代碼

簡單原理:

  1. 把待 hook 的 originalSelector 生成 aliasSelector
    1. 把待 hook 的 originalSelector 添加前綴aspects_ -> aliasSelector -> 用 block & aliasSelector生成 aspectContainer
    2. 經過 associated 把 aspectContainer 綁定到 self( 對象或class),key爲 aliasSelector
  2. 把 originalSelector 的 IMP 設置爲 _objc_msgForward(會觸發消息轉發,不會查詢方法列表了)
  3. swizzle forwardInvocation
    1. 在自定義的 forwardInvocation 中經過 associated & selector -> aliasSelector 獲取 aspectContainer
    2. 根據 before/insert/after 的規則執行 originalSelector & block

_objc_msgForward

_objc_msgForward 是一個函數指針(和 IMP 的類型同樣),是用於消息轉發的:當向一個對象發送一條消息,但它並無實現的時候,_objc_msgForward 會直接走消息轉發。看一個不存在方法的例子:

❤️hook對象

動態生成一個當前對象的子類,並將當前對象與子類關聯,而後替換子類的 forwardInvocation 方法(具體參考源碼)。那麼就能夠將當前對象變成一個子類的實例,同時對於外部使用者而言,仍能夠把它繼續當成原對象使用,並且全部的 swizzle 操做都發生在子類,這樣作的好處是你不須要去更改對象自己的類

Aspects 優勢

  1. 不會影響其餘對象
  2. 當你在 remove aspects 的時候,若是發現當前對象的 aspect 都被移除了,那麼,你能夠將 isa 指針從新指回對象自己的類,從而消除了該對象的 swizzle

Aspects 缺點

  1. 沒有解決上面提到的問題二:originalMethod 中使用 _cmd 的問題,須要咱們注意一下

參考:

juejin.im/post/5a2fd6…

wereadteam.github.io/2016/06/30/…

www.cocoachina.com/ios/2017091…

相關文章
相關標籤/搜索