iOS中Method swizzling的使用

一、Method Swizzling简介

  Method Swizzling被业内称为黑魔法、黑科技。字面意思是方法交换,其中交换的是方法的实现
具体点的来说,我们用@selector(方法选择器) 取出来的是一个方法的编号(指向方法的指针) ,用SEL类型表示;它所指向的是一个IMP(方法实现的指针) ,而我们交换的就是这个IMP,从而达到方法实现交换的效果。

  • @selector(方法名)

@selector()方法名称的描述,只取类方法的编号不记录具体的方法,具体的方法是 IMP,取出的结果是SEL类型。

(1)编译时,通过编译器指令@selector 来获取.

//定义一个类方法的指针,selector查找是当前类(包含子类)的方法
SEL aSelector = @selector(methodName);

(2)运行时,通过字符串来获取一个方法名 NSSelectorFromString

SEL aSelector = NSSelectorFromString(@"methodName");

Objective-C 数据结构中,存在一个name - selector 的映射表如图:

The Selector table

  方法以 selector 作为索引。selector的数据类型是SEL。虽然SEL 定义成 char*,我们可以把它想象成int。每个方法的名字对应一个唯一的 int 值。比如, 方法 addObject:可能 对应的是 12。 当寻找该方法时,使用的是 selector,而不是名字 @"addObject:"

在编译的时候,只要有方法的调用,编译器都会通过 selector 来查找,所以 (假设addObjectselector12)

[myObject addObject: param];

将会编译变成:

objc_msgSend(myObject, 12, param);

  这里,objc_msgSend()函数将会使用 myObjectisa 指针来找到 myObject 的类空间结构,并在类空间结构中查找 selector 12所对应的方法。如果没有找到,那么将使用指向父类的指针找到父类空间结构进行 selector 12 的查找。 如果仍然没有找到,就继续往父类的父类一 直找,直到找到为止, 如果到了根类NSObject中仍然找不到,将会抛出异常。

  • IMP:(Implementation缩写)

一个函数指针,保存了方法地址。
(1)它是指向一个方法具体实现的指针,每一个方法都有一个对应的IMP,所以,我们可以直接调用方法的IMP指针,来避免方法调用死循环的问题。

(2)当你发起一个 ObjC 消息之后,最终它会执行的那段代码,就是由IMP这个函数指针指向了这个方法实现的。
IMP的声明为:

typedef id(*IMP)(id, SEL,...);

IMP是一个函数指针,这个被指向的函数包含一个接收消息的对象id(self指针),调用方法的选标SEL(方法名),以及不定个数的方法参数,并返回一个id。也就是说IMP是消息最终调用的执行代码,是方法真正的实现代码 。我们可以像在语言里面一样使用这个函数指针。

//Method 是一个类实例,里面的结构体有一个方法选标 SEL – 表示该方法的名称,一个types – 表示该方法参数的类型,一个 IMP  - 指向该方法的具体实现的函数指针。
typedef struct objc_method *Method;
typedef struct objc_ method {
    SEL method_name;    //方法名
    char *method_types; //方法类型
    IMP method_imp;    //具体方法实施的指针
};
//获取了这个实例方法类Mehtod
Method method = class_getInstanceMethod([self class], NSSelectorFromString(@"dealloc")); 
//通过实例方法类获取对应的地址IMP
IMP classResumeIMP = method_getImplementation(method); 

二、Method Swizzling原理

Method Swizzing是发生在运行时的,主要用于在运行时将两个Method进行交换,我们可以将Method Swizzling代码写到任何地方,但是只有在这段Method Swilzzling代码执行完毕之后互换才起作用。

  • 每一个OC实例对象都保存有isa指针和实例变量,其中isa指针所属类,类维护一个运行时可接收的方法列表(MethodLists);方法列表(MethodLists)中保存selector的方法名和方法实现(IMP,指向Method实现的指针)的映射关系。在运行时,通过selecter找到匹配的IMP,从而找到的具体的实现函数。
    MethodLists图
  • 开发中可以利用Objective-C的动态特性,在运行时替换selector对应的方法实现(IMP),达到给hook的目的。下图是利用Method Swizzling来替换selector对应IMP后的方法列表示意图。
    hook后的MethodLists示意图

三、Method Swizzling的具体用途AOP(面向切面编程)

  举个例子,比如说:在所有页面添加统计功能,也就是用户进入这个页面就统计一次。

方案:

(1) 手动添加
直接简单粗暴的在每个控制器中加入统计,复制、粘贴、复制、粘贴…
上面这种方法太Low了,消耗时间而且以后非常难以维护,会让后面的开发人员骂死的。

(2)继承
我们可以使用继承的方式来解决这个问题。创建一个基类,在这个基类中添加统计方法,其他类都继承自这个基类。
然而,这种方式修改还是很大,而且定制性很差。以后有新人加入之后,都要嘱咐其继承自这个基类,所以这种方式并不可取。

(3)Category
我们可以为UIViewController建一个Category,然后在所有控制器中引入这个Category。当然我们也可以添加一个PCH文件,然后将这个Category添加到PCH文件中。

(4)Method Swizzling
我们可以使用苹果的“黑魔法”Method SwizzlingMethod Swizzling本质上就是对IMPSEL进行交换。

Method Swizzling的常见用途:

  1. 页面统计(AOP)、NSMutableArrayinsert等插入nilhook:給全局图片名称添加前缀,分类中为已有的属性或者方法添加钩子(增加一段代码)。
  2. 用于记录或者存储,比方说记录ViewController进入次数、Btn的点击事件、ViewController的停留时间等等。 可以通过Runtime获取到具体ViewControllerBtn信息,然后传给服务器。
  3. 添加需要而系统没提供的方法,比方说修改Statusbar颜色。用于轻量化、模块化处理。
  4. Method Swizzle动态给指定的方法添加代码,以解决Cross-cutting concern的编程方式叫做Aspect Oriented Programming,将逻辑处理和事件记录的代码解耦。
  5. AOP可以把琐碎的事务从主逻辑中分离出来,作为单独的模块,它是对面向对象编程模式的一种补充。
  6. 比较好的AOP库,封装了runtimeMethod Swizzling这些黑科技,该库只有两个API
+ (id<AspectToken>)aspect_hookSelector:(SEL)selector
                                  withOptions:(AspectOptions)options
                                   usingBlock:(id)block
                                        error:(NSError **)error;
- (id<AspectToken>)aspect_hookSelector:(SEL)selector
                                  withOptions:(AspectOptions)options
                                   usingBlock:(id)block
                                        error:(NSError **)error;

注意要点

  • swizzling 应该只在 +load 中完成。

  • swizzling 应该只在 dispatch_once 中完成。由于 swizzling 改变了全局的状态,所以我们需要确保每个预防措施在运行时都是可用的。原子操作就是这样一个用于确保代码只会被执行一次的预防措施,就算是在不同的线程中也能确保代码只执行一次。Grand Central Dispatchdispatch_once 满足了所需要的需求,并且应该被当做使用swizzling 的初始化单例方法的标准。

  • 尝试先调用class_addMethod方法,以保证即便originalSelector只在父类中实现,也能达到Method Swizzling的目的。

  • xxx_viewWillAppear:方法中[self xxx_viewWillAppear:animated];代码并不会造成死循环,因为Method Swizzling之后,调用[self xxx_viewWillAppear:animated];实际执行的代码已经是原来viewWillAppear中的代码了。

(1) +load的执行时机:+load 方法会在加载类的时候就被调用,也就是iOS 应用启动的时候,就会加载所有的类,main函数之前,就会调用每个类的 +load 方法

(2) 子类的+load方法会在它的所有父类(不包括父类分类中的+load)的+load方法执行之后执行
分类的+load方法会在所有的主类的+load方法执行之后执行

(3) 不同的类之间的+load方法的调用顺序是不确定的

(4) + initialize:方法类似一个懒加载,initialize是在类或者其子类的第一个方法被调用前调用,且默认只加载一次;+ initialize的调用发生在+init 方法之前。

+load+ initialize差异比较

+(void)load +(void)initialize
执行时机 在main函数之前执行 在类的方法被第一次调用时执行
若自身未定义,是否沿用父类的方法?
类别中的定义 全都执行,但后于类中的方法 覆盖类中的方法,只执行一次

swizzling代码如下

NSObject+Swizzling.m

#import "NSObject+Swizzling.h"
#import <objc/runtime.h>

@implementation NSObject (Swizzling)

/**
 交换两个对象方法的实现
 
 @param srcClass 被替换方法的类
 @param srcSel 被替换的方法编号
 @param swizzledSel 用于替换的方法编号
 */
+ (void)ff_swizzleInstanceMethodWithSrcClass:(Class)srcClass
                                      srcSel:(SEL)srcSel
                                 swizzledSel:(SEL)swizzledSel{
    
    Method srcMethod = class_getInstanceMethod(srcClass, srcSel);
    Method swizzledMethod = class_getInstanceMethod(srcClass, swizzledSel);
    if (!srcClass || !srcMethod || !swizzledMethod) return;
    
    //加一层保护措施,如果添加成功,则表示该方法不存在于本类,而是存在于父类中,不能交换父类的方法,否则父类的对象调用该方法会crash;添加失败则表示本类存在该方法
    BOOL addMethod = class_addMethod(srcClass, srcSel, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod));
    if (addMethod){
        //添加方法实现IMP成功后,再将原有的实现替换到swizzledMethod方法上,从而实现方法的交换,并且未影响到父类方法的实现
        class_replaceMethod(srcClass, swizzledSel, method_getImplementation(srcMethod), method_getTypeEncoding(srcMethod));
    }else{
        //添加失败,调用交互两个方法的实现
        method_exchangeImplementations(srcMethod, swizzledMethod);
    }
}

@end

交换例子

#import "UIViewController+swizzling.h"
#import <objc/runtime.h>
#import "NSObject+Swizzling.h"

@implementation UIViewController (swizzling)

+ (void)load {
    [super load];
    
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        Class class = [self class];
        // 原方法名和替换方法名
        SEL originalSelector = @selector(viewDidAppear:);
        SEL swizzledSelector = @selector(swizzle_viewDidAppear:);
        
        [NSObject ff_swizzleInstanceMethodWithSrcClass:class srcSel:originalSelector swizzledSel:swizzledSelector];
    });
}

/**
 页面出现的时候会进入到这里实现,即使在子类重写了viewDidAppear:方法,
 那么在调用[super viewDidAppear:animated]的时候还是会进入这里。
 */
- (void)swizzle_viewDidAppear:(BOOL)animated {
    //cmd在Objective-C的方法中表示当前方法的selector,正如同self表示当前方法调用的对象实例一样。
    NSLog(@"%@ --- %@ (IMP = UIViewController swizzle_viewDidAppear)",self, NSStringFromSelector(_cmd));
    //这里面调用自己在method swizzle交换了方法实现后就不会出现循环了
    //swizzle_viewDidAppear:方法的实现其实是调用UIViewController的viewDidAppear的原生实现方法
    [self swizzle_viewDidAppear:animated];
}

@end

这里只是简单介绍了一下正确swizzle的思路,还有很多不完善的地方,推荐大家去看RSSwizzleswizzle思路也是动态查找父类的实现,但它的实现方式十分优雅,非常值得一看。最后,Method swizzling虽然能给我们带来很多便捷,但是调式困难,以及使用不当带来的麻烦也是难以排查,所以还是谨慎使用。

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

推荐阅读更多精彩内容