ios runtime之Method Swizzling及其應用場景

iosmedia發表於2019-04-19

關於Method Swizzling

最近對一些ios的apm系統比較感興趣,所以就研究了一些相關的技術。首先從最基本的Method Swizzling開始。

Method Swizzling是OC runtime提供的一種動態替換方法實現的技術,我們利用它可以替換系統或者我們自定義類的方法實現,進而達到我們的特殊目的。

程式碼地址-github: MethodSwizzling

Method Swizzling原理

為什麼Method Swizzling能替換一個類的方法呢?我們首先要理解一下其替換的原理。

OC中的方法在runtime.h中的定義如下:

struct objc_method{
    SEL method_name                                          OBJC2_UNAVAILABLE;
    char *method_types                                       OBJC2_UNAVAILABLE;
    IMP method_imp                                           OBJC2_UNAVAILABLE;
}                                                            OBJC2_UNAVAILABLE;
}
複製程式碼
  • method_name: 方法名
  • method_types: 方法型別,主要儲存著方法的引數型別和返回值型別
  • method_imp: 方法的實現,函式指標

由此,我們也可以發現 OC 中的方法名是不包括引數型別的,也就是說下面兩個方法在 runtime 看來就是同一個方法:

- (void)viewWillAppear:(BOOL)animated;
- (void)viewWillAppear:(NSString *)string;
複製程式碼

原則上,方法的名稱 method_name 和方法的實現 method_imp 是一一對應的,而 Method Swizzling 的原理就是動態地改變它們的對應關係,以達到替換方法實現的目的。

Method Swizzling應用

runtime中和方法替換相關的函式

class_getInstanceMethod

OBJC_EXPORT Method _Nullable
class_getInstanceMethod(Class _Nullable cls, SEL _Nonnull name);
複製程式碼
  • 作用:獲取一個類的例項方法
  • cls : 方法所在的類
  • name: 選擇子的名稱(選擇子就是方法名稱)

class_getClassMethod

OBJC_EXPORT Method _Nullable
class_getClassMethod(Class _Nullable cls, SEL _Nonnull name);
複製程式碼
  • 作用:獲取一個類的類方法
  • cls : 方法所在的類
  • name: 選擇子名稱

method_getImplementation

OBJC_EXPORT IMP _Nonnull
method_getImplementation(Method _Nonnull m);
複製程式碼
  • 作用: 根據方法獲取方法的指標
  • m : 方法

method_getTypeEncoding

OBJC_EXPORT const char * _Nullable
method_getTypeEncoding(Method _Nonnull m);
複製程式碼
  • 作用: 獲取方法的引數和返回值型別描述
  • m : 方法

class_addMethod

OBJC_EXPORT BOOL
class_addMethod(Class _Nullable cls, SEL _Nonnull name, IMP _Nonnull imp, 
                const char * _Nullable types);
複製程式碼
  • 作用: 給類新增一個新方法和該方法的實現
  • 返回值: yes,表示新增成功; No,表示新增失敗
  • cls: 將要新增方法的類
  • name: 將要新增的方法名
  • imp: 實現這個方法的指標
  • types: 要新增的方法的返回值和引數

method_exchangeImplementations

OBJC_EXPORT void
method_exchangeImplementations(Method _Nonnull m1, Method _Nonnull m2);
複製程式碼
  • 交換兩個方法

class_replaceMethod

OBJC_EXPORT IMP _Nullable
class_replaceMethod(Class _Nullable cls, SEL _Nonnull name, IMP _Nonnull imp, 
                    const char * _Nullable types) ;
複製程式碼
  • 作用: 指定替換方法的實現
  • cls : 將要替換方法的類
  • name: 將要替換的方法名
  • imp: 新方法的指標
  • types: 新方法的返回值和引數描述

替換一個類的例項方法

eg: 替換UIViewController中的viewDidLoad方法。

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

@implementation UIViewController (MI)

+ (void)load{
    
    // 替換ViewController中的viewDidLoad方法
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        SEL origin_selector  = @selector(viewDidLoad);
        SEL swizzed_selector = @selector(mi_viewDidLoad);
        
        Method origin_method = class_getInstanceMethod([self class], origin_selector);
        Method swizzed_method = class_getInstanceMethod([self class], swizzed_selector);
        
        BOOL did_add_method = class_addMethod([self class],
                                              origin_selector,
                                              method_getImplementation(swizzed_method),
                                              method_getTypeEncoding(swizzed_method));
        if (did_add_method) {
            NSLog(@"debugMsg: ViewController類中沒有viewDidLoad方法(可能在其父類h中),所以先新增後替換");
            class_replaceMethod([self class],
                                swizzed_selector,
                                method_getImplementation(origin_method),
                                method_getTypeEncoding(origin_method));
        }else{
            NSLog(@"debugMsg: 直接交換方法");
            method_exchangeImplementations(origin_method, swizzed_method);
        }
        
    });
}

- (void)mi_viewDidLoad
{
    [self mi_viewDidLoad];
    NSLog(@"debugMsg: 替換成功");
}
複製程式碼

對以上程式碼做簡要說明:

  • 在category的+(void)load方法中新增替換的程式碼,在類初始載入的時候會被自動呼叫。
  • dispath_once確保只執行一次
  • 先呼叫class_addMethod方法,保證即使父類中存在要替換的方法,也能替換成功

替換成功。控制檯資訊:

2019-04-17 17:25:16.937849+0800 MethodSwizzling[4975:639584] debugMsg: 直接交換方法
2019-04-17 17:25:17.025214+0800 MethodSwizzling[4975:639584] debugMsg: 替換成功
複製程式碼

替換一個類的例項方法到另一個類

當我們對私有類庫(不知道該類的標頭檔案,只知道有這個類並且已知該類中的一個方法),此時我們需要hook這個類的方法到一個新類中。

eg: 我們要hook person類中有一個speak:方法方法:

#import "Person.h"

@implementation Person

- (void)speak:(NSString *)language
{
    NSLog(@"person speak language: %@",language);
}

+ (void)sleep:(NSUInteger)hour
{
    NSLog(@"person sleep: %lu",hour);
}

@end
複製程式碼

我們新建ChinesePerson,hook speak:方法到ChinesePerson中。

#import "ChinesePerson.h"
#import <objc/runtime.h>

@implementation ChinesePerson

+ (void)load
{
    Class origin_class  = NSClassFromString(@"Person");
    Class swizzed_class = [self class];
    
    SEL origin_selector = NSSelectorFromString(@"speak:");
    SEL swizzed_selector = NSSelectorFromString(@"mi_speak:");
    
    Method origin_method = class_getInstanceMethod(origin_class, origin_selector);
    Method swizzed_method = class_getInstanceMethod(swizzed_class, swizzed_selector);
    
    BOOL add_method = class_addMethod(origin_class,
                                      swizzed_selector,
                                      method_getImplementation(swizzed_method),
                                      method_getTypeEncoding(swizzed_method));
    if (!add_method) {
        return;
    }
    
    swizzed_method = class_getInstanceMethod(origin_class, swizzed_selector);
    if (!swizzed_method) {
        return;
    }
    
    BOOL did_add_method = class_addMethod(origin_class,
                                          origin_selector,
                                          method_getImplementation(swizzed_method),
                                          method_getTypeEncoding(swizzed_method));
    if (did_add_method) {
        class_replaceMethod(origin_class,
                            swizzed_selector,
                            method_getImplementation(origin_method),
                            method_getTypeEncoding(origin_method));
    }else{
        method_exchangeImplementations(origin_method, swizzed_method);
    }
    
}

- (void)mi_speak:(NSString *)language
{
    if ([language isEqualToString:@"Chinese"]) {
        [self mi_speak:language];
    }
}
複製程式碼

替換成功。控制檯資訊(只列印漢語):

2019-04-17 17:25:17.025362+0800 MethodSwizzling[4975:639584] person speak language: Chinese
複製程式碼

替換類方法

eg: 我們替換person類中的sleep:方法:

#import "Person+MI.h"
#import <objc/runtime.h>

@implementation Person (MI)
+ (void)load
{
    Class class = [self class];
    SEL origin_selector  = @selector(sleep:);
    SEL swizzed_selector = @selector(mi_sleep:);
    
    Method origin_method = class_getClassMethod(class, origin_selector);
    Method swizzed_method = class_getClassMethod(class,swizzed_selector);
    
    if (!origin_method || !swizzed_method) {
        return;
    }
    
    IMP origin_imp = method_getImplementation(origin_method);
    IMP swizzed_imp = method_getImplementation(swizzed_method);
    const char* origin_type = method_getTypeEncoding(origin_method);
    const char* swizzed_type = method_getTypeEncoding(swizzed_method);
    
    // 新增方法到MetaClass中
    Class meta_class = objc_getMetaClass(class_getName(class));
    class_replaceMethod(meta_class, swizzed_selector, origin_imp, origin_type);
    class_replaceMethod(meta_class, origin_selector, swizzed_imp, swizzed_type);
    
}

+ (void)mi_sleep:(NSUInteger)hour
{
    if (hour >= 7) {
        [self mi_sleep:hour];
    }
}
@end

複製程式碼

控制檯列印(睡眠大於等於7小時才列印----呼籲健康睡眠):

2019-04-17 17:25:17.025465+0800 MethodSwizzling[4975:639584] person sleep: 8
複製程式碼

類方法的hook和例項方法的hook有兩點不同:

  • 獲取Method的方法變更為class_getClassMethod(Class cls, SEL name),不是class_getInstanceMethod(Class cls, SEL name);
  • 對於類方法的動態新增,需要將方法新增到MetaClass中,因為例項方法記錄在class的method-list中, 類方法是記錄在meta-class中的method-list中的.

替換類簇中的方法

#import "MIMutableDictionary.h"
#import <objc/runtime.h>

@implementation MIMutableDictionary

+ (void)load
{
    Class origin_class = NSClassFromString(@"__NSDictionaryM");
    Class swizzed_class = [self class];
    SEL origin_selector = @selector(setObject:forKey:);
    SEL swizzed_selector = @selector(mi_setObject:forKey:);
    Method origin_method = class_getInstanceMethod(origin_class, origin_selector);
    Method swizzed_method = class_getInstanceMethod(swizzed_class, swizzed_selector);
    IMP origin_imp = method_getImplementation(origin_method);
    IMP swizzed_imp = method_getImplementation(swizzed_method);
    const char* origin_type = method_getTypeEncoding(origin_method);
    const char* swizzed_type = method_getTypeEncoding(swizzed_method);

    class_replaceMethod(origin_class, swizzed_selector, origin_imp, origin_type);
    class_replaceMethod(origin_class, origin_selector, swizzed_imp, swizzed_type);
}

- (void)mi_setObject:(id)objContent forKey:(id<NSCopying>)keyContent
{
    if (objContent && keyContent) {
        NSLog(@"執行了進去");
        [self mi_setObject:objContent forKey:keyContent];
    }
}


@end
複製程式碼

應用

不推薦在專案中過多的使用Method Swizzling,不然的話原生的類都被hook的非常亂,專案出問題時非常難定位問題。有一篇文章說它是ios中的一個毒瘤。 iOS屆的毒瘤-MethodSwizzling

儘管如此,我們還是瞭解並梳理一下其應用場景來感受一下這種技術。

防止陣列取值時越界crash

不僅僅是陣列,NSDictionary的利用runtime防止崩潰是同樣的原理。

#import "NSArray+Safe.h"
#import <objc/runtime.h>

@implementation NSArray (Safe)

+ (void)load
{
    // objectAtIndex: 方式取元素
    Method origin_method = class_getInstanceMethod(objc_getClass("__NSArrayI"), @selector(objectAtIndex:));
    Method replaced_method = class_getInstanceMethod(objc_getClass("__NSArrayI"), @selector(safeObjectAtIndex:));
    method_exchangeImplementations(origin_method, replaced_method);
    
    Method origin_method_muta = class_getInstanceMethod(objc_getClass("__NSArrayM"), @selector(objectAtIndex:));
    Method replaced_method_muta = class_getInstanceMethod(objc_getClass("__NSArrayM"), @selector(safeMutableObjectAtIndex:));
    method_exchangeImplementations(origin_method_muta, replaced_method_muta);
    
    // 直接通過陣列下標的方式取元素
    Method origin_method_sub = class_getInstanceMethod(objc_getClass("__NSArrayI"), @selector(objectAtIndexedSubscript:));
    Method replaced_method_sub = class_getInstanceMethod(objc_getClass("__NSArrayI"), @selector(safeObjectAtIndexedSubscript:));
    method_exchangeImplementations(origin_method_sub, replaced_method_sub);
    
    Method origin_method_muta_sub = class_getInstanceMethod(objc_getClass("__NSArrayM"), @selector(objectAtIndexedSubscript:));
    Method replaced_method_muta_sub = class_getInstanceMethod(objc_getClass("__NSArrayM"), @selector(safeMutableObjectAtIndexedSubscript:));
    method_exchangeImplementations(origin_method_muta_sub, replaced_method_muta_sub);
    
}

- (id)safeObjectAtIndex:(NSUInteger)index
{
    if (self.count > index && self.count) {
        return [self safeObjectAtIndex:index];
    }
    NSLog(@"errorMsg:陣列[NSArray]越界...");
    return nil;
}

- (id)safeMutableObjectAtIndex:(NSUInteger)index
{
    if (self.count > index && self.count) {
        return [self safeMutableObjectAtIndex:index];
    }
    NSLog(@"errorMsg:陣列[NSMutableArray]越界...");
    return nil;
}

-(id)safeObjectAtIndexedSubscript:(NSUInteger)index
{
    if (self.count > index && self.count) {
        return [self safeObjectAtIndexedSubscript:index];
    }
    NSLog(@"errorMsg:陣列[NSArray]越界...");
    return nil;
}

- (id)safeMutableObjectAtIndexedSubscript:(NSUInteger)index
{
    if (self.count > index && self.count) {
        return [self safeMutableObjectAtIndexedSubscript:index];
    }
    NSLog(@"errorMsg:陣列[NSMutableArray]越界...");
    return nil;
}

@end

複製程式碼

使用:

- (void)test2
{
    NSArray *arr = @[@"a",@"b",@"c",@"d",@"e",@"f"];
    NSLog(@"atIndex方式: %@",[arr objectAtIndex:10]);
    NSLog(@"下標方式: %@",arr[10]);
}
複製程式碼

控制檯輸出log:

2019-04-18 19:14:18.139417+0800 MethodSwizzling[25379:1703659] errorMsg:陣列[NSArray]越界...
2019-04-18 19:14:18.139536+0800 MethodSwizzling[25379:1703659] atIndex方式: (null)
2019-04-18 19:14:18.139793+0800 MethodSwizzling[25379:1703659] errorMsg:陣列[NSArray]越界...
2019-04-18 19:14:18.139868+0800 MethodSwizzling[25379:1703659] 下標方式: (null)
複製程式碼

改變app中所有按鈕的大小

常規做法是遍歷檢視中的所有子檢視,把所有的button整體改變。 此時我們使用runtime改變按鈕的大小。

#import "UIButton+Size.h"
#import <objc/runtime.h>

@implementation UIButton (Size)

+ (void)load
{
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        // 增大所有按鈕大小
        Method origin_method = class_getInstanceMethod([self class], @selector(setFrame:));
        Method replaced_method = class_getInstanceMethod([self class], @selector(miSetFrame:));
        method_exchangeImplementations(origin_method, replaced_method);
        
    });
   
}

- (void)miSetFrame:(CGRect)frame
{
    frame = CGRectMake(frame.origin.x, frame.origin.y, frame.size.width+20, frame.size.height+20);
    NSLog(@"設定按鈕大小生效");
    [self miSetFrame:frame];
}

@end
複製程式碼

處理按鈕重複點選

如果重複過快的點選同一個按鈕,那麼就會多次觸發和按鈕繫結的事件。處理這種case的方式有很多種,通過Method Swillzing也能解決這種問題。

.h檔案:

#import <UIKit/UIKit.h>

NS_ASSUME_NONNULL_BEGIN

@interface UIButton (QuickClick)
@property (nonatomic,assign) NSTimeInterval delayTime;
@end

NS_ASSUME_NONNULL_END
複製程式碼
#import "UIButton+QuickClick.h"
#import <objc/runtime.h>

@implementation UIButton (QuickClick)
static const char* delayTime_str = "delayTime_str";

+ (void)load
{
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        Method originMethod =   class_getInstanceMethod(self, @selector(sendAction:to:forEvent:));
        Method replacedMethod = class_getInstanceMethod(self, @selector(miSendAction:to:forEvent:));
        method_exchangeImplementations(originMethod, replacedMethod);
    });
}

- (void)miSendAction:(nonnull SEL)action to:(id)target forEvent:(UIEvent *)event
{
    if (self.delayTime > 0) {
        if (self.userInteractionEnabled) {
            [self miSendAction:action to:target forEvent:event];
        }
        self.userInteractionEnabled = NO;
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW,
                                     (int64_t)(self.delayTime * NSEC_PER_SEC)),
                                     dispatch_get_main_queue(), ^{
                                         self.userInteractionEnabled = YES;
                                     });
    }else{
        [self miSendAction:action to:target forEvent:event];
    }
}

- (NSTimeInterval)delayTime
{
    return [objc_getAssociatedObject(self, delayTime_str) doubleValue];
}

- (void)setDelayTime:(NSTimeInterval)delayTime
{
    objc_setAssociatedObject(self, delayTime_str, @(delayTime), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

@end
複製程式碼

程式碼

github: MethodSwizzling

參考

相關文章