iOS-底层探索17:Method-Swizzling 方法交换

iOS 底层探索 文章汇总

目录


一、Method-Swizzling是什么

Method swizzling指的是改变一个已存在的选择器对应的实现的过程,它依赖于Objectvie-C中方法的调用能够在运行时进行改变——通过改变类的调度表(dispatch table)中的选择器到最终函数间的映射关系。
交换前后的SELIMP的对应关系如下:

二、Method-Swizzling实现

#import "ViewController+NACategory.h"
#import <objc/runtime.h>

@implementation ViewController (NACategory)

+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        Class class = [self class];

        SEL originalSelector = @selector(viewWillAppear:);
        SEL swizzledSelector = @selector(category_viewWillAppear:);

        Method originalMethod = class_getInstanceMethod(class, originalSelector);
        Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);

        BOOL success = class_addMethod(class, originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod));
        if (success) {
            class_replaceMethod(class, swizzledSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod));
        } else {
            method_exchangeImplementations(originalMethod, swizzledMethod);
        }
    });
}

- (void)category_viewWillAppear:(BOOL)animated {
    [self category_viewWillAppear:animated];
    
    // TODO: - To do something...
}

@end
1、Swizzling应该在+load方法中实现?

+load+initializeObjective-C Runtime 会自动调用的两个类方法。但是它们被调用的时机却是有差别的,+load方法是在类被加载的时候调用的,而+initialize 方法是在类或它的子类收到第一条消息之前被调用的,这里所指的消息包括实例方法和类方法的调用。也就是说 +initialize方法是以懒加载的方式被调用的,如果程序一直没有给某个类或它的子类发送消息,那么这个类的 +initialize方法是永远不会被调用的。此外 +load方法还有一个非常重要的特性,那就是子类、父类和分类中的 +load 方法的实现是被区别对待的。换句话说在 Objective-C Runtime 自动调用 +load 方法时,分类中的 +load 方法并不会对主类中的 +load 方法造成覆盖。综上所述,+load 方法是实现 Method Swizzling 逻辑的最佳“场所”。

2、Swizzling应该在dispatch_once中实现

还是因为Swizzling会改变全局,我们需要在运行时采取所有可用的防范措施。保障原子性就是一个措施,它确保代码即使在多线程环境下也只会被执行一次。GCD中的diapatch_once就提供这些保障,它应该被当做Swizzling的标准实践。

3、为什么需要调用 class_addMethod 方法

通过class_addMethod尝试添加你要交换的方法
如果添加成功,即本类中没有这个方法,则通过class_replaceMethod进行替换,其内部会调用class_addMethod进行添加
如果添加不成功,即类中有这个方法,则通过method_exchangeImplementations进行交换

4、Swizzling在+load方法中实现存在的问题

iOS-底层探索14:分类的加载(类的加载下)文章中我们知道当主类为懒加载类、分类为非懒加载分类(+load)时分类会迫使主类变为非懒加载类样式来提前加载数据。因此大量在+load中实现 Method Swizzling 逻辑也会让主类和分类提前加载影响启动速度。

三、Method-Swizzling常见问题

1、父类实现了方法A,子类没有实现方法A。父类调用方法A会Crash,子类调用方法A正常。

//*********NAPerson类*********
@interface NAPerson : NSObject
- (void)personInstanceMethod;
@end

@implementation NAPerson
- (void)personInstanceMethod{
    NSLog(@"person对象方法:%s",__func__);  
}
@end

//*********NAStudent类*********
@interface NAStudent : NAPerson
@end

@implementation NAStudent
@end

//*********调用*********
- (void)viewDidLoad {
    [super viewDidLoad];

   // 黑魔法坑点一: 子类没有实现 - 父类实现
    NAStudent *s = [[NAStudent alloc] init];
    [s personInstanceMethod];
    
    NAPerson *p = [[NAPerson alloc] init];
    [p personInstanceMethod];
}

方法交换代码如下,是通过NAStudent的分类NACate实现的

@implementation NAStudent (NACate)

+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        SEL oriSEL = @selector(personInstanceMethod);
        SEL swizzledSEL = @selector(na_studentInstanceMethod);
        
        Method oriMethod = class_getInstanceMethod(self, oriSEL);
        Method swiMethod = class_getInstanceMethod(self, swizzledSEL);
        method_exchangeImplementations(oriMethod, swiMethod);
    });
}

- (void)na_studentInstanceMethod {
    [self na_studentInstanceMethod];
    NSLog(@"NAStudent分类添加的na对象方法:%s",__func__);
}

@end

运行代码会出现如下Crash:

原因分析:

  • [s personInstanceMethod];中不报错是因为通过Method-SwizzlingNAStudentpersonInstanceMethod方法的imp交换成了lg_studentInstanceMethod,而NAStudent中有这个方法(在NACate分类中),所以不会报错。

  • [p personInstanceMethod];造成Crash是因为通过方法交换,相当于在调用na_studentInstanceMethod方法。但是NAPerson中没有na_studentInstanceMethod方法,因此就会Crash

修改方法交换代码避免父类imp找不到
+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        SEL oriSEL = @selector(personInstanceMethod);
        SEL swizzledSEL = @selector(na_studentInstanceMethod);
        
        Method oriMethod = class_getInstanceMethod(self, oriSEL);
        Method swiMethod = class_getInstanceMethod(self, swizzledSEL);
        
        BOOL success = class_addMethod(self, oriSEL, method_getImplementation(swiMethod), method_getTypeEncoding(swiMethod));
        //success = YES:说明NAStudent中没有oriSEL(不在父类中找),并添加了oriSEL->swiMethod(IMP)
        if (success) {
            //替换swizzledSEL->oriMethod(IMP)
            class_replaceMethod(self, swizzledSEL, method_getImplementation(oriMethod), method_getTypeEncoding(oriMethod));
        } else {
            method_exchangeImplementations(oriMethod, swiMethod);
        }
    });
}

运行后打印如下:

使用class_addMethod的原因在上面已作了说明。

  • class_addMethod添加方法成功说明NAStudent本类中没有personInstanceMethod方法,然后会添加personInstanceMethod并指向na_studentInstanceMethod实现IMP
  • 再调用class_replaceMethod方法会替换na_studentInstanceMethod方法的实现为na_studentInstanceMethod(imp)
  • 因此这里只会对NAStudent本类中的方法进行交换,并不会对父类中的方法产生影响。

class_replaceMethodclass_addMethodmethod_exchangeImplementations的源码实现如下:

其中class_replaceMethodclass_addMethod中都调用了addMethod方法,区别在于replace赋值的不同,下面是addMethod的源码实现:

2、子类没有实现,父类也没有实现,下面的调用有什么问题?

//*********NAPerson类*********
@interface NAPerson : NSObject
- (void)personInstanceMethod;
@end

@implementation NAPerson
@end

//*********NAStudent类*********
@interface NAStudent : NAPerson
@end

@implementation NAStudent
@end

//*********调用*********
- (void)viewDidLoad {
    [super viewDidLoad];

   // 黑魔法坑点二: 子类父类都没有实现
    NAStudent *s = [[NAStudent alloc] init];
    [s personInstanceMethod];
    
    //NAPerson *p = [[NAPerson alloc] init];
    //[p personInstanceMethod];
}

NAStudent (NACate)load方法不变
运行结果如下:

可见产生了循环调用
通过断点调试可以发现loadclass_addMethod方法返回YES,因此oriSEL->swiMethod(IMP)成功,但是personInstanceMethod没有实现,class_replaceMethod方法中swizzledSEL->oriMethod(IMP)失败。因此执行代码[s personInstanceMethod];会调用na_studentInstanceMethod方法实现(IMP)。但na_studentInstanceMethod方法中执行[self na_studentInstanceMethod];并不会调用personInstanceMethod方法实现(IMP)。因此自己调自己,即递归死循环。

优化:避免递归死循环
  • 如果oriMethod为空,通过class_addMethodoriSEL添加swiMethod方法的IMP
  • 通过method_setImplementationswiMethodIMP指向不做任何事的空实现

虽然这样能解决子类的问题,但父类调用personInstanceMethod方法还是会出问题。

3、Method-Swizzling - 类方法

//*********NAStudent类*********
@interface NAStudent : NAPerson
+ (void)sayHello;
@end

@implementation NAStudent
@end

//*********NAStudent分类*********
@implementation NAStudent (NACate)

+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        [self ClassMethodSwizzling];
    });
}

+ (void)ClassMethodSwizzling {
    SEL oriSEL = @selector(sayHello);
    SEL swizzledSEL = @selector(na_studentClassMethod);
    
    Method oriMethod = class_getClassMethod([self class], oriSEL);
    Method swiMethod = class_getClassMethod([self class], swizzledSEL);
    
    if (!oriMethod) { // 避免动作没有意义
        // 在oriMethod为nil时,替换后将swizzledSEL复制一个不做任何事的空实现,代码如下:
        class_addMethod(object_getClass(self), oriSEL, method_getImplementation(swiMethod), method_getTypeEncoding(swiMethod));
        method_setImplementation(swiMethod, imp_implementationWithBlock(^(id self, SEL _cmd){
            CJLog(@"空的IMP")
        }));
    }
    
    BOOL success = class_addMethod(object_getClass(self), oriSEL, method_getImplementation(swiMethod), method_getTypeEncoding(swiMethod));
    if (success) {
        class_replaceMethod(object_getClass(self), swizzledSEL, method_getImplementation(oriMethod), method_getTypeEncoding(oriMethod));
    } else {
        method_exchangeImplementations(oriMethod, swiMethod);
    }
}

+ (void)na_studentClassMethod {
    [self na_studentClassMethod];
    CJLog(@"NAStudent分类添加的na类方法:%s",__func__);
}

//*********调用*********
- (void)viewDidLoad {
    [super viewDidLoad];

    [NAStudent sayHello];
}

调用结果:

空的IMP
NAStudent分类添加的na类方法:+[NAStudent(NACate) na_studentClassMethod]

四、Method-Swizzling的应用

1、处理Button重复点击

@interface UIButton (QuickClick)
@property (nonatomic,assign) NSTimeInterval delayTime;
@end

@implementation UIButton (QuickClick)
static const char* delayTime_str = "delayTime_str";
 
+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        Method originMethod = class_getInstanceMethod(self, @selector(sendAction:to:forEvent:));
        Method replacedMethod = class_getInstanceMethod(self, @selector(miSendAction:to:forEvent:));
        method_exchangeImplementations(originMethod, replacedMethod);
    });
}
 
- (void)miSendAction:(nonnull SEL)action to:(id)target forEvent:(UIEvent *)event {
    if (self.delayTime > 0) {
        if (self.userInteractionEnabled) {
            [self miSendAction:action to:target forEvent:event];
        }
        self.userInteractionEnabled = NO;
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW,
                                     (int64_t)(self.delayTime * NSEC_PER_SEC)),
                                     dispatch_get_main_queue(), ^{
                                         self.userInteractionEnabled = YES;
                                     });
    } else {
        [self miSendAction:action to:target forEvent:event];
    }
}
 
- (NSTimeInterval)delayTime {
    return [objc_getAssociatedObject(self, delayTime_str) doubleValue];
}
 
- (void)setDelayTime:(NSTimeInterval)delayTime {
    objc_setAssociatedObject(self, delayTime_str, @(delayTime), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
 
@end

2、解决往Array或Dictionary中插入nil导致的crash

iOSNSNumber、NSArray、NSDictionary等这些类都是类簇,一个NSArray的实现可能由多个类组成。所以如果想对NSArray进行Swizzling,必须获取到其“真身”进行Swizzling,直接对NSArray进行操作是无效的

下面列举了NSArrayNSDictionary本类的类名,可以通过Runtime函数取出本类。

类名 本类
NSArray __NSArrayI
NSMutableArray __NSArrayM
NSDictionary __NSDictionaryI
NSMutableDictionary __NSDictionaryM

我们可以用method swizzling修改-[__NSDictionaryM setObject:forKey:]方法,让它在设值时,先判断是否value为空,为空则不设置。代码如下:

@implementation NSMutableDictionary (Safe)
 
+ (void)load {
    Class dictCls = NSClassFromString(@"__NSDictionaryM");
    Method originalMethod = class_getInstanceMethod(dictCls, @selector(setObject:forKey:));
    Method swizzledMethod = class_getInstanceMethod(dictCls, @selector(na_setObject:forKey:));
    method_exchangeImplementations(originalMethod, swizzledMethod);
}
 
- (void)na_setObject:(id)anObject forKey:(id<NSCopying>)aKey {
    if (!anObject)
        return;
    [self na_setObject:anObject forKey:aKey];
}
 
@end
@implementation NSArray (Safe)
 
+ (void)load {
    Method originalMethod = class_getClassMethod(self, @selector(arrayWithObjects:count:));
    Method swizzledMethod = class_getClassMethod(self, @selector(na_arrayWithObjects:count:));
    method_exchangeImplementations(originalMethod, swizzledMethod);
}
 
+ (instancetype)na_arrayWithObjects:(const id [])objects count:(NSUInteger)cnt {
    id nObjects[cnt];
    int i=0, j=0;
    for (; i<cnt && j<cnt; i++) {
        if (objects[i]) {
            nObjects[j] = objects[i];
            j++;
        }
    }
    
    return [self na_arrayWithObjects:nObjects count:j];
}
@end
 
@implementation NSMutableArray (Safe)
 
+ (void)load {
    Class arrayCls = NSClassFromString(@"__NSArrayM");
    
    Method originalMethod1 = class_getInstanceMethod(arrayCls, @selector(insertObject:atIndex:));
    Method swizzledMethod1 = class_getInstanceMethod(arrayCls, @selector(na_insertObject:atIndex:));
    method_exchangeImplementations(originalMethod1, swizzledMethod1);
    
    Method originalMethod2 = class_getInstanceMethod(arrayCls, @selector(setObject:atIndex:));
    Method swizzledMethod2 = class_getInstanceMethod(arrayCls, @selector(na_setObject:atIndex:));
    method_exchangeImplementations(originalMethod2, swizzledMethod2);
}
 
- (void)na_insertObject:(id)anObject atIndex:(NSUInteger)index {
    if (!anObject)
        return;
    [self na_insertObject:anObject atIndex:index];
}
 
- (void)na_setObject:(id)anObject atIndex:(NSUInteger)index {
    if (!anObject)
        return;
    [self na_setObject:anObject atIndex:index];
}
 
@end

防止数组越界代码:

@implementation NSArray (CJLArray)
//如果下面代码不起作用,造成这个问题的原因大多都是其调用了super load方法。在下面的load方法中,不应该调用父类的load方法。这样会导致方法交换无效
+ (void)load {
    Method fromMethod = class_getInstanceMethod(objc_getClass("__NSArrayI"), @selector(objectAtIndex:));
    Method toMethod = class_getInstanceMethod(objc_getClass("__NSArrayI"), @selector(cjl_objectAtIndex:));
    method_exchangeImplementations(fromMethod, toMethod);
}

- (id)cjl_objectAtIndex:(NSUInteger)index {
    //判断下标是否越界,如果越界就进入异常拦截
    if (self.count-1 < index) {
        // 这里做一下异常处理,不然都不知道出错了。
#ifdef DEBUG  // 调试阶段
        return [self cjl_objectAtIndex:index];
#else // 发布阶段
        @try {
            return [self cjl_objectAtIndex:index];
        } @catch (NSException *exception) {
            // 在崩溃后会打印崩溃信息,方便我们调试。
            NSLog(@"---------- %s Crash Because Method %s  ----------\n", class_getName(self.class), __func__);
            NSLog(@"%@", [exception callStackSymbols]);
            return nil;
        } @finally {
            
        }
#endif
    } else { // 如果没有问题,则正常进行方法调用
        return [self cjl_objectAtIndex:index];
    }
}

@end
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,242评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,769评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,484评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,133评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,007评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,080评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,496评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,190评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,464评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,549评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,330评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,205评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,567评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,889评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,160评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,475评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,650评论 2 335

推荐阅读更多精彩内容