iOS runtime实战应用:Method Swizzling

转自其他博客

现在IT届流行一个词叫“黑科技”,泛指一些让人“不明觉厉的”新技术或新产品,那在iOS开发中有什么高大上的“黑科技”呢?runtime中的Method Swizzling当仁不让,它是一把双刃剑,高手耍起来威力无限,菜鸟耍起来则可能伤及自身。对于这样的黑科技,我们当然要掌握并努力驾驭之。

什么是Method Swizzling

Method Swizzling(貌似没有很正式的中文名,下文使用暴力的中文来代替:方法交换),顾名思义,就是将两个方法的实现交换,即由原来的A-AImp、B-BImp对应关系变成了A-BImp、B-AImp。

那为什么无缘无故要将两个方法的实现交换呢?

1、hook:在开发中,经常用到系统提供的API,但出于某些需求,我们可能会对某些方法的实现不太满意,就想去修改它以达到更好的效果,Hook由此诞生。iOS开发会使用Method Swizzling来达到这样的效果:当特定的消息发出时,会先到达我们提前预置的消息处理函数,取得控制权来加工消息以及后续处理。

2、面向切面编程:实际上,要改变一个方法的实现有几种方法,比如继承重写、分类重写等等,但在开发中,往往由于业务需要需要在代码中添加一些琐碎的、跟主要业务逻辑无关的东西,使用这两者都有局限性,使用方法交换动态給指定的方法添加代码以达到解耦的效果。

Method Swizzling原理

每个类都维护一个方法(Method)列表,Method则包含SEL和其对应IMP的信息,方法交换做的事情就是把SEL和IMP的对应关系断开,并和新的IMP生成对应关系。

交换前:Asel->AImp Bsel->BImp
交换后:Asel->BImp Bsel->AImp

Method Swizzling相关函数介绍

获取通过SEL获取一个方法
class_getInstanceMethod
获取一个方法的实现
method_getImplementation
获取一个OC实现的编码类型
method_getTypeEncoding
給方法添加实现
class_addMethod
用一个方法的实现替换另一个方法的实现
class_replaceMethod
交换两个方法的实现
method_exchangeImplementations
Method Swizzling实现的过程

    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        //case1: 替换实例方法
        Class selfClass = [self class];
        //case2: 替换类方法
        Class selfClass = object_getClass([self class]);

        //源方法的SEL和Method
        SEL oriSEL = @selector(viewWillAppear:);
        Method oriMethod = class_getInstanceMethod(selfClass, oriSEL);

        //交换方法的SEL和Method
        SEL cusSEL = @selector(customViewWillApper:);
        Method cusMethod = class_getInstanceMethod(selfClass, cusSEL);

        //先尝试給源方法添加实现,这里是为了避免源方法没有实现的情况
        BOOL addSucc = class_addMethod(selfClass, oriSEL, method_getImplementation(cusMethod), method_getTypeEncoding(cusMethod));
        if (addSucc) {
            //添加成功:将源方法的实现替换到交换方法的实现
            class_replaceMethod(selfClass, cusSEL, method_getImplementation(oriMethod), method_getTypeEncoding(oriMethod));
        }else {
            //添加失败:说明源方法已经有实现,直接将两个方法的实现交换即可
            method_exchangeImplementations(oriMethod, cusMethod);
        }
    });
Method Swizzling注意事项

1、方法交换应该保证唯一性和原子性
唯一性:应该尽可能在+load方法中实现,这样可以保证方法一定会调用且不会出现异常。
原子性:使用dispatch_once来执行方法交换,这样可以保证只运行一次。

2、一定要调用原始实现
由于iOS的内部实现对我们来说是不可见的,使用方法交换可能会导致其代码结构改变,而对系统产生其他影响,因此应该调用原始实现来保证内部操作的正常运行。

3、方法名必须不能产生冲突
这个是常识,避免跟其他库产生冲突。

4、做好记录
记录好被影响过的方法,不然时间长了或者其他人debug代码时候可能会对一些输出信息感到困惑。

5、如果非迫不得已,尽量少用方法交换
虽然方法交换可以让我们高效地解决问题,但是如果处理不好,可能会导致一些莫名其妙的bug。

Method Swizzling应用实例

1、hook: 給全局图片名称添加前缀

想象这样一个场景,你所负责的项目下个版本需要更换所有图标,然而你拿到新的图片资源比旧图片多了一个前缀“new_”,你可以一个个去改标题,当然也可以抖机灵地使用下面这个方法。

@implementation UIImage (Hook)

+ (void)load {

    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{

        Class selfClass = object_getClass([self class]);

        SEL oriSEL = @selector(imageNamed:);
        Method oriMethod = class_getInstanceMethod(selfClass, oriSEL);

        SEL cusSEL = @selector(myImageNamed:);
        Method cusMethod = class_getInstanceMethod(selfClass, cusSEL);

        BOOL addSucc = class_addMethod(selfClass, oriSEL, method_getImplementation(cusMethod), method_getTypeEncoding(cusMethod));
        if (addSucc) {
            class_replaceMethod(selfClass, cusSEL, method_getImplementation(oriMethod), method_getTypeEncoding(oriMethod));
        }else {
            method_exchangeImplementations(oriMethod, cusMethod);
        }

    });
}

+ (UIImage *)myImageNamed:(NSString *)name {

    NSString * newName = [NSString stringWithFormat:@"%@%@", @"new_", name];
    return [self myImageNamed:newName];
}

@end
流程分析:

1、在+load方法中将imageNamed和myImageNamed的实现交换了,现在的状态变成了
imageNamed -> myImageNamed(IMP)
myImageNamed -> imageNamed(IMP)

2、当用户调用imageNamed时,其实现是myImageNamed(IMP),在例子中的代码就是:

NSString * newName = [NSString stringWithFormat:@"%@%@", @"new_", name];
return [self myImageNamed:newName];
3、代码中对图片名称做了处理,加上前缀。

4、代码调用了[self myImageNamed:newName],看起来会陷入死循环,但其实myImageNamed的实现已经是imageNamed(IMP),因此该调用相当于调用系统原来的生成图片的实现。

5、一句话总结:方法交换之后,“方法的实现” 变成了 “你的处理代码” + “方法的实现”

结果:

当调用设置图片的方法时

imgV.image = [UIImage imageNamed:@"home_icon"];
实际上相当于自动給图片名称加了前缀

imgV.image = [UIImage imageNamed:@"new_home_icon"];

2、面向切面编程: 数据统计

想象这样一个场景,出于某些需求,我们需要跟踪记录APP中按钮的点击次数和频率等数据,怎么解决?当然通过继承按钮类或者通过类别实现是一个办法,但是带来其他问题比如别人不一定会去实例化你写的子类,或者其他类别也实现了点击方法导致不确定会调用哪一个,抖机灵的方法如下:

@implementation UIButton (Hook)

+ (void)load {

    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{

        Class selfClass = [self class];

        SEL oriSEL = @selector(sendAction:to:forEvent:);
        Method oriMethod = class_getInstanceMethod(selfClass, oriSEL);

        SEL cusSEL = @selector(mySendAction:to:forEvent:);
        Method cusMethod = class_getInstanceMethod(selfClass, cusSEL);

        BOOL addSucc = class_addMethod(selfClass, oriSEL, method_getImplementation(cusMethod), method_getTypeEncoding(cusMethod));
        if (addSucc) {
            class_replaceMethod(selfClass, cusSEL, method_getImplementation(oriMethod), method_getTypeEncoding(oriMethod));
        }else {
            method_exchangeImplementations(oriMethod, cusMethod);
        }

    });
}

- (void)mySendAction:(SEL)action to:(id)target forEvent:(UIEvent *)event {
    [CountTool addClickCount];
    [self mySendAction:action to:target forEvent:event];
}

@end

这里只是举了一两个小例子,更多用法读者可以深入理解这两种编程思维,举一反三。

Method Swizzling方法封装

细心的读者已经发现了,这个方法交换的代码好像变化不大,如果多处应用的话显得有些冗余,强迫症绝对不能接受,是的,必须将其封装之。


+ (void)swizzleMethods:(Class)class originalSelector:(SEL)origSel swizzledSelector:(SEL)swizSel {

    Method origMethod = class_getInstanceMethod(class, origSel);
    Method swizMethod = class_getInstanceMethod(class, swizSel);

    BOOL didAddMethod = class_addMethod(class, origSel, method_getImplementation(swizMethod), method_getTypeEncoding(swizMethod));
    if (didAddMethod) {
        class_replaceMethod(class, swizSel, method_getImplementation(origMethod), method_getTypeEncoding(origMethod));
    } else {
        method_exchangeImplementations(origMethod, swizMethod);
    }
}
后记

个人认为Method Swizzling是OC动态性的最好诠释,深入地去学习并理解其特性,将有助于我们在业务量不断增大的同时还能保持代码的低耦合度,降低维护的工作量和难度。

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

推荐阅读更多精彩内容

  • 转至元数据结尾创建: 董潇伟,最新修改于: 十二月 23, 2016 转至元数据起始第一章:isa和Class一....
    40c0490e5268阅读 1,635评论 0 9
  • 对于从事 iOS 开发人员来说,所有的人都会答出【runtime 是运行时】什么情况下用runtime?大部分人能...
    梦夜繁星阅读 3,674评论 7 64
  • 该文章属于刘小壮原创,转载请注明:刘小壮[https://www.jianshu.com/u/2de707c93d...
    刘小壮阅读 43,348评论 141 274
  • 上初中那会儿,班里有个奇女子。她像妖精一样,在一个秋天突然转进我们班,又像妖精一样,在一个夏天彻底消失。时隔五年,...
    book君阅读 432评论 0 0
  • 发现自己还是喜欢写点儿东西的,虽然很多时候也只是随自己情绪断篇儿没有什么逻辑可言,可谁又一直要那么理性的生活呢 缘...
    星多多阅读 276评论 0 0