runtime method swizzling

前言

在我学习runtime的method swizzling特性之前,有很多同事或者朋友经常在我耳边说起swizzling特性,一个个在我面前说这个东西千万不能用,会引起很多问题的。但是,在我学习完这一节的知识后,我终于明白其所以然。

学习完swizzling特性后,我很喜欢她。她就像一把双刃剑,用好了可以带你飞,乱用则会反伤。但是,我更相信她的强大,更相信自己够能驾驭她!一起来学习吧!

Method Swizzling

试想一下,苹果的源码是闭源的,我们只有类名和类的属性、方法等声明,却看不到实现,这时候我们若想改变其中一个方法的实现,有哪些方案呢?笔者想到的有以下几种方案:

  1. 继承于这个类,然后通过重写方法(很常用,比如基类控制器,可以在视图加载完成时做一些公共的配置等)
  2. 通过类别重写方法,暴力抢先(此法太暴力,尽量不要这么做)
  3. swizzling(本文特讲内容)

Swizzling原理

在Objective-C中调用一个方法,其实是向一个对象发送消息,而查找消息的唯一依据是selector的名字。所以,我们可以利用Objective-C的runtime机制,实现在运行时交换selector对应的方法实现以达到我们的目的。

每个类都有一个方法列表,存放着selector的名字和方法实现的映射关系。IMP有点类似函数指针,指向具体的Method实现。

我们先看看SEL与IMP之间的关系图:

20130718230259187.png

从上图可以看出来,每一个SEL与一个IMP一一对应,正常情况下通过SEL可以查找到对应消息的IMP实现。

但是,现在我们要做的就是把链接线解开,然后连到我们自定义的函数的IMP上。当然,交换了两个SEL的IMP,还是可以再次交换回来了。交换后变成这样的,如下图:

20130718230430859.png

从图中可以看出,我们通过swizzling特性,将selectorC的方法实现IMPc与selectorN的方法实现IMPn交换了,当我们调用selectorC,也就是给对象发送selectorC消息时,所查找到的对应的方法实现就是IMPn而不是IMPc了。

在+load方法中交换

Swizzling应该在+load方法中实现,因为+load方法可以保证在类最开始加载时会调用。因为method swizzling的影响范围是全局的,所以应该放在最保险的地方来处理是非常重要的。+load能够保证在类初始化的时候一定会被加载,这可以保证统一性。试想一下,若是在实际时需要的时候才去交换,那么无法达到全局处理的效果,而且若是临时使用的,在使用后没有及时地使用swizzling将系统方法与我们自定义的方法实现交换回来,那么后续的调用系统API就可能出问题。

类文件在工程中,一定会加载,因此可以保证+load会被调用。

不要在+initialize中交换

+initialize是类第一次初始化时才会被调用,因为这个类有可能一直都没有使用到,因此这个类可能永远不会被调用。

类文件虽然在工程中,但是如果没有任何地方调用过,那么是不会调用+initialize方法的。

使用dispatch_once保证只交换一次

方法交换应该要线程安全,而且保证只交换一次,除非只是临时交换使用,在使用完成后又交换回来。

最常用的用法是在+load方法中使用dispatch_once来保证交换是安全的。因为swizzling会改变全局,我们需要在运行时采取相应的防范措施。保证原子操作就是一个措施,确保代码即使在多线程环境下也只会被执行一次。而diapatch_once就提供这些保障,因此我们应该将其加入到swizzling的使用标准规范中。

通用交换IMP写法

网上有很多的版本,但是有很多是不全面的,考虑的范围不够全面。下面我们来写一个通用的写法,现在扩展到NSObject中,因为NSObject是根类,这样其它类都可以使用了:

@interface NSObject (Swizzling)

+ (void)swizzleSelector:(SEL)originalSelector withSwizzledSelector:(SEL)swizzledSelector;

@end

#import "NSObject+Swizzling.h"
#import <objc/runtime.h>


// 实现代码如下
@implementation NSObject (Swizzling)

+ (void)swizzleSelector:(SEL)originalSelector withSwizzledSelector:(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);
  }
}

@end

因为方法可能不是在这个类里,可能是在其父类中才有实现,因此先尝试添加方法的实现,若添加成功了,则直接替换一下实现即可。若添加失败了,说明已经存在这个方法实现了,则只需要交换这两个方法的实现就可以了。

尽量使用method_exchangeImplementations函数来交换,因为它是原子操作的,线程安全。尽量不要自己手动写这样的代码:

IMP imp1 = method_getImplementation(m1);
IMP imp2 = method_getImplementation(m2);
method_setImplementation(m1, imp2);
method_setImplementation(m2, imp1);

虽然method_exchangeImplementations函数的本质也是这么写法,但是它内部做了线程安全处理。

当然,我们也可以写成C语言函数,而不是归属于类的方法:

// C语言版
void swizzleSelector(Class class, SEL originalSelector, SEL swizzledSelector) {
  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);
  }
}

简单使用swizzling

最简单的方法实现交换如下:

Method originalMethod = class_getInstanceMethod([NSArray class], @selector(lastObject));

Method newMedthod = class_getInstanceMethod([NSArray class], NSSelectorFromString(@"gg_lastObject"));

method_exchangeImplementations(originalMethod, newMedthod);

// NSArray提供了这样的实现
- (id)gg_lastObject {
  if (self.count == 0) {
    NSLog(@"%s 数组为空,直接返回nil", __FUNCTION__);
    
    return nil;
  }
  
  return [self gg_lastObject];
}

看到gg_lastObject这个方法递归调用自己了吗?为什么不是调用return [self lastObject]?因为我们交换了方法的实现,那么系统在调用lastObject方法是,找的是gg_lastObject方法的实现,而手动调用gg_lastObject方法时,会调用lastObject方法的实现。不清楚?回到前面看一看那个交换IMP的图吧!

我们通过使用swizzling只是为了添加个打印?当然不是,我们还可以做很多事的。比如,上面我们还做了防崩溃处理。

NSMutableArray扩展交换处理崩溃

还记得那些调用数组的addObject:方法加入一个nil值是的崩溃情景吗?还记得[__NSPlaceholderArray initWithObjects:count:]因为有nil值而崩溃的提示吗?还记得调用objectAtIndex:时出现崩溃提示empty数组问题吗?那么通过swizzling特性,我们可以做到不让它崩溃,而只是打印一些有用的日志信息。

我们先来看看NSMutableArray的扩展实现:

#import "NSMutableArray+Swizzling.h"
#import <objc/runtime.h>
#import "NSObject+Swizzling.h"

@implementation NSMutableArray (Swizzling)

+ (void)load {
  static dispatch_once_t onceToken;
  dispatch_once(&onceToken, ^{
    [self swizzleSelector:@selector(removeObject:)
     withSwizzledSelector:@selector(gg_safeRemoveObject:)];
    
    [objc_getClass("__NSArrayM") swizzleSelector:@selector(addObject:)
     withSwizzledSelector:@selector(gg_safeAddObject:)];
    [objc_getClass("__NSArrayM") swizzleSelector:@selector(removeObjectAtIndex:)
                            withSwizzledSelector:@selector(gg_safeRemoveObjectAtIndex:)];
 
    [objc_getClass("__NSArrayM") swizzleSelector:@selector(insertObject:atIndex:)
     withSwizzledSelector:@selector(gg_insertObject:atIndex:)];
    
    [objc_getClass("__NSPlaceholderArray") swizzleSelector:@selector(initWithObjects:count:) withSwizzledSelector:@selector(gg_initWithObjects:count:)];
    
    [objc_getClass("__NSArrayM") swizzleSelector:@selector(objectAtIndex:) withSwizzledSelector:@selector(gg_objectAtIndex:)];
  });
}

- (instancetype)gg_initWithObjects:(const id  _Nonnull __unsafe_unretained *)objects count:(NSUInteger)cnt {
  BOOL hasNilObject = NO;
  for (NSUInteger i = 0; i < cnt; i++) {
    if ([objects[i] isKindOfClass:[NSArray class]]) {
      NSLog(@"%@", objects[i]);
    }
    if (objects[i] == nil) {
      hasNilObject = YES;
      NSLog(@"%s object at index %lu is nil, it will be filtered", __FUNCTION__, i);
      
//#if DEBUG
//      // 如果可以对数组中为nil的元素信息打印出来,增加更容易读懂的日志信息,这对于我们改bug就好定位多了
//      NSString *errorMsg = [NSString stringWithFormat:@"数组元素不能为nil,其index为: %lu", i];
//      NSAssert(objects[i] != nil, errorMsg);
//#endif
    }
  }
  
  // 因为有值为nil的元素,那么我们可以过滤掉值为nil的元素
  if (hasNilObject) {
    id __unsafe_unretained newObjects[cnt];
    
    NSUInteger index = 0;
    for (NSUInteger i = 0; i < cnt; ++i) {
      if (objects[i] != nil) {
          newObjects[index++] = objects[i];
      }
    }
    
    return [self gg_initWithObjects:newObjects count:index];
  }

  return [self gg_initWithObjects:objects count:cnt];
}


- (void)gg_safeAddObject:(id)obj {
  if (obj == nil) {
    NSLog(@"%s can add nil object into NSMutableArray", __FUNCTION__);
  } else {
    [self gg_safeAddObject:obj];
  }
}

- (void)gg_safeRemoveObject:(id)obj {
  if (obj == nil) {
    NSLog(@"%s call -removeObject:, but argument obj is nil", __FUNCTION__);
    return;
  }
  
  [self gg_safeRemoveObject:obj];
}

- (void)gg_insertObject:(id)anObject atIndex:(NSUInteger)index {
  if (anObject == nil) {
    NSLog(@"%s can't insert nil into NSMutableArray", __FUNCTION__);
  } else if (index > self.count) {
    NSLog(@"%s index is invalid", __FUNCTION__);
  } else {
    [self gg_insertObject:anObject atIndex:index];
  }
}

- (id)gg_objectAtIndex:(NSUInteger)index {
  if (self.count == 0) {
    NSLog(@"%s can't get any object from an empty array", __FUNCTION__);
    return nil;
  }
  
  if (index > self.count) {
    NSLog(@"%s index out of bounds in array", __FUNCTION__);
    return nil;
  }
  
  return [self gg_objectAtIndex:index];
}

- (void)gg_safeRemoveObjectAtIndex:(NSUInteger)index {
  if (self.count <= 0) {
    NSLog(@"%s can't get any object from an empty array", __FUNCTION__);
    return;
  }
  
  if (index >= self.count) {
    NSLog(@"%s index out of bound", __FUNCTION__);
    return;
  }

  [self gg_safeRemoveObjectAtIndex:index];
}

@end

然后,我们测试nil值的情况,是否还会崩溃呢?

NSMutableArray *array = [@[@"value", @"value1"] mutableCopy];
[array lastObject];
  
[array removeObject:@"value"];
[array removeObject:nil];
[array addObject:@"12"];
[array addObject:nil];
[array insertObject:nil atIndex:0];
[array insertObject:@"sdf" atIndex:10];
[array objectAtIndex:100];
[array removeObjectAtIndex:10];
  
NSMutableArray *anotherArray = [[NSMutableArray alloc] init];
[anotherArray objectAtIndex:0];
  
NSString *nilStr = nil;
NSArray *array1 = @[@"ara", @"sdf", @"dsfdsf", nilStr];
NSLog(@"array1.count = %lu", array1.count);
  
// 测试数组中有数组
NSArray *array2 = @[@[@"12323", @"nsdf", nilStr], @[@"sdf", @"nilsdf", nilStr, @"sdhfodf"]];

哈哈,都不崩溃了,而且还打印出崩溃原因。是不是很神奇?如果充分利用这种特性,是不是可以给我们带来很多便利之处?

上面只是swizzling的一种应用场景而已。其实利用swizzling特性还可以做很多事情的,比如处理按钮重复点击问题等。

参考源代码:https://github.com/CoderJackyHuang/RuntimeDemo

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

推荐阅读更多精彩内容