共计 9952 个字符,预计需要花费 25 分钟才能阅读完成。
面向切面编程
所谓的面向切面编程(AOP),原理就是在不更改正常业务的流程的前提下,通过一个动态代理类,实现对目标对象嵌入的附加的操作。
简单说,就是在不影响我们现在正常业务的情况下,对某些类的某些方法嵌入操作。我们可以很通俗的理解一个方法可以有方法前和方法后这两个切面,当然还可以把方法执行过程看过一个整的切面去 hook。
在我们的 iOS 开发中,AOP 的实现方法就是使用 Runtime 的 Swizzling Method 改变 selector 指向的实现,在新的实现中添加新的操作,执行完新实现之后,再处理之前的实现逻辑。
Aspects
Aspects 是 iOS 平台比较成熟的 AOP 的框架,这次我们主要来研究一下这个库的源码。
基于 Aspects 1.4.1 版本。
总览
Aspects 给出了两个方法,一个类方法一个实例方法,使用起来非常简单。
+ (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;
传参也很容易理解,selector 自然就是我们要 hook 的方法,options 使我们要 hook 的位置,下面具体再说,block 是一个回调,也就是我们所说的要嵌入的代码逻辑,error 就是 hook 失败,当然了失败的原因较多,我们下面会提到。
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.
};
options 也是一个枚举的类型,看一下里面定义的字段就很容易明白了,AspectPositionAfter 是表示嵌入的放大要在被 hook 方法原来逻辑之前之后执行,AspectPositionBefore 是之前执行,AspectPositionInstead 表示要用嵌入的代码替换掉之前的逻辑,AspectOptionAutomaticRemoval 表示 hook 执行后,移除 hook。
因为是 NS_OPTIONS 类型,可多选。
重要的类
除了上述核心的方法是通过 NSObject 的 Category 的方式给出,还有以下几个类比较重要。
AspectsContainer
: 一个对象或者类的所有的 Aspects 整体情况
AspectIdentifier
: 一个 Aspects 的具体内容,这里主要包含了单个的 aspect 的具体信息,包括执行时机,要执行 block 所需要用到的具体信息:包括方法签名、参数等等
AspectInfo
: 一个 Aspect 执行环境,主要是 NSInvocation 信息。
核心方法 aspect_add
Aspects 给出的两个方法最终都是调用了aspect_add
。
static id aspect_add(id self, SEL selector, AspectOptions options, id block, NSError **error) {NSCParameterAssert(self);
NSCParameterAssert(selector);
NSCParameterAssert(block);
__block AspectIdentifier *identifier = nil;
aspect_performLocked(^{
// 是否允许 hook -
if (aspect_isSelectorAllowedAndTrack(self, selector, options, error)) {// 取到 sel 对应的 container (一个类不同 sel 对应不同的 container?)
AspectsContainer *aspectContainer = aspect_getContainerForObject(self, selector);
identifier = [AspectIdentifier identifierWithSelector:selector object:self options:options block:block error:error];
if (identifier) {[aspectContainer addAspect:identifier withOptions:options];
// Modify the class to allow message interception 拦截.
aspect_prepareClassAndHookSelector(self, selector, error);
}
}
});
return identifier;
}
__block AspectIdentifier *identifier = nil;
每次在添加 hook 的时候,都会先创建一个 AspectIdentifier。
__block 是为了能在下面的 block 中修改 identifier。
aspect_performLocked
是封装了一个自旋锁。
在自旋锁中会有一个 if 语句来判断 selector 是否能被 hook。那我们就先来看一下是否能被 hook 的判断方法aspect_isSelectorAllowedAndTrack
aspect_isSelectorAllowedAndTrack
-
黑名单
aspect_isSelectorAllowedAndTrack
方法中维护了一个 NSSet,在初始化的时候加入了一些方法名,在源码中是下面这些。[NSSet setWithObjects:@"retain", @"release", @"autorelease", @"forwardInvocation:", nil];
这说明了后面这几个方法是不允许被 hook 的,如果 hook 了这些方法会有错误的信息提示。
- dealloc 方法
在 hook 的方法中,dealloc 属于一个特殊情况,因为这个方法是在对象要被销毁的时候创建,所以 Aspects 为了安全起见,在 hook dealloc 方法的时候。options 只允许时 AspectPositionBefore,也就是插入的逻辑只能在 dealloc 原有逻辑之前处理,不允许替换或者在 dealloc 之后。
- 没找到方法
这个就没啥疑问了,如果我们 hook 了一个根本不存在的方法也会有错误提示。
-
方法只允许 hook 一次(元类相关)
这个有点麻烦,因为从错误提示的枚举来看,他是对应
AspectErrorSelectorAlreadyHookedInClassHierarchy
这一项的,从字面意思来看是说方法已经被 hook 了。最开始我对这个类的层级不是很明白,我的最初理解的类的层级是父类和子类不能同时 hook,经过验证这种理解是错误的。
后来我仔细地看了一下源码,他的元类判断中是这么写的:
if (class_isMetaClass(object_getClass(self))) {}
判断的是 object_getClass(self),通过 runtime 的源码我们可以知道,object_getClass 得到的是传参的 isa 指针指向的结构,意识就是 self 如果是对象,object_getClass(self)得到的是对应的类,如果 self 是类,那就得到了元类。
那什么时候会提示这个错误呢,我举一个例子吧。我创建了一个
TestHookViewController
类,继承自UIViewController
,如果我在TestHookViewController
中像下面这样写就会有错误提示了。[[UIViewController class] aspect_hookSelector:@selector(viewWillAppear:) withOptions:0 usingBlock:^(id<AspectInfo> info, BOOL animated) {NSLog(@"%s",__func__); } error:NULL]; [[TestHookViewController class] aspect_hookSelector:@selector(viewWillAppear:) withOptions:0 usingBlock:^(id<AspectInfo> info, BOOL animated) {NSLog(@"%s",__func__); } error:NULL];
当然了,这两个 hook 的前后位置不同,打印台输出的提示也是不一样的,虽然都是一个类层级方法只允许 hook 一次的错误原因。这个大家自行尝试一下。
if 语句里面就是关于方法是否重复 hook 的判断逻辑,这里牵扯到一个相关类,AspectTracker。我们现在就来看一下这个类。
AspectTracker 类
虽然是说 AspectTracker 类,但是代码结构还是接着上面的咱们说到的位置继续往下走。
因为 AspectTracker 主要就是用在方法只允许 hook 一次的判断中。
Aspects 维护了一个字典,来储存被 hook 方法的类和对应的 AspectTracker。
Class currentClass = [self class];
AspectTracker *tracker = swizzledClassesDict[currentClass];
通过上面的方式取到对应的 AspectTracker。这里提一句,这里的代码我们应该先看下面的,要先了解 AspectTracker 是怎么存储到字典里面的。
这里我们要看下面这个 do-while 循环
currentClass = klass;
AspectTracker *subclassTracker = nil;
do {tracker = swizzledClassesDict[currentClass];
if (!tracker) {tracker = [[AspectTracker alloc] initWithTrackedClass:currentClass];
swizzledClassesDict[(id<NSCopying>)currentClass] = tracker;
}
if (subclassTracker) {[tracker addSubclassTracker:subclassTracker hookingSelectorName:selectorName];
} else {[tracker.selectorNames addObject:selectorName];
}
// All superclasses get marked as having a subclass that is modified.
subclassTracker = tracker;
}while ((currentClass = class_getSuperclass(currentClass)));
如果最开始没有 tracker,会初始化一个,然后存到字典中。最开始的时候 subclassTracker 为 nil,所以 selector 会 add 到 tracker.selectorNames。
然后 currentClass 重新赋值
currentClass = class_getSuperclass(currentClass)
再次执行 do 逻辑里面的代码,这次 subclassTracker 就有值了(上一次循环的 tracker),就会执行[tracker addSubclassTracker:subclassTracker hookingSelectorName:selectorName];
我们进到 addSubclassTracker
源码中可以看到,tracker 被存到 selectorNamesToSubclassTrackers
这个字典中,关键的是这个字典的 key 是 selectorName,value 是一个集合,tracker 是放在这个集合里面的。为什么要通过集合来存 tracker 呢?
因为这里是有子类的情况的,一个类的子类可能有多个,如果在不同的子类中 hook 了这个父类的一个方法,也就是父类中的这一个 selector 被多次 hook,所以也会有不同的 tracker,所以使用一个集合来储存。
其实说到这个地方大家就差不多可以理解了,如果满足了 if (class_isMetaClass(object_getClass(self)))
这个判断,我们会把这个类 hook 的方法通过封装为 AspectTracker
来进行记录,当然包括他的层层父类,都对对应一个AspectTracker
,而且父类中的还会记录子类中 hook 的方法。这部分代码最好是 debug 跟一下,会明显一点。
上面我们先看了 tracker 是怎样被存起来的,接来下再来看关于只能被 hook 一次的判断。
首先要判断子类中是否 hook
if ([tracker subclassHasHookedSelectorName:selectorName]) {// 内部省略}
subclassHasHookedSelectorName
内部实现很简单
- (BOOL)subclassHasHookedSelectorName:(NSString *)selectorName {return self.selectorNamesToSubclassTrackers[selectorName] != nil;
}
就是查询一下 selectorNamesToSubclassTrackers 这个字典中,通过 seletorName 是否能取到值,上面已经说过了,这个字典中通过 key:selectorName value: set 的方法储存了子类的 tracker。
如果能取到值,就说明子类中已经 hook 了这个方法了,父类中就不能在 hook。
如果没能取到值,说明当前类可能就是个子类,此时需要看一下他的父类中是否 hook 了这个 selector,所以就会执行下面的的 do-while 循环。此处的代码就不展示了。
但这个位置初步的关于方法能否被 hook 就已经判断完了。如果可以 seletor 可以被 hook,继续 if 里面的代码。
Swizzling Method
我们直接说 Swizzling Method 这一最重要的逻辑吧。
Swizzling Method 主要有两部分,一个是对对象的 forwardInvocation 进行 swizzling,另一个是对传入的 selector 进行 swizzling。
我们来看 aspect_prepareClassAndHookSelector
方法的源码。
替换 forwardInvocation
首先是Class klass = aspect_hookClass(self, error);
static Class aspect_hookClass(NSObject *self, NSError **error) {NSCParameterAssert(self);
Class statedClass = self.class;
Class baseClass = object_getClass(self);
NSString *className = NSStringFromClass(baseClass);
//
// 省略一部分代码
//
//
if (subclass == nil) {
// 动态创建子类 +
subclass = objc_allocateClassPair(baseClass, subclassName, 0);
if (subclass == nil) {NSString *errrorDesc = [NSString stringWithFormat:@"objc_allocateClassPair failed to allocate class %s.", subclassName];
AspectError(AspectErrorFailedToAllocateClassPair, errrorDesc);
return nil;
}
// 替换 forwardInvocation 方法
aspect_swizzleForwardInvocation(subclass);
// subclass 的 class 方法交替换 替换为 statedClass 的 class 方法 subclass 元类也替换
aspect_hookedGetClass(subclass, statedClass);
aspect_hookedGetClass(object_getClass(subclass), statedClass);
objc_registerClassPair(subclass);
}
// 改变当前类的 isa 指针指向
object_setClass(self, subclass);
return subclass;
}
我们从源码中可以看出逻辑,主要是动态创建了一个 subclass,名为 subclass,其实最终把我们 hook 的类的 isa 指针指向了这个 subclass,实为是一个父类。
aspect_swizzleForwardInvocation(subclass);
上面这个方法是替换了 forwardInvocation:
方法。
当然了,也是替换的这个 subclass 的 forwardInvocation:
方法,把 forwardInvocation:
替换为 __ASPECTS_ARE_BEING_CALLED__
这个方法,主要的 hook 后的代码执行处理逻辑都在这个 __ASPECTS_ARE_BEING_CALLED__
中。
在替换了 aspect_hookClass
方法之后,同时修改了 subclass 以及其 subclass metaclass 的 class 方法,使他返回当前对象的 class。这个地方有点绕,其实最终目的就是把所有的 swizzling 都放到了这个 subclass 中处理,不影响原来的类,而且对于外部的使用者,又可以把它当做原对象使用。
替换 selector
执行完 aspect_hookClass
完之后,forwardInvocation:
方法已经被替换,下面会执行 swizzling selector 的代码。
在 swizzling selector 的时候,将 selector 指向了消息转发 IMP,同时生成一个 aliasSelector,指向原方法的 IMP。
这里代码就不往外粘了。
处理 forwardInvocation
其实上面已经把整个过程分析完了,我们也知道,最后转发的代码最终会在 __ASPECTS_ARE_BEING_CALLED__
函数的处理中。所以最后我们来看看这个函数就可以了。
static void __ASPECTS_ARE_BEING_CALLED__(__unsafe_unretained NSObject *self, SEL selector, NSInvocation *invocation) {NSCParameterAssert(self);
NSCParameterAssert(invocation);
SEL originalSelector = invocation.selector;
SEL aliasSelector = aspect_aliasForSelector(invocation.selector);
invocation.selector = aliasSelector;
AspectsContainer *objectContainer = objc_getAssociatedObject(self, aliasSelector);
AspectsContainer *classContainer = aspect_getContainerForClass(object_getClass(self), aliasSelector);
AspectInfo *info = [[AspectInfo alloc] initWithInstance:self invocation:invocation];
NSArray *aspectsToRemove = nil;
// Before hooks.
aspect_invoke(classContainer.beforeAspects, info);
aspect_invoke(objectContainer.beforeAspects, info);
// Instead hooks.
BOOL respondsToAlias = YES;
if (objectContainer.insteadAspects.count || classContainer.insteadAspects.count) {aspect_invoke(classContainer.insteadAspects, info);
aspect_invoke(objectContainer.insteadAspects, info);
}else {Class klass = object_getClass(invocation.target);
do {if ((respondsToAlias = [klass instancesRespondToSelector:aliasSelector])) {[invocation invoke]; // 根据 aliasSelector 找到之前的逻辑 执行
break;
}
}while (!respondsToAlias && (klass = class_getSuperclass(klass)));
}
// After hooks.
aspect_invoke(classContainer.afterAspects, info);
aspect_invoke(objectContainer.afterAspects, info);
// If no hooks are installed, call original implementation (usually to throw an exception)
// 没有找到之前方法的实现 - 消息转发
if (!respondsToAlias) {
invocation.selector = originalSelector;
SEL originalForwardInvocationSEL = NSSelectorFromString(AspectsForwardInvocationSelectorName);
if ([self respondsToSelector:originalForwardInvocationSEL]) {((void( *)(id, SEL, NSInvocation *))objc_msgSend)(self, originalForwardInvocationSEL, invocation);
}else {[self doesNotRecognizeSelector:invocation.selector];
}
}
// Remove any hooks that are queued for deregistration.
[aspectsToRemove makeObjectsPerformSelector:@selector(remove)];
}
从源码中很容易看出来,分别处理不同的 hook 点,然后中间有根据
aliasSelector 找到之前方法的实现,然后执行。
小结
初步的源码分析就是这个样子,没有太关注一些细节,也存在一些自己现在还不是很熟悉的处理方式,毕竟涉及到太多的 swizzling,消息转发一类的方法,这一块的只是需要后期在多研究 runtime 来提高。
代码中有一些其他的比较小的方法没有讲到,大家自己自行看一下。
参考
https://wereadteam.github.io/…
http://blog.ypli.xyz/ios/aop-…
https://blog.csdn.net/weixin_…