method swizzling 总结

最近项目中需要导入新功能,说是新功能,实际上完全是一个新的APP。只不过在我们的项目中开放了一个管理中心,可以调用或者跳转到新功能中,实现对新APP的集成。通过将新功能封装成库(framework),在项目中调用。这其中就会遇到各种各样的问题。逼不得已,有些问题用到了method swizzling来解决。自己一直不强记具体怎么写,不过临时找起来也挺麻烦的,这里就做个笔记,下次方便copy。

-原理

理解method swizzling先要从method开始。在oc的世界里,很多类型实际上都是c语言中的结构体。例如说method,本质是objc_method :

typedef struct objc_method *Method;
typedef struct objc_ method {
    SEL method_name;//方法名
    char *method_types;//参数类型
    IMP method_imp;//指向方法具体实现的函数指针
};

一目了然,是由方法名,参数类型和方法的实现组成。

同样,SEL也是结构体:

typedef struct objc_selector  *SEL;

objc_selector并没有在头文件中查到具体结构,不过通过下面代码可以打印出结果:

- (NSInteger)maxIn:(NSInteger)a theOther:(NSInteger)b {
    return (a > b) ? a : b;
}
NSLog(@"SEL=%s", @selector(maxIn:theOther:));
输出:SEL=maxIn:theOther:
SEL sel1 = @selector(maxIn:theOther:);
NSLog(@"sel : %p", sel1);
输出:sel : 0x100002d72

方法的selector用于表示运行时方法的名字。Objective-C在编译时,会依据每一个方法的名字、参数序列,生成一个唯一的整型标识(Int类型的地址),这个标识就是SEL。所以在同一个类中,不能存在2个同名的方法,即使参数类型不同也不行。不同的类可以拥有相同的 selector,这个没有问题,因为不同类的实例对象performSelector相同的selector时,会在各自的消息选标(selector)/实现地址(address) 方法链表中根据selector 去查找具体的方法实现IMP,然后用这个方法实现去执行具体的实现代码。这是一个动态绑定的过程,在编译的时候,我们不知道最终会执行哪一些代码,只有在执行的时候,通过selector去查询,我们才能确定具体的执行代码。
IMP实际上是一个函数指针,指向方法实现的首地址。

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

这个函数的第一个参数是指向self的指针(如果是实例方法,则是类实例的内存地址;如果是类方法,则是指向元类的指针),第二个参数是方法选择器(selector),接下来是方法的实际参数列表,并返回一个id。也就是说 IMP 是消息最终调用的执行代码,是方法真正的实现代码 。我们可以像在C语言里面一样使用这个函数指针。
所以很容易理解objc_method结构体这么做的原因,当通过消息来调用类中某个函数时,首先会根据SEL,即方法名在对象的方法列表中查询,然后查找到对应的方法实现IMP,执行方法。method swizlling的作用,便是修改method中SEL和IMP的对应关系(IMP函数指针指向),使SEL与新的swizllingIMP对应,从而实现原先调用SEL方法名,但实际在运行时执行的使我们swizllingIMP中的代码。盗图如下,selector2和selector3实现IMP交换:

method swizzling理解图

-应用场景

比方说最开头时提到的情况,新导入的功能,我们需要对所有的新界面做出统一的皮肤修改,例如背景色设置。这时候我们只有导入功能的framework,没办法到具体代码中添加新方法,更不能试图子类化方式来统一修改。更遗憾的是,framework中并没有公开这些需要改变界面的类的头文件。所以也无法通过category的方式来做出改变。
但我们可以在运行时中,窥探出framework中的究竟。通过找到framework中,viewcontroller的基类,例如XXXBase开头的XXXBaseViewcontroller,然后利用method swizzling方式,替换viewdidappear方法的实现,加入我们队UI的额外修改。从而实现在运行时,对新功能界面背景色的修改。
除此之外,很多情况,例如对viewcontroller生命周期函数,统一添加log;对某些方法添加统一的提前判断,等等,都可以通过method swizzling方式完美解决。总而言之,method swizzling就像是一把手术刀,在原有的应用体系中切出一个横面,直达目的,做出修改。

-实现

首先实现一个具有代表性的场景:替换UIViewcontroller中的viewDidAppear:方法。

#import "UIViewController+swizzling.h"
#import @implementation UIViewController (swizzling)
+ (void)load {
    Class class = [self class];
    //取得函数名称
    SEL originSEL = @selector(viewDidAppear:);
    SEL swizzleSEL = @selector(swizzleViewDidAppear:);
    //根据函数名,从class的method list中取得对应的method结构体,如果是实例方法用class_getInstanceMethod,类方法用class_getClassMethod()。会从当前的Class中寻找对应方法名的实现,若没有则向上遍历父类中查找。若父类中也没有,则返回NULL。
    Method originMethod = class_getInstanceMethod(class, originSEL);
    Method swizzleMethod = class_getInstanceMethod(class, swizzleSEL);
    //class_addMethod向class中添加对应方法名和方法实现。如果该class(不包含父类)已含有该方法名,则返回NO。
    BOOL didAddMethod = class_addMethod(class, originSEL,     
                method_getImplementation(swizzleMethod), method_getTypeEncoding(swizzleMethod));
    if (didAddMethod) {
        //因为已向class中添加了swizzledMethod实现对应的方法名,只需要替换swizzledSelector的实现为originalMethod。
        class_replaceMethod(class,
                            swizzleSEL,
                            method_getImplementation(originMethod),
                            method_getTypeEncoding(originMethod));
    } else {
        //class中本来就含有originSEL的method,只需要交换originalMethod和swizzledMethod的实现。
        method_exchangeImplementations(originMethod,
                                       swizzleMethod);
    }
}
// 我们自己实现的方法,也就是和self的swizzleViewDidAppear方法进行交换的方法。
- (void)swizzleViewDidAppear:(BOOL)animated {
    [self swizzleViewDidAppear:animated];
}
@end

代码中有详细的注释,实现思路不再赘述。稍微说一下的是,swizzleViewDidAppear:中调用了[self swizzleViewDidAppear:animated],初学者可能会提问,说这样不是会死循环么?其实不然,因为swizzleViewDidAppear:和viewDidAppear:已经交换了实现方法,当viewcontroller的viewDidAppear:被调用时,走到了swizzleViewDidAppear:,在swizzleViewDidAppear:中只有调用[self swizzleViewDidAppear:animated],才能走到viewDidAppear:的实现方法中。
另外,swizzle方法都是会写在+ load这个类方法中。可以看一下objective-c对这个方法的解释。简单来说,oc的runtime会首先加载各个类或者类的category中的+load方法。当我们需要利用method swizzling来修改类的布局时,需要在类或者类的category的+load方法中修改,从而保证对类的布局尽早做出修改。+load的调用顺序需要注意一下:

-对于有依赖关系的两个库中,被依赖的类的load会优先调用。但在一个库之内,调用顺序是不确定的。
-对于一个类而言,没有load方法实现就不会调用,不会考虑对NSObject的继承。
-一个类的load方法不用写明[super load],父类就会收到调用,并且在子类之前
-Category的load也会收到调用,但顺序上在主类的load调用之后

-拓展 AOP编程思想运用

理解了method swizzling的原理和实现,很容易的联想到,在日志添加,事件统计等具有面向性,统一性的功能上,有很大的作用。这一类功能,如果不通过method swizzling方式,可能需要利用继承或者category,来改写或者在每个类的方法中添加重复的代码片段,即增加工作量,又破坏了原有逻辑的连贯性。这时,AOP编程思想就显得非常重要。
Aspect Oriented Programming(AOP),即面向切面编程。在利用java开发大型系统时,AOP经常会被提到,例如性能监测,访问控制,事务管理、缓存、对象池管理以及日志记录等等,这些功能也都是具有横切性的特性。利用AOP可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率。
在iOS中,AOP思想的运用没有在Java那么广泛。但method swizzling方式的运用,给了在oc世界实践AOP编程思想的可能。通过将功能横切注入,实现既不破坏原有代码整体性,又能实现添加或替换的功能。
github上第三方库Aspects已经利用method swizzling为我们提供了AOP实现的思路。Aspects中提供了两个接口:

+ (id)aspect_hookSelector:(SEL)selector
              withOptions:(AspectOptions)options
               usingBlock:(id)block
                    error:(NSError **)error;
- (id)aspect_hookSelector:(SEL)selector
              withOptions:(AspectOptions)options
               usingBlock:(id)block
                    error:(NSError **)error;

AspectOptions给了我们选择,可以将你的操作block添加在目的方法之前,之后执行,或者是直接替换。有了这两个API,我们甚至可以跟在Java中一样,通过配置文件的方式,来实现面向切面的操作。
例如我们设置dictionary格式:

    NSDictionary*config =@{
                           @"MainViewController":@{
                                   GLLoggingPageImpression:@"page imp - main page",
                                   GLLoggingTrackedEvents:@[
                                           @{
                                               GLLoggingEventName:@"button one clicked",
                                               GLLoggingEventSelectorName:@"buttonOneClicked:",
                                               GLLoggingEventHandlerBlock: ^(id aspectInfo) {
                                                   NSLog(@"button one clicked");
                                               },
                                               },
                                           @{
                                               GLLoggingEventName:@"button two clicked",
                                               GLLoggingEventSelectorName:@"buttonTwoClicked:",
                                               GLLoggingEventHandlerBlock: ^(id aspectInfo) {
                                                   NSLog(@"button two clicked");
                                               },
                                               },
                                           ],
                                   },
                           @"DetailViewController":@{
                                   GLLoggingPageImpression:@"page imp - detail page",
                                   }
                           };

然后通过解析,利用Aspects的API,实现AOP。具体实现可以参考Demo

-两面性

任何事物都是具有两面性的,method swizzling也是一样。在我们看到它极大的扩展了我们改变程序运行时的能力的同时,也很容易发现带来的副作用。

- 运行时改变,代码原有逻辑与运行时不一致,很难在review代码过程中找出可能存在的问题
- method的实现需要在运行时状态才能确定,增加了debug难度
- 大量method swizzling的运用,或者未加管理的添加,导致方式实现指向混乱
- 全局修改容易带来副作用。新导入的模块例如framework,并不需要现有项目中method swizzling的处理,甚至可能出现冲突。

所以说,method swizzling是一把利刃,如果玩不好很容易误伤自己或他人。并且你也很难保证所有人都是玩刀的好手。method swizzling要解决的难题,是否最终应该通过应用架构的设计来完善?或者它的作用范围更应该局限在架构的整体设计中,而不该在具体功能中出现。

参考链接:
iOS黑魔法-Method Swizzling
面向切面编程

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

推荐阅读更多精彩内容

  • 转至元数据结尾创建: 董潇伟,最新修改于: 十二月 23, 2016 转至元数据起始第一章:isa和Class一....
    40c0490e5268阅读 1,690评论 0 9
  • 我们常常会听说 Objective-C 是一门动态语言,那么这个「动态」表现在哪呢?我想最主要的表现就是 Obje...
    Ethan_Struggle阅读 2,182评论 0 7
  • 这篇文章完全是基于南峰子老师博客的转载 这篇文章完全是基于南峰子老师博客的转载 这篇文章完全是基于南峰子老师博客的...
    西木阅读 30,548评论 33 466
  • 继上Runtime梳理(四) 通过前面的学习,我们了解到Objective-C的动态特性:Objective-C不...
    小名一峰阅读 744评论 0 3
  • 目录 Objective-C Runtime到底是什么 Objective-C的元素认知 Runtime详解 应用...
    Ryan___阅读 1,935评论 1 3