runtime实践之Method Swizzling
利用 Objective-C 的 Runtime 特性,我们可以给语言做扩展,帮助解决项目开发中的一些设计和技术问题。这一篇,我们来探索一些利用 Objective-C Runtime 的黑色技巧。这些技巧中最具争议的或许就是 Method Swizzling 。
介绍一个技巧,最好的方式就是提出具体的需求,然后用它跟其他的解决方法做比较。
所以,先来看看我们的需求:对 App 的用户行为进行追踪和分析。简单说,就是当用户看到某个 View 或者点击某个 Button 的时候,就把这个事件记下来。
手动添加
最直接粗暴的方式就是在每个 viewDidAppear 里添加记录事件的代码。
@implementation MyViewController () - (void)viewDidAppear:(BOOL)animated
{
[super viewDidAppear:animated]; // Custom code // Logging
[Logging logWithEventName:@“my view did appear”];
} - (void)myButtonClicked:(id)sender
{
// Custom code // Logging
[Logging logWithEventName:@“my button clicked”];
}
这种方式的缺点也很明显:它破坏了代码的干净整洁。因为 Logging 的代码本身并不属于 ViewController 里的主要逻辑。随着项目扩大、代码量增加,你的 ViewController里会到处散布着 Logging 的代码。这时,要找到一段事件记录的代码会变得困难,也很容易忘记添加事件记录的代码。
你可能会想到用继承或类别,在重写的方法里添加事件记录的代码。代码可以是长的这个样子:
@implementation UIViewController () - (void)myViewDidAppear:(BOOL)animated
{
[super viewDidAppear:animated]; // Custom code // Logging
[Logging logWithEventName:NSStringFromClass([self class])];
} - (void)myButtonClicked:(id)sender
{
// Custom code // Logging
NSString *name = [NSString stringWithFormat:@“my button in %@ is clicked”, NSStringFromClass([self class])];
[Logging logWithEventName:name];
}
Logging 的代码都很相似,通过继承或类别重写相关方法是可以把它从主要逻辑中剥离出来。但同时也带来新的问题:
- 你需要继承
UIViewController,UITableViewController,UICollectionViewController所有这些 ViewController ,或者给他们添加类别; - 每个 ViewController 里的 ButtonClick 方法命名不可能都一样;
- 你不能控制别人如何去实例化你的子类;
- 对于类别,你没办法调用到原来的方法实现。大多时候,我们重写一个方法只是为了添加一些代码,而不是完全取代它。
- 如果有两个类别都实现了相同的方法,运行时没法保证哪一个类别的方法会给调用。
Method Swizzling
Method Swizzling 利用 Runtime 特性把一个方法的实现与另一个方法的实现进行替换。
上一篇文章 有讲到每个类里都有一个 Dispatch Table ,将方法的名字(SEL)跟方法的实现(IMP,指向 C 函数的指针)一一对应。Swizzle 一个方法其实就是在程序运行时在 Dispatch Table 里做点改动,让这个方法的名字(SEL)对应到另个 IMP 。
首先定义一个类别,添加将要 Swizzled 的方法:
@implementation UIViewController (Logging) - (void)swizzled_viewDidAppear:(BOOL)animated
{
// call original implementation
[self swizzled_viewDidAppear:animated]; // Logging
[Logging logWithEventName:NSStringFromClass([self class])];
}
代码看起来可能有点奇怪,像递归不是么。当然不会是递归,因为在 runtime 的时候,函数实现已经被交换了。调用 viewDidAppear: 会调用你实现的 swizzled_viewDidAppear:,而在 swizzled_viewDidAppear: 里调用 swizzled_viewDidAppear: 实际上调用的是原来的 viewDidAppear: 。
接下来实现 swizzle 的方法 :
@implementation UIViewController (Logging) void swizzleMethod(Class class, SEL originalSelector, SEL swizzledSelector)
{
// the method might not exist in the class, but in its superclass
Method originalMethod = class_getInstanceMethod(class, originalSelector);
Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector); // class_addMethod will fail if original method already exists
BOOL didAddMethod = class_addMethod(class, originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod)); // the method doesn’t exist and we just added one
if (didAddMethod) {
class_replaceMethod(class, swizzledSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod));
}
else {
method_exchangeImplementations(originalMethod, swizzledMethod);
}
}
这里唯一可能需要解释的是 class_addMethod 。要先尝试添加原 selector 是为了做一层保护,因为如果这个类没有实现 originalSelector ,但其父类实现了,那 class_getInstanceMethod 会返回父类的方法。这样 method_exchangeImplementations替换的是父类的那个方法,这当然不是你想要的。所以我们先尝试添加 orginalSelector ,如果已经存在,再用 method_exchangeImplementations 把原方法的实现跟新的方法实现给交换掉。
最后,我们只需要确保在程序启动的时候调用 swizzleMethod 方法。比如,我们可以在之前 UIViewController 的 Logging 类别里添加 +load: 方法,然后在 +load: 里把 viewDidAppear 给替换掉:
@implementation UIViewController (Logging) + (void)load
{
swizzleMethod([self class], @selector(viewDidAppear:), @selector(swizzled_viewDidAppear:));
}
一般情况下,类别里的方法会重写掉主类里相同命名的方法。如果有两个类别实现了相同命名的方法,只有一个方法会被调用。但 +load: 是个特例,当一个类被读到内存的时候, runtime 会给这个类及它的每一个类别都发送一个 +load: 消息。
其实,这里还可以更简化点:直接用新的 IMP 取代原 IMP ,而不是替换。只需要有全局的函数指针指向原 IMP 就可以。
void (gOriginalViewDidAppear)(id, SEL, BOOL); void newViewDidAppear(UIViewController *self, SEL _cmd, BOOL animated)
{
// call original implementation
gOriginalViewDidAppear(self, _cmd, animated); // Logging
[Logging logWithEventName:NSStringFromClass([self class])];
} + (void)load
{
Method originalMethod = class_getInstanceMethod(self, @selector(viewDidAppear:));
gOriginalViewDidAppear = (void *)method_getImplementation(originalMethod); if(!class_addMethod(self, @selector(viewDidAppear:), (IMP) newViewDidAppear, method_getTypeEncoding(originalMethod))) {
method_setImplementation(originalMethod, (IMP) newViewDidAppear);
}
}
通过 Method Swizzling ,我们成功把逻辑代码跟处理事件记录的代码解耦。当然除了 Logging ,还有很多类似的事务,如 Authentication 和 Caching。这些事务琐碎,跟主要业务逻辑无关,在很多地方都有,又很难抽象出来单独的模块。这种程序设计问题,业界也给了他们一个名字 - Cross Cutting Concerns。
而像上面例子用 Method Swizzling 动态给指定的方法添加代码,以解决 Cross Cutting Concerns 的编程方式叫:Aspect Oriented Programming
runtime实践之Method Swizzling的更多相关文章
- Objective-C Runtime 运行时之四:Method Swizzling
理解Method Swizzling是学习runtime机制的一个很好的机会.在此不多做整理,仅翻译由Mattt Thompson发表于nshipster的Method Swizzling一文. Me ...
- Method Swizzling和AOP(面向切面编程)实践
Method Swizzling和AOP(面向切面编程)实践 参考: http://www.cocoachina.com/ios/20150120/10959.html 上一篇介绍了 Objectiv ...
- Objective-C Runtime 运行时之四:Method Swizzling(转载)
理解Method Swizzling是学习runtime机制的一个很好的机会.在此不多做整理,仅翻译由Mattt Thompson发表于nshipster的Method Swizzling一文. Me ...
- 理解Objective-C Runtime(四)Method Swizzling
Objective-C对象收到消息之后,究竟会调用何种方法需要在运行期间才能解析出来.那你也许会问:与给定的选择子名称相应的方法是不是也可以在runtime改变呢?没错,就是这样.若能善用此特性,则可 ...
- runtime 第四部分method swizzling
接上一篇 http://www.cnblogs.com/ddavidXu/p/5924597.html 转载来源http://www.jianshu.com/p/6b905584f536 http:/ ...
- Method Swizzling 和 AOP 实践(转)
上一篇介绍了 Objective-C Messaging.利用 Objective-C 的 Runtime 特性,我们可以给语言做扩展,帮助解决项目开发中的一些设计和技术问题.这一篇,我们来探索一些利 ...
- Method Swizzling (方法调配)
Method Swizzling是改变一个selector的实际实现的技术.通过这一技术,我们可以在运行时通过修改类的分发表中selector对应的函数,来修改方法的实现. 例如,我们想跟踪在程序中每 ...
- Objective-C运行时编程 - 方法混写 Method Swizzling
摘要: 本文描述方法混写对实例.类.父类.不存在的方法等情况处理,属于Objective-C(oc)运行时(runtime)编程范围. 编程环境:Xcode 6.1.1, Yosemite,iOS 8 ...
- OBJC运行时方法替换(Method swizzling)
在上周associated objects一文中,我们开始探索Objective-C运行时的一些黑魔法.本周我们继续前行,来讨论可能是最受争议的运行时技术:method swizzling. Me ...
随机推荐
- 洛谷P2532 [AHOI2012]树屋阶梯(Catalan数)
P2532 [AHOI2012]树屋阶梯 题目描述 输入输出格式 输入格式: 一个正整数N(1<=N<=500),表示阶梯的高度. 输出格式: 一个正整数,表示搭建方法的个数.(注:搭建方 ...
- IT兄弟连 Java语法教程 编译Java程序
编写好Java程序的源代码后,接下来就应该编译该Java源文件来生成字节码文件了. 编译Java程序需要使用JDK中提供的javac命令,因为已经把javac命令所在的路径添加到了系统的Path环境变 ...
- try catch finally的执行顺序学习
try catch finally的执行顺序学习 首先执行try,如果有异常执行catch,无论如何都会执行finally,当有return以后,函数就会把这个数据存储在某个位置,然后告诉主函数, ...
- BOM核心API
前言 我觉得前端中最难的不是业务逻辑,而是API太多,知道的太少. BOM是什么? BOM是Browser Object Model的缩写,即浏览器对象模型.浏览器对象模型提供了可以与浏览器窗口进行互 ...
- Codeforces Round #562 (Div. 2) A.Circle Metro
链接:https://codeforces.com/contest/1169/problem/A 题意: The circle line of the Roflanpolis subway has n ...
- Net Core IIS Express In
IIS Express In Asp.Net Core IIS Express是一个Mini版的IIS,能够支持所有的Web开发任务,但是这种设计有一些缺陷,例如只能通过localhost:< ...
- python 7 dict和set
dict Python内置了字典:dict的支持,dict全称dictionary,在其他语言中也称为map,使用键-值(key-value)存储,具有极快的查找速度. 举个例子,假设要根据同学的名字 ...
- 看懂物联网fr
看懂物联网 2015-10-11 物联网世界 1.第三次IT浪潮 互联网时代的特征是信息驱动了生产力,无论众包.订单式生产这些理论:还是B2C.O2O各类业务模式:归根结底,是信息优化了生产关 ...
- setTimout( , 0) 详解
setTimout( , 0) 一.前言 前端工程师们工作久了,一般都会在某些地方看见过这样的代码: setTimeout(function(){ // TODO }, 0); 举个实例,移动端我们经 ...
- B. Game of the Rows
B. Game of the Rows time limit per test 1 second memory limit per test 256 megabytes input standard ...