概述
iOS中,runtime方法交換,這個相信同學(xué)們都不陌生,原理也有很多文章講解過了,本文就不在講解了。
這里只是列舉幾個可能會出現(xiàn)的問題,帶著同學(xué)們分析問題的原因,以及如何處理。同學(xué)們可以先看問題,自行分析一下原因以及如何處理。
下文所用 Demo github鏈接。
問題
項目中有個UIView的Category,通過runtime添加了一個新的屬性UIEdgeInsets yzk_responseEdge1,然后通過method swizzle 重寫了pointInside:(CGPoint)point withEvent:(UIEvent *)event方法,用于擴大點擊事件。
再另一個工具庫中,同樣有個UIButton的Category,通過runtime添加了一個新的屬性UIEdgeInsets yzk_responseEdge2,然后通過method swizzle 重寫了pointInside:(CGPoint)point withEvent:(UIEvent *)event方法,用于擴大點擊事件。
當(dāng)我們使用UIButton或其子類的時候,預(yù)期是yzk_responseEdge1或yzk_responseEdge2都可以生效。實際上,僅僅有yzk_responseEdge2生效。
這里附上Demo的核心代碼,全部邏輯可自行下載Demo。
- 方法交換的邏輯,熟悉的同學(xué)可以跳過這段代碼。
// NSObject+FRRuntimeAdditions.h
@interface NSObject (FRRuntimeAdditions)
+ (void)swizzleInstanceMethod:(SEL)originalSEL with:(SEL)replacementSEL;
+ (void)swizzleClassMethod:(SEL)originalSEL with:(SEL)replacementSEL;
@end
// NSObject+FRRuntimeAdditions.m
void class_swizzleInstanceMethod(Class class, SEL originalSEL, SEL replacementSEL)
{
//class_getInstanceMethod(),如果子類沒有實現(xiàn)相應(yīng)的方法,則會返回父類的方法。
Method originMethod = class_getInstanceMethod(class, originalSEL);
Method replaceMethod = class_getInstanceMethod(class, replacementSEL);
//class_addMethod() 判斷originalSEL是否在子類中實現(xiàn),如果只是繼承了父類的方法,沒有重寫,那么直接調(diào)用method_exchangeImplementations,則會交換父類中的方法和當(dāng)前的實現(xiàn)方法。此時如果用父類調(diào)用originalSEL,因為方法已經(jīng)與子類中調(diào)換,所以父類中找不到相應(yīng)的實現(xiàn),會拋出異常unrecognized selector.
//當(dāng)class_addMethod() 返回YES時,說明子類未實現(xiàn)此方法(根據(jù)SEL判斷),此時class_addMethod會添加(名字為originalSEL,實現(xiàn)為replaceMethod)的方法。此時在將replacementSEL的實現(xiàn)替換為originMethod的實現(xiàn)即可。
//當(dāng)class_addMethod() 返回NO時,說明子類中有該實現(xiàn)方法,此時直接調(diào)用method_exchangeImplementations交換兩個方法的實現(xiàn)即可。
//注:如果在子類中實現(xiàn)此方法了,即使只是單純的調(diào)用super,一樣算重寫了父類的方法,所以class_addMethod() 會返回NO。
if(class_addMethod(class, originalSEL, method_getImplementation(replaceMethod),method_getTypeEncoding(replaceMethod)))
{
class_replaceMethod(class,replacementSEL, method_getImplementation(originMethod), method_getTypeEncoding(originMethod));
}else {
method_exchangeImplementations(originMethod, replaceMethod);
}
}
void class_swizzleClassMethod(Class class, SEL originalSEL, SEL replacementSEL)
{
//類方法實際上是儲存在類對象的類(即元類)中,即類方法相當(dāng)于元類的實例方法,所以只需要把元類傳入,其他邏輯和交互實例方法一樣。
Class class2 = object_getClass(class);
class_swizzleInstanceMethod(class2, originalSEL, replacementSEL);
}
@implementation NSObject (FRRuntimeAdditions)
+ (void)swizzleInstanceMethod:(SEL)originalSEL with:(SEL)replacementSEL {
class_swizzleInstanceMethod(self, originalSEL, replacementSEL);
}
+ (void)swizzleClassMethod:(SEL)originalSEL with:(SEL)replacementSEL {
class_swizzleClassMethod(self, originalSEL, replacementSEL);
}
@end
- UIView的Category
// UIView+Edge.h
@interface UIView (Edge)
@property (nonatomic, assign) UIEdgeInsets yzk_responseEdge1;
@end
// UIView+Edge.m
@implementation UIView (Edge)
+ (void)load {
[self swizzleInstanceMethod:@selector(pointInside:withEvent:)
with:@selector(yzk_pointInside:withEvent:)];
}
- (void)setYzk_responseEdge1:(UIEdgeInsets)yzk_responseEdge1 {
objc_setAssociatedObject(self, @selector(yzk_responseEdge1), [NSValue valueWithUIEdgeInsets:yzk_responseEdge1], OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
- (UIEdgeInsets)yzk_responseEdge1 {
NSValue *value = objc_getAssociatedObject(self, _cmd);
return value ? [value UIEdgeInsetsValue] : UIEdgeInsetsZero;
}
- (BOOL)yzk_pointInside:(CGPoint)point withEvent:(nullable UIEvent *)event {
BOOL inside = [self yzk_pointInside:point withEvent:event];
if (inside) {
return YES;
}
CGRect hitFrame = UIEdgeInsetsInsetRect(self.bounds, self.yzk_responseEdge1);
return CGRectContainsPoint(hitFrame, point);
}
@end
- UIButton的Category
// UIButton+Edge.h
@interface UIButton (Edge)
@property (nonatomic, assign) UIEdgeInsets yzk_responseEdge2;
@end
// UIButton+Edge.m
@implementation UIButton (Edge)
+ (void)load {
[self swizzleInstanceMethod:@selector(pointInside:withEvent:)
with:@selector(yzk2_pointInside:withEvent:)];
}
- (void)setYzk_responseEdge2:(UIEdgeInsets)yzk_responseEdge2 {
objc_setAssociatedObject(self, @selector(yzk_responseEdge2), [NSValue valueWithUIEdgeInsets:yzk_responseEdge2], OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
- (UIEdgeInsets)yzk_responseEdge2 {
NSValue *value = objc_getAssociatedObject(self, _cmd);
return value ? [value UIEdgeInsetsValue] : UIEdgeInsetsZero;
}
- (BOOL)yzk2_pointInside:(CGPoint)point withEvent:(nullable UIEvent *)event {
BOOL inside = [self yzk2_pointInside:point withEvent:event];
if (inside) {
return YES;
}
CGRect hitFrame = UIEdgeInsetsInsetRect(self.bounds, self.yzk_responseEdge2);
return CGRectContainsPoint(hitFrame, point);
}
@end
- Controller中創(chuàng)建2個UIButton,分別使用不同的屬性,測試效果
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
UIView *view1 = [[UIView alloc] initWithFrame:CGRectMake(0, 100, 200, 50)];
view1.backgroundColor = [UIColor cyanColor];
[self.view addSubview:view1];
UIView *view2 = [[UIView alloc] initWithFrame:CGRectMake(0, 150, 200, 50)];
view2.backgroundColor = [UIColor orangeColor];
[self.view addSubview:view2];
UIButton *bt1 = [UIButton buttonWithType:UIButtonTypeSystem];
bt1.frame = CGRectMake(50, 10, 100, 30);
bt1.backgroundColor = [UIColor purpleColor];
bt1.yzk_responseEdge1 = UIEdgeInsetsMake(-10, -50, -10, -50);
[bt1 setTitle:@"button" forState:UIControlStateNormal];
[bt1 addTarget:self action:@selector(btn1Click) forControlEvents:UIControlEventTouchUpInside];
[view1 addSubview:bt1];
UIButton *bt2 = [UIButton buttonWithType:UIButtonTypeSystem];
bt2.frame = CGRectMake(50, 10, 100, 30);
bt2.backgroundColor = [UIColor purpleColor];
bt2.yzk_responseEdge2 = UIEdgeInsetsMake(-10, -50, -10, -50);
[bt2 setTitle:@"button" forState:UIControlStateNormal];
[bt2 addTarget:self action:@selector(btn2Click) forControlEvents:UIControlEventTouchUpInside];
[view2 addSubview:bt2];
}
- (void)btn1Click {
NSLog(@"button 1");
}
- (void)btn2Click {
NSLog(@"button 2");
}
@end
工程中全部用到的文件如下:

Demo 效果如下:

當(dāng)我們點擊青色區(qū)域時,控制臺沒有任何log輸出。當(dāng)我們點擊橙色區(qū)域時,控制臺輸出“button 2”。
這說明屬性yzk_responseEdge1不生效,yzk_responseEdge2生效。
為什么會出現(xiàn)這種現(xiàn)象?如何修改才能達到我們的預(yù)期,使yzk_responseEdge1和yzk_responseEdge2均生效?
分析
首先我們從方法交換的流程開始分析。為了方便后文講述,我們標(biāo)記各IMP如下圖

由于上圖加載順序,我們會優(yōu)先加載UIButton的Category,即優(yōu)先交換UIButton的方法。此時UIView尚未交換,所以此時ori_btn_imp為 class_getInstanceMethod獲取到的父類pointInside:withEvent:實現(xiàn),即ori_view_imp。
交換后結(jié)果如下:

所以當(dāng)調(diào)用 UIButton及其子類 的 pointInside:withEvent: 方法時,會走入 new_btn_imp,即
- (BOOL)yzk2_pointInside:(CGPoint)point withEvent:(nullable UIEvent *)event {
BOOL inside = [self yzk2_pointInside:point withEvent:event];
if (inside) {
return YES;
}
CGRect hitFrame = UIEdgeInsetsInsetRect(self.bounds, self.yzk_responseEdge2);
return CGRectContainsPoint(hitFrame, point);
}
BOOL inside = [self yzk2_pointInside:point withEvent:event]調(diào)用,預(yù)期調(diào)用的應(yīng)該是父類(UIView)交換后的實現(xiàn)new_view_imp,但實際上調(diào)用的是原始實現(xiàn)ori_view_imp:
// new_view_imp
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event {
BOOL inside = [self yzk_pointInside:point withEvent:event];
if (inside) {
return YES;
}
CGRect hitFrame = UIEdgeInsetsInsetRect(self.bounds, self.yzk_responseEdge1);
return CGRectContainsPoint(hitFrame, point);
}
// ori_view_imp
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event {
return CGRectContainsPoint(self.bounds, point);
}
所以對于Button及其子類,View.yzk_responseEdge1不能生效,Button.yzk_responseEdge2可以生效。
解決方案
錯誤歸根究底,是由于Button交換時,原始的origin_btn_imp不正確導(dǎo)致的。這里作者有2種解決方案,歡迎大家在評論區(qū)補充更好的方案。
-
第一種方案,由于
origin_btn_imp是繼承自UIView的,只需要保證優(yōu)先hook UIView,保證繼承的origin_btn_imp= 交換后新的new_view_imp即可:調(diào)整Category的加載順序,優(yōu)先交換UIView的方法。UIButton經(jīng)過交換后結(jié)果如下:
swizzle_r2.png當(dāng)調(diào)用UIButton及其子類 的
pointInside:withEvent:方法時- 調(diào)用
new_btn_imp。 - 調(diào)用
yzk2_pointInside:withEvent:方法 - 調(diào)用
new_view_imp
此時設(shè)置
yzk_responseEdge1和yzk_responseEdge2均可生效。缺點:加載順序不方便維護。
- 調(diào)用
-
第二種方案,給
origin_btn_imp一個默認(rèn)實現(xiàn),不繼承UIView:給 UIButton
pointInside:withEvent:添加一個實現(xiàn),例如:- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event { return [super pointInside:point withEvent:event]; }注意盡管方法里面沒有任何邏輯,單純調(diào)用
super,依然和未實現(xiàn)方法有區(qū)別。UIButton交換時,獲取到的ori_btn_imp為我們這里新寫的實現(xiàn)。當(dāng)調(diào)用UIButton及其子類 的
pointInside:withEvent:方法時- 調(diào)用
new_btn_imp。 - 調(diào)用
yzk2_pointInside:withEvent:方法 - 調(diào)用
ori_btn_imp,即我們新添加的實現(xiàn)。 - 調(diào)用 UIView 的
pointInside:withEvent:方法 - 調(diào)用
new_view_imp。
優(yōu)點:加載順序不會影響結(jié)果。
缺點:由Category復(fù)寫主類方法,多Category復(fù)寫同一方法可能造成隱患。
- 調(diào)用
總結(jié)
method swizzle 的過程中,交換的順序,子類是否實現(xiàn)方法,都會影響到最終的調(diào)用結(jié)果。
如果想更深入了解的小伙伴,可以去看 Objective-C-Method-Swizzling 這篇文章。
