Method Swizzling 方法欺骗

本文为大地瓜原创,欢迎知识共享,转载请注明出处。
虽然你不注明出处我也没什么精力和你计较。
作者微信号:christgreenlaw


本文的原文是Method Swizzling。本文只对其进行翻译。


方法欺骗是一个对已经存在的selector的实现进行更改的过程。由于OC的方法请求(method invocation)可以在运行时更改,这一技术是借由更改类分发表(class's dispatch table,也就是selector和函数的映射表)中selector和底层函数的映射关系而实现的。

比如说,我们想让一个iOS app中每一个展现出来的view controller都能追踪自己被展示了多少次:

每个vc都可以在自己的viewDidAppear:的实现中添加跟踪代码,但是这会产生无数的重复代码。继承也是一种实现方案,但这需要继承UiViewControllerUINavigationController,以及所有其他的vc类,这种做法也会有代码重复。

幸运的是,另一种方法是:在分类(category)中进行方法欺骗(method swizzling)。以下是实现方式:

#import <objc/runtime.h>

@implementation UIViewController (Tracking)

+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        Class class = [self class];

        SEL originalSelector = @selector(viewWillAppear:);
        SEL swizzledSelector = @selector(xxx_viewWillAppear:);

        Method originalMethod = class_getInstanceMethod(class, originalSelector);
        Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);

        // When swizzling a class method, use the following:
        // Class class = object_getClass((id)self);
        // ...
        // Method originalMethod = class_getClassMethod(class, originalSelector);
        // Method swizzledMethod = class_getClassMethod(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);
        }
    });
}

#pragma mark - Method Swizzling

- (void)xxx_viewWillAppear:(BOOL)animated {
    [self xxx_viewWillAppear:animated];
    NSLog(@"viewWillAppear: %@", self);
}

@end

现在呢,任何一个UIViewController的实例、或者其子类的实例调用viewWillAppear:时,都会打印一条日志信息。

向vc的生命周期、响应事件、视图绘制、或者是Foundation networking stack中注入行为,这些做法都是方法欺骗的优秀例子。方法欺骗的适用场景非常多,OC的开发者经验越丰富,这种使用就会越多。

我们不去理会为什么、以及在哪里使用欺诈,使用欺诈的方式永远是不变的:

+load vs. +initialize

Swizzling should always be done in +load.

OC运行时为每个类都会自动触发两个方法。+load消息在class最初加载的时候发送,而+initialize仅仅在应用程序第一次调用类上的方法或者使用类实例时调用。两个方法都是optional 的,都仅是在有实现的情况下才会调用。

由于方法欺诈影响全局状态,所以将冲突的可能性最小化就显得尤为重要。+load保证会在类初始化时调用,也就为改变全局行为提供了一定的一致性。相反的是,+initialize并不保证在什么时候执行---实际上,如果那个类永远不被app直接发送消息的话,它能永远都得不到调用。

dispatch_once

Swizzling should always be done in a dispatch_once.

需要再次强调一下,由于欺诈改变全局状态,我们需要在运行时尽可能的谨慎。原子性就是需要注意的一点,原子性保证代码仅会执行一次,即使在多线程下也是这样。GCD的dispatch_once提供了我们所需要的行为,就像在initializing singletons中一样。我们在进行方法欺诈时也应该把这个当做一个标准写法。

Selectors, Methods, & Implementations

OC中,selectors、methods、implementations都是runtime的一个特定方面,尽管在一般的描述中,这些术语通常可以互换地表示消息发送的过程。(大地瓜注:平时我们说这几个术语时一般都是指的发送消息,但实际上它们是runtime中不同的几个方面

以下是这几个术语在苹果的 Objective-C Runtime Reference 中的描述:

  • Selector (typedef struct objc_selector *SEL): selectors用于在运行时表示方法的名字。一个方法的selector是一串在OC runtime注册的C字符串。类在加载时,编译器生成的selectors自动由runtime完成匹配。
  • Method (typedef struct objc_method *Method): 一个不透明的类型,用于在类定义中代表一个方法。
  • Implementation (typedef id (*IMP)(id, SEL, ...)): 这个数据类型是一个指向实现方法的函数的起始位置的指针。这个函数使用当前CPU架构实现的标准的C调用规范。第一个参数是指向自己的指针(也就是类的特定实例的内存,或者说,对于类方法来说就是指向元类的指针)。第二个参数是method selector。接下来是method arguments。

要理解这些概念之间的关系,最好的描述方式就是:一个类(Class)维护一个分发表,以解决运行时的消息发送;表中的每条记录都是一个方法(Method),记录标志了一个特定的name,也就是the selector(SEL),指向一个实现(IMP),也就是底层C函数的指针。

要欺诈一个方法,也就是要更改一个类的分发表,用以将一个现存的selector解析到一个不同的实现上,同时将原始的method实现解析到一个新的selector上。

Invoking _cmd

下面的代码好像会引起一个无限循环:

- (void)xxx_viewWillAppear:(BOOL)animated {
    [self xxx_viewWillAppear:animated];
    NSLog(@"viewWillAppear: %@", NSStringFromClass([self class]));
}

令人惊讶的是,并不会。在欺诈的过程中,xxx_viewWillAppear:已被分配给UIViewController -viewWillAppear:原始实现。一般根据直觉来讲,在自身的实现中,给self调用一个方法会引起错误,但是在这种情况下,如果我们还记得到底是怎样调用的,这一切就解释的通了。然而,如果我们在这个方法中调用viewWillAppear:,反倒会真的引起无限循环了,因为这个方法的实现已经在运行时被欺诈给viewWillAppear:了。

一定要记得将你的欺诈方法加一个前缀,就像你创建任何其他有争议的分类方法一样。

思考

欺诈一般被认为是一种黑魔法(voodoo techique),容易产生不可预测的行为,以及不可预见的结果。虽然它并不是百分百安全,但是如果你能够注意以下问题的话,方法欺诈还是很安全的:

  • 永远要触发方法的原始实现。(除非你真的有充分的理由不这样做):API提供了输入和输出的约束(contract,本意为合同),但是内部的实现是黑箱。欺诈一个方法然后又不调用原始的实现也许会造成底层私有状态崩溃,甚至会引起你应用程序的错误。
  • 避免冲突:给分类方法加前缀(prefix category methods),
    然后一定要确保你的代码中任何其他地方都没有在你这个功能上搞事情。(and make damn well sure that nothing else in your code base(or any of your dependencies) are monkeying around with the same piece of functionality as you are)
  • 明白当前到底在干什么:单纯地复制粘贴欺诈代码而不理解院里的话是很危险的,并且也浪费了学习OC runtime的机会。读一下Objective-C Runtime Reference然后搜索<objc/runtime.h>以理解事情到底是怎么运行的。永远要努力理解,而不是只是胡想。(Always endeavor to replace magic thinking with understanding.)
  • 小心行事:不管你对欺诈Foundation、UIKit还是其他内置的framework有多大的信心,你要知道下一个版本可能代码就会崩溃。你要做好准备,进一步努力以保证玩火的时候别引火上身。

JRSwizzle 是一个牛逼的欺诈库,支持cocoapods。

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

推荐阅读更多精彩内容

  • 转至元数据结尾创建: 董潇伟,最新修改于: 十二月 23, 2016 转至元数据起始第一章:isa和Class一....
    40c0490e5268阅读 1,709评论 0 9
  • 目录 Objective-C Runtime到底是什么 Objective-C的元素认知 Runtime详解 应用...
    Ryan___阅读 1,939评论 1 3
  • 这篇文章完全是基于南峰子老师博客的转载 这篇文章完全是基于南峰子老师博客的转载 这篇文章完全是基于南峰子老师博客的...
    西木阅读 30,554评论 33 466
  • 对于从事 iOS 开发人员来说,所有的人都会答出【runtime 是运行时】什么情况下用runtime?大部分人能...
    梦夜繁星阅读 3,721评论 7 64
  • 我们常常会听说 Objective-C 是一门动态语言,那么这个「动态」表现在哪呢?我想最主要的表现就是 Obje...
    Ethan_Struggle阅读 2,192评论 0 7