阅读器

iOS开发iOS

 
本文由TracyYih[博客]翻译自NSHipster的文章Method Swizzling
 
在上周associated objects一文中,我们开始探索Objective-C运行时的一些黑魔法。本周我们继续前行,来讨论可能是最受争议的运行时技术:method swizzling。
 
Method swizzling指的是改变一个已存在的选择器对应的实现的过程,它依赖于Objectvie-C中方法的调用能够在运行时进改变——通过改变类的调度表(dispatch table)中选择器到最终函数间的映射关系。
 
举个例子,假设我们想跟踪在一个iOS应用中每个视图控制器展现给用户的次数:
 
我们可以给每个视图控制器对应的viewWillAppear:实现方法中增加相应的跟踪代码,但是这样做会产生大量重复的代码。子类化可能是另一个选择,但要求你将UIViewController、 UITableViewController、 UINavigationController 以及所有其他视图控制器类都子类化,这也会导致代码重复。
 
幸好,还有另一个方法,在分类中进行method swizzling,下面来看怎么做:
  1. #import <objc/runtime.h>
  2. @implementation UIViewController (Tracking)
  3. + (void)load {
  4. static dispatch_once_t onceToken;
  5. dispatch_once(&onceToken, ^{
  6. Class class = [self class];
  7. // When swizzling a class method, use the following:
  8. // Class class = object_getClass((id)self);
  9. SEL originalSelector = @selector(viewWillAppear:);
  10. SEL swizzledSelector = @selector(xxx_viewWillAppear:);
  11. Method originalMethod = class_getInstanceMethod(class, originalSelector);
  12. Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);
  13. BOOL didAddMethod =
  14. class_addMethod(class,
  15. originalSelector,
  16. method_getImplementation(swizzledMethod),
  17. method_getTypeEncoding(swizzledMethod));
  18. if (didAddMethod) {
  19. class_replaceMethod(class,
  20. swizzledSelector,
  21. method_getImplementation(originalMethod),
  22. method_getTypeEncoding(originalMethod));
  23. } else {
  24. method_exchangeImplementations(originalMethod, swizzledMethod);
  25. }
  26. });
  27. }
  28. #pragma mark - Method Swizzling
  29. - (void)xxx_viewWillAppear:(BOOL)animated {
  30. [self xxx_viewWillAppear:animated];
  31. NSLog(@"viewWillAppear: %@", self);
  32. }
  33. @end
在计算机学科中,指针变换(pointer swizzling)是指将基于名字或位置的引用转变为直接的指针引用。 然而在Objective-C中,这个词的起源并不完全知道,但关于这一借鉴其实也很好理解,method swizzling可以通过选择器来改变它引用的函数指针。
 
现在,当UIViewController或它子类的任何实例触发viewWillAppear:方法都会打印一条log日志。
 
向视图控制器的生命周期中注入操作、事件的响应、视图的绘制,或Foundation中的网络堆栈都是能够利用method swizzling产生明显效果的场景。还有一些其他的场景使用swizzling会是一个合适的选择,这随着Objective-C开发者经验不断丰富会变得越来越明显。
 
先不说为什么和在哪些地方使用swizzling,来看一下应该怎样实现:
 
+load vs. +initialize
Swizzling应该在+load方法中实现。
每个类的这两个方法会被Objective-C运行时系统自动调用,+load是在一个类最开始加载时调用,+initialize是在应用中第一次调用该类或它的实例的方式之前调用。这两个方法都是可选的,只有实现了才会被执行。
 
因为method swizzling会影响全局,所以减少冒险情况就很重要。+load能够保证在类初始化的时候就会被加载,这为改变系统行为提供了一些统一性。但+initialize并不能保证在什么时候被调用——事实上也有可能永远也不会被调用,例如应用程序从未直接的给该类发送消息。
 
dispatch_once
Swizzling应该在dispatch_once中实现。
 
还是因为swizzling会改变全局,我们需要在运行时采取所有可用的防范措施。保障原子性就是一个措施,它确保代码即使在多线程环境下也只会被执行一次。GCD中的diapatch_once就提供这些保障,它应该被当做swizzling的标准实践。
 
选择器、方法及实现
在Objective-C中,尽管这些词经常被放在一起来描述消息传递的过程,但选择器、方法及实现分别代表运行时的不同方面。
 
下面是苹果Objective-C Runtime Reference文档中对它们的描述:
1.选择器(typedef struct objc_selector *SEL):选择器用于表示一个方法在运行时的名字,一个方法的选择器是一个注册到(或映射到)Objective-C运行时中的C字符串,它是由编译器生成并在类加载的时候被运行时系统自动映射。
 
2.方法(typedef struct objc_method *Method):一个代表类定义中一个方法的不明类型。
 
3.实现(typedef id (*IMP)(id, SEL, ...)):这种数据类型是实现某个方法的函数开始位置的指针,函数使用的是基于当前CPU架构的标准C调用规约。第一个参数是指向self的指针(也就是该类的某个实例的内存空间,或者对于类方法来说,是指向元类(metaclass)的指针)。第二个参数是方法的选择器,后面跟的都是参数。
 
理解这些概念之间关系最好的方式是:一个类(Class)维护一张调度表(dispatch table)用于解析运行时发送的消息;调度表中的每个实体(entry)都是一个方法(Method),其中key值是一个唯一的名字——选择器(SEL),它对应到一个实现(IMP)——实际上就是指向标准C函数的指针。
 
Method Swizzling就是改变类的调度表让消息解析时从一个选择器对应到另外一个的实现,同时将原始的方法实现混淆到一个新的选择器。
 
调用_cmd
下面这段代码看起来像是会导致一个死循环:
  1. - (void)xxx_viewWillAppear:(BOOL)animated {
  2. [self xxx_viewWillAppear:animated];
  3. NSLog(@"viewWillAppear: %@", NSStringFromClass([self class]));
  4. }
 
但其实并没有,在Swizzling的过程中,xxx_viewWillAppear:会被重新分配给UIViewController的-viewWillAppear:的原始实现。一个优秀程序员应有的直觉会告诉你在一个方法的实现中通过self调用当前方法自身会产生错误,但是在当前这种情况下,如果我们记住到底是怎么回事更有意义。反而,如果我们在这个方法中调用viewWillAppear:才会真的导致死循环,因为这个方法的实现会在运行时被swizzle到viewWillAppear:的选择器。
 
记住给swizzled方法加上前缀,这和你需要给可能产生冲突的分类方法加前缀是一个道理。
 
注意事项
Swizzling被普遍认为是一种巫术,容易导致不可预料的行为和结果。尽管不是最安全的,但是如果你采取下面这些措施,method swizzling还是很安全的。
 
1.始终调用方法的原始实现(除非你有足够的理由不这么做): API为输入和输出提供规约,但它里面具体的实现其实是个黑匣子,在Method Swizzling过程中不调用它原始的实现可能会破坏一些私有状态,甚至是程序的其他部分。
 
2.避免冲突:给分类方法加前缀,一定要确保不要让你代码库中其他代码(或是依赖库)在做与你相同的事。
 
3.理解:只是简单的复制粘贴swizzling代码而不去理解它是怎么运行的,这不仅非常危险,而且还浪费了学习Objective-C运行时的机会。阅读 Objective-C Runtime Reference 和 <objc/rumtime.h> 去理解代码是怎样和为什么这样执行的,努力的用你的理解来消灭你的疑惑。
 
谨慎行事:不管你多么自信你能够swizzling Foundation、UIKit 或者其他内置框架,请记住所有这些都可能在下一个版本中就不好使。提前做好准备,防范于未然才不至于到时候焦头烂额。
 
不敢放心大胆的直接使用Objective-C运行时?Jonathan ‘Wolf’ Rentzsch提供了经过实战检验的、支持CocoaPads的库JRSwizzle,它会为你考虑好了一切。
 
与associated objects一样,method swizzling是一个强大的技术,但是你也应该谨慎使用。

ios method swizzling的更多相关文章

  1. iOS Method Swizzling和分类的妙用AppDelegate轻量化处理

    http://www.cocoachina.com/ios/20151117/14167.html 简介 在iOS工程中,AppDelegate往往会有上千行,甚至几千行,这样就会给维护AppDele ...

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

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

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

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

  4. iOS运行时与method swizzling

    C语言是静态语言,它的工作方式是通过函数调用,这样在编译时我们就已经确定程序如何运行的.而Objective-C是动态语言,它并非通过调用类的方 法来执行功能,而是给对象发送消息,对象在接收到消息之后 ...

  5. IOS 开发之 Method Swizzling + Category

    ios 分类中如果增加的方法与被扩展的类方法名重复,则原方法就没法被调用….看以下例子 例如: @interface ClassA : NSObject - (NSString *) myMethod ...

  6. iOS 使用Method Swizzling隐藏Status Bar

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

  7. iOS执行时与method swizzling

    C语言是静态语言,它的工作方式是通过函数调用,这样在编译时我们就已经确定程序怎样执行的.而Objective-C是动态语言,它并不是通过调用类的方法来执行功能,而是给对象发送消息,对象在接收到消息之后 ...

  8. ios逆向工程-内部钩子(Method Swizzling)

    Method Swizzling(方法调配) 怎么说呢,先了解什么是钩子为什么用钩子,学过C++的朋友应该清楚,hook就是用来获得(截断/改变)底层调用的方法.这样我们可以自由的修改或者读取一些想要 ...

  9. IOS 开发之 Method Swizzling

    ios 分类中如果增加的方法与被扩展的类方法名重复,则原方法就没法被调用….看以下例子 例如: @interface ClassA : NSObject - (NSString *) myMethod ...

随机推荐

  1. android中用Intent传数据,如果用传递的是一个类,就将类实现Parcelable接口

    Parcelable,内存单位,跨进程使用,或者intent传递对象的时候使用.android中用Intent传数据,如果用传递的是一个对象,就将对象实现Parcelable接口,而不是将对象序列化. ...

  2. DOM节点例子

    elementNode.setAttribute(name,value) 1.name: 要设置的属性名. 2.value: 要设置的属性值. elementNode.getAttribute(nam ...

  3. linux使用crontab实现PHP执行计划定时任务

    linux使用crontab实现PHP执行计划定时任务 前几天写过一篇文章,利用单纯的php实现定时执行任务,但是效率不佳,对于linux来说用crontab实现更加合理 首先说说cron,它是一个l ...

  4. Linux系统的启动流程

    Linux系统的启动流程: 1.通电(通常按下电源键,开始通电) 2.加载BIOS (通常看到显示器提示按F2进入主板) 3.读取MBR (MBR硬盘的入口地址,用来装载引导) 4.进入引导 (通常有 ...

  5. jsTree插件简介(三)

    UI-plugin JSTree的UI插件:用来处理选择.不选和鼠标悬浮树选项的插件. 一.属性包括: 1.select_limit:指定一次可以选中几个节点,默认为-1,表示无限制选中. 2.sel ...

  6. 大数除法(除数在int范围内)

    #include<iostream> #include<cstdio> #include<cstring> #define N 1000 using namespa ...

  7. swift Hashable Equatable

    /// You can use any type that conforms to the `Hashable` protocol in a set or /// as a dictionary ke ...

  8. 安卓app测试之Monkeyrunner

    一.MonkeyRunner简介 MonkeyRunner提供了系列的API ,MonkeyRunner可以完成模拟事件及截图操作 ,分为以下三类: MonkeyRunner:用来连接设备或模拟器的 ...

  9. scala学习(2)---option空值处理

    https://blog.csdn.net/shadowsama/article/details/78148919 https://www.cnblogs.com/mustone/p/5648914. ...

  10. springBoot启动及发布

    1.在项目编辑器(IDEA)中启动 运行springBoot项目Application类中main方法,这两个按钮都可以.如下图: 当然还有我们配置的启动按钮,这是最常用的启动方式,不再赘述,如下图: ...