Objective-C 方法混写

方法混写

在 Objctive-C 中,混写(Swizzling)指的是偷梁换柱,将一个东西透明的换成另外一个东西,在 Runtime 运行时替换方法实现。
这个技术有什么作用呢?我们利用混写技术可以改变那些没有源代码的对象的行为,这其中包括了一些系统对象的行为,也包括了一些第三方 SDK 。

给 NSNotificationCenter 添加日志

接下来展示一个案例,每次在给 NSNotificationCenter 添加观察者的时候就打印日志。

创建 NSObject+MethodSwizzle

创建一个 NSObject 的 Category,叫做 NSObject+MethodSwizzle 用于实现方法混写。

// NSObject+MethodSwizzle.h
#import <Foundation/Foundation.h>
// 为 NSObject 添加方法混写实现
@interface NSObject (MethodSwizzle)
+ (IMP)swizzleSelector:(SEL)origSelector withIMP:(IMP)newIMP;
@end


// NSObject+MethodSwizzle.m
#import "NSObject+MethodSwizzle.h"
#import <objc/runtime.h>
@implementation NSObject (MethodSwizzle)

// 把 origSelector 的当前实现替换为新的实现
+(IMP)swizzleSelector:(SEL)origSelector withIMP:(IMP)newIMP{
    // 当前 class
    Class class = [self class];
    // 获取 origSelector 的 Method
    Method origMethod = class_getInstanceMethod(class, origSelector);
    // 获取 origSelector 的实现
    IMP origIMP = method_getImplementation(origMethod);

    // 若是 class_addMethod 给 class 成功添加了方法,那么返回 YES,否则返回 NO。
    // class_addMethod 会覆盖父类的方法实现,若是覆写方法会返回 YES。
    // class_addMethod 返回 NO,那么我们可以知道这个类是直接实现了 origSelector 对应的方法
    BOOL isAdd = class_addMethod(class, origSelector, newIMP, method_getTypeEncoding(origMethod));
    if (!isAdd) {
        // 替换 origMethod 方法的实现
        method_setImplementation(origMethod, newIMP);
    }
    // 返回 origSelector 对应的方法的实现
    return origIMP;
}
@end

NSObject+MethodSwizzle 的 + (IMP)swizzleSelector:(SEL)origSelector withIMP:(IMP)newIMP;
方法用于把类中的 origSelector 这个 Selector 对应的方法实现替换为 newIMP 这个方法实现。
在做方法混写的时候我们需要考虑三个情况:

  1. 该类直接实现了这个方法
  2. 该类的类层次结构中的某个父类实现了这个方法
  3. 该类压根没有实现这个方法
    我们使用了 class_addMethod 来处理以上三种情况。

创建 NSNotificationCenter+MethodSwizzle

创建 NSNotificationCenter 的 Category,叫做 NSNotificationCenter+MethodSwizzle 用于给 NSNotificationCenter 添加混写方法。

// NSNotificationCenter+MethodSwizzle.h
@interface NSNotificationCenter (MethodSwizzle)
// 给自己添加方法混写
+ (void)swizzleAddObserver;
@end

// NSNotificationCenter+MethodSwizzle.m
#import "NSNotificationCenter+MethodSwizzle.h"
#import "NSObject+MethodSwizzle.h"

@implementation NSNotificationCenter (MethodSwizzle)

typedef void (*voidIMP)(id,SEL,...);
static voidIMP sOrigAddObserver = NULL;

// 用于替换 NSNotificationCenter 的 addObserver:selector:name:object: 的默认实现
static void MyAddObserver(id self,SEL _cmd,id observer,SEL selector,NSString *name,id object){
    // 打印日志
    NSLog(@"添加观察者:%@",observer);
    NSAssert(sOrigAddObserver, @"旧方法找不到");
    if(sOrigAddObserver){
        // 调用原来的方法实现
        sOrigAddObserver(self,_cmd,observer,selector,name,object);
    }
}

// 方法混写实现
+ (void)swizzleAddObserver{
    //  swizzleAddObserver 只能被调用一次,可以采用更好的方法来实现这个需求
    NSAssert(!sOrigAddObserver, @"swizzleAddObserver 只能调用一次");
    // 准备混写的方法
    SEL sel = @selector(addObserver:selector:name:object:);
    // 替换方法新实现,并拿到旧方法实现
    sOrigAddObserver = (void *)[self swizzleSelector:sel withIMP:(IMP)MyAddObserver];
}
@end

NSNotificationCenter+MethodSwizzle 的swizzleAddObserver会将 addObserver:selector:name:object: 的系统默认实现替换为自己的实现 MyAddObserver,
MyAddObserver 在每次 NSNotificationCenter 添加观察者时会打印一个日志,然后再调用 addObserver:selector:name:object: 的系统默认实现。

在 ViewController 中使用

- (void)viewDidLoad {
    [super viewDidLoad];
    // 加载方法混写
    [NSNotificationCenter swizzleAddObserver];
    // 给 NSNotificationCenter 添加一个观察者
    Observer *observer = [[Observer alloc] init];
    [[NSNotificationCenter defaultCenter] addObserver:observer
                                             selector:@selector(somthingHappened:)
                                                 name:@"SomethingHappenedNotification"
                                               object:nil];
}

控制台输出

添加观察者:<Observer: 0x60800001b660>

在执行 NSNotificationCenter 添加观察者的代码的同时,控制台输出了我们预埋的日志,
可以看出 NSNotificationCenter 的 addObserver:selector:name:object: 方法实现已经被我们替换为自己的方法实现了。

总结

方法混写是一个非常强大的技术,但是用不好也会引发一些莫名其妙的 bug 。
通常方法混写技术最好不要用于线上代码,但在开发过程中对于调试,性能调优和研究系统框架实现又是有非常大的作用。

参考

本文是《iOS 编程实战》的读书笔记,对阅读的内容进行总结。当我们看懂了之后,不一定懂;我们跟着书上代码敲了一遍之后,还是不一定懂;只有我们能够把自己理解的内容写下来或者通过其它方式表达出来的时候,这个才是真的懂了;

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

推荐阅读更多精彩内容