1.用到的運(yùn)行時(shí)基礎(chǔ)知識(shí)介紹
- SEL : 方法選擇器,SEL是函數(shù)objc_msgSend第二個(gè)參數(shù)的數(shù)據(jù)類型,表示方法選擇器 ;其實(shí)它就是映射到方法的C字符串,你可以通過(guò)Objc編譯器命令@selector()/NSSelectorFromString()或者Runtime系統(tǒng)的sel_registerName函數(shù)來(lái)獲取一個(gè)SEL類型的方法選擇器
- Method : 就是一個(gè)指向objc_method結(jié)構(gòu)體指針,它存儲(chǔ)了方法名(method_name)、方法類型(method_types)和一個(gè)指向方法實(shí)現(xiàn)的函數(shù)指針(method_imp)等信息
- IMP : IMP本質(zhì)上就是一個(gè)函數(shù)指針,指向方法的實(shí)現(xiàn),當(dāng)你向某個(gè)對(duì)象發(fā)送一條信息,可以由這個(gè)函數(shù)指針來(lái)指定方法的實(shí)現(xiàn),它最終就會(huì)執(zhí)行那段代碼,這樣可以繞開消息傳遞階段而去執(zhí)行另一個(gè)方法實(shí)現(xiàn)
- class_getInstanceMethod(Class cls, SEL name),獲取指定類中,方法選擇器對(duì)應(yīng)的方法,需要注意
在搜索方法的時(shí)候,會(huì)先從指定類的方法列表中搜索,如果搜索不到(當(dāng)前類沒(méi)有實(shí)現(xiàn)此方法),就會(huì)去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)
獲取某個(gè)方法對(duì)應(yīng)的方法實(shí)現(xiàn) - OBJC_EXPORT BOOL class_addMethod(Class cls, SEL name, IMP imp, const char *types) 為該類添加一個(gè)方法,但不會(huì)去重寫已有實(shí)現(xiàn)的方法,如果已經(jīng)有相同方法名稱則會(huì)返回NO;
注意:父類中有對(duì)應(yīng)的方法而當(dāng)前類沒(méi)有,會(huì)添加成功
/**
* 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) ,替換某個(gè)類中方法選擇器對(duì)應(yīng)方法的方法實(shí)現(xiàn)
/**
* 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) 交換兩個(gè)方法的方法實(shí)現(xiàn)
/**
* 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進(jìn)行一層判斷處理
+ (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:保證代碼塊只會(huì)被執(zhí)行一次并且線程安全,不過(guò)load方法中并不需要,并不會(huì)被多次調(diào)用
- 為什么需要用到class_addMethod
- 我們想要對(duì)一個(gè)類的某個(gè)方法進(jìn)行擴(kuò)展,應(yīng)該把這種擴(kuò)展限制在當(dāng)前類和當(dāng)前類的子類中,不應(yīng)該影響到父類中的方法,因?yàn)檫@個(gè)父類可能還有其他派生類,修改了父類的方法可能造成一連串非預(yù)期的結(jié)果
- 異常場(chǎng)景 : 假設(shè)我們要擴(kuò)展的方法(testButtonLog)是繼承自父類(TestButton)的,
且當(dāng)前類沒(méi)有對(duì)此方法(testButtonLog)進(jìn)行重寫
,按照寫法一
實(shí)現(xiàn)方法交換,最終實(shí)現(xiàn)的結(jié)果是把父類(TestButton)中的方法(testButtonLog)和方法(xl_testButtonLog)進(jìn)行了交換,這樣造成的后果如下: - 如果是當(dāng)前類(或當(dāng)前類的子類)的實(shí)例對(duì)象調(diào)用 testButtonLog時(shí),運(yùn)行時(shí)會(huì)去執(zhí)行 xl_testButtonLog , 沒(méi)有什么問(wèn)題
- 如果是當(dāng)前類的父類的實(shí)例對(duì)象調(diào)用testButtonLog時(shí),會(huì)有什么后果? 會(huì)調(diào)用當(dāng)前類的xl_testButtonLog方法,如果我們?cè)趚l_testButtonLog方法中這樣寫
```
- (void)xl_testButtonLog
{
NSLog(@"%s",func);
[self xl_testButtonLog]; // 調(diào)用此方法不會(huì)造成遞歸調(diào)用,因?yàn)樯厦孀隽朔椒ń粨Q,調(diào)用此方法,在運(yùn)行時(shí)會(huì)去執(zhí)行testButtonLog
}
```
會(huì)閃退,報(bào)錯(cuò)xl_testButtonLog方法找不到,原因:當(dāng)前類的父類中并沒(méi)有xl_testButtonLog方法具體可以看MethodSwizzlingDemo中情景2.2示例
閃退代碼示例
- 使用class_addMethod后,如果當(dāng)前類沒(méi)有實(shí)現(xiàn)要擴(kuò)展的方法(testButtonLog), class_addMethod會(huì)自動(dòng)在當(dāng)前類中添加一個(gè)方法(testButtonLog),這樣交換后的結(jié)果就是當(dāng)前類中的方法(testButtonLog)和方法(xl_testButtonLog)進(jìn)行交換,不影響父類testButtonLog的正常調(diào)用
- 具體效果演示,可以參考demoMethodSwizzlingDemo中三種情景的演示
3. 擴(kuò)展工具類
- 對(duì)于Method Swizzling的實(shí)現(xiàn)方法可以抽取出來(lái),封裝到NSObject的分類中,方便以后使用,具體看demoMethodSwizzlingDemo中的NSObject+MethodSwizzling類
- 交換兩個(gè)實(shí)例方法
/**
交換兩個(gè)實(shí)例方法
*/
+ (void)xl_exchangeInstanceMethod1:(SEL)originalSelector method2:(SEL)swizzledSelector
{
Class class = [self class]; // 這個(gè)地方要注意
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 交換兩個(gè)類方法
/**
交換兩個(gè)類方法
*/
+ (void)xl_exchangeClassMethod1:(SEL)originalSelector method2:(SEL)swizzledSelector
{
Class class = object_getClass((id)self); // 這個(gè)地方要注意
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);
}
}
- 注意點(diǎn):交換類方法時(shí),獲取類要用object_getClass((id)self)方法獲取class, 注意和通過(guò)[self class]獲取的class是不同的對(duì)象,可以通過(guò)打印hash值來(lái)看
Class class = object_getClass((id)self); // 這個(gè)地方要注意
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就像一把瑞士小刀,如果使用得當(dāng),它會(huì)有效地解決問(wèn)題。但使用不當(dāng),將帶來(lái)很多麻煩。在stackoverflow上有人已經(jīng)提出這樣一個(gè)問(wèn)題:What are the Dangers of Method Swizzling in Objective C?,它的危險(xiǎn)性主要體現(xiàn)以下幾個(gè)方面:
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