Objective-C執行時特性:Method Swizzling魔法

weixin_34185364發表於2019-02-08

OC執行時特性,為我們提供了一個叫做Method Swizzling的方法魔法利器,利用它我們可以更加隨心所欲的在執行時期間對編譯器已經的方法再次動手腳,主要包括:交換類中某兩個方法的實現、重新新增或替換某個方法的具體實現。

執行時的幾種特殊型別

Class: 類名,通過類的class類方法獲得,例如:[UIViewController class];
SEL:選擇器,也就是方法名,通過@selector(方法名:)獲得,例如:@selector(buttonClicked:);
Method:方法,即執行時類中定義的方法,包括方法名(SEL)和方法實現(IMP)兩部分,通過執行時方法class_getInstanceMethod或class_getClassMethod獲得;
IMP:方法實現型別,指的是方法的實現部分,通過執行時方法class_getMethodImplementation或method_getImplementation獲得;

替換類中某兩個類方法或例項方法的實現

關鍵執行時函式:method_exchangeImplementations(method1, method2)
這裡隨便定義一個Test類,類中定義兩個例項方法和類方法並在.m檔案中實現,在執行時將兩個例項方法的實現對調,以及將兩個類方法的實現對調。注意執行時程式碼寫在類的load方法內,該方法只會在該類第一次載入時呼叫一次,且寫執行時程式碼的地方需要引入執行時標頭檔案#import <objc/runtime.h>。

Test類定義:

#import <Foundation/Foundation.h> 
@interface Test : NSObject

/** * 定義兩個公有例項方法 */
- (void)instanceMethod1;
- (void)instanceMethod2;

/** * 定義兩個公有類方法 */
+ (void)classMethod1;
+ (void)classMethod2;

@end
#import "Test.h" #import <objc/runtime.h> 
@implementation Test

/** * runtime程式碼寫在類第一次調載入的時候(load方法有且只有一次會被呼叫) */
+ (void)load {
    /* 1. 獲取當前類名 */
    Class class = [self class];
    
    /* 2. 獲取方法名(選擇器) */
    SEL selInsMethod1 = @selector(instanceMethod1);
    SEL selInsMethod2 = @selector(instanceMethod2);
    
    SEL selClassMethod1 = @selector(classMethod1);
    SEL selClassMethod2 = @selector(classMethod2);
    
    /* 3. 根據方法名獲取方法物件 */
    Method InsMethod1 = class_getInstanceMethod(class, selInsMethod1);
    Method InsMethod2 = class_getInstanceMethod(class, selInsMethod2);
    
    Method ClassMethod1 = class_getClassMethod(class, selClassMethod1);
    Method ClassMethod2 = class_getClassMethod(class, selClassMethod2);
    
    /* 4. 交換例項方法的實現和類方法的實現 */
    if (!InsMethod1 || !InsMethod2) {
        NSLog(@"例項方法實現執行時交換失敗!");
        return;
    }
    /* 交換例項方法的實現 */
    method_exchangeImplementations(InsMethod1, InsMethod2);
    if (!ClassMethod1 || !ClassMethod2) {
        NSLog(@"類方法實現執行時交換失敗!");
        return;
    }
    /* 交換類方法的實現 */
    method_exchangeImplementations(ClassMethod1, ClassMethod2);
}

/** * 例項方法的原實現 */
- (void)instanceMethod1 {
    NSLog(@"instanceMethod1...");
}
- (void)instanceMethod2 {
    NSLog(@"instanceMethod2...");
}

/** * 類方法的原實現 */
+ (void)classMethod1 {
    NSLog(@"classMethod1...");
}
+ (void)classMethod2 {
    NSLog(@"classMethod2...");
}

@end

測試程式碼:

#import "ViewController.h" #import "Test.h" 
@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    /* 測試類方法呼叫 */
    [Test classMethod1];
    [Test classMethod2];
    
    Test *test = [[Test alloc] init];
    /* 測試例項方法呼叫 */
    [test instanceMethod1];
    [test instanceMethod2];
}

@end

通過下面的輸出結果可知,兩個例項方法和類方法的實現都被互換了:

2017-03-06 17:47:13.684 SingleView[41495:1196960] classMethod2...
2017-03-06 17:47:13.684 SingleView[41495:1196960] classMethod1...
2017-03-06 17:47:13.685 SingleView[41495:1196960] instanceMethod2...
2017-03-06 17:47:13.685 SingleView[41495:1196960] instanceMethod1...

重新設定類中某個方法的實現

關鍵執行時函式:method_setImplementation(method, IMP)

理解了上面的例子,我們現在略微修改其中執行時程式碼,通過重新設定方法的實現實現上面同樣的效果

修改後的執行時程式碼為:

/**
 * runtime程式碼寫在類第一次調載入的時候(load方法有且只有依次會被呼叫)
 */
+ (void)load {
    /* 1. 獲取當前類名 */
    Class class = [self class];
    
    /* 2. 獲取方法名(選擇器) */
    SEL selInsMethod1 = @selector(instanceMethod1);
    SEL selInsMethod2 = @selector(instanceMethod2);
    
    SEL selClassMethod1 = @selector(classMethod1);
    SEL selClassMethod2 = @selector(classMethod2);
    
    /* 3. 根據方法名獲取方法物件 */
    Method InsMethod1 = class_getInstanceMethod(class, selInsMethod1);
    Method InsMethod2 = class_getInstanceMethod(class, selInsMethod2);
    
    Method ClassMethod1 = class_getClassMethod(class, selClassMethod1);
    Method ClassMethod2 = class_getClassMethod(class, selClassMethod2);
    
    /* 下面程式碼為修改部分... ... */
    /* 4. 獲取方法的實現 */
    IMP impInsMethod1 = method_getImplementation(InsMethod1);
    IMP impInsMethod2 = method_getImplementation(InsMethod2);
    
    IMP impClassMethod1 = method_getImplementation(ClassMethod1);
    IMP impClassMethod2 = method_getImplementation(ClassMethod2);
    
    /* 5. 重新設定方法的實現 */
    /* 重新設定instanceMethod1的實現為instanceMethod2的實現 */
    method_setImplementation(InsMethod1, impInsMethod2);
    /* 重新設定instanceMethod2的實現為instanceMethod1的實現 */
    method_setImplementation(InsMethod2, impInsMethod1);
    
    /* 重新設定classMethod1的實現為classMethod2的實現 */
    method_setImplementation(ClassMethod1, impClassMethod2);
    /* 重新設定classMethod2的實現為classMethod1的實現 */
    method_setImplementation(ClassMethod2, impClassMethod1);
}

執行後列印結果和上面方法實現交換的例子結果相同:

2017-03-06 18:27:53.032 SingleView[41879:1212691] classMethod2...
2017-03-06 18:27:53.032 SingleView[41879:
1212691] classMethod1...
2017-03-06 18:27:53.033 SingleView[41879:1212691] instanceMethod2...
2017-03-06 18:27:53.033 SingleView[41879:1212691] instanceMethod1...

替換類中某個方法的實現

關鍵執行時函式:class_replaceMethod
這種方法只能替換例項方法的實現,而不能替換類方法的實現,修改的程式碼如下

/**
 * runtime程式碼寫在類第一次調載入的時候(load方法有且只有依次會被呼叫)
 */
+ (void)load {
    /* 1. 獲取當前類名 */
    Class class = [self class];
    
    /* 2. 獲取方法名(選擇器) */
    SEL selInsMethod1 = @selector(instanceMethod1);
    SEL selInsMethod2 = @selector(instanceMethod2);
    
    SEL selClassMethod1 = @selector(classMethod1);
    SEL selClassMethod2 = @selector(classMethod2);
    
    /* 3. 根據方法名獲取方法物件 */
    Method InsMethod1 = class_getInstanceMethod(class, selInsMethod1);
    Method InsMethod2 = class_getInstanceMethod(class, selInsMethod2);
    
    Method ClassMethod1 = class_getClassMethod(class, selClassMethod1);
    Method ClassMethod2 = class_getClassMethod(class, selClassMethod2);
    
    /* 4. 獲取方法的實現 */
    IMP impInsMethod1 = method_getImplementation(InsMethod1);
    IMP impInsMethod2 = method_getImplementation(InsMethod2);
    
    IMP impClassMethod1 = method_getImplementation(ClassMethod1);
    IMP impClassMethod2 = method_getImplementation(ClassMethod2);
    
    /* 下面程式碼為修改部分... ... */
    /* 5. 獲取方法編碼型別 */
    const char* typeInsMethod1 = method_getTypeEncoding(InsMethod1);
    const char* typeInsMethod2 = method_getTypeEncoding(InsMethod2);
    
    const char* typeClassMethod1 = method_getTypeEncoding(ClassMethod1);
    const char* typeClassMethod2 = method_getTypeEncoding(ClassMethod2);
    
    /* 替換InsMethod1的實現為InsMethod2的實現 */
    class_replaceMethod(class, selInsMethod1, impInsMethod2, typeInsMethod2);
    /* 替換InsMethod2的實現為InsMethod1的實現 */
    class_replaceMethod(class, selInsMethod2, impInsMethod1, typeInsMethod1);
    
    class_replaceMethod(class, selClassMethod1, impClassMethod2, typeClassMethod2);
    class_replaceMethod(class, selClassMethod2, impClassMethod1, typeClassMethod1);
}
通過結果可見例項方法的實現成功被替換,而類方法的實現沒有被替換:

2017-03-06 18:47:03.598 SingleView[42106:1221468] classMethod1...
2017-03-06 18:47:03.599 SingleView[42106:1221468] classMethod2...
2017-03-06 18:47:03.600 SingleView[42106:1221468] instanceMethod2...
2017-03-06 18:47:03.600 SingleView[42106:1221468] instanceMethod1...

以上介紹的是同一個類中方法實現的再改動,實際上也可以修改或交換不同類之間方法的實現。

在執行時為類補加新的方法

關鍵執行時函式:class_addMethod()
除了在編譯期顯式的定義方法,還可以在執行時補加新的例項方法,但不可以新增新的類方法,這裡接上面的例子為Test在執行時新增一個新的名為newInsMethod的方法,方法的實現設定為InsMethod1的實現,修改後的執行時程式碼為:

/**
 * runtime程式碼寫在類第一次調載入的時候(load方法有且只有依次會被呼叫)
 */
+ (void)load {
    /* 1. 獲取當前類名 */
    Class class = [self class];
    
    /* 2. 獲取方法名(選擇器) */
    SEL selInsMethod1 = @selector(instanceMethod1);
    SEL selInsMethod2 = @selector(instanceMethod2);
    
    SEL selClassMethod1 = @selector(classMethod1);
    SEL selClassMethod2 = @selector(classMethod2);
    
    /* 3. 根據方法名獲取方法物件 */
    Method InsMethod1 = class_getInstanceMethod(class, selInsMethod1);
    Method InsMethod2 = class_getInstanceMethod(class, selInsMethod2);
    
    Method ClassMethod1 = class_getClassMethod(class, selClassMethod1);
    Method ClassMethod2 = class_getClassMethod(class, selClassMethod2);
    
    /* 4. 獲取方法的實現 */
    IMP impInsMethod1 = method_getImplementation(InsMethod1);
    IMP impInsMethod2 = method_getImplementation(InsMethod2);
    
    IMP impClassMethod1 = method_getImplementation(ClassMethod1);
    IMP impClassMethod2 = method_getImplementation(ClassMethod2);
    
    /* 5. 獲取方法編碼型別 */
    const char* typeInsMethod1 = method_getTypeEncoding(InsMethod1);
    const char* typeInsMethod2 = method_getTypeEncoding(InsMethod2);
    
    const char* typeClassMethod1 = method_getTypeEncoding(ClassMethod1);
    const char* typeClassMethod2 = method_getTypeEncoding(ClassMethod2);
    
    /* 下面程式碼為修改部分... ... */
    /* 6. 為類新增新的例項方法和類方法 */
    SEL selNewInsMethod = @selector(newInsMethod);
    BOOL isInsAdded = class_addMethod(class, selNewInsMethod, impInsMethod1, typeInsMethod1);
    if (!isInsAdded) {
        NSLog(@"新例項方法新增失敗!");
    }
}

測試新函式程式碼

/* 測試執行時新新增例項方法呼叫 */
    Test *test = [[Test alloc] init];
    [test newInsMethod];

執行結果列印出“instanceMethod1…”證明新例項方法動態新增成功:

2017-03-06 19:07:15.447 SingleView[42354:1230571] instanceMethod1...

除了在執行時為類新增新的方法,還可以通過其他執行時函式class_addIvar、class_addProperty、class_addProtocol等動態地為類新增新的變數、屬性和協議等等。

相關文章