摘要: 本文描述方法混写对实例、类、父类、不存在的方法等情况处理,属于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,转载请注明。

修订版本

1.0 草稿

什么是方法混写

方法混写(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)即可。现在,开始介绍方法混写。

编程案例

本节先混写NSString中的实例方法、类及父类方法,然后说明NSMutableString及NSMutableArray等可变类型的混写处理,最后介绍处理不存在的方法混写。

1.本类方法

混写已存在的实例方法或类方法的做法是类似的,步骤为:

  1. 定义完成业务的新方法
  2. 定义方法签名SEL,由 @selector 编译器指令来获取
  3. 获取方法定义Method,由 class_getInstanceMethod 得到实例方法定义 或 class_getClassMethod得到类方法定义
  4. 交换二者实现,由 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_getInstanceMethodclass_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.不存在的方法

混写不存在的方法不应直接使用 method_exchangeImplementations

注意事项

本节汇总方法混写的风险及最佳编程实践。方法混写虽然强大,但也存在不少风险。

  • 方法混写的影响是全局的,且只限于本应用运行期间。这点可以通过创建两个应用,一个混写,另一个不混写,两者调用同一个被混写的方法,可发现,不混写的应用并没得到混写的结果。
  • 若在 +load 中进行混写,则在类被加载时此方法被执行,比 main 方法执行的时机还早。若在 +initialize 中混写,则混写不一定生效,因为只有主动调用该 +initialize 所在类的类方法或实例方法时,+initialize 方法才被加载。
  • 因为方法混写的影响是全局的,要避免在并发处理中出现竞争情况,所以一般在 +load 中使用 dispatch_once 保证混写只被执行一次。
  • 综上,最佳编程实践是在 +load 中通过 dispatch_once 编写混写代码。
  • 有些人不建议使用方法混写,而建议实现一个新方法,因为混写后,会对整个工程造成影响,团队成员若不知道你混写的特殊操作,可能会对他们的处理有影响。但是,这也有一个问题,即是,若不混写,无法保证团队成员会主动调用特殊处理的新方法。

参考

  1. Method Swizzling
  2. Objective-C Class Loading and Initialization

Objective-C运行时编程 - 方法混写 Method Swizzling的更多相关文章

  1. Objective-C 2.0的运行时编程

    Objective-C 2.0 的运行时环境叫做Morden Runtime,iOS 和Mac OS X 64-bit 的程序都运行在这个环境,也就是说Mac OS X 32-bit 的程序运行在旧的 ...

  2. iOS运行时编程(Runtime Programming)和Java的反射机制对比

    运行时进行编程,类似Java的反射.运行时编程和Java反射的对比如下:   1.相同点   都可以实现的功能:获取类信息.属性设置获取.类的动态加载(NSClassFromString(@“clas ...

  3. Objective-C运行时编程 - 实现自动化description方法的思路及代码示例

    发布自米高 | Michael - 博客园,源地址:http://www.cnblogs.com/michaellfx/p/4232205.html,转载请注明. 本文结构 基础实现 性能优化 参考 ...

  4. 趣谈iOS运行时的方法调用原理

    一个成熟的计算机语言必然有丰富的体系,复杂的容错机制,处理逻辑以及判断逻辑.但这些复杂的逻辑都是围绕一个主线丰富和展开的,所以在学习计算机语言的时候,先掌握核心,然后了解其原理,明白程序语言设计的实质 ...

  5. OC运行时和方法机制笔记

    在OC当中,属性是对字段的一种特殊封装手段. 在编译期,编译器会将对字段的访问替换为内存偏移量,实质是一种硬编码. 如果增加一个字段,那么对象的内存排布就会改变,需要重新编译才行. OC的做法是,把实 ...

  6. java 利用java运行时的方法得到当前屏幕截图的方法(转)

    将截屏图片保存到本地路径: package com.test; import java.awt.AWTException; import java.awt.Dimension; import java ...

  7. C# 运行时替换方法(需要unsafe编译)

    /* https://stackoverflow.com/questions/7299097/dynamically-replace-the-contents-of-a-c-sharp-method ...

  8. 用IDEA时,类/方法提示"class/method **** is never used"

    https://segmentfault.com/q/1010000005996275?_ea=978306 清理下缓存试下.在 File -> Invalidate Caches 下,会重启 ...

  9. Java虚拟机运行时数据区域划分

        Java虚拟机数据运行时区域 方法区(Method Area) 存储加载的类信息,常量,静态变量,编译器编译后的代码等数据.虽然JVM规范把方法区描述为堆的一个逻辑部分,但它却有一个别名叫做N ...

随机推荐

  1. Drupal处理缓存的方式

    Drupal的后台数据库中有很多以cache开头的表,这些都是Drupal的缓存数据表. Drupal的缓存机制使用了接口方式,所有的缓存对象都必须实现DrupalCacheInterface接口: ...

  2. android 性能优化大纲

    性能优化系列 分为三个部分:视图篇 逻辑篇  和代码规范篇 . ------2016/9/6  视图篇      主要涵盖视图树层级优化.自定义视图.图片优化,常用布局性能缺陷等多个方面 .把平常经常 ...

  3. 《VC++ 6简明教程》即VC++ 6.0入门精讲 学习进度及笔记

    VC++6.0入门→精讲 2013.06.09,目前,每一章的“自测题”和“小结”三个板块还没有看(备注:第一章的“实验”已经看完). 2013.06.16 第三章的“实验”.“自测题”.“小结”和“ ...

  4. 【quick-cocos2d-x】Lua 面向对象(OOP)编程与元表元方法

    版权声明:本文为博主原创文章,转载请注明出处. 面向对象是一种对现实世界理解和抽象的方法,是计算机编程技术发展到一定阶段后的产物. 早期的计算机编程是基于面向过程的方法,通过设计一个算法就可以解决当时 ...

  5. 大数据时代的数据存储,非关系型数据库MongoDB

    在过去的很长一段时间中,关系型数据库(Relational Database Management System)一直是最主流的数据库解决方案,他运用真实世界中事物与关系来解释数据库中抽象的数据架构. ...

  6. 关于微信聊天与朋友圈如何快速切换 Mark

    用微信时,你是否遇到这样的情况.你正刷着朋友圈,享受着各种鸡汤,这时候,你收到一条微信,一看是女王大人,不得不回.你诚恳的回了一条,等了二十秒不见有什么回应,于是就退了出来,进入朋友圈找到那篇没看完的 ...

  7. java 开发环境

    jdk:包括jre,自己下载即可. 客户端只需安装jre即可. 安装路径:C:\jdk7.0\jdk1.7.0_25\bin (适时更改) 环境变量是从前往后找 测试成功:cmd      java ...

  8. asp.net mvc下ckeditor使用

    资源下载:ckeditor 第一步,引入必须文件“~/ckeditor/ckeditor.js” 第二步,替换文本域 <%: Html.TextArea("Content", ...

  9. Mysql的AB复制(主从复制)原理及实现

    Mysql复制(replication)是一个异步的复制,从一个Mysql 实例(Master)复制到另一个Mysql 实例(Slave).实现整个主从复制,需要由Master服务器上的IO进程,和S ...

  10. 徐汉彬:亿级Web系统搭建——单机到分布式集群(转载)

    文章转载自http://www.csdn.net/article/2014-11-06/2822529/1 当一个Web系统从日访问量10万逐步增长到1000万,甚至超过1亿的过程中,Web系统承受的 ...