Objective-C的hook方案(一):  Method Swizzling

转自:http://blog.csdn.net/yiyaaixuexi/article/details/9374411

在没有一个类的实现源码的情况下,想改变其中一个方法的实现,除了继承它重写、和借助类别重名方法暴力抢先之外,还有更加灵活的方法吗?在Objective-C编程中,如何实现hook呢?标题有点大,计划分几篇来总结。

本文主要介绍针对selector的hook,主角被标题剧透了———— Method Swizzling 。

Method Swizzling 原理

在Objective-C中调用一个方法,其实是向一个对象发送消息,查找消息的唯一依据是selector的名字。利用Objective-C的动态特性,可以实现在运行时偷换selector对应的方法实现,达到给方法挂钩的目的。

每个类都有一个方法列表,存放着selector的名字和方法实现的映射关系。IMP有点类似函数指针,指向具体的Method实现。

我们可以利用 method_exchangeImplementations 来交换2个方法中的IMP,

我们可以利用 class_replaceMethod 来修改类,

我们可以利用 method_setImplementation 来直接设置某个方法的IMP,
……

归根结底,都是偷换了selector的IMP,如下图所示:

Method Swizzling 实践

举个例子好了,我想钩一下NSArray的lastObject 方法,只需两个步骤。

第一步:给NSArray加一个我自己的lastObject

  1. #import "NSArray+Swizzle.h"
  2. @implementation NSArray (Swizzle)
  3. - (id)myLastObject
  4. {
  5. id ret = [self myLastObject];
  6. NSLog(@"**********  myLastObject *********** ");
  7. return ret;
  8. }
  9. @end

乍一看,这不递归了么?别忘记这是我们准备调换IMP的selector,[self myLastObject] 将会执行真的 [self lastObject] 。

第二步:调换IMP

  1. #import <objc/runtime.h>
  2. #import "NSArray+Swizzle.h"
  3. int main(int argc, char *argv[])
  4. {
  5. @autoreleasepool {
  6. Method ori_Method =  class_getInstanceMethod([NSArray class], @selector(lastObject));
  7. Method my_Method = class_getInstanceMethod([NSArray class], @selector(myLastObject));
  8. method_exchangeImplementations(ori_Method, my_Method);
  9. NSArray *array = @[@"0",@"1",@"2",@"3"];
  10. NSString *string = [array lastObject];
  11. NSLog(@"TEST RESULT : %@",string);
  12. return 0;
  13. }
  14. }

控制台输出Log:

  1. 2013-07-18 16:26:12.585 Hook[1740:c07] **********  myLastObject ***********
  2. 2013-07-18 16:26:12.589 Hook[1740:c07] TEST RESULT : 3

结果很让人欣喜,是不是忍不住想给UIWebView的loadRequest: 加 TODO 了呢?

Method Swizzling 的封装

之前在github上找到的RNSwizzle,推荐给大家,可以搜一下。

  1. //
  2. //  RNSwizzle.m
  3. //  MethodSwizzle
  4. #import "RNSwizzle.h"
  5. #import <objc/runtime.h>
  6. @implementation NSObject (RNSwizzle)
  7. + (IMP)swizzleSelector:(SEL)origSelector
  8. withIMP:(IMP)newIMP {
  9. Class class = [self class];
  10. Method origMethod = class_getInstanceMethod(class,
  11. origSelector);
  12. IMP origIMP = method_getImplementation(origMethod);
  13. if(!class_addMethod(self, origSelector, newIMP,
  14. method_getTypeEncoding(origMethod)))
  15. {
  16. method_setImplementation(origMethod, newIMP);
  17. }
  18. return origIMP;
  19. }
  20. @end

Method Swizzling 危险不危险

针对这个问题,我在stackoverflow上看到了满意的答案,这里翻译一下,总结记录在本文中,以示分享:

使用 Method Swizzling 编程就好比切菜时使用锋利的刀,一些人因为担心切到自己所以害怕锋利的刀具,可是事实上,使用钝刀往往更容易出事,而利刀更为安全。
Method swizzling 可以帮助我们写出更好的,更高效的,易维护的代码。但是如果滥用它,也将会导致难以排查的bug。

背景

好比设计模式,如果我们摸清了一个模式的门道,使用该模式与否我们自己心里有数。单例模式就是一个很好的例子,它饱受争议但是许多人依旧使用它。Method Swizzling也是一样,一旦你真正理解它的优势和弊端,使用它与否你应该就有你自己的观点。

讨论

这里是一些 Method Swizzling的陷阱:

  • Method swizzling is not atomic
  • Changes behavior of un-owned code
  • Possible naming conflicts
  • Swizzling changes the method's arguments
  • The order of swizzles matters
  • Difficult to understand (looks recursive)
  • Difficult to debug

我将逐一分析这些点,增进对Method Swizzling的理解的同时,并搞懂如何应对。

Method swizzling is not atomic

我所见过的使用method swizzling实现的方法在并发使用时基本都是
安全的。95%的情况里这都不会是个问题。通常你替换一个方法的实现,是希望它在整个程序的生命周期里有效的。也就是说,你会把 method
swizzling 修改方法实现的操作放在一个加号方法 +(void)load里,并在应用程序的一开始就调用执行。你将不会碰到并发问题。假如你在
+(void)initialize初始化方法中进行swizzle,那么……rumtime可能死于一个诡异的状态。

Changes behavior of un-owned code


是swizzling的一个问题。我们的目标是改变某些代码。swizzling方法是一件灰常灰常重要的事,当你不只是对一个NSButton类的实例
进行了修改,而是程序中所有的NSButton实例。因此在swizzling时应该多加小心,但也不用总是去刻意避免。

想象一下,如果
你重写了一个类的方法,而且没有调用父类的这个方法,这可能会引起问题。大多数情况下,父类方法期望会被调用(至少文档是这样说的)。如果你在
swizzling实现中也这样做了,这会避免大部分问题。还是调用原始实现吧,如若不然,你会费很大力气去考虑代码的安全问题。

Possible naming conflicts

命名冲突贯穿整个Cocoa的问题. 我们常常在类名和类别方法名前加上前缀。不幸的是,命名冲突仍是个折磨。但是swizzling其实也不必过多考虑这个问题。我们只需要在原始方法命名前做小小的改动来命名就好,比如通常我们这样命名:

  1. @interface NSView : NSObject
  2. - (void)setFrame:(NSRect)frame;
  3. @end
  4. @implementation NSView (MyViewAdditions)
  5. - (void)my_setFrame:(NSRect)frame {
  6. // do custom work
  7. [self my_setFrame:frame];
  8. }
  9. + (void)load {
  10. [self swizzle:@selector(setFrame:) with:@selector(my_setFrame:)];
  11. }
  12. @end

这段代码运行正确,但是如果my_setFrame: 在别处被定义了会发生什么呢?

这个问题不仅仅存在于swizzling,这里有一个替代的变通方法:

  1. @implementation NSView (MyViewAdditions)
  2. static void MySetFrame(id self, SEL _cmd, NSRect frame);
  3. static void (*SetFrameIMP)(id self, SEL _cmd, NSRect frame);
  4. static void MySetFrame(id self, SEL _cmd, NSRect frame) {
  5. // do custom work
  6. SetFrameIMP(self, _cmd, frame);
  7. }
  8. + (void)load {
  9. [self swizzle:@selector(setFrame:) with:(IMP)MySetFrame store:(IMP *)&SetFrameIMP];
  10. }
  11. @end

看起来不那么Objectice-C了(用了函数指针),这样避免了selector的命名冲突。

最后给出一个较完美的swizzle方法的定义:

  1. typedef IMP *IMPPointer;
  2. BOOL class_swizzleMethodAndStore(Class class, SEL original, IMP replacement, IMPPointer store) {
  3. IMP imp = NULL;
  4. Method method = class_getInstanceMethod(class, original);
  5. if (method) {
  6. const char *type = method_getTypeEncoding(method);
  7. imp = class_replaceMethod(class, original, replacement, type);
  8. if (!imp) {
  9. imp = method_getImplementation(method);
  10. }
  11. }
  12. if (imp && store) { *store = imp; }
  13. return (imp != NULL);
  14. }
  15. @implementation NSObject (FRRuntimeAdditions)
  16. + (BOOL)swizzle:(SEL)original with:(IMP)replacement store:(IMPPointer)store {
  17. return class_swizzleMethodAndStore(self, original, replacement, store);
  18. }
  19. @end

Swizzling changes the method's arguments

我认为这是最大的问题。想正常调用method swizzling 将会是个问题。

  1. [self my_setFrame:frame];

直接调用my_setFrame: , runtime做的是

  1. objc_msgSend(self, @selector(my_setFrame:), frame);

runtime去寻找my_setFrame:的方法实现, _cmd参数为 my_setFrame: ,但是事实上runtime找到的方法实现是原始的 setFrame: 的。

一个简单的解决办法:使用上面介绍的swizzling定义。

The order of swizzles matters

多个swizzle方法的执行顺序也需要注意。假设 setFrame: 只定义在NSView中,想像一下按照下面的顺序执行:

  1. [NSButton swizzle:@selector(setFrame:) with:@selector(my_buttonSetFrame:)];
  2. [NSControl swizzle:@selector(setFrame:) with:@selector(my_controlSetFrame:)];
  3. [NSView swizzle:@selector(setFrame:) with:@selector(my_viewSetFrame:)];

What
happens when the method on NSButton is swizzled? Well most swizzling
will ensure that it's not replacing the implementation of setFrame: for
all views, so it will pull up the instance method. This will use the
existing implementation to re-define setFrame: in the NSButton class so
that exchanging implementations doesn't affect all views. The existing
implementation is the one defined on NSView. The same thing will happen
when swizzling on NSControl (again using the NSView implementation).

When
you call setFrame: on a button, it will therefore call your swizzled
method, and then jump straight to the setFrame: method originally
defined on NSView. The NSControl and NSView swizzled implementations
will not be called.

But what if the order were:

  1. [NSView swizzle:@selector(setFrame:) with:@selector(my_viewSetFrame:)];
  2. [NSControl swizzle:@selector(setFrame:) with:@selector(my_controlSetFrame:)];
  3. [NSButton swizzle:@selector(setFrame:) with:@selector(my_buttonSetFrame:)];

Since
the view swizzling takes place first, the control swizzling will be
able to pull up the right method. Likewise, since the control swizzling
was before the button swizzling, the button will pull up the control's
swizzled implementation of setFrame:. This is a bit confusing, but this
is the correct order. How can we ensure this order of things?

Again,
just use load to swizzle things. If you swizzle in load and you only
make changes to the class being loaded, you'll be safe. The load method
guarantees that the super class load method will be called before any
subclasses. We'll get the exact right order!

这段贴了原文,硬翻译太拗口……总结一下就是:多个有继承关系的类的对象swizzle时,从子类对象开始 。 如果先swizzle父类对象,那么后面子类对象swizzle时就无法拿到真正的原始方法实现了。 

(感谢评论中 qq373127202 的提醒,在此更正一下,十分感谢)

多个有继承关系的类的对象swizzle时,先从父对象开始。 这样才能保证子类方法拿到父类中的被swizzle的实现。在+(void)load中swizzle不会出错,就是因为load类方法会默认从父类开始调用。

Difficult to understand (looks recursive)

(新方法的实现)看起来像递归,但是看看上面已经给出的 swizzling 封装方法, 使用起来就很易读懂.
这个问题是已完全解决的了!

Difficult to debug

debug时打出的backtrace,其中掺杂着被swizzle的方法名,一团糟啊!上面介绍的swizzle方案,使backtrace中打印出的方法名还是很清晰的。但仍然很难去debug,因为很难记住swizzling影响过什么。给你的代码写好文档(即使只有你一个人会看到)。养成一个好习惯,不会比调试多线程问题还难的。

结论

如果使用恰当,Method swizzling 还是很安全的.一个简单安全的方法是,仅在load中swizzle。 和许多其他东西一样,它也是有危险性的,但理解它了也就可以正确恰当的使用它了。

Objective-C的hook方案(一): Method Swizzling的更多相关文章

  1. 理解Objective-C Runtime(四)Method Swizzling

    Objective-C对象收到消息之后,究竟会调用何种方法需要在运行期间才能解析出来.那你也许会问:与给定的选择子名称相应的方法是不是也可以在runtime改变呢?没错,就是这样.若能善用此特性,则可 ...

  2. iOS 使用Method Swizzling隐藏Status Bar

    在iOS 6中,隐藏Status Bar很的简单. // iOS 6及曾经,隐藏状态栏 [[UIApplication sharedApplication] setStatusBarHidden:YE ...

  3. 【原】iOS动态性(三) Method Swizzling以及AOP编程:在运行时进行代码注入

    概述 今天我们主要讨论iOS runtime中的一种黑色技术,称为Method Swizzling.字面上理解Method Swizzling可能比较晦涩难懂,毕竟不是中文,不过你可以理解为“移花接木 ...

  4. Method Swizzling

    学习博客: http://www.cocoachina.com/ios/20160121/15076.html (这个作者太牛了,写了我一直想知道的类簇的swizz方法) 一. 一般的swizz 先给 ...

  5. iOS中AOP与Method Swizzling 项目中的应用

    引子:项目中需要对按钮点击事件进行统计分析,现在项目中就是在按钮的响应代码中添加点击事件,非常繁琐.所以使用了AOP(面向切面编程),将统计的业务逻辑统一抽离出来. 项目中添加的开源库:https:/ ...

  6. Method Swizzling和AOP(面向切面编程)实践

    Method Swizzling和AOP(面向切面编程)实践 参考: http://www.cocoachina.com/ios/20150120/10959.html 上一篇介绍了 Objectiv ...

  7. (方法调配)Method Swizzling

    一.概念 方法调配:因为Objective-C是运行时语言,也就是说究竟会调用何种方法要在运行期才能解析出来.那么我们其实也可以在运行时改变选择子名称.这样我们既不需要查看到源代码,又没有必要去重写子 ...

  8. Method Swizzling 和 AOP 实践(转)

    上一篇介绍了 Objective-C Messaging.利用 Objective-C 的 Runtime 特性,我们可以给语言做扩展,帮助解决项目开发中的一些设计和技术问题.这一篇,我们来探索一些利 ...

  9. 分享使用method swizzling的经历

    原文:http://www.cnblogs.com/lujianwenance/p/5800232.html   这是一个很蛋疼的过程,先说一下需求,列表页预加载更多(60%).当我看到这个需求的时候 ...

随机推荐

  1. [mysqldumpslow 报错] Died at /usr/local/mysql/bin/mysqldumpslow line 161, &lt;&gt; chunk 236.

    mysqldumpslow报错:Died at /usr/local/mysql/bin/mysqldumpslow line 161, <> chunk 236. 总结:是由于top数目 ...

  2. IOS-时间与字符串互相转换

    有时会遇到这种问题,须要把时间和时间戳互相转换 比方把"这种格式 或者是把""转换成"2014-07-16 15:54:36" 首先来第一个: 当前时 ...

  3. [置顶] hdu2815 扩展Baby step,Giant step入门

    题意:求满足a^x=b(mod n)的最小的整数x. 分析:很多地方写到n是素数的时候可以用Baby step,Giant step, 其实研究过Baby step,Giant step算法以后,你会 ...

  4. Codeforces Round #256 (Div. 2/C)/Codeforces448C_Painting Fence(分治)

    解题报告 给篱笆上色,要求步骤最少,篱笆怎么上色应该懂吧,.,刷子能够在横着和竖着刷,不能跳着刷,,, 假设是竖着刷,应当是篱笆的条数,横着刷的话.就是刷完最短木板的长度,再接着考虑没有刷的木板,,. ...

  5. js——DOM操作(二)

    表格属性: tHead:表格头 tBodies:表格正文 tFoot:表格尾 rows:行 cells:列 表单操作: <form id="form1"> <in ...

  6. 教你wamp下多域名如何配置

    wamp下多域名配置问题 1.找到wamp安装目录的apache安装目录 找到 httpd.conf文件 例如我安装的目录为 E:\wamp\bin\apache\apache2.2.8\conf\h ...

  7. gets()函数

    基本信息: 可以无限读取,不会判断上限,以回车结束读取(这个换行符也被读取了),所以程序员应该确保buffer的空间足够大,以便在执行读操作时不发生溢出. 函数原型: char*gets(char*b ...

  8. Linux学习之echo命令

    语法: # echo [Options] [String] 方括号中的项目是可选的.字符串可以定义为字符的有限序列(如字母,数字,符号,标点符号). 当echo命令不带任何选项或字符串使用时,它会在显 ...

  9. iOS的推送机制APNs:本地推送&远程推送

    本地推送: 本地推送主要应用在备忘录,闹钟等本地的,基于时间定时的消息提醒.本篇不做详细描述. 远程推送:APNS(苹果推送通知服务) iOS远程推送机制的原理及流程: 注册推送(橙色部分):若该Ap ...

  10. Android RelativeLayout常用属性介绍

    下面介绍一下RelativeLayout用到的一些重要的属性: 第一类:属性值为true或false android:layout_centerHrizontal 水平居中 android:layou ...