Method Swizzling和AOP(面向切面编程)实践
Method Swizzling和AOP(面向切面编程)实践
参考: http://www.cocoachina.com/ios/20150120/10959.html
上一篇介绍了 Objective-C Messaging。利用 Objective-C 的 Runtime 特性,我们可以给语言做扩展,帮助解决项目开发中的一些设计和技术问题。这一篇,我们来探索一些利用 Objective-C Runtime 的黑色技巧。这些技巧中最具争议的或许就是 Method Swizzling 。
介绍一个技巧,最好的方式就是提出具体的需求,然后用它跟其他的解决方法做比较。
所以,先来看看我们的需求:对 App 的用户行为进行追踪和分析。简单说,就是当用户看到某个 View 或者点击某个 Button 的时候,就把这个事件记下来。
手动添加
最直接粗暴的方式就是在每个 viewDidAppear 里添加记录事件的代码。
|
1
2
3
4
5
6
7
8
9
10
11
|
@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 的代码。这时,要找到一段事件记录的代码会变得困难,也很容易忘记添加事件记录的代码。
你可能会想到用继承或者类别,在重写的方法里添加事件记录的代码。比如用类别的代码大概长这个样子:
|
1
2
3
4
5
6
7
8
9
10
11
12
|
@implementation UIViewController (Logging)- (void)viewDidAppear:(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 的代码都很相似,通过继承或类别重写相关方法是可以把它从主要逻辑中剥离出来。但同时也带来新的问题:
1.你需要继承 UIViewController, UITableViewController, UICollectionViewController 所有这些 ViewController ,或者给他们添加类别;
2.每个 ViewController 里的 ButtonClick 方法命名不可能都一样;
3.你不能控制别人如何去实例化你的子类;
4.对于类别,你没办法调用到原来的方法实现。大多时候,我们重写一个方法只是为了添加一些代码,而不是完全取代它。
5.如果有两个类别都实现了相同的方法,运行时没法保证哪一个类别的方法会给调用。
Method Swizzling
Method Swizzling 利用 Runtime 特性把一个方法的实现与另一个方法的实现进行替换。
上一篇文章有讲到每个类里都有一个 Dispatch Table ,将方法的名字(SEL)跟方法的实现(IMP,指向 C 函数的指针)一一对应。Swizzle 一个方法其实就是在程序运行时在 Dispatch Table 里做点改动,让这个方法的名字(SEL)对应到另个 IMP 。
首先定义一个类别,添加将要 Swizzled 的方法:
|
1
2
3
4
5
|
@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 的方法 :
|
1
2
3
4
5
6
7
8
9
10
11
|
@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 给替换掉:
|
1
2
3
4
|
@implementation UIViewController (Logging)+ (void)load{ swizzleMethod([self class], @selector(viewDidAppear:), @selector(swizzled_viewDidAppear:));} |
一般情况下,类别里的方法会重写掉主类里相同命名的方法。如果有两个类别实现了相同命名的方法,只有一个方法会被调用。但 +load: 是个特例,当一个类被读到内存的时候, runtime 会给这个类及它的每一个类别都发送一个 +load: 消息。
其实,这里还可以更简化点:直接用新的 IMP 取代原 IMP ,而不是替换。只需要有全局的函数指针指向原 IMP 就可以。
|
1
2
3
4
5
6
7
8
9
10
11
12
|
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
Aspect Oriented Programming (面向切面编程)
Wikipedia 里对 AOP 是这么介绍的:
An aspect can alter the behavior of the base code by applying advice (additional behavior) at various join points (points in a program) specified in a quantification or query called a pointcut (that detects whether a given join point matches).
在 Objective-C 的世界里,这句话意思就是利用 Runtime 特性给指定的方法添加自定义代码。有很多方式可以实现 AOP ,Method Swizzling 就是其中之一。而且幸运的是,目前已经有一些第三方库可以让你不需要了解 Runtime ,就能直接开始使用 AOP 。
Aspects 就是一个不错的 AOP 库,封装了 Runtime , Method Swizzling 这些黑色技巧,只提供两个简单的API:
|
1
2
3
4
5
6
7
|
+ (id<aspecttoken>)aspect_hookSelector:(SEL)selector withOptions:(AspectOptions)options usingBlock:(id)block error:(NSError **)error;- (id<aspecttoken>)aspect_hookSelector:(SEL)selector withOptions:(AspectOptions)options usingBlock:(id)block error:(NSError **)error;</aspecttoken></aspecttoken> |
使用 Aspects 提供的 API,我们之前的例子会进化成这个样子:
|
1
2
3
4
5
6
7
8
|
@implementation UIViewController (Logging)+ (void)load{ [UIViewController aspect_hookSelector:@selector(viewDidAppear:) withOptions:AspectPositionAfter usingBlock:^(id<aspectinfo> aspectInfo) { NSString *className = NSStringFromClass([[aspectInfo instance] class]); [Logging logWithEventName:className]; } error:NULL];}</aspectinfo> |
你可以用同样的方式在任何你感兴趣的方法里添加自定义代码,比如 IBAction 的方法里。更好的方式,你提供一个 Logging 的配置文件作为唯一处理事件记录的地方:
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
|
@implementation AppDelegate (Logging)+ (void)setupLogging{ NSDictionary *config = @{ @"MainViewController": @{ GLLoggingPageImpression: @"page imp - main page", GLLoggingTrackedEvents: @[ @{ GLLoggingEventName: @"button one clicked", GLLoggingEventSelectorName: @"buttonOneClicked:", GLLoggingEventHandlerBlock: ^(id<aspectinfo> aspectInfo) { [Logging logWithEventName:@"button one clicked"]; }, }, @{ GLLoggingEventName: @"button two clicked", GLLoggingEventSelectorName: @"buttonTwoClicked:", GLLoggingEventHandlerBlock: ^(id<aspectinfo> aspectInfo) { [Logging logWithEventName:@"button two clicked"]; }, }, ], }, @"DetailViewController": @{ GLLoggingPageImpression: @"page imp - detail page", } }; [AppDelegate setupWithConfiguration:config];}+ (void)setupWithConfiguration:(NSDictionary *)configs{ // Hook Page Impression [UIViewController aspect_hookSelector:@selector(viewDidAppear:) withOptions:AspectPositionAfter usingBlock:^(id<aspectinfo> aspectInfo) { NSString *className = NSStringFromClass([[aspectInfo instance] class]); [Logging logWithEventName:className]; } error:NULL]; // Hook Events for (NSString *className in configs) { Class clazz = NSClassFromString(className); NSDictionary *config = configs[className]; if (config[GLLoggingTrackedEvents]) { for (NSDictionary *event in config[GLLoggingTrackedEvents]) { SEL selekor = NSSelectorFromString(event[GLLoggingEventSelectorName]); AspectHandlerBlock block = event[GLLoggingEventHandlerBlock]; [clazz aspect_hookSelector:selekor withOptions:AspectPositionAfter usingBlock:^(id<aspectinfo> aspectInfo) { block(aspectInfo); } error:NULL]; } } }}</aspectinfo></aspectinfo></aspectinfo></aspectinfo> |
然后在 -application:didFinishLaunchingWithOptions: 里调用 setupLogging:
|
1
2
3
|
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { // Override point for customization after application launch. [self setupLogging]; return YES;} |
最后的话
利用 objective-C Runtime 特性和 Aspect Oriented Programming ,我们可以把琐碎事务的逻辑从主逻辑中分离出来,作为单独的模块。它是对面向对象编程模式的一个补充。Logging 是个经典的应用,这里做个抛砖引玉,发挥想象力,可以做出其他有趣的应用。
使用 Aspects 完整的例子可以从这里获得:AspectsDemo。
如果你有什么问题和想法,欢迎留言或者发邮件给我 peng@glowing.com 进行讨论。
Reference
method replacement for fun and profit
Method Swizzling和AOP(面向切面编程)实践的更多相关文章
- AOP 面向切面编程, Attribute在项目中的应用
一.AOP(面向切面编程)简介 在我们平时的开发中,我们一般都是面对对象编程,面向对象的特点是继承.多态和封装,我们的业务逻辑代码主要是写在这一个个的类中,但我们在实现业务的同时,难免也到多个重复的操 ...
- AOP面向切面编程的四种实现
一.AOP(面向切面编程)的四种实现分别为最原始的经典AOP.代理工厂bean(ProxyFacteryBean)和默认自动代理DefaultAdvisorAutoProxyCreator以及Bea ...
- [转] AOP面向切面编程
AOP面向切面编程 AOP(Aspect-Oriented Programming,面向切面的编程),它是可以通过预编译方式和运行期动态代理实现在不修改源代码的情况下给程序动态统一添加功能的一种技术. ...
- C# AOP 面向切面编程之 调用拦截
有时候我们需要在代码中对方法调用进行拦截,并修改参数和返回值,这种操作叫做AOP(面向切面编程) 不过需要注意的是,AOP的效率很慢,在需要高效率场合慎用. 以下是C#的AOP方法: 首先建立一个控制 ...
- 从壹开始前后端分离【 .NET Core2.0 +Vue2.0 】框架之十 || AOP面向切面编程浅解析:简单日志记录 + 服务切面缓存
代码已上传Github+Gitee,文末有地址 上回<从壹开始前后端分离[ .NET Core2.0 Api + Vue 2.0 + AOP + 分布式]框架之九 || 依赖注入IoC学习 + ...
- Spring:AOP面向切面编程
AOP主要实现的目的是针对业务处理过程中的切面进行提取,它所面对的是处理过程中的某个步骤或阶段,以获得逻辑过程中各部分之间低耦合性的隔离效果. AOP是软件开发思想阶段性的产物,我们比较熟悉面向过程O ...
- java aop面向切面编程
最近一直在学java的spring boot,一直没有弄明白aop面向切面编程是什么意思.看到一篇文章写得很清楚,终于弄明白了,原来跟python的装饰器一样的效果.http://www.cnblog ...
- Spring Boot2(六):使用Spring Boot整合AOP面向切面编程
一.前言 众所周知,spring最核心的两个功能是aop和ioc,即面向切面和控制反转.本文会讲一讲SpringBoot如何使用AOP实现面向切面的过程原理. 二.何为aop aop全称Aspec ...
- 谈一谈AOP面向切面编程
AOP是什么 : AOP面向切面编程他是一种编程思想,是指在程序运行期间,将某段代码动态的切入到指定方法的指定位置,将这种编程方式称为面向切面编程 AOP使用场景 : 日志 事务 使用AOP的好处是: ...
随机推荐
- JS-节点属性(常用!)
(开始很生疏,不想看的知识点,后来DOM中用的特别多,才发现很重要.还有几个点,是比较容易忽略的,值得注意!) <!DOCTYPE html><html> <head&g ...
- JS-Array数组对象
<!DOCTYPE html><html> <head> <meta charset="UTF-8"> <title>A ...
- 百度云管家 5.3.6 VIP破解不限速版下载分享|百度云管家破解提速
百度云管家PC客户端v5.3.6绿色版本,属于VIP破解不限速版.百度网盘为您提供文件的网络备份.同步和分享服务.空间大.速度快.安全稳固,支持教育网加速,支持手机端.它支持便捷地查看.上传.下载云端 ...
- lunix的查看Tomcat目录下日志的快速操作
可以使用cd命令,cd命令的功能是切换到指定的目录: 命令格式:cd [目录名] 有几个符号作为目录名有特殊的含义: "/"代表根目录. ".."代表上一级目录 ...
- swfUpload 上传图片
前端: <script src="~/Scripts/swfupload/swfupload.js"></script> <script src=&q ...
- PyCharm 教程(四)显示行号
PyCharm 教程(四)显示行号 在PyCharm 里,显示行号有两种办法: 1,临时设置.右键单击行号处,选择 Show Line Numbers. 但是这种方法,只对一个文件有效,并且,重启Py ...
- ecshop if标签,超过N条,就输出记录 elseif、库存显示方式
<!--商品详情右侧 相关商品推荐--> <!-- {if $related_goods} --> <!--{foreach from=$related_goods it ...
- adapter(转自Devin Zhang)
1.概念 Adapter是连接后端数据和前端显示的适配器接口,是数据和UI(View)之间一个重要的纽带.在常见的View(ListView,GridView)等地方都需要用到Adapter.如下图直 ...
- Python socket编程
一.什么是socket: socket通常也称作"套接字",用于描述IP地址和端口,是一个通信链的句柄,应用程序通常通过"套接字"向网络发出请求或者应答网络请求 ...
- Pycharm 使用
Pycharm基本使用http://edu.51cto.com/index.php?do=lession&id=118722 Pycharm的基本使用 在Pycharm下为你的Python ...