Objective-C Runtime 运行时之 Method Swizzling (黑魔法)

大家也可以访问我的个人博客哟!

  1. Method Swizzling说明
    ① 原理
  2. 使用方式
    ① Swizzling应该放在+load中执行
    ② Swizzling应该放在dispatch_once中执行<br />
  3. 注意事项

Method Swizzling说明

最开始从事iOS开发的时候,曾经被问及过“黑魔法”。当时一脸( ⊙ o ⊙ )这样的表情,内心巨大的波动着:我的天呐!黑魔法!这是个什么东西!<br />

随着学习的不断深入,渐渐了解到Runtime中的Method Swizzling通常被称作是一种黑魔法。Method Swizzling翻译过来大概是方法混合,通过这一技术,我们可以在运行时通过修改类的分发表中selector对应的函数,来修改方法的实现。

原理

在OC中调用一个方法,其实是向一个对象发送一个消息。每一个类都有一个方法列表,存储着selector的名字和方法实现的映射关系。利用OC的动态特性,可以在运行时偷换selector的方法实现。

imp指向

通过 method_exchangeImplementations 可以交换两个方法的IMP;<br />
通过 method_setImplementation 可以直接设置某个方法的IMP;<br />

如下图所示:

换方法
例子

先看一个简单的小例子,在VC中的viewDidLoad中写入如下代码:

    /**
     *  获取对象方法信息  Method class_getInstanceMethod(Class cls, SEL name)
     *
     *  @param cls     类
     *  @param aMethod 对象方法
     *
     *  @return 对象方法信息 - Method类型
     */
     Method methodA = class_getInstanceMethod([self class], @selector(aMethod));
     Method methodB = class_getInstanceMethod([self class], @selector(bMethod));
     // 交换两个方法的实现
     method_exchangeImplementations(methodA, methodB);
     
     // 添加一个按钮
     UIButton *btn = [UIButton buttonWithType:UIButtonTypeCustom];
     btn.frame = CGRectMake(70, 100, 170, 30);
     [btn setTitle:@"点击触发A方法" forState:UIControlStateNormal];
     [btn addTarget:self action:@selector(aMethod) forControlEvents:UIControlEventTouchUpInside];
     [btn setBackgroundColor:[UIColor purpleColor]];
     [self.view addSubview:btn];

并在VC中添加方法 aMethod 和 bMethod。

     - (void)aMethod{
         NSLog(@"a方法");
     }

     - (void)bMethod{
        NSLog(@"b方法");
     }

我们给按钮添加一个点击方法,触发的是aMethod,理论上控制台应该打印的是 “a方法”。但是我们在上面已经偷偷的将 aMethod 的实现和 bMethod 的实现。所以控制台打印出的是 “b方法”。效果如下图:

换方法效果

使用方式

Swizzling应该放在+load中执行

在Objective-C中,运行时会自动调用每个类的两个方法。+load会在类初始加载时调用,+initialize会在第一次调用类的类方法或实例方法之前被调用。这两个方法是可选的,且只有在实现了它们时才会被调用。由于method swizzling会影响到类的全局状态,因此要尽量避免在并发处理中出现竞争的情况。+load能保证在类的初始化过程中被加载,并保证这种改变应用级别的行为的一致性。相比之下,+initialize在其执行时不提供这种保证–事实上,如果在应用中没为给这个类发送消息,则它可能永远不会被调用。

Swizzling应该放在dispatch_once中执行

与上面相同,因为swizzling会改变全局状态,所以我们需要在运行时采取一些预防措施。原子性就是这样一种措施,它确保代码只被执行一次,不管有多少个线程。GCD的dispatch_once可以确保这种行为,我们应该将其作为method swizzling的最佳实践。

例子

我们想跟踪在程序中每一个view controller展示给用户的次数:当然,我们可以在每个view controller的viewDidAppear中添加跟踪代码;但是这太过麻烦,需要在每个view controller中写重复的代码。创建一个子类可能是一种实现方式,但需要同时创建UIViewController, UITableViewController, UINavigationController及其它UIKit中view controller的子类,这同样会产生许多重复的代码。这种情况下,我们就可以使用Method Swizzling,如在代码所示:

     #import <objc/runtime.h>

     @implementation UIViewController (MethodSwizzling)

     + (void)load{

         static dispatch_once_t onceToken;
         dispatch_once(&onceToken, ^{
    
             Class class = [self class];
    
             SEL originalSelector = @selector(viewWillAppear:);
    
             SEL swizzleSelector = @selector(lt_viewWillAppear:);
    
             Method originalMethod = class_getInstanceMethod(class, originalSelector);
    
             Method swizzleMethod = class_getInstanceMethod(class, swizzleSelector);
    
             BOOL addMethod = class_addMethod(class, originalSelector, method_getImplementation(swizzleMethod), method_getTypeEncoding(swizzleMethod));
    
             if (addMethod) {
        
                     class_replaceMethod(class, swizzleSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod));
        
             } else {
        
                      method_exchangeImplementations(originalMethod, swizzleMethod);
        
             }
    
             });

     }

     - (void)lt_viewWillAppear:(BOOL)animated{

         [self lt_viewWillAppear:animated];

        NSLog(@"viewWillAppear: %@", self);

     }

在这里,我们通过method swizzling修改了UIViewController的@selector(viewWillAppear:)对应的函数指针,使其实现指向了我们自定义的lt_viewWillAppear的实现。这样,当UIViewController及其子类的对象调用viewWillAppear时,都会打印一条日志信息。

上面的例子很好地展示了使用method swizzling来一个类中注入一些我们新的操作。当然,还有许多场景可以使用method swizzling,在此不多举例。

选择器、方法与实现

在Objective-C中,选择器(selector)、方法(method)和实现(implementation)是运行时中一个特殊点,虽然在一般情况下,这些术语更多的是用在消息发送的过程描述中。

以下是Objective-C Runtime Reference中的对这几个术语一些描述:

  1. Selector(typedef struct objc_selector *SEL):用于在运行时中表示一个方法的名称。一个方法选择器是一个C字符串,它是在Objective-C运行时被注册的。选择器由编译器生成,并且在类被加载时由运行时自动做映射操作。
  2. Method(typedef struct objc_method *Method):在类定义中表示方法的类型
  3. Implementation(typedef id (*IMP)(id, SEL, ...)):这是一个指针类型,指向方法实现函数的开始位置。这个函数使用为当前CPU架构实现的标准C调用规范。每一个参数是指向对象自身的指针(self),第二个参数是方法选择器。然后是方法的实际参数。
    理解这几个术语之间的关系最好的方式是:一个类维护一个运行时可接收的消息分发表;分发表中的每个入口是一个方法(Method),其中key是一个特定名称,即选择器(SEL),其对应一个实现(IMP),即指向底层C函数的指针。

为了swizzle一个方法,我们可以在分发表中将一个方法的现有的选择器映射到不同的实现,而将该选择器对应的原始实现关联到一个新的选择器中。

调用_cmd

我们回过头来看看前面新的方法的实现代码:

    - (void)lt_viewWillAppear:(BOOL)animated{

        [self lt_viewWillAppear:animated];

        NSLog(@"viewWillAppear: %@", self);

    }

咋看上去是会导致无限循环的。但令人惊奇的是,并没有出现这种情况。在swizzling的过程中,方法中的[self lt_viewWillAppear:animated]已经被重新指定到UIViewController类的-viewWillAppear:中。在这种情况下,不会产生无限循环。不过如果我们调用的是[self viewWillAppear:animated],则会产生无限循环,因为这个方法的实现在运行时已经被重新指定为xxx_viewWillAppear:了。

注意事项

Swizzling通常被称作是一种黑魔法,容易产生不可预知的行为和无法预见的后果。虽然它不是最安全的,但如果遵从以下几点预防措施的话,还是比较安全的:

  1. 总是调用方法的原始实现(除非有更好的理由不这么做):API提供了一个输入与输出约定,但其内部实现是一个黑盒。Swizzle一个方法而不调用原始实现可能会打破私有状态底层操作,从而影响到程序的其它部分。
  2. 避免冲突:给自定义的分类方法加前缀,从而使其与所依赖的代码库不会存在命名冲突。
  3. 明白是怎么回事:简单地拷贝粘贴swizzle代码而不理解它是如何工作的,不仅危险,而且会浪费学习Objective-C运行时的机会。阅读Objective-C Runtime Reference和查看<objc/runtime.h>头文件以了解事件是如何发生的。
  4. 小心操作:无论我们对Foundation, UIKit或其它内建框架执行Swizzle操作抱有多大信心,需要知道在下一版本中许多事可能会不一样。

本文是基于南峰子的技术博客文章的整理,原文链接地址:�http://southpeak.github.io/2014/11/06/objective-c-runtime-4/

感谢原作者的贡献!

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

推荐阅读更多精彩内容