1.用到的運行時基礎知識介紹
- SEL : 方法選擇器,SEL是函數objc_msgSend第二個參數的數據類型,表示方法選擇器 ;其實它就是映射到方法的C字符串,你可以通過Objc編譯器命令@selector()/NSSelectorFromString()或者Runtime系統的sel_registerName函數來獲取一個SEL類型的方法選擇器
- Method : 就是一個指向objc_method結構體指針,它存儲了方法名(method_name)、方法類型(method_types)和一個指向方法實現的函數指針(method_imp)等信息
- IMP : IMP本質上就是一個函數指針,指向方法的實現,當你向某個對象發送一條信息,可以由這個函數指針來指定方法的實現,它最終就會執行那段代碼,這樣可以繞開消息傳遞階段而去執行另一個方法實現
- class_getInstanceMethod(Class cls, SEL name),獲取指定類中,方法選擇器對應的方法,需要注意
在搜索方法的時候,會先從指定類的方法列表中搜索,如果搜索不到(當前類沒有實現此方法),就會去superclasses 中搜索
/**
* @note This function searches superclasses for implementations, whereas \c class_copyMethodList does not.
*/
OBJC_EXPORT Method class_getInstanceMethod(Class cls, SEL name)
OBJC_AVAILABLE(10.0, 2.0, 9.0, 1.0);
- OBJC_EXPORT IMP method_getImplementation(Method m)
獲取某個方法對應的方法實現 - OBJC_EXPORT BOOL class_addMethod(Class cls, SEL name, IMP imp, const char *types) 為該類添加一個方法,但不會去重寫已有實現的方法,如果已經有相同方法名稱則會返回NO;
注意:父類中有對應的方法而當前類沒有,會添加成功
/**
* Adds a new method to a class with a given name and implementation.
*
* @param cls The class to which to add a method.
* @param name A selector that specifies the name of the method being added.
* @param imp A function which is the implementation of the new method. The function must take at least two arguments—self and _cmd.
* @param types An array of characters that describe the types of the arguments to the method.
*
* @return YES if the method was added successfully, otherwise NO
* (for example, the class already contains a method implementation with that name).
*
* @note class_addMethod will add an override of a superclass's implementation,
* but will not replace an existing implementation in this class.
* To change an existing implementation, use method_setImplementation.
*/
OBJC_EXPORT BOOL class_addMethod(Class cls, SEL name, IMP imp,
const char *types)
OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0);
- OBJC_EXPORT IMP class_replaceMethod(Class cls, SEL name, IMP imp, const char *types) ,替換某個類中方法選擇器對應方法的方法實現
/**
* Replaces the implementation of a method for a given class.
*
* @param cls The class you want to modify.
* @param name A selector that identifies the method whose implementation you want to replace.
* @param imp The new implementation for the method identified by name for the class identified by cls.
* @param types An array of characters that describe the types of the arguments to the method.
* Since the function must take at least two arguments—self and _cmd, the second and third characters
* must be “@:” (the first character is the return type).
*
* @return The previous implementation of the method identified by \e name for the class identified by \e cls.
*
* @note This function behaves in two different ways:
* - If the method identified by \e name does not yet exist, it is added as if \c class_addMethod were called.
* The type encoding specified by \e types is used as given.
* - If the method identified by \e name does exist, its \c IMP is replaced as if \c method_setImplementation were called.
* The type encoding specified by \e types is ignored.
*/
OBJC_EXPORT IMP class_replaceMethod(Class cls, SEL name, IMP imp,
const char *types)
OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0);
- method_exchangeImplementations(Method m1, Method m2) 交換兩個方法的方法實現
/**
* Exchanges the implementations of two methods.
*
* @param m1 Method to exchange with second method.
* @param m2 Method to exchange with first method.
*
* @note This is an atomic version of the following:
* \code
* IMP imp1 = method_getImplementation(m1);
* IMP imp2 = method_getImplementation(m2);
* method_setImplementation(m1, imp2);
* method_setImplementation(m2, imp1);
* \endcode
*/
OBJC_EXPORT void method_exchangeImplementations(Method m1, Method m2)
OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0);
2. 方法交換
2.1 寫法一:有缺陷版本,不建議使用
+ (void)load
{
Class class = [self class];
SEL originalSelector = @selector(testButtonLog);
SEL swizzledSelector = @selector(xl_testButtonLog);
Method originalMethod = class_getInstanceMethod(class, originalSelector);
Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);
method_exchangeImplementations(originalMethod, swizzledMethod);
}
2.2 寫法二: 建議寫法,交換前用class_addMethod進行一層判斷處理
+ (void)load
{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
Class class = [self class];
SEL originalSelector = @selector(testButtonLog);
SEL swizzledSelector = @selector(xl_testButtonLog);
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);
}
});
}
2.3 比較兩種寫法差異
- dispatch_once:保證代碼塊只會被執行一次并且線程安全,不過load方法中并不需要,并不會被多次調用
- 為什么需要用到class_addMethod
- 我們想要對一個類的某個方法進行擴展,應該把這種擴展限制在當前類和當前類的子類中,不應該影響到父類中的方法,因為這個父類可能還有其他派生類,修改了父類的方法可能造成一連串非預期的結果
- 異常場景 : 假設我們要擴展的方法(testButtonLog)是繼承自父類(TestButton)的,
且當前類沒有對此方法(testButtonLog)進行重寫
,按照寫法一
實現方法交換,最終實現的結果是把父類(TestButton)中的方法(testButtonLog)和方法(xl_testButtonLog)進行了交換,這樣造成的后果如下: - 如果是當前類(或當前類的子類)的實例對象調用 testButtonLog時,運行時會去執行 xl_testButtonLog , 沒有什么問題
- 如果是當前類的父類的實例對象調用testButtonLog時,會有什么后果? 會調用當前類的xl_testButtonLog方法,如果我們在xl_testButtonLog方法中這樣寫
```
- (void)xl_testButtonLog
{
NSLog(@"%s",func);
[self xl_testButtonLog]; // 調用此方法不會造成遞歸調用,因為上面做了方法交換,調用此方法,在運行時會去執行testButtonLog
}
```
會閃退,報錯xl_testButtonLog方法找不到,原因:當前類的父類中并沒有xl_testButtonLog方法具體可以看MethodSwizzlingDemo中情景2.2示例
閃退代碼示例
- 使用class_addMethod后,如果當前類沒有實現要擴展的方法(testButtonLog), class_addMethod會自動在當前類中添加一個方法(testButtonLog),這樣交換后的結果就是當前類中的方法(testButtonLog)和方法(xl_testButtonLog)進行交換,不影響父類testButtonLog的正常調用
- 具體效果演示,可以參考demoMethodSwizzlingDemo中三種情景的演示
3. 擴展工具類
- 對于Method Swizzling的實現方法可以抽取出來,封裝到NSObject的分類中,方便以后使用,具體看demoMethodSwizzlingDemo中的NSObject+MethodSwizzling類
- 交換兩個實例方法
/**
交換兩個實例方法
*/
+ (void)xl_exchangeInstanceMethod1:(SEL)originalSelector method2:(SEL)swizzledSelector
{
Class class = [self class]; // 這個地方要注意
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);
}
}
-
使用示例:
image.png 交換兩個類方法
/**
交換兩個類方法
*/
+ (void)xl_exchangeClassMethod1:(SEL)originalSelector method2:(SEL)swizzledSelector
{
Class class = object_getClass((id)self); // 這個地方要注意
Method originalMethod = class_getClassMethod(class, originalSelector);
Method swizzledMethod = class_getClassMethod(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);
}
}
- 注意點:交換類方法時,獲取類要用object_getClass((id)self)方法獲取class, 注意和通過[self class]獲取的class是不同的對象,可以通過打印hash值來看
Class class = object_getClass((id)self); // 這個地方要注意
Class class2 = [self class];
NSLog(@" class = %@ , class hash = %zd",class,[class hash]);
NSLog(@" class2 = %@ , class2 hash = %zd",class2,[class2 hash]);
//2017-05-17 18:38:00.171 EHGhostDrone3[26572:677703] class = NSObject , class hash = 4663643704
//2017-05-17 18:38:00.171 EHGhostDrone3[26572:677703] class2 = NSObject , class2 hash = 4663643784
4. Method Swizzling的弊端
Method Swizzling就像一把瑞士小刀,如果使用得當,它會有效地解決問題。但使用不當,將帶來很多麻煩。在stackoverflow上有人已經提出這樣一個問題:What are the Dangers of Method Swizzling in Objective C?,它的危險性主要體現以下幾個方面:
Method swizzling is not atomic
Changes behavior of un-owned code
Possible naming conflicts
Swizzling changes the method's arguments
The order of swizzles matters
Difficult to understand (looks recursive)
Difficult to debug