前言
在“Runtime病院”住院的后两天,分析了一下AOP的实现原理。“出院”后,发现Aspect库还没有详细分析,于是就有了这篇文章,今天就来说说iOS 是如何实现Aspect Oriented Programming。
目录
- 1.Aspect Oriented Programming简介
- 2.什么是Aspects
- 3.Aspects 中4个基本类 解析
- 4.Aspects hook前的准备工作
- 5.Aspects hook过程详解
- 6.关于Aspects的一些 “坑”
一.Aspect Oriented Programming简介
面向切面的程序设计(aspect-oriented programming,AOP,又译作面向方面的程序设计、观点导向编程、剖面导向程序设计)是计算机科学中的一个术语,指一种程序设计范型。该范型以一种称为侧面(aspect,又译作方面)的语言构造为基础,侧面是一种新的模块化机制,用来描述分散在对象、类或函数中的横切关注点(crosscutting concern)。
侧面的概念源于对面向对象的程序设计的改进,但并不只限于此,它还可以用来改进传统的函数。与侧面相关的编程概念还包括元对象协议、主题(subject)、混入(mixin)和委托。
AOP通过预编译方式和运行期动态代理实现程序功能的统一维护的一种技术。
OOP(面向对象编程)针对业务处理过程的实体及其属性和行为进行抽象封装,以获得更加清晰高效的逻辑单元划分。
AOP则是针对业务处理过程中的切面进行提取,它所面对的是处理过程中的某个步骤或阶段,以获得逻辑过程中各部分之间低耦合性的隔离效果。
OOP和AOP属于两个不同的“思考方式”。OOP专注于对象的属性和行为的封装,AOP专注于处理某个步骤和阶段的,从中进行切面的提取。
举个例子,如果有一个判断权限的需求,OOP的做法肯定是在每个操作前都加入权限判断。那日志记录怎么办?在每个方法的开始结束的地方都加上日志记录。AOP就是把这些重复的逻辑和操作,提取出来,运用动态代理,实现这些模块的解耦。OOP和AOP不是互斥,而是相互配合。
在iOS里面使用AOP进行编程,可以实现非侵入。不需要更改之前的代码逻辑,就能加入新的功能。主要用来处理一些具有横切性质的系统性服务,如日志记录、权限管理、缓存、对象池管理等。
二. 什么是Aspects
Aspects是一个轻量级的面向切面编程的库。它能允许你在每一个类和每一个实例中存在的方法里面加入任何代码。可以在以下切入点插入代码:before(在原始的方法前执行) / instead(替换原始的方法执行) / after(在原始的方法后执行,默认)。通过Runtime消息转发实现Hook。Aspects会自动的调用super方法,使用method swizzling起来会更加方便。
这个库很稳定,目前用在数百款app上了。它也是PSPDFKit的一部分,PSPDFKit是一个iOS 看PDF的framework库。作者最终决定把它开源出来。
三.Aspects 中4个基本类 解析
我们从头文件开始看起。
1.Aspects.h
1 2 3 4 5 6 7 |
typedef NS_OPTIONS(NSUInteger, AspectOptions) { AspectPositionAfter = 0, /// Called after the original implementation (default) AspectPositionInstead = 1, /// Will replace the original implementation. AspectPositionBefore = 2, /// Called before the original implementation. AspectOptionAutomaticRemoval = 1 << 3 /// Will remove the hook after the first execution. }; |
在头文件中定义了一个枚举。这个枚举里面是调用切片方法的时机。默认是AspectPositionAfter在原方法执行完之后调用。AspectPositionInstead是替换原方法。AspectPositionBefore是在原方法之前调用切片方法。AspectOptionAutomaticRemoval是在hook执行完自动移除。
1 2 3 4 5 |
@protocol AspectToken - (BOOL)remove; @end |
定义了一个AspectToken的协议,这里的Aspect Token是隐式的,允许我们调用remove去撤销一个hook。remove方法返回YES代表撤销成功,返回NO就撤销失败。
1 2 3 4 5 6 7 |
@protocol AspectInfo - (id)instance; - (NSInvocation *)originalInvocation; - (NSArray *)arguments; @end |
又定义了一个AspectInfo协议。AspectInfo protocol是我们block语法里面的第一个参数。
instance方法返回当前被hook的实例。originalInvocation方法返回被hooked方法的原始的invocation。arguments方法返回所有方法的参数。它的实现是懒加载。
头文件中还特意给了一段注释来说明Aspects的用法和注意点,值得我们关注。
1 2 3 4 5 |
/** Aspects uses Objective-C message forwarding to hook into messages. This will create some overhead. Don't add aspects to methods that are called a lot. Aspects is meant for view/controller code that is not called a 1000 times per second. Adding aspects returns an opaque token which can be used to deregister again. All calls are thread safe. */ |
Aspects利用的OC的消息转发机制,hook消息。这样会有一些性能开销。不要把Aspects加到经常被使用的方法里面。Aspects是用来设计给view/controller 代码使用的,而不是用来hook每秒调用1000次的方法的。
添加Aspects之后,会返回一个隐式的token,这个token会被用来注销hook方法的。所有的调用都是线程安全的。
关于线程安全,下面会详细分析。现在至少我们知道Aspects不应该被用在for循环这些方法里面,会造成很大的性能损耗。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
@interface NSObject (Aspects) /// Adds a block of code before/instead/after the current `selector` for a specific class. /// /// @param block Aspects replicates the type signature of the method being hooked. /// The first parameter will be `id`, followed by all parameters of the method. /// These parameters are optional and will be filled to match the block signature. /// You can even use an empty block, or one that simple gets `id`. /// /// <a href="http://www.jobbole.com/members/smartsl">@note</a> Hooking static methods is not supported. /// @return A token which allows to later deregister the aspect. + (id)aspect_hookSelector:(SEL)selector withOptions:(AspectOptions)options usingBlock:(id)block error:(NSError **)error; /// Adds a block of code before/instead/after the current `selector` for a specific instance. - (id)aspect_hookSelector:(SEL)selector withOptions:(AspectOptions)options usingBlock:(id)block error:(NSError **)error; @end |
Aspects整个库里面就只有这两个方法。这里可以看到,Aspects是NSobject的一个extension,只要是NSObject,都可以使用这两个方法。这两个方法名字都是同一个,入参和返回值也一样,唯一不同的是一个是加号方法一个是减号方法。一个是用来hook类方法,一个是用来hook实例方法。
方法里面有4个入参。第一个selector是要给它增加切面的原方法。第二个参数是AspectOptions类型,是代表这个切片增加在原方法的before / instead / after。第4个参数是返回的错误。
重点的就是第三个入参block。这个block复制了正在被hook的方法的签名signature类型。block遵循AspectInfo协议。我们甚至可以使用一个空的block。AspectInfo协议里面的参数是可选的,主要是用来匹配block签名的。
返回值是一个token,可以被用来注销这个Aspects。
注意,Aspects是不支持hook 静态static方法的
1 2 3 4 5 6 7 8 9 10 11 12 13 |
typedef NS_ENUM(NSUInteger, AspectErrorCode) { AspectErrorSelectorBlacklisted, /// Selectors like release, retain, autorelease are blacklisted. AspectErrorDoesNotRespondToSelector, /// Selector could not be found. AspectErrorSelectorDeallocPosition, /// When hooking dealloc, only AspectPositionBefore is allowed. AspectErrorSelectorAlreadyHookedInClassHierarchy, /// Statically hooking the same method in subclasses is not allowed. AspectErrorFailedToAllocateClassPair, /// The runtime failed creating a class pair. AspectErrorMissingBlockSignature, /// The block misses compile time signature info and can't be called. AspectErrorIncompatibleBlockSignature, /// The block signature does not match the method or is too large. AspectErrorRemoveObjectAlreadyDeallocated = 100 /// (for removing) The object hooked is already deallocated. }; extern NSString *const AspectErrorDomain; |
这里定义了错误码的类型。出错的时候方便我们调试。
2.Aspects.m
1 2 3 4 |
#import "Aspects.h" #import #import #import |
#import 导入这个头文件是为了下面用到的自旋锁。#import 和 #import 是使用Runtime的必备头文件。
1 2 3 4 |
typedef NS_OPTIONS(int, AspectBlockFlags) { AspectBlockFlagsHasCopyDisposeHelpers = (1 << 25), AspectBlockFlagsHasSignature = (1 << 30) }; |
定义了AspectBlockFlags,这是一个flag,用来标记两种情况,是否需要Copy和Dispose的Helpers,是否需要方法签名Signature 。
在Aspects中定义的4个类,分别是AspectInfo,AspectIdentifier,AspectsContainer,AspectTracker。接下来就分别看看这4个类是怎么定义的。
3. AspectInfo
1 2 3 4 5 6 |
@interface AspectInfo : NSObject - (id)initWithInstance:(__unsafe_unretained id)instance invocation:(NSInvocation *)invocation; @property (nonatomic, unsafe_unretained, readonly) id instance; @property (nonatomic, strong, readonly) NSArray *arguments; @property (nonatomic, strong, readonly) NSInvocation *originalInvocation; @end |
AspectInfo对应的实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
#pragma mark - AspectInfo @implementation AspectInfo @synthesize arguments = _arguments; - (id)initWithInstance:(__unsafe_unretained id)instance invocation:(NSInvocation *)invocation { NSCParameterAssert(instance); NSCParameterAssert(invocation); if (self = [super init]) { _instance = instance; _originalInvocation = invocation; } return self; } - (NSArray *)arguments { // Lazily evaluate arguments, boxing is expensive. if (!_ar |