你好,歡迎來到IOS教程網

 Ios教程網 >> IOS編程開發 >> IOS開發基礎 >> Method Swizzling的各種姿勢

Method Swizzling的各種姿勢

編輯:IOS開發基礎

因為Objective-C的runtime機制, Method Swizzling這個黑魔法解決了我們實際開發中諸多常規手段所無法解決的問題, 比如代碼的插樁,Hook,Patch等等. 我們首先看看常規的Method Swizzling是怎樣用的, NSHipster有一篇介紹基本用法的文章Method Swizzling, 我們就先以這篇文章中的示例開始說起吧:

#import @implementation UIViewController (Tracking)

+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        Class class = [self class];

        SEL originalSelector = @selector(viewWillAppear:);
        SEL swizzledSelector = @selector(xxx_viewWillAppear:);

        Method originalMethod = class_getInstanceMethod(class, originalSelector);
        Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);

        BOOL didAddMethod =
            class_addMethod(class,
                originalSelector,
                method_getImplementation(swizzledMethod),
                method_getTypeEncoding(swizzledMethod));

        if (didAddMethod) {
            class_replaceMethod(class,
                swizzledSelector,
                method_getImplementation(originalMethod),
                method_getTypeEncoding(originalMethod));
        } else {
            method_exchangeImplementations(originalMethod, swizzledMethod);
        }
    });
}

#pragma mark - Method Swizzling

- (void)xxx_viewWillAppear:(BOOL)animated {
    [self xxx_viewWillAppear:animated];
    NSLog(@"viewWillAppear: %@", self);
}

@end

簡要說明一下以上代碼的幾個重點:

  • 通過在Category的+ (void)load方法中添加Method Swizzling的代碼,在類初始加載時自動被調用,load方法按照父類到子類,類自身到Category的順序被調用.

  • 在dispatch_once中執行Method Swizzling是一種防護措施,以保證代碼塊只會被執行一次並且線程安全,不過此處並不需要,因為當前Category中的load方法並不會被多次調用.

  • 嘗試先調用class_addMethod方法,以保證即便originalSelector只在父類中實現,也能達到Method Swizzling的目的.

  • xxx_viewWillAppear:方法中[self xxx_viewWillAppear:animated];代碼並不會造成死循環,因為Method Swizzling之後, 調用xxx_viewWillAppear:實際執行的代碼已經是原來viewWillAppear中的代碼了.

其實以上的代碼也可以簡寫為以下:


+ (void)load {
    Class class = [self class];
    
    SEL originalSelector = @selector(viewWillAppear:);
    SEL swizzledSelector = @selector(xxx_viewWillAppear:);
    Method originalMethod = class_getInstanceMethod(class, originalSelector);
    Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);
    if (!originalMethod || !swizzledMethod) {
        return;
    }
    
    IMP originalIMP = method_getImplementation(originalMethod);
    IMP swizzledIMP = method_getImplementation(swizzledMethod);
    const char *originalType = method_getTypeEncoding(originalMethod);
    const char *swizzledType = method_getTypeEncoding(swizzledMethod);
    
    // 這兒的先後順序是有講究的,如果先執行後一句,那麼在執行完瞬間方法被調用容易引發死循環
    class_replaceMethod(class,swizzledSelector,originalIMP,originalType);
    class_replaceMethod(class,originalSelector,swizzledIMP,swizzledType);
}

這是因為class_replaceMethod方法其實能夠覆蓋到class_addMethod和method_setImplementation兩種場景, 對於第一個class_replaceMethod來說, 如果viewWillAppear:實現在父類, 則執行class_addMethod, 否則就執行method_setImplementation將原方法的IMP指定新的代碼塊; 而第二個class_replaceMethod完成的工作便只是將新方法的IMP指向原來的代碼.

但此處需要特別注意交換的順序,應該優化把新的方法指定原IMP,再修改原有的方法的IMP.


除了以上的場景之外,其它場景下我們如何使用Method Swizzling呢?


1.在不同類之間實現Method Swizzling

上面示例是通過Category來新增一個方法然後實現Method Swizzling的, 但有一些場景可能並不適合使用Category(比如私有的類,未獲取到該類的聲明), 此時我們應該如何來做Method Swizzling呢?

例如已知一個className為Car的類中有一個實例方法- (void)run:(double)speed, 目前需要Hook該方法對速度小於120才執行run的代碼, 按照方法交換的流程, 代碼應該是這樣的:


#import @interface MyCar : NSObject
@end

@implementation MyCar

+ (void)load {
    Class originalClass = NSClassFromString(@"Car");
    Class swizzledClass = [self class];
    SEL originalSelector = NSSelectorFromString(@"run:");
    SEL swizzledSelector = @selector(xxx_run:);
    Method originalMethod = class_getInstanceMethod(originalClass, originalSelector);
    Method swizzledMethod = class_getInstanceMethod(swizzledClass, swizzledSelector);
    
    // 向Car類中新添加一個xxx_run:的方法
    BOOL registerMethod = class_addMethod(originalClass,
                                          swizzledSelector,
                                          method_getImplementation(swizzledMethod),
                                          method_getTypeEncoding(swizzledMethod));
    if (!registerMethod) {
        return;
    }
    
    // 需要更新swizzledMethod變量,獲取當前Car類中xxx_run:的Method指針
    swizzledMethod = class_getInstanceMethod(originalClass, swizzledSelector);
    if (!swizzledMethod) {
        return;
    }
    
    // 後續流程與之前的一致
    BOOL didAddMethod = class_addMethod(originalClass,
                                        originalSelector,
                                        method_getImplementation(swizzledMethod),
                                        method_getTypeEncoding(swizzledMethod));
    if (didAddMethod) {
        class_replaceMethod(originalClass,
                            swizzledSelector,
                            method_getImplementation(originalMethod),
                            method_getTypeEncoding(originalMethod));
    } else {
        method_exchangeImplementations(originalMethod, swizzledMethod);
    }
}

- (void)xxx_run:(double)speed {
    if (speed < 120) {
        [self xxx_run:speed];
    }
}

@end

與之前的流程相比,在前面添加了兩個邏輯:

  • 利用runtime向目標類Car動態添加了一個新的方法,此時Car類與MyCar類一樣具備了xxx_run:這個方法,MyCar的利用價值便結束了;

  • 為了完成後續Car類中run:與xxx_run:的方法交換,此時需要更新swizzledMethod變量為Car中的xxx_run:方法所對應的Method.

以上所有的邏輯也可以合並簡化為以下:

+ (void)load {
    Class originalClass = NSClassFromString(@"Car");
    Class swizzledClass = [self class];
    SEL originalSelector = NSSelectorFromString(@"run:");
    SEL swizzledSelector = @selector(xxx_run:);
    Method originalMethod = class_getInstanceMethod(originalClass, originalSelector);
    Method swizzledMethod = class_getInstanceMethod(swizzledClass, swizzledSelector);
    
    IMP originalIMP = method_getImplementation(originalMethod);
    IMP swizzledIMP = method_getImplementation(swizzledMethod);
    const char *originalType = method_getTypeEncoding(originalMethod);
    const char *swizzledType = method_getTypeEncoding(swizzledMethod);
        
    class_replaceMethod(originalClass,swizzledSelector,originalIMP,originalType);
    class_replaceMethod(originalClass,originalSelector,swizzledIMP,swizzledType);
}


簡化後的代碼便與之前使用Category的方式並沒有什麼差異, 這樣代碼就很容易覆蓋到這兩種場景了, 但我們需要明確此時class_replaceMethod所完成的工作卻是不一樣的.

  • 第一個class_replaceMethod直接在Car類中注冊了xxx_run:方法, 並且指定的IMP為當前run:方法的IMP;

  • 第二個class_replaceMethod與之前的邏輯一致, 當run:方法是實現在Car類或Car的父類, 分別執行method_setImplementation或class_addMethod;

2.如何實現類方法的Method Swizzling

以上的代碼都是實現的對實例方法的交換, 那如何來實現對類方法的交換呢, 依舊直接貼代碼吧:


@interface NSDictionary (Test)
@end

@implementation NSDictionary (Test)

+ (void)load {
    Class cls = [self class];
    SEL originalSelector = @selector(dictionary);
    SEL swizzledSelector = @selector(xxx_dictionary);
    
    // 使用class_getClassMethod來獲取類方法的Method
    Method originalMethod = class_getClassMethod(cls, originalSelector);
    Method swizzledMethod = class_getClassMethod(cls, swizzledSelector);
    if (!originalMethod || !swizzledMethod) {
        return;
    }
    
    IMP originalIMP = method_getImplementation(originalMethod);
    IMP swizzledIMP = method_getImplementation(swizzledMethod);
    const char *originalType = method_getTypeEncoding(originalMethod);
    const char *swizzledType = method_getTypeEncoding(swizzledMethod);
    
    // 類方法添加,需要將方法添加到MetaClass中
    Class metaClass = objc_getMetaClass(class_getName(cls));
    class_replaceMethod(metaClass,swizzledSelector,originalIMP,originalType);
    class_replaceMethod(metaClass,originalSelector,swizzledIMP,swizzledType);
}

+ (id)xxx_dictionary {
    id result = [self xxx_dictionary];
    return result;
}

@end

相比實例方法的Method Swizzling,流程有兩點差異:

  • 獲取Method的方法變更為class_getClassMethod(Class cls, SEL name),從函數命名便直觀體現了和class_getInstanceMethod(Class cls, SEL name)的差別;

  • 對於類方法的動態添加,需要將方法添加到MetaClass中,因為實例方法記錄在class的method-list中, 類方法是記錄在meta-class中的method-list中的.

3.在類簇中如何實現Method Swizzling

在上面的代碼中我們實現了對NSDictionary中的+ (id)dictionary方法的交換,但如果我們用類似代碼嘗試對- (id)objectForKey:(id)key方法進行交換後, 你便會發現這似乎並沒有什麼用.

這是為什麼呢? 平常我們在Xcode調試時,在下方Debug區域左側的Variables View中,常常會發現如__NSArrayI或是__NSCFConstantString這樣的Class類型, 這便是在Foundation框架中被廣泛使用的類簇, 詳情請參看Apple文檔class cluster的內容.

所以針對類簇的Method Swizzling問題就轉變為如何對這些類簇中的私有類做Method Swizzling, 在上面介紹的不同類之間做Method Swizzling便已經能解決該問題, 下面一個簡單的示例通過交換NSMutableDictionary的setObject:forKey:方法,讓調用這個方法時當參數object或key為空的不會拋出異常:

@interface MySafeDictionary : NSObject
@end

@implementation MySafeDictionary

+ (void)load {
    Class originalClass = NSClassFromString(@"__NSDictionaryM");
    Class swizzledClass = [self class];
    SEL originalSelector = @selector(setObject:forKey:);
    SEL swizzledSelector = @selector(safe_setObject:forKey:);
    Method originalMethod = class_getInstanceMethod(originalClass, originalSelector);
    Method swizzledMethod = class_getInstanceMethod(swizzledClass, swizzledSelector);
    
    IMP originalIMP = method_getImplementation(originalMethod);
    IMP swizzledIMP = method_getImplementation(swizzledMethod);
    const char *originalType = method_getTypeEncoding(originalMethod);
    const char *swizzledType = method_getTypeEncoding(swizzledMethod);
    
    class_replaceMethod(originalClass,swizzledSelector,originalIMP,originalType);
    class_replaceMethod(originalClass,originalSelector,swizzledIMP,swizzledType);
}

- (void)safe_setObject:(id)anObject forKey:(id)aKey {
    if (anObject && aKey) {
        [self safe_setObject:anObject forKey:aKey];
    }
    else if (aKey) {
        [(NSMutableDictionary *)self removeObjectForKey:aKey];
    }
}

@end

4.在Method Swizzling之後如何恢復

使用了Method Swizzling的各種姿勢之後, 是否有考慮如何恢復到交換之前的現場呢?

一種方案就是通過一個開關標識符, 如果需要從邏輯上面恢復到交換之前, 就設置一下這個標識符, 在實現中判定如果設定了該標識符, 邏輯就直接調用原方法的實現, 其它什麼事兒也不干, 這是目前大多數代碼的實現方法, 當然也是非常安全的方式, 只不過當交換方法過多時, 每一個交換的方法體中都需要增加這樣的邏輯, 並且也需要維護大量這些標識符變量, 只是會覺得不夠優雅, 所以此處也就不展開詳細討論了.

那下面來討論一下有沒有更好的方案, 以上描述的Method Swizzling各種場景和處理的技巧, 但綜合總結之後最核心的其實也只做了兩件事情:

  • class_addMethod 添加一個新的方法, 可能是把其它類中實現的方法添加到目標類中, 也可能是把父類實現的方法添加一份在子類中, 可能是添加的實例方法, 也可能是添加的類方法, 總之就是添加了方法.

  • 交換IMP 交換方法的實現IMP,完成這個步驟除了使用method_exchangeImplementations這個方法外, 也可以是調用了method_setImplementation方法來單獨修改某個方法的IMP, 或者是采用在調用class_addMethod方法中設定了IMP而直接就完成了IMP的交換, 總之就是對IMP的交換.

那我們來分別看一下這兩件事情是否都還能恢復:

  • 對於class_addMethod, 我們首先想到的可能就是有沒有對應的remove方法呢, 在Objective-C 1.0的時候有class_removeMethods這個方法, 不過在2.0的時候就已經被禁用了, 也就是蘋果並不推薦我們這樣做, 想想似乎也是挺有道理的, 本來runtime的接口看著就挺讓人心驚膽戰的, 又是添加又是刪除總覺得會出岔子, 所以只能放棄remove的想法, 反正方法添加在那兒倒也沒什麼太大的影響.

  • 針對IMP的交換, 在Method Swizzling時做的交換動作, 如果需要恢復其實要做的動作還是交換回來罷了, 所以是可以做到的, 不過需要怎樣做呢? 對於同一個類, 同一個方法, 可能會在不同的地方被多次做Method Swizzling, 所以要回退某一次的Method Swizzling, 我們就需要記錄下來這一次交換的時候是哪兩個IMP做了交換, 恢復的時候再換回來即可. 另一個問題是如果已經經過多次交換, 我們怎樣找到這兩個IMP所對應的Mehod呢, 還好runtime提供了一個class_copyMethodList方法, 可以直接取出Method列表, 然後我們就可以逐個遍歷找到IMP所對應的Method了, 下面是對上一個示例添加恢復之後實現的代碼邏輯:

#import @interface MySafeDictionary : NSObject
@end

static NSLock *kMySafeLock = nil;
static IMP kMySafeOriginalIMP = NULL;
static IMP kMySafeSwizzledIMP = NULL;

@implementation MySafeDictionary

+ (void)swizzlling {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        kMySafeLock = [[NSLock alloc] init];
    });
    
    [kMySafeLock lock];
    
    do {
        if (kMySafeOriginalIMP || kMySafeSwizzledIMP) break;
        
        Class originalClass = NSClassFromString(@"__NSDictionaryM");
        if (!originalClass) break;
        
        Class swizzledClass = [self class];
        SEL originalSelector = @selector(setObject:forKey:);
        SEL swizzledSelector = @selector(safe_setObject:forKey:);
        Method originalMethod = class_getInstanceMethod(originalClass, originalSelector);
        Method swizzledMethod = class_getInstanceMethod(swizzledClass, swizzledSelector);
        if (!originalMethod || !swizzledMethod) break;
        
        IMP originalIMP = method_getImplementation(originalMethod);
        IMP swizzledIMP = method_getImplementation(swizzledMethod);
        const char *originalType = method_getTypeEncoding(originalMethod);
        const char *swizzledType = method_getTypeEncoding(swizzledMethod);
        
        kMySafeOriginalIMP = originalIMP;
        kMySafeSwizzledIMP = swizzledIMP;
        
        class_replaceMethod(originalClass,swizzledSelector,originalIMP,originalType);
        class_replaceMethod(originalClass,originalSelector,swizzledIMP,swizzledType);
    } while (NO);
    
    [kMySafeLock unlock];
}

+ (void)restore {
    [kMySafeLock lock];
    
    do {
        if (!kMySafeOriginalIMP || !kMySafeSwizzledIMP) break;
        
        Class originalClass = NSClassFromString(@"__NSDictionaryM");
        if (!originalClass) break;
        
        Method originalMethod = NULL;
        Method swizzledMethod = NULL;
        unsigned int outCount = 0;
        Method *methodList = class_copyMethodList(originalClass, &outCount);
        for (unsigned int idx=0; idx < outCount; idx++) {
            Method aMethod = methodList[idx];
            IMP aIMP = method_getImplementation(aMethod);
            if (aIMP == kMySafeSwizzledIMP) {
                originalMethod = aMethod;
            }
            else if (aIMP == kMySafeOriginalIMP) {
                swizzledMethod = aMethod;
            }
        }
        // 盡可能使用exchange,因為它是atomic的
        if (originalMethod && swizzledMethod) {
            method_exchangeImplementations(originalMethod, swizzledMethod);
        }
        else if (originalMethod) {
            method_setImplementation(originalMethod, kMySafeOriginalIMP);
        }
        else if (swizzledMethod) {
            method_setImplementation(swizzledMethod, kMySafeSwizzledIMP);
        }
        kMySafeOriginalIMP = NULL;
        kMySafeSwizzledIMP = NULL;
    } while (NO);
    
    [kMySafeLock unlock];
}

- (void)safe_setObject:(id)anObject forKey:(id)aKey {
    if (anObject && aKey) {
        [self safe_setObject:anObject forKey:aKey];
    }
    else if (aKey) {
        [(NSMutableDictionary *)self removeObjectForKey:aKey];
    }
}

@end

注意 這段代碼的Method Swizzling和恢復都需要主動調用, 並且相比上面其它的示例, 這段代碼還添加如鎖機制來加之保護. 這個示例是以不同的類來實現的Method Swizzling和恢復, 如果是Category或者是類方法, 根據之前的示例也需要做相應的調整.




  1. 上一頁:
  2. 下一頁:
蘋果刷機越獄教程| IOS教程問題解答| IOS技巧綜合| IOS7技巧| IOS8教程
Copyright © Ios教程網 All Rights Reserved