iOS method swizzle 注意事項

概述

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_responseEdge1yzk_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

工程中全部用到的文件如下:

compile.png

Demo 效果如下:

demo.png

當(dāng)我們點擊青色區(qū)域時,控制臺沒有任何log輸出。當(dāng)我們點擊橙色區(qū)域時,控制臺輸出“button 2”。

這說明屬性yzk_responseEdge1不生效,yzk_responseEdge2生效。

為什么會出現(xiàn)這種現(xiàn)象?如何修改才能達到我們的預(yù)期,使yzk_responseEdge1yzk_responseEdge2均生效?

分析

首先我們從方法交換的流程開始分析。為了方便后文講述,我們標(biāo)記各IMP如下圖

swizzle.png

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

交換后結(jié)果如下:

swizzle_r1.png

所以當(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_responseEdge1yzk_responseEdge2均可生效。

    缺點:加載順序不方便維護。

  • 第二種方案,給 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ù)寫同一方法可能造成隱患。

總結(jié)

method swizzle 的過程中,交換的順序,子類是否實現(xiàn)方法,都會影響到最終的調(diào)用結(jié)果。

如果想更深入了解的小伙伴,可以去看 Objective-C-Method-Swizzling 這篇文章。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容