Swizzling Method

彭二青年發表於2019-04-23

什麼是Swizzling Method


method Swizzling是OC runtime機制提供的一種可以動態替換方法的實現的技術,我們可以利用它替換系統或者自定義類的方法實現,來滿足我們特別的需求

建立在Runtime之上的Swizzling Method

Demo在此

Swizzling Method的實現原理

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中方法名相同引數不同的方法會被視為同一個方法
原則上,方法的名稱method_name和方法的實現method_imp是一一對應的,而Swizzling Method的原理就是動態的改變他們的對應關係,以達到替換方法的目的。

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: 新方法的返回值和引數描述

Swizzling Method的常見應用場景

  • 替換一個類的例項方法
    eg:替換UIViewController中的viewDidLoad方法
//將方法替換包裝成函式待呼叫
void MethodSwizzle(Class c, SEL oriSEL, SEL overrideSEL)
{
    Method originMethod = class_getInstanceMethod(c, oriSEL);
    Method swizzleMethod = class_getInstanceMethod(c, overrideSEL);
    
    BOOL did_add_method = class_addMethod(c, oriSEL, method_getImplementation(swizzleMethod), method_getTypeEncoding(swizzleMethod));//新增Method,其鍵值為originSelector,值為swizzleMethod的實現
    
    if (did_add_method) {
        NSLog(@"debugMsg: ViewController類中沒有viewDidLoad方法(可能在其父類h中),所以先新增後替換");
        class_replaceMethod(c, overrideSEL, method_getImplementation(originMethod), method_getTypeEncoding(originMethod));
    } else {
        NSLog(@"debugMsg: 直接交換方法");
        method_exchangeImplementations(originMethod, swizzleMethod);
    }
}
//呼叫load方法
+ (void)load
{
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        MethodSwizzle([self class], @selector(viewDidLoad), @selector(wn_viewDidLoad));
    });
}
//替換方法的實現
- (void)wn_viewDidLoad
{
    NSLog(@"呼叫了wn_viewDidLoad");
    [self wn_viewDidLoad];
}
複製程式碼
  • 替換一個類的例項方法到另一個類中去實現
    這種情況一般用在當我們不清楚私有庫的具體實現,只知道該類名稱和該類的一個方法,此時我們需要hook這個類的方法到另一個新類中。
    eg: 我們hook Animal類中的有一個eat:方法\
//Animal.h
@interface Animals : NSObject

- (void)eat:(NSString *)food;

@end

//Animal.m
@implementation Animals

- (void)eat:(NSString *)food
{
    NSLog(@"food Name = %@",food);
}
複製程式碼

然後新建一個類Dog, hook eat:方法到Dog中。如下:

//Dog.m
//方法替換的函式實現
void SwizzleMethod(Class oriClass, Class overClass, SEL oriSEL, SEL overSEL)
{
    Method origin_method = class_getInstanceMethod(oriClass, oriSEL);
    Method swizzle_method = class_getInstanceMethod(overClass, overSEL);
    
    //判斷oriClass是否已經存在overSEL方法,若已存在,則return
    BOOL exit_overSel = class_addMethod(oriClass, overSEL, method_getImplementation(swizzle_method), method_getTypeEncoding(swizzle_method));
    if (!exit_overSel) return;
    
    //獲取oriClass的overSEL的Method例項
    swizzle_method = class_getInstanceMethod(oriClass, overSEL);
    if (!swizzle_method) return;
    
    BOOL exit_origin = class_addMethod(oriClass, oriSEL, method_getImplementation(swizzle_method), method_getTypeEncoding(swizzle_method));
    if (exit_origin) {
        class_replaceMethod(oriClass, overSEL, method_getImplementation(origin_method), method_getTypeEncoding(origin_method));
    } else {
        method_exchangeImplementations(origin_method, swizzle_method);
    }
}
//呼叫替換方法
+ (void)load
{
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        SwizzleMethod(NSClassFromString(@"Animals"), [self class], NSSelectorFromString(@"eat:"), NSSelectorFromString(@"dog_eat:"));
    });
}
//替換方法的實現
- (void)dog_eat:(NSString *)food
{
    if ([food isEqualToString:@"dogFood"]) {
        [self dog_eat:food];
    } else {
        NSLog(@"不是狗糧");
    }
}
複製程式碼

我們來測試一下,在viewController中輸出

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view.
    
    Animals *animal = [[Animals alloc] init];
    [animal eat:@"dogFood"];
    [animal eat:@"pigFood"];
}
複製程式碼

列印結果為:

Test[9442:2104359] food Name = dogFood
Test[9442:2104359] 不是狗糧
複製程式碼
  • 替換類方法 嘗試替換Animals的類方法:run:
//Animals.h
+ (void)run:(NSInteger)kilo;
//Animals.m
+ (void)run:(NSInteger)kilo
{
    NSLog(@"勝利了!!跑了 %ld kilo",(long)kilo);
}

//Animals (Run)
void ExchangeMethod(Class class, SEL oriSEL, SEL exchangeSEL)
{
    Method origin_method = class_getClassMethod(class, oriSEL);
    Method exchange_method = class_getClassMethod(class, exchangeSEL);
    
    if (!origin_method || !exchange_method) return;
    IMP origin_imp = method_getImplementation(origin_method);
    IMP swizzle_imp = method_getImplementation(exchange_method);
    
    const char *origin_type= method_getTypeEncoding(origin_method);
    const char *swizzle_type = method_getTypeEncoding(exchange_method);
    
    Class metaClass = objc_getMetaClass(class_getName(class));
    class_replaceMethod(metaClass, oriSEL, swizzle_imp, swizzle_type);
    class_replaceMethod(metaClass, exchangeSEL, origin_imp, origin_type);
}
+ (void)load
{
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        ExchangeMethod([self class], @selector(run:), @selector(ex_run:));
    });
}

+ (void)ex_run:(NSInteger)kilo
{
    if (kilo >= 10) {
        [self ex_run:kilo];
    } else {
        NSLog(@"失敗了!!=_= ,只跑了%ld kilo",kilo);
    }
}
//ViewConroller.m
- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view.

    [Animals run:11];
    [Animals run:7];
}
複製程式碼

輸出結果為:

Test[9494:2117807] 勝利了!!跑了 11 kilo
Test[9494:2117807] 失敗了!!=_= ,只跑了7 kilo
複製程式碼

注意:

類方法的替換有個需要特別注意的地方:若把以下部分程式碼:

IMP origin_imp = method_getImplementation(origin_method);
IMP swizzle_imp = method_getImplementation(exchange_method);
const char *origin_type= method_getTypeEncoding(origin_method);
const char *swizzle_type = method_getTypeEncoding(exchange_method);
    
Class metaClass = objc_getMetaClass(class_getName(class));
class_replaceMethod(metaClass, oriSEL, swizzle_imp, swizzle_type);
class_replaceMethod(metaClass, exchangeSEL, origin_imp, origin_type);
複製程式碼

換成:

Class meta_class = objc_getMetaClass(class_getName(class));
class_replaceMethod(meta_class, origin_selector, method_getImplementation(swizzle_method), swizzle_type = method_getTypeEncoding(swizzle_method));
class_replaceMethod(meta_class, swizzle_selector, method_getImplementation(origin_method), method_getTypeEncoding(origin_method));
複製程式碼

執行會直接crash,因為方法替換未成功,呼叫方法是導致了死迴圈。具體原因未知,但猜測肯定和MetaClass有關


  • 替換類簇中的方法
//MutableDictionary.m
//方法實現
void ExchangeDicMethod(Class oriClass, Class curClass, SEL oriSEL, SEL curSEL)
{
    Method origin_method = class_getInstanceMethod(oriClass, oriSEL);
    Method current_method = class_getInstanceMethod(curClass, curSEL);
    
    if (!origin_method || !current_method) return;
    
    class_replaceMethod(oriClass, curSEL, method_getImplementation(origin_method), method_getTypeEncoding(origin_method));
    class_replaceMethod(oriClass, oriSEL, method_getImplementation(current_method), method_getTypeEncoding(current_method));
}
@implementation MutableDictionary
//方法呼叫
+ (void)load
{
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        ExchangeDicMethod(NSClassFromString(@"__NSDictionaryM"), [self class], @selector(setObject:forKey:), @selector(ex_setObject:forKey:));
    });
}
//替換方法實現
- (void)ex_setObject:(id)obj forKey:(id<NSCopying>)key
{
    if (obj && key) {
        NSLog(@"方法安全執行");
        [self ex_setObject:obj forKey:key];
    } else {
        NSLog(@"setObject:forKey:方法未執行,obj或者key未空");
    }
}
@end
複製程式碼

常見的Method Swizzling應用事例

  • 防止陣列取值時越界crash
+ (void)load
{
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        [NSClassFromString(@"__NSArrayI") jr_swizzleMethod:@selector(objectAtIndex:)
                                                withMethod:@selector(SF_ObjectAtIndex_NSArrayI:)
                                                     error:nil];
        [NSClassFromString(@"__NSArrayI") jr_swizzleMethod:@selector(objectAtIndexedSubscript:)
                                                withMethod:@selector(SF_ObjectAtIndexedSubscript_NSArrayI:)
                                                     error:nil];
        [NSClassFromString(@"__NSArrayM") jr_swizzleMethod:@selector(objectAtIndex:)
                                                withMethod:@selector(SF_ObjectAtIndex_NSArrayM:)
                                                     error:nil];
        [NSClassFromString(@"__NSArrayM") jr_swizzleMethod:@selector(objectAtIndexedSubscript:)
                                                withMethod:@selector(SF_ObjectAtIndexedSubscript_NSArrayM:)
                                                     error:nil];
}

- (id)SF_ObjectAtIndex_NSArrayI:(NSUInteger)index
{
    @autoreleasepool {
        if (index >= self.count
            || index < 0
            || !self.count) {
            @try {
                return [self SF_ObjectAtIndex_NSArrayI:index];
            } @catch (NSException *exception) {
                NSLog(@"%@", [NSThread callStackSymbols]);
                return nil;
            } @finally {
            }
        } else {
            return [self SF_ObjectAtIndex_NSArrayI:index];
        }
    }
}
- (id)SF_ObjectAtIndexedSubscript_NSArrayI:(NSUInteger)index
{
    @autoreleasepool {
        if (index >= self.count
            || index < 0
            || !self.count) {
            @try {
                return [self SF_ObjectAtIndexedSubscript_NSArrayI:index];
            } @catch (NSException *exception) {
                NSLog(@"%@", [NSThread callStackSymbols]);
                return nil;
            } @finally {
            }
        } else {
            return [self SF_ObjectAtIndexedSubscript_NSArrayI:index];
        }
    }
}
- (id)SF_ObjectAtIndex_NSArrayM:(NSUInteger)index
{
    @autoreleasepool {
        if (index >= self.count
            || index < 0
            || !self.count) {
            @try {
                return [self SF_ObjectAtIndex_NSArrayM:index];
            } @catch (NSException *exception) {
                NSLog(@"%@", [NSThread callStackSymbols]);
                return nil;
            } @finally {
            }
        } else {
            return [self SF_ObjectAtIndex_NSArrayM:index];
        }
    }
}

- (id)SF_ObjectAtIndexedSubscript_NSArrayM:(NSUInteger)index
{
    @autoreleasepool {
        if (index >= self.count
            || index < 0
            || !self.count) {
            @try {
                return [self SF_ObjectAtIndexedSubscript_NSArrayM:index];
            } @catch (NSException *exception) {
                NSLog(@"%@", [NSThread callStackSymbols]);
                return nil;
            } @finally {
            }
        } else {
            return [self SF_ObjectAtIndexedSubscript_NSArrayM:index];
        }
    }
}
複製程式碼

此處使用到了流行的包裝庫:JRSwizzle, 也可以在Demo中找到JRSwizzle原始碼

  • 改變某類檢視的大小,例如蓋面app中所有UIButton的大小
    我們就可以這樣實現:\
#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
複製程式碼
  • 處理按鈕重複點選\

如果重複過快的點選同一個按鈕,就會多次觸發點選事件。我們可以這麼解決:

//UIButton+QuickClick.h
@interface UIButton (QuickClick)
@property (nonatomic,assign) NSTimeInterval delayTime;
@end
//UIButton+QuickClick.m
@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
{
    NSTimeInterval interval = [objc_getAssociatedObject(self, delayTime_str) doubleValue];
    return interval;
}
- (void)setDelayTime:(NSTimeInterval)delayTime
{
    objc_setAssociatedObject(self, delayTime_str, @(delayTime), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
@end
複製程式碼


專案Demo在此

Swizzling Method的使用注意事項

後續補充...

如在文中發現任何錯誤,請及時告知,第一時間更正;

參考


相關文章