Method Swizzling的各種姿勢


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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
#import <objc/runtime.h>

@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中的代碼了.

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
+ (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,originalSelector,swizzledIMP,swizzledType);
class_replaceMethod(class,swizzledSelector,originalIMP,originalType);
}

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

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

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

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
#import <objc/runtime.h>

@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.

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
+ (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與之前的邏輯一致, 當run:方法是實現在Car類或Car的父類, 分別執行method_setImplementationclass_addMethod;
  • 第二個class_replaceMethod則直接在Car類中注冊了xxx_run:方法, 並且指定的IMP為當前run:方法的IMP;

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
@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,originalSelector,swizzledIMP,swizzledType);
class_replaceMethod(metaClass,swizzledSelector,originalIMP,originalType);
}

+ (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便已經能解決該問題, 下面一個簡單的示例通過交換NSMutableDictionarysetObject:forKey:方法,讓調用這個方法時當參數object或key為空的不會拋出異常:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
@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<NSCopying>)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了, 下面是對上一個示例添加恢復之后實現的代碼邏輯: 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
#import <objc/runtime.h>

@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;

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) {
method_setImplementation(aMethod, kMySafeOriginalIMP);
}
else if (aIMP == kMySafeOriginalIMP) {
method_setImplementation(aMethod, kMySafeSwizzledIMP);
}
}
kMySafeOriginalIMP = NULL;
kMySafeSwizzledIMP = NULL;
} while (NO);

[kMySafeLock unlock];
}

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

@end

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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM