Objective-C运行时编程 - 方法混写 Method Swizzling
2015-03-09 06:09
239 查看
摘要: 本文描述方法混写对实例、类、父类、不存在的方法等情况处理,属于Objective-C(oc)运行时(runtime)编程范围。
编程环境:Xcode 6.1.1, Yosemite,iOS 8.1.3。
关键字:方法混写(Method Swizzling) oc运行时
什么是方法混写
编程案例
本类方法
不存在的方法
注意事项
参考
发布自米高 | Michael - 博客园,原文地址:http://www.cnblogs.com/michaellfx/p/4322666.html,转载请注明。
我们在ViewController类中定义一个方法
生成的汇编代码如下
.前缀的命令非ARM汇编,整理上面代码后,如下所示
本篇不讲ARM汇编,故在此作个简单说明。函数调用时,ARM使用r0、r1、r2、r3四个寄存器传递参数,上述代码没改变r0,即r0为默认值self。移动r1三个指令可看作r1 = hello。由于
定义完成业务的新方法
定义方法签名SEL,由
获取方法定义Method,由
交换二者实现,由
先演示混写NSString的实例方法
只要调用
如果你觉得这样会死循环,可以试着把
另一个问题是,混写这种带返回值的方法,不能简单的在实现中返回原方法的调用,如
作为拓展方法,一般的做法是加上前缀,以便区分拓展方法与原方法,出问题也能快速找到源头。
混写类方法与实例代码几乎一样,除了获取方法定义部分将
由于
在混写可变集合如NSMutableArray时有个注意点。不可变的集合如NSArray可使用前面的办法直接混写其方法,但是可变集合如NSMutableArray虽然从定义上看是NSMutableArray,但是通过NSMutableArray实例的class属性打印出来的类型却是
由名称获取类的Class结构有三种方式:
Class class = [NSMutableArray array].class;
Class class = NSClassFromString(@”__NSArrayM”);
Class class = objc_getClass(“__NSArrayM”);
再补充一点,NSArray是一种特殊的类,英文叫做Class cluster,中文翻译过来是类簇,在设计模式中,这个叫做工厂类,它在外层提供了很多方法接口,但是这些方法的实现是由具体的内部类来实现的。当使用NSArray生成一个对象时,初始化方法会判断哪个“自己内部的类”最适合生成这个对象,然后这个“工厂”就会生成这个具体的类对象返回给你。这种又外层类提供统一抽象的接口,然后具体实现让隐藏的,具体的内部类来实现,在设计模式中称为“抽象工厂”模式。
也就是说,对于普通的类,我们使用上述方法是没有问题的,然而对于Class cluster这种工厂类,则需要找到它的真正类型才行。
方法混写的影响是全局的,且只限于本应用运行期间。这点可以通过创建两个应用,一个混写,另一个不混写,两者调用同一个被混写的方法,可发现,不混写的应用并没得到混写的结果。
若在
因为方法混写的影响是全局的,要避免在并发处理中出现竞争情况,所以一般在
综上,最佳编程实践是在
有些人不建议使用方法混写,而建议实现一个新方法,因为混写后,会对整个工程造成影响,团队成员若不知道你混写的特殊操作,可能会对他们的处理有影响。但是,这也有一个问题,即是,若不混写,无法保证团队成员会主动调用特殊处理的新方法。
Objective-C Class Loading and Initialization
编程环境:Xcode 6.1.1, Yosemite,iOS 8.1.3。
关键字:方法混写(Method Swizzling) oc运行时
本文结构
修订版本什么是方法混写
编程案例
本类方法
不存在的方法
注意事项
参考
发布自米高 | Michael - 博客园,原文地址:http://www.cnblogs.com/michaellfx/p/4322666.html,转载请注明。
修订版本[/b]
1.0 草稿什么是方法混写[/b]
方法混写(Method Swizzling)即是运行期间改变objective-c方法实现的一种手段。以[对象 操作]方式向对象发送消息在编译期间只是被编译器处理成objc_msgSend系列函数的调用如
objc_msgSend(对象, 操作, 其余参数),至于对象是否能响应此消息以及如何响应,编译期间是无法确定的,此行为推迟到运行期间确认,且运行期间还可做更多处理,如本文介绍的方法混写。这里展开下objc_msgSend问题,不感兴趣可跳过,不影响理解后面内容。
我们在ViewController类中定义一个方法
hello,然后在
viewDidLoad中调用,示例代码为
ViewController.m ////////////////////////////////// - (void)hello { // 什么都不做,只是演示 } - (void)viewDidLoad { // 简单起见,不调用父类方法 [self hello]; }
生成的汇编代码如下
//////////////// viewDidLoad部分 ////////////////////////////// "-[ViewController viewDidLoad]": Lfunc_begin6: .loc 1 45 0 @ /Users/michael/Developer/sss/sss/ViewController.m:45:0 .cfi_startproc @ BB#0: @DEBUG_VALUE: -[ViewController viewDidLoad]:self <- R0 @DEBUG_VALUE: -[ViewController viewDidLoad]:_cmd <- R1 .loc 1 48 0 prologue_end @ /Users/michael/Developer/sss/sss/ViewController.m:48:0 movw r1, :lower16:(L_OBJC_SELECTOR_REFERENCES_8-(LPC6_0+4)) Ltmp35: movt r1, :upper16:(L_OBJC_SELECTOR_REFERENCES_8-(LPC6_0+4)) LPC6_0: add r1, pc ldr r1, [r1] b.w _objc_msgSend Ltmp36: Lfunc_end6: .cfi_endproc //////////////// L_OBJC_SELECTOR_REFERENCES_8部分 ////////////////////////////// L_OBJC_SELECTOR_REFERENCES_8: .long L_OBJC_METH_VAR_NAME_7 .private_extern _OBJC_CLASS_$_ViewController @ @"OBJC_CLASS_$_ViewController" .section __DATA,__objc_data .globl _OBJC_CLASS_$_ViewController .align 2 //////////////// L_OBJC_SELECTOR_REFERENCES_8部分 ////////////////////////////// L_OBJC_METH_VAR_NAME_7: @ @"\01L_OBJC_METH_VAR_NAME_7" .asciz "hello" .section __DATA,__objc_selrefs,literal_pointers,no_dead_strip .align 2 @ @"\01L_OBJC_SELECTOR_REFERENCES_8"
.前缀的命令非ARM汇编,整理上面代码后,如下所示
"-[ViewController viewDidLoad]": movw r1, :lower16:(L_OBJC_SELECTOR_REFERENCES_8-(LPC6_0+4)) movt r1, :upper16:(L_OBJC_SELECTOR_REFERENCES_8-(LPC6_0+4)) add r1, pc ldr r1, [r1] b.w _objc_msgSend
本篇不讲ARM汇编,故在此作个简单说明。函数调用时,ARM使用r0、r1、r2、r3四个寄存器传递参数,上述代码没改变r0,即r0为默认值self。移动r1三个指令可看作r1 = hello。由于
hello方法无参数,则只需传递两个隐藏参数:self、SEL(hello)即可。现在,开始介绍方法混写。
编程案例[/b]
本节先混写NSString中的实例方法、类及父类方法,然后说明NSMutableString及NSMutableArray等可变类型的混写处理,最后介绍处理不存在的方法混写。1.本类方法[/b]
混写已存在的实例方法或类方法的做法是类似的,步骤为:定义完成业务的新方法
定义方法签名SEL,由
@selector编译器指令来获取
获取方法定义Method,由
class_getInstanceMethod得到实例方法定义 或
class_getClassMethod得到类方法定义
交换二者实现,由
method_exchangeImplementations(旧方法,新方法)完成。
method_setImplementation方法也可完成替换,但是要调用两次,不如
method_exchangeImplementations(旧方法,新方法)方便。
先演示混写NSString的实例方法
@implementation NSString (MethodSwizzling) + (void)load { static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ SEL srcSel = @selector(isEqualToString:); SEL dstSel = @selector(ms_isEqualToString:); Method srcMethod = class_getInstanceMethod(self, srcSel); Method dstMethod = class_getInstanceMethod(self, dstSel); method_exchangeImplementations(srcMethod, dstMethod); }); } - (BOOL)ms_isEqualToString:(NSString *)aString { NSLog(@"ms_isEqualToString"); BOOL result = [self ms_isEqualToString:aString]; return result; } @end
只要调用
isEqualToString:则会进入
ms_isEqualToString:。由于在
load方法中交换了
isEqualToString:、
ms_isEqualToString:实现,则在实现混写时内部还是调用
ms_isEqualToString:这是可能令人感到疑惑的地方。
如果你觉得这样会死循环,可以试着把
BOOL result = [self ms_isEqualToString:aString];替换为
BOOL result = [self isEqualToString:aString];。当你真这么做了,在运行期间,方法一交换,则
isEqualToString:aString:为
ms_isEqualToString:,这真会死循环的。
另一个问题是,混写这种带返回值的方法,不能简单的在实现中返回原方法的调用,如
return [self ms_isEqualToString:aString];这样将导致死循环。应该使用一个变量接受原方法的返回值,再将此值返回给调用者。
作为拓展方法,一般的做法是加上前缀,以便区分拓展方法与原方法,出问题也能快速找到源头。
混写类方法与实例代码几乎一样,除了获取方法定义部分将
class_getInstanceMethod函数换成
class_getClassMethod,示例代码如下
@implementation NSMutableString (MethodSwizzling) + (void)load { static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ SEL srcSel = @selector(stringWithCapacity:); SEL dstSel = @selector(ms_stringWithCapacity:); Method srcMethod = class_getClassMethod(self, srcSel); Method dstMethod = class_getClassMethod(self, dstSel); method_exchangeImplementations(srcMethod, dstMethod); }); } + (NSMutableString *)ms_stringWithCapacity:(NSUInteger)capacity { NSLog(@"ms_stringWithCapacity"); NSMutableString *string = [self ms_stringWithCapacity:capacity]; return string; } @end
由于
class_getInstanceMethod、
class_getClassMethod会沿继承链向上搜索直至根类NSObject(假设是NSObject继承体系),则混写父类已存在的实例方法和类方法的代码和前面的例子完全一样。
在混写可变集合如NSMutableArray时有个注意点。不可变的集合如NSArray可使用前面的办法直接混写其方法,但是可变集合如NSMutableArray虽然从定义上看是NSMutableArray,但是通过NSMutableArray实例的class属性打印出来的类型却是
__NSArrayM。类型与我们混写时定义的分类不同,则直接混写无效。正确的做法是通过
Class class = NSClassFromString(@"__NSArrayM");获取其类型,然后开始混写,示例如下
@implementation NSMutableArray (MethodSwizzling) + (void)load { static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ Class class = NSClassFromString(@"__NSArrayM"); SEL srcSel = @selector(addObject:); SEL dstSel = @selector(ms_addObject:); Method srcMethod = class_getInstanceMethod(class, srcSel); Method dstMethod = class_getInstanceMethod(class, dstSel); method_exchangeImplementations(srcMethod, dstMethod); }); } - (void)ms_addObject:(id)anObject { NSLog(@"ms_addObject"); [self ms_addObject:anObject]; } @end
由名称获取类的Class结构有三种方式:
Class class = [NSMutableArray array].class;
Class class = NSClassFromString(@”__NSArrayM”);
Class class = objc_getClass(“__NSArrayM”);
objc_getClass在ARC环境下有警告。第一种方式有编译时检查,不容易出现拼写问题,相对安全些。本文为方便起见,使用第二种方式。
再补充一点,NSArray是一种特殊的类,英文叫做Class cluster,中文翻译过来是类簇,在设计模式中,这个叫做工厂类,它在外层提供了很多方法接口,但是这些方法的实现是由具体的内部类来实现的。当使用NSArray生成一个对象时,初始化方法会判断哪个“自己内部的类”最适合生成这个对象,然后这个“工厂”就会生成这个具体的类对象返回给你。这种又外层类提供统一抽象的接口,然后具体实现让隐藏的,具体的内部类来实现,在设计模式中称为“抽象工厂”模式。
也就是说,对于普通的类,我们使用上述方法是没有问题的,然而对于Class cluster这种工厂类,则需要找到它的真正类型才行。
2.不存在的方法[/b]
混写不存在的方法不应直接使用method_exchangeImplementations。
注意事项[/b]
本节汇总方法混写的风险及最佳编程实践。方法混写虽然强大,但也存在不少风险。方法混写的影响是全局的,且只限于本应用运行期间。这点可以通过创建两个应用,一个混写,另一个不混写,两者调用同一个被混写的方法,可发现,不混写的应用并没得到混写的结果。
若在
+load中进行混写,则在类被加载时此方法被执行,比
main方法执行的时机还早。若在
+initialize中混写,则混写不一定生效,因为只有主动调用该
+initialize所在类的类方法或实例方法时,
+initialize方法才被加载。
因为方法混写的影响是全局的,要避免在并发处理中出现竞争情况,所以一般在
+load中使用
dispatch_once保证混写只被执行一次。
综上,最佳编程实践是在
+load中通过
dispatch_once编写混写代码。
有些人不建议使用方法混写,而建议实现一个新方法,因为混写后,会对整个工程造成影响,团队成员若不知道你混写的特殊操作,可能会对他们的处理有影响。但是,这也有一个问题,即是,若不混写,无法保证团队成员会主动调用特殊处理的新方法。
参考[/b]
Method SwizzlingObjective-C Class Loading and Initialization
相关文章推荐
- Objective-C运行时编程指南之动态方法决议
- 001_你使用过Objective-C的运行时编程(Runtime Programming)么?如果使用过,你用它做了什么?你还能记得你所使用的相关的头文件或者某些方法的名称吗?
- Objective-C运行时编程 - 实现自动化description方法的思路及代码示例
- objective-c 编程总结(第六篇)运行时操作 - 方法交换
- Objective-C运行时编程指南之介绍
- Objective-C 2.0的运行时编程-消息转发
- objective-c 编程总结(第八篇)运行时操作 - 消息转发
- Objective-C 2.0的运行时编程
- Objective-C 协议和运行时检查方法、类是否存在
- 《UNIX 环境高级编程》源代码在Linux环境运行方法
- Objective-C 2.0的运行时编程
- Runtime ---Objective-C运行时编程指南
- 改注册表,在一定时间内只能运行指定程序,知道用组策略管理器,可以实现,不过我想用编程的方法。
- iOS学习笔记10—Objective-C的运行时编程(Runtime Programming)
- 利用Objective-C运行时hook函数的三种方法
- 利用Objective-C运行时hook函数的三种方法
- 《UNIX 环境高级编程》源代码在Linux环境运行方法
- Objective-C 2.0的运行时编程(转)
- 《UNIX 环境高级编程》源代码在linux环境运行方法
- objective-c 编程总结(第九篇)运行时操作 - 序列化