runtime - Method Swizzling

在开发中,如果我们想要改变某个类的方法或者替换为自己的方法,无外乎以下几种方式:

  1. 继承这个类,然后对这个方法进行重写;

  2. 通过分类直接改变方法实现;

  3. 利用runtime的 Method Swizzling 进行方法替换。

什么是 Method Swizzling ?

Method Swizzling顾名思义,就是方法调和的意思,这里我们可以简单理解为方法的改写。比较正式的解释可以理解为改变一个已存在的选择器对应的实现的过程,它依赖于Objectvie-C中方法的调用能够在运行时进改变——通过改变类的调度表dispatch table中选择器到最终函数间的映射关系。

Method Swizzling 原理

在OC中,调用方法既是给对象发送消息,而查找消息的依据则是根据selector名称,在OC中,每个selector都对应着相应的方法实现存放在方法分发表(dispatch table)里边,如果我们在程序运行时人为地改变selector和方法实现(IMP)的对应关系,那么就达到了方法改变的目的,也就是Method Swizzling。

selector和IMP的一一对应关系如下图所示:

图片来自`念茜`的博客

从上边的图片可以看出,每个selector都唯一地对应着一个IMP,利用Method Swizzling我们可以达到下图所示的效果:

图片来自`念茜`的博客

正常的话selectorC -> IMPc,selectorN -> IMPn,但是通过Method Swizzling,们可以达到图中的效果,即让selectorC -> IMPn,selectorN -> IMPc。其实归根结底,我们只是改变了selector和IMP的对应关系而已。

Method Swizzling 怎么使用?

Method Swizzling应该放在+ (void)load方法中,因为Method Swizzling影响范围是全局性的,而放在+ (void)load方法里边能够保证在类最开始加载的时候就执行,而且还可能会避免一些难以解决的bug。也有人说应该使用dispatch_once来保证线程安全,关于应不应该放在dispatch_once里边,笔者在这里并不是很清楚,如果有知道的朋友还请指教。

可能会用到的API有以下几个:

/**
 *  返回某个类的具体实例方法
 *  aClass:要操作的那个类
 *  aSelector:某个方法的selector
*/
Method class_getInstanceMethod(Class aClass, SEL aSelector) 

/**
 *  给某个类增加新方法,如果增加成功,返回YES;否则返回NO
 *  cls:要增加方法的类
 *  name:要增加方法的selecor
 *  imp:新方法的方法实现
 *  types:一连串的字符串用来标识方法和参数类型,这个参数在之前文章一提到过
*/
BOOL class_addMethod(Class cls, SEL name, IMP imp, const char *types) 

/**
 *  交换两个方法的实现
 *  m1:被交换的方法
 *  m2:交换的方法
*/
void method_exchangeImplementations( Method m1, Method m2) 

使用实例一

参考这篇文章的思路,如果我们要实现用户点击某个页面时进行统计功能:

UIViewController增加分类Swizzling,在.m文件中

+ (void)load 
{
    [super load];
    
    // 通过class_getInstanceMethod()函数从当前对象中的method list获取method结构体,如果是类方法就使用class_getClassMethod()函数获取。
    Method fromMethod = class_getInstanceMethod([self class], @selector(viewDidLoad));
    Method toMethod = class_getInstanceMethod([self class], @selector(zf_ViewDidLoad));
    
    // 如果`class_addMethod()`返回YES,这里我们才执行方法交换的动作
    if (!class_addMethod([self class], @selector(viewDidLoad), method_getImplementation(toMethod), method_getTypeEncoding(toMethod))) {
    method_exchangeImplementations(fromMethod, toMethod);
    }
}

// 我们自己实现的方法,也就是和self的viewDidLoad方法进行交换的方法。
- (void)zf_ViewDidLoad 
{
    NSString *str = [NSString stringWithFormat:@"%@", self.class];
    // 我们在这里加一个判断,将系统的UIViewController的对象剔除掉
    if(![str containsString:@"UI"]){
        NSLog(@"统计打点 : %@", self.class);
    }
    
    // 注意这里,可能会有疑惑,这样不就造成循环引用了么?其实并不会,当我们调用下边这句代码的时候,其实调用的是系统的`[self ViewDidLoad]`方法,如果不明白就去看看上边那个图
    [self swizzlingViewDidLoad];
}

#import分类文件,这样当我们运行的时候就会得到我们想要的结果了:

2016-07-07 11:04:57.582 ZFRuntime[831:72600] 统计打点 : ViewController

使用实例二(通用交换IMP写法)

因为大部分类都继承自NSObject类,为了扩展到大部分类,我们这里给它搞一个分类Swizzling,在.h文件中增加一个类方法:

+ (void)swizzleSelector:(SEL)oriSelector withSwizzledSelector:(SEL)swiSelector;

.m中,我们来实现它:

+ (void)swizzleSelector:(SEL)oriSelector withSwizzledSelector:(SEL)swiSelector

{
Class class = [self class];

// ‘class_getInstanceMethod’获取类实例方法,不要和‘class_getClassMethod’获取类方法混错
Method oriMethod = class_getInstanceMethod(class, oriSelector);
Method swiMethod = class_getInstanceMethod(class, swiSelector);

/**
 *  BOOL class_addMethod(Class cls, SEL name, IMP imp, const char *types)
 *  说明:‘class_addMethod’为类动态添加方法
 *  cls   为哪个类添加方法
 *  name  方法名(这个名字似乎可以随便起)
 *  imp   这个方法的实现(实现函数比不至少带有两个参数(self _cmd),这个参数可用过‘method_getImplementation’方法获取)
 *  types 定义该方法返回值类型和参数类型的字符串(官方文档:由于函数至少带有有两个参数self _cmd,所以type的字符串第2、3个字符必须是‘@:’,至于第一个字符是什么,要依据返回值类型而定。这个参数的获取可以使用‘ const char * method_getTypeEncoding( Method method) ’这个方法)
    更多请参考:https://developer.apple.com/library/mac/documentation/Cocoa/Conceptual/ObjCRuntimeGuide/Articles/ocrtTypeEncodings.html#//apple_ref/doc/uid/TP40008048-CH100
 *
 *  @return 如果方法添加成功,返回YES;否则返回NO
 */
BOOL didAddMethod = class_addMethod(class, oriSelector, method_getImplementation(swiMethod), method_getTypeEncoding(swiMethod));

// 只有当方法添加成功的时候我们才进行替换的动作
if (didAddMethod) {
    /**
     *  ‘IMP class_replaceMethod(Class cls, SEL name, IMP imp, const char *types)’
     *  (替换某个类的方法实现)
     *  cls   要修改的类
     *  name  新的方法的方法名
     *  imp   原方法实现
     *  types 同上
     *
     *  @return 新方法实现
     */
    class_replaceMethod(class, swiSelector, method_getImplementation(oriMethod), method_getTypeEncoding(oriMethod));
}else {
    /**
     *  ‘void method_exchangeImplementations( Method m1, Method m2)’
     *  交换两个方法的实现
     *  m1 原方法
     *  m2 新的方法
     */
    method_exchangeImplementations(oriMethod, swiMethod);
    }
}

开发中经常会遇到数组越界或者字典去取插入空值等情况,通过这里的学习我们就可以对其进行处理。

我们可以为NSArray增加分类Swizzling,在.m中我们这样做:

+ (void)load
{
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
    [self swizzleSelector:@selector(lastObject) withSwizzledSelector:@selector(zf_lastObject)];
    });
    
    // NSArray真正的类是`__NSArrayI`而不是`NSArray`
    Method fromMethod = class_getInstanceMethod(objc_getClass("__NSArrayI"), @selector(objectAtIndex:));
    Method toMethod = class_getInstanceMethod(objc_getClass("__NSArrayI"), @selector(zf_objectAtIndex:));
    method_exchangeImplementations(fromMethod, toMethod);
}

- (id)zf_lastObject
{
    if (self.count == 0) {
        // NSLog(@"空数组 %s", __func__);
        return nil;
    }
    // 调用自己,这里因为方法交换了,其实调用的是系统的‘lastObject’方法
    return [self zf_lastObject];
}

- (id)zf_objectAtIndex:(NSUInteger)index {
    if (self.count-1 < index) {
        // 这里做一下异常处理,不然都不知道出错了。
        @try {
            return [self zf_objectAtIndex:index];
    }
        @catch (NSException *exception) {
            // 在崩溃后会打印崩溃信息,方便我们调试。
            NSLog(@"---------- %s Crash Because Method %s  ----------\n", class_getName(self.class), __func__);
            NSLog(@"%@", [exception callStackSymbols]);
            return nil;
        }
        @finally {}
    } else {
        return [self zf_objectAtIndex:index];
    }
}

这样当我们对一个数组个数为0的数组进行操作时,就可以实现我们自己想要的操作了。

还可以实现对NSMutableArray崩溃的处理,同样的我们给NSMutableArray增加分类Swizzling,在.m文件中,我们:

+ (void)load
{
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
    
    // NSMutableArray真正的类是`__NSArrayM`,而不是`NSMutableArray`
        [objc_getClass("__NSArrayM") swizzleSelector:@selector(objectAtIndex:) withSwizzledSelector:@selector(zf_objectAtIndex:)];
    });
}

- (id)zf_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 zf_objectAtIndex:index];
}

这样当我们在取数据越界时就不会崩溃了,同样地还可以改变其他方法,都是类似的步骤。

注释:上边中关于NSArray NSDictionary类簇问题,可以参考这里这里,github上也有对Method Swizzling的封装,这里推荐这个

如果Method Swizzling用不好,可能会产生很多头疼的问题,但是用好了,会在我们项目开发中带来极大的方便。(stackoverflow上对Method Swizzling的评价)

另:本文参考了如下博客,感谢原作者

Demo地址

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

推荐阅读更多精彩内容