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-swizzling

method replacement for fun and profit

Aspects

Method Swizzling和AOP(面向切面编程)实践的更多相关文章

  1. AOP 面向切面编程, Attribute在项目中的应用

    一.AOP(面向切面编程)简介 在我们平时的开发中,我们一般都是面对对象编程,面向对象的特点是继承.多态和封装,我们的业务逻辑代码主要是写在这一个个的类中,但我们在实现业务的同时,难免也到多个重复的操 ...

  2. AOP面向切面编程的四种实现

     一.AOP(面向切面编程)的四种实现分别为最原始的经典AOP.代理工厂bean(ProxyFacteryBean)和默认自动代理DefaultAdvisorAutoProxyCreator以及Bea ...

  3. [转] AOP面向切面编程

    AOP面向切面编程 AOP(Aspect-Oriented Programming,面向切面的编程),它是可以通过预编译方式和运行期动态代理实现在不修改源代码的情况下给程序动态统一添加功能的一种技术. ...

  4. C# AOP 面向切面编程之 调用拦截

    有时候我们需要在代码中对方法调用进行拦截,并修改参数和返回值,这种操作叫做AOP(面向切面编程) 不过需要注意的是,AOP的效率很慢,在需要高效率场合慎用. 以下是C#的AOP方法: 首先建立一个控制 ...

  5. 从壹开始前后端分离【 .NET Core2.0 +Vue2.0 】框架之十 || AOP面向切面编程浅解析:简单日志记录 + 服务切面缓存

    代码已上传Github+Gitee,文末有地址 上回<从壹开始前后端分离[ .NET Core2.0 Api + Vue 2.0 + AOP + 分布式]框架之九 || 依赖注入IoC学习 + ...

  6. Spring:AOP面向切面编程

    AOP主要实现的目的是针对业务处理过程中的切面进行提取,它所面对的是处理过程中的某个步骤或阶段,以获得逻辑过程中各部分之间低耦合性的隔离效果. AOP是软件开发思想阶段性的产物,我们比较熟悉面向过程O ...

  7. java aop面向切面编程

    最近一直在学java的spring boot,一直没有弄明白aop面向切面编程是什么意思.看到一篇文章写得很清楚,终于弄明白了,原来跟python的装饰器一样的效果.http://www.cnblog ...

  8. Spring Boot2(六):使用Spring Boot整合AOP面向切面编程

    一.前言 众所周知,spring最核心的两个功能是aop和ioc,即面向切面和控制反转.本文会讲一讲SpringBoot如何使用AOP实现面向切面的过程原理. 二.何为aop ​ aop全称Aspec ...

  9. 谈一谈AOP面向切面编程

    AOP是什么 : AOP面向切面编程他是一种编程思想,是指在程序运行期间,将某段代码动态的切入到指定方法的指定位置,将这种编程方式称为面向切面编程 AOP使用场景 : 日志 事务 使用AOP的好处是: ...

随机推荐

  1. mybatis的批量删除

    公司工程用的是Mybatis的example的类,自动生成了对数据库的操作,批量操作的今天用到了,两种方式,一种需要拓展它生成的类,另一种自带的. 批量删除的id是以集合List传递 id以List& ...

  2. Android onLowMemory()和onTrimMemory()

    1. OnLowMemory OnLowMemory是Android提供的API,在系统内存不足,所有后台程序(优先级为background的进程,不是指后台运行的进程)都被杀死时,系统会调用OnLo ...

  3. mysql中文坑爹的东西

    1.首先pb需要utf8格式,所以由ansi转换成utf8格式才能传输 2.在接收端,需要将utf8转换成ansi才能用 3.在插入数据库之前需要将sql从ansi转换成utf8格式 4.数据库设置字 ...

  4. re正则表达式16_managing complex regexes

    Managing Complex Regexes Regular expressions are fine if the text pattern you need to match is simpl ...

  5. Xcode如何编译Debug版和Release版

    在Run和Stop按钮的右边有一个工程名 点击工程名,选择Manage Schemes 选择Edit... 左侧选择Run ProjectName.app 右侧选择Info页,在Build Confi ...

  6. 使用 Object.create 创建对象,super 关键字,class 关键字

    ECMAScript 5 中引入了一个新方法:Object.create().可以调用这个方法来创建一个新对象.新对象的原型就是调用 create 方法时传入的第一个参数: var a = {a: 1 ...

  7. js日历插件 中文、英文日历

    日历插件 来源网站:http://www.cnblogs.com/yank/archive/2008/08/14/1267746.html 六款英文日历 http://www.bobd.cn/desi ...

  8. Robot Framework--10 万能的evaluate

    转自:http://blog.csdn.net/tulituqi/article/details/10124559 这一讲我们重点来介绍一下一个常用的关键字evaluate. 我觉得这个关键字在RF里 ...

  9. Apache源码包安装和子配置文件介绍--update.2014-12-5

    安装apache: 官网:http://httpd.apache.org/download.cgi#apache24 1.wget http://mirror.bit.edu.cn/apache//h ...

  10. Lua 之数据结构

    Lua 之数据结构 数组 通过整数下标访问的table中的元素,即是数组,下标默认从1开始. 一个创建二维数组的例子: mt = {} , do mt[i] = {} , do mt[i][j] = ...