AOP( Aspect-OrientedProgramming): 面向切面编程
何为面向切面编程?
在运行时,动态地将代码切入到类的指定方法、指定位置上的编程思想就是面向切面的编程。
OOP(Object-oriented programming):面向对象编程
何为面向对象编程?
面向对象编程是一种实现的方法,在这种方法中,程序被组织成许多组互相协作的对象,每个对象代表某个类的一个实例,而类则属于一个通过继承关系形成的层次结构。
AOP&OOP
面向对象的特点是继承、多态和封装。而封装就要求将功能分散到不同的对象中去,这在软件设计中往往称为职责分配。实际上也就是说,让不同的类设计不同的方法。这样代码就分散到一个个的类中去了。这样做的好处是降低了代码的复杂程度,使类可重用。
一般而言,我们管切入到指定类指定方法的代码片段称为切面,而切入到哪些类、哪些方法则叫切入点。有了AOP,我们就可以把几个类共有的代码,抽取到一个切片中,等到需要时再切入对象中去,从而改变其原有的行为。
这样看来,AOP其实只是OOP的补充而已。OOP从横向上区分出一个个的类来,而AOP则从纵向上向对象中加入特定的代码。有了AOP,OOP变得立体了。
事务拦截,安全可变容器
iOS中有各类容器的概念,容器分可变容器和非可变容器,可变容器一般内部在实现上是一个链表,在进行各类(insert 、remove、 delete、 update )难免有空操作、指针越界的问题。
最粗暴的方式就是在使用可变容器的时间,每次操作都必须手动做空判断、索引比较这些操作:
1 | NSMutableDictionary *dic = [[NSMutableDictionary alloc] init]; |
在代码中大量的使用这鞋操作实在是太过于繁琐了,试想如果可变容器自身如何能做这些兼容岂不是更好。可能会想到继承的方法来解决,但是项目中尽可能的避免过多的派生(至于派生的弊端这里就不多说了);或者想到分类,这里也不尽人意。
Method Swizzling 移花接木
runtime 这里就不多多说了(swift里面已经对这个概念的说法从心转变成了 Reflection<反射>),objective c中每个方法的名字(SEL)跟函数的实现(IMP)是一一对应的,Swizzle的原理只是在这个地方做下手脚,将原来方法名与实现的指向交叉处理,就能达到一个新的效果。
废话少说,直接上代码:
这里使用NSMutableArray 做实例,为NSMutableArray追加一个新的方法
1 | @implementation NSMutableArray (safe) |
1 | - (void)swizzleMethod:(SEL)origSelector withMethod:(SEL)newSelector |
这里唯一可能需要解释的是 class_addMethod 。要先尝试添加原 selector 是为了做一层保护,因为如果这个类没有实现 originalSelector ,但其父类实现了,那 class_getInstanceMethod 会返回父类的方法。这样 method_exchangeImplementations 替换的是父类的那个方法,这当然不是你想要的。所以我们先尝试添加 orginalSelector ,如果已经存在,再用 method_exchangeImplementations 把原方法的实现跟新的方法实现给交换掉。
safeAddObject 代码看起来可能有点奇怪,像递归不是么。当然不会是递归,因为在 runtime 的时候,函数实现已经被交换了。调用 objectAtIndex: 会调用你实现的 safeObjectAtIndex:,而在 NSMutableArray: 里调用 safeObjectAtIndex: 实际上调用的是原来的 objectAtIndex: 。
如此以来,一直担心的问题就迎刃而解了,不仅在可变数组、可变字典等容器内都可以做自己想做的事情。
Aspects 一个基于Objective-c的AOP开发框架
- Aspects 使用姿势:
1 | [UIViewController aspect_hookSelector:@selector(viewWillAppear:) withOptions:AspectPositionAfter usingBlock:^(id<AspectInfo> aspectInfo, BOOL animated) { |
前插、后插、替换某个方法都可以。使用类的方式很简单,NSClassFromString 即可,Selector 也一样 NSSelectorFromString,这样就能通过外部传入 String,内部动态构造 Class 和 Selector 来达到 Fix 的效果了。
这种方式的安全性在于:
- 不需要中间 JS 文件,准备工作全部在 Native 端完成。
- 没有使用 App Store 不友好的类/方法。
假设线上运行这这样一个 Class,由于疏忽,没有对参数做检查,导致特定情况下会 Crash。
1 | @interface MightyCrash: NSObject |
现在我们要避免 Crash,就可以通过这种方式来修复
1 | [Felix fixIt]; |
运行一下看看
1 | MightyCrash *mc = [[MightyCrash alloc] init]; |
It Works, 是不是有那么点意思了。以下是可以正常运行的代码,仅供参考。
1 | #import <Aspects.h> |
- 业务埋点、日志打印分离
重构代码时经常会从一些问题入手,例如轻量级controller、MVVM等,这些无非是对原有逻辑进一步抽象、区分、分离,重新抽象数据模型、viewmodel;相关代码放入分类;考虑业务层次抽取剥离父类;mananger、factory等。经历一大翻工作controller 中代码终于减少了,但是仍旧留下一堆的埋点、日志log的相关代码。
Aspects是一个很不错的 AOP 库,封装了 Runtime , Method Swizzling 这些黑色技巧,只提供两个简单的API:
1 | + (id<aspecttoken>)aspect_hookSelector:(SEL)selector |
相对来说如果想要捕捉到viewDidAppear 的log打印,或者是页面PV的统计上报,我们从原有的业务中将这部分代码剥离出来,掉换IMP指向以后我们可以在usingBlock 内做自己想做的事情, 这样就能达到上述想要的目的了。
- Aspects扩展使用:
页面的PV统计,事件点击统计,可以事先写在配置文件里面:
1 | { @"MainViewController": @{ |