聊聊 AOP 模式与 ObjC 对它的实现
ic8811
8年前
<p>说到 AOP 可能有些经验的小伙伴都有所了解,今天我们来聊聊关于它的内容,以及 ObjC 中如何实现它。</p> <h2>AOP 简述</h2> <p>在聊具体实现之前,我们先从设计层面介绍一下 AOP 的由来,以及它解决了什么问题。 了解这个模式后,其实它可以用到任何的语言实现中。 AOP 的全称叫做 <strong>Aspect-oriented programming</strong> ,维基百科上面有对它的完整解释: </p> <p>按照字面翻译过来,它叫做面向方面编程。是不是听起来怪怪的,不好理解。 下面我们就用最简单的语言来描述一下。 所谓 AOP 其实就是给你的程序提供一个可拆卸的组件化能力。 这么说可能还不够直观,咱们直接上代码。</p> <p>比如你的 APP 需要用到事件统计功能,这个场景应该大多数伙伴都会遇到过。 无论你是用 UMeng, Google Analytics, 还是其他的统计平台等等, 你应该都会写过类似的代码:</p> <pre> <code class="language-objectivec">- (void)viewDidLoad { [super viewDidLoad]; [Logger log:@"View Did Load"]; UILabel *label = [[UILabel alloc] init]; [self.view addSubview:label]; } </code></pre> <p>上面这段代码会在试图控制器开始加载的时候,用 Logger 类记录一个统计事件。 这些统计相关的代码是我们为了完成我们记录 APP 运行数据这个逻辑的。 其实 viewDidLoad 方法本身的逻辑并不是为了完成统计,而是进行一些初始化操作, 比如这里初始化了 UILabel。</p> <p>这就导致了一个设计上的瑕疵, 数据统计的代码和我们实际的业务逻辑代码混杂在一起了。 加入过了一段时间后, 你再回过头来要改业务逻辑代码的时候,你看到 Logger 的统计代码混杂在一起,就必须要花点时间将他们的含义区分出来。</p> <p>随着业务逻辑代码不断增多,类似的混杂也会越来越多,这样的耦合势必会增加维护的成本。正式因为这个原因,所以诞生了 AOP 模式。 AOP 其实就是在不影响程序整体功能的情况下,将 Logger 这样的逻辑,从主业务逻辑中抽离出来的能力。</p> <p>简单来说, 有了 AOP 之后, 我们的业务逻辑代码就变成了这样:</p> <pre> <code class="language-objectivec">- (void)viewDidLoad { [super viewDidLoad]; UILabel *label = [[UILabel alloc] init]; [self.view addSubview:label]; } </code></pre> <p>这里不再会出现 Logger 的统计逻辑的代码,但是统计功能依然是生效的。 当然,不出现在主业务代码中,不代表统计代码就消失了。 而是用 AOP 模式 hook 到别的地方去了。</p> <h2>ObjC 对 AOP 的实现</h2> <p>对 AOP 做了一个基本的介绍后,咱们就来看看如何实现它吧, 这里只举了 ObjC 的例子,但 AOP 是一个模式,理论上大多数语言都可以实现这个模式。</p> <p>ObjC 中实现 AOP 最直接的方法就是使用 runtime 中的 Method Swizzling。 </p> <p>还回到我们之前的 viewDidLoad 方法中,前面我们提到使用 AOP 后, viewDidLoad 方法中就会只包含业务逻辑代码,而和主业务不相干的统计代码就被剥离出去了。 具体是怎么做到的呢,我们看下面的代码:</p> <pre> <code class="language-objectivec">#import <objc/runtime.h> #import "ViewController.h" #import "Logger.h" @interfaceAOPHelper:NSObject + (void) setup; @end @implementationAOPHelper IMP originalViewDidLoadIMP; void replacedViewDidLoad() { originalViewDidLoadIMP(); [Logger log:@"View Did Load"]; } + (void)setup { originalViewDidLoadIMP = class_getMethodImplementation([ViewController class], @selector(viewDidLoad)); Method originalViewDidLoad = class_getInstanceMethod([ViewController class], @selector(viewDidLoad)); method_setImplementation(originalViewDidLoad, (IMP) replacedViewDidLoad); } @end </code></pre> <p>AOPHelper 所做的工作就是 hook 我们主业务的逻辑, 首先通过 class_getMethodImplementation 得到 viewDidLoad 原始的实现, 也就是包含业务代码的那个方法实现。 然后通过 class_getInstanceMethod 和 method_setImplementation 两个方法用我们自己的实现 replacedViewDidLoad 替换掉原有的 viewDidLoad 实现。</p> <p>再来看看我们自己的 replacedViewDidLoad:</p> <pre> <code class="language-objectivec">void replacedViewDidLoad() { originalViewDidLoadIMP(); [Logger log:@"View Did Load"]; } </code></pre> <p>这里面加入了 Logger 类的统计逻辑, 并且也通过 originalViewDidLoadIMP 调用了原有的业务逻辑实现。 originalViewDidLoadIMP 是通过前面 class_getMethodImplementation 方法取得的。</p> <p>概括的来说,就是我们先将业务逻辑代码和统计代码分成两个组件, 然后在 AOPHelper 中将他们组合起来。 这样两个逻辑在代码层面各司其职,互不影响。 而在运行时,通过 AOP 的 hook 机制又将他们组合了起来。</p> <h2>额外的收益</h2> <p>使用了 AOP 之后,还会有一些额外的好处。 比如,如果你在哪一天想换一个统计平台, 那么你不需要到处改代码了, 只需要把统计层面的代码修改一下就可以。 再比如,你想打一个不带统计能力的安装包,只需要将 hook 的部分去掉,就自动去掉统计逻辑了。 当想用的时候,再把它装回来即可。 这个感觉就像是乐高积木一样,通过 AOP 可以把某些能力变成一个模块,即插即用。</p> <p>刚才说的不带统计安装包的场景其实在我们的真是环境中还是存在的,比如一个开发团队在初期测试自己的 APP 功能的时候,就不希望测试时候的会话计入统计数据中。</p> <p>当然,AOP 带来收益的同时,也会有一些损失,比如在写代码的时候就不那么直观了。 这就取决于你对项目整体设计的权衡了,是健壮性优先,还是开发便捷性优先。根据不同的个人风格,不同的项目规模各自权衡。</p> <h2>结语</h2> <p>AOP 作为一种设计模式,他能为需要这种设计的项目解决实际中的问题。 你的项目中是否也要考虑使用 AOP 呢,我的建议是对于稍大些并且后期有维护价值的项目就值得使用。</p> <p>Aspects, 可以不需要繁琐的手工调用 Method Swizzling。 后面我们会继续介绍 Aspects 这个库。</p> <p>如果你觉得这篇文章有帮助,还可以关注微信公众号 swift-cafe,会有更多我的原创内容分享给你~</p> <p> </p> <p>来自:http://www.swiftcafe.io/2017/01/03/objc-aop/</p> <p> </p>