Aspects切面¶
不修改原来的函数,在函数的执行前后插入一些代码,就是面向切面(AOP)。
AOP:切面编程。业务逻辑隔离,代码耦合度降低。
面向对象的扩展
通过预编译的方式或者运行时期动态代理
继承¶
耦合程度高。
需要对基类修改。
AOP¶
分类:具体业务逻辑拆分
runtime。
SDWebImage。 Manager downloader和cache。下载功能逻辑。
埋点 打log:抽取一个基类:log方法。如果A类不具备log功能,继承基类,对ModuleA造成一定污染,入侵。
原理¶
Hook person的test方法,不是交换imp,而是先交换person中的ForwardInvocation,指向自定义的函数。
允许开发者在不修改原有代码结构的情况下,为程序添加额外的行为,比如日志记录、性能监控、事务处理、异常处理等。在 iOS 开发中,这通常是通过运行时(Runtime)方法交换(Method Swizzling)来实现的。
Aspects 库简化了面向切面编程的实现。Aspects 库通过 Objective-C 的动态特性,允许开发者在运行时拦截消息,并为其添加前置、后置或替换执行的代码。这里是它的基本原理:
-
消息发送机制:Objective-C 是一门动态语言,它的方法调用是通过消息发送(message sending)实现的。当你对一个对象发送消息时(即调用一个方法),Objective-C 的运行时系统会寻找这个消息对应的方法实现,并执行它。
-
Method Swizzling:这是 Objective-C 运行时的一个特性,允许开发者在运行时交换两个方法的实现。这意味着你可以将一个方法的实现动态地替换为另一个方法的实现。
-
Block(闭包):Aspects 使用了 Objective-C 的 block 语法来允许开发者定义在原方法执行前、执行后或替换原方法时要执行的代码块。
-
动态方法解析:Aspects 可能会使用 Objective-C 的动态方法解析来动态地添加方法实现,或者在某个方法被调用时进行拦截。
-
关联对象:Aspects 可能会使用 Objective-C 的关联对象(associated objects)功能来在运行时给现有类添加存储属性,这样可以存储需要在方法拦截中使用的信息。
当使用 Aspects 库时,你会指定一个切点(pointcut),这是你想要插入自定义行为的方法。然后,你会提供一个 block,当原始方法被调用时,这个 block 会被执行。Aspects 库会处理所有的运行时逻辑,确保当你的方法被调用时,你的自定义代码会被执行。
这种方式非常强大,但也需要谨慎使用,因为它改变了对象的正常行为,可能会导致难以追踪的问题。此外,由于它依赖于 Objective-C 的运行时特性,它可能不适用于纯 Swift 代码,或者在 Swift 代码中可能需要特别的桥接处理。
类¶
AspectOptions¶
是个枚举,用来定义切面的时机
typedef NS_OPTIONS(NSUInteger, AspectOptions) {
/// 原有方法调用前执行(default)
AspectPositionAfter = 0, /// Called after the original implementation (default)
/// 替换原有方法
AspectPositionInstead = 1, /// Will replace the original implementation.
/// 原有方法调用后执行
AspectPositionBefore = 2, /// Called before the original implementation.
/// 执行完之后就恢复切面操作,即撤销hook。只执行一次(调用完就删除切面逻辑)
AspectOptionAutomaticRemoval = 1 << 3 /// Will remove the hook after the first execution.
};
AspectInfo¶
- NSInvocation sign target 信息
AspectIdentifier¶
- runtime的封装 记录hook方法 block签名信息
- 每次hook都记录在了AspectIdentifier
就是一个model用来存储hook方法的原有方法、切面block、block方法签名、切面时机等
@interface AspectIdentifier : NSObject
+ (instancetype)identifierWithSelector:(SEL)selector object:(id)object options:(AspectOptions)options block:(id)block error:(NSError **)error;
- (BOOL)invokeWithInfo:(id<AspectInfo>)info;
@property (nonatomic, assign) SEL selector;//原来方法的SEL
@property (nonatomic, strong) id block;//保存要执行的切面block,即原方法执行前后要调用的方法
@property (nonatomic, strong) NSMethodSignature *blockSignature;//block的方法签名
@property (nonatomic, weak) id object;//target,即保存当前对象
@property (nonatomic, assign) AspectOptions options;//是个枚举,切面执行时机
@end
AspectsContainer¶
容器类,以关联对象的形式存储在当前类或对象中,主要用来存储当前类或对象所有的切面信息
- 容器 一个类有很多hook
- 3个数组 before、instead、after
@interface AspectsContainer : NSObject
- (void)addAspect:(AspectIdentifier *)aspect withOptions:(AspectOptions)injectPosition;
- (BOOL)removeAspect:(id)aspect;
- (BOOL)hasAspects;
@property (atomic, copy) NSArray *beforeAspects;//存储原方法 调用前要执行的操作
@property (atomic, copy) NSArray *insteadAspects;//存储替换原方法的操作
@property (atomic, copy) NSArray *afterAspects;//存储原方法调用后要执行的操作
@end
AspectTracker¶
追踪类 对象所有hook操作。
作用:方便撤销。
调用流程¶
对外暴露的核心API¶
/// 作用域:针对所有对象生效
/// @param selector 需要hook的方法
/// @param options 是个枚举,主要定义了切面的时机(调用前、替换、调用后)
/// @param block 需要在selector前后插入执行的代码块
/// @param error 错误信息
+ (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;
Aspects 利用消息转发机制,通过hook第三层的转发方法forwardInvocation:
,然后根据切面的时机来动态调用block。接下来详细分析巧妙的设计
- 类A的方法m被添加切面方法。
- 创建类A的子类B,并hook子类B的
forwardInvocation:
方法拦截消息转发,使forwardInvocation:
的 IMP 指向事先准备好的 ASPECTS_ARE_BEING_CALLED 函数(后面简称 ABC 函数),block方法的执行就在 ABC 函数中。 - 把类A的对象的isa指针指向B,这样就把消息的处理转发到类B上,类似 KVO 的机制,同时会更改 class 方法的IMP,把它指向类A的 class 方法,当外界调用 class 时获取的还是类A,并不知道中间类B的存在。
- 对于方法m,类B会直接把方法m的 IMP 指向
_objc_msgForward
方法,这样当调用方法m时就会走消息转发流程,触发 ABC 函数。
详细分析 执行入口¶
存储切面信息¶
存储切面信息主要用到了上面介绍的AspectIdentifier、AspectsContainer两个类
- 获取当前类的容器对象 aspectContainer ,如果没有则创建一个
- 创建一个标识符对象 identifier ,用来存储原方法信息、block、切面时机等信息
- 把标识符对象 identifier 添加到容器中
类方法 对象方法都调用了aspect_add
- 初始化AspectIdentifier这个类。
- IMP
- block
- 获取block的签名信息,然后进行比对。
- 比较签名信息
- block sign > sel sign 直接pass
- Block sign > 1,第一个默认参数遵循了AspectInfo协议
- 从第二位开始 一一比较,通过for循环遍历。
- 添加到容器中
- 通过options添加到对应的数组中。
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;
//自旋锁 保证安全性 block内容的执行是互斥的
aspect_performLocked(^{
//aspect_isSelectorAllowedAndTrack 过滤。有一些方法不能hook
if (aspect_isSelectorAllowedAndTrack(self, selector, options, error)) {
//获取容器对象,主要用来存储当前类或对象所有切面信息。
//容器的对象以关联对象的方式添加到了当前对象上,key值为:前缀+selector
AspectsContainer *aspectContainer = aspect_getContainerForObject(self, selector);
//初始化切片信息。 创建标识符,用来存储SEL、block、切片时机(调用前、调用后)等信息
identifier = [AspectIdentifier identifierWithSelector:selector object:self options:options block:block error:error];
if (identifier) {
//把identifier添加到容器中
[aspectContainer addAspect:identifier withOptions:options];
// Modify the class to allow message interception.
// 修改类以允许消息拦截
aspect_prepareClassAndHookSelector(self, selector, error);
}
}
});
return identifier;
}
执行入口调用了 aspect_add(self, selector, options, block, error) 方法,这个方法是线程安全的,接下来一步步解析具体做了什么
aspect_isSelectorAllowedAndTrack¶
过滤拦截
static BOOL aspect_isSelectorAllowedAndTrack(NSObject *self, SEL selector, AspectOptions options, NSError **error) {
static NSSet *disallowedSelectorList;
static dispatch_once_t pred;
dispatch_once(&pred, ^{//初始化黑名单列表,有些方法禁止hook的
disallowedSelectorList = [NSSet setWithObjects:@"retain", @"release", @"autorelease", @"forwardInvocation:", nil];
});
// Check against the blacklist.
//第一步:检查是否在黑名单内
NSString *selectorName = NSStringFromSelector(selector);
if ([disallowedSelectorList containsObject:selectorName]) {
NSString *errorDescription = [NSString stringWithFormat:@"Selector %@ is blacklisted.", selectorName];
AspectError(AspectErrorSelectorBlacklisted, errorDescription);
return NO;
}
// Additional checks.
//第二步:dealloc方法只能在调用前插入
AspectOptions position = options&AspectPositionFilter;
if ([selectorName isEqualToString:@"dealloc"] && position != AspectPositionBefore) {
NSString *errorDesc = @"AspectPositionBefore is the only valid position when hooking dealloc.";
AspectError(AspectErrorSelectorDeallocPosition, errorDesc);
return NO;
}
//第三步:检查类是否存在这个方法
if (![self respondsToSelector:selector] && ![self.class instancesRespondToSelector:selector]) {
NSString *errorDesc = [NSString stringWithFormat:@"Unable to find selector -[%@ %@].", NSStringFromClass(self.class), selectorName];
AspectError(AspectErrorDoesNotRespondToSelector, errorDesc);
return NO;
}
// Search for the current class and the class hierarchy IF we are modifying a class object
//第四步:如果是类而非实例(这个是类,不是类方法,是指hook的作用域对所有对象都生效),则在整个类即继承链中,同一个方法只能被hook一次,即对于所有实例对象都生效的操作,整个继承链中只能被hook一次。
if (class_isMetaClass(object_getClass(self))) {
Class klass = [self class];
NSMutableDictionary *swizzledClassesDict = aspect_getSwizzledClassesDict();
Class currentClass = [self class];
AspectTracker *tracker = swizzledClassesDict[currentClass];
if ([tracker subclassHasHookedSelectorName:selectorName]) {
NSSet *subclassTracker = [tracker subclassTrackersHookingSelectorName:selectorName];
NSSet *subclassNames = [subclassTracker valueForKey:@"trackedClassName"];
NSString *errorDescription = [NSString stringWithFormat:@"Error: %@ already hooked subclasses: %@. A method can only be hooked once per class hierarchy.", selectorName, subclassNames];
AspectError(AspectErrorSelectorAlreadyHookedInClassHierarchy, errorDescription);
return NO;
}
do {
tracker = swizzledClassesDict[currentClass];
if ([tracker.selectorNames containsObject:selectorName]) {
if (klass == currentClass) {
// Already modified and topmost!
return YES;
}
NSString *errorDescription = [NSString stringWithFormat:@"Error: %@ already hooked in %@. A method can only be hooked once per class hierarchy.", selectorName, NSStringFromClass(currentClass)];
AspectError(AspectErrorSelectorAlreadyHookedInClassHierarchy, errorDescription);
return NO;
}
} while ((currentClass = class_getSuperclass(currentClass)));
// Add the selector as being modified.
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)));
} else {
return YES;
}
return YES;
}
- 不允许hook:retain 、 release 、 autorelease 、 forwardInvocation。
- 允许hook dealloc ,但是只能在 dealloc 执行前,这都是为了程序的安全性设置的
- 检查这个方法是否存在,不存在则不能hook
- Aspects对于hook的生效作用域做了区分:所有实例对象&某个具体实例对象。对于所有实例对象在整个继承链中,同一个方法只能被hook一次,这么做的目的是为了规避循环调用的问题(详情可以了解下 supper 关键字)
aspect_prepareClassAndHookSelector¶
Aspects 的核心原理是消息转发,runtime中有个方法 _objc_msgForward
,直接调用可以触发消息转发机制。
假如要hook的方法叫 m1 ,那么把 m1 的 IMP 指向 _objc_msgForward ,这样当调用方法 m1 时就自动触发消息转发机制,详细实现如下
static void aspect_prepareClassAndHookSelector(NSObject *self, SEL selector, NSError **error) {
NSCParameterAssert(selector);
Class klass = aspect_hookClass(self, error);
Method targetMethod = class_getInstanceMethod(klass, selector);
IMP targetMethodIMP = method_getImplementation(targetMethod);
if (!aspect_isMsgForwardIMP(targetMethodIMP)) {
// Make a method alias for the existing method implementation, it not already copied.
const char *typeEncoding = method_getTypeEncoding(targetMethod);
SEL aliasSelector = aspect_aliasForSelector(selector);
if (![klass instancesRespondToSelector:aliasSelector]) {
__unused BOOL addedAlias = class_addMethod(klass, aliasSelector, method_getImplementation(targetMethod), typeEncoding);
NSCAssert(addedAlias, @"Original implementation for %@ is already copied to %@ on %@", NSStringFromSelector(selector), NSStringFromSelector(aliasSelector), klass);
}
// We use forwardInvocation to hook in.
// 把函数的调用直接触发消息转发函数,转发函数已经被hook,所以在转发函数时进行hook的调用
class_replaceMethod(klass, selector, aspect_getMsgForwardIMP(self, selector), typeEncoding);
AspectLog(@"Aspects: Installed hook for -[%@ %@].", klass, NSStringFromSelector(selector));
}
}
aspect_hookClass¶
类似kvo的机制,隐式的创建一个中间类。记录一个哪个对象 哪个类 进行hook。
- 可以做到hook只对单一对象有效
- 避免了对原有类的侵入
这一步主要做了几个操作:
- 如果已经存在中间类,则直接返回
- 如果是类对象,则不用创建中间类,并把这个类存储在 swizzledClasses 集合中,标记这个类已经被hook了
- 如果存在kvo的情况,那么系统已经帮我们创建好了中间类,那就直接使用
- 对于不存在kvo且是实例对象的,则单独创建一个继承当前类的中间类 midcls ,并hook它的 forwardInvocation: 方法,并把当前对象的isa指针指向 midcls ,这样就做到了hook操作只针对当前对象有效,因为其他对象的isa指针指向的还是原有类
aspect_hookClass
- aspect_swizzleClassInPlace
/**
1. 记录类
2. 交换forwardInvocation
万物皆NSObject
*/
static Class aspect_hookClass(NSObject *self, NSError **error) {
NSCParameterAssert(self);
//获取当前类的对象。对象 获取类对象,类对象 返回自身
Class statedClass = self.class;
//获取isa指针
Class baseClass = object_getClass(self);
NSString *className = NSStringFromClass(baseClass);
// Already subclassed
if ([className hasSuffix:AspectsSubclassSuffix]) {
return baseClass;
// We swizzle a class object, not a single object.
}else if (class_isMetaClass(baseClass)) {
return aspect_swizzleClassInPlace((Class)self);
// Probably a KVO'ed class. Swizzle in place. Also swizzle meta classes in place.
}else if (statedClass != baseClass) {
return aspect_swizzleClassInPlace(baseClass);
}
// Default case. Create dynamic subclass.
//动态生成一个子类
const char *subclassName = [className stringByAppendingString:AspectsSubclassSuffix].UTF8String;
Class subclass = objc_getClass(subclassName);
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;
}
aspect_swizzleForwardInvocation(subclass);
//hook class方法,把子类的class方法的IMP指向父类,这样外界并不知道内部创建了子类
aspect_hookedGetClass(subclass, statedClass);
aspect_hookedGetClass(object_getClass(subclass), statedClass);
objc_registerClassPair(subclass);
}
//把当前对象的isa指向子类,类似kvo的用法
object_setClass(self, subclass);
return subclass;
}
aspect_swizzleForwardInvocation¶
主要功能就是把当前类的 forwardInvocation: 替换成__ASPECTS_ARE_BEING_CALLED__
,这样当触发消息转发的时候,就会调用 __ASPECTS_ARE_BEING_CALLED__
方法
对于__ASPECTS_ARE_BEING_CALLED__
方法是 Aspects 的核心操作,主要就是做消息的调用和分发,控制方法的调用的时机
static NSString *const AspectsForwardInvocationSelectorName = @"__aspects_forwardInvocation:";
//hook forwardInvocation方法,用来拦截消息的发送
static void aspect_swizzleForwardInvocation(Class klass) {
NSCParameterAssert(klass);
// If there is no method, replace will act like class_addMethod.
IMP originalImplementation = class_replaceMethod(klass, @selector(forwardInvocation:), (IMP)__ASPECTS_ARE_BEING_CALLED__, "v@:@");
if (originalImplementation) {
class_addMethod(klass, NSSelectorFromString(AspectsForwardInvocationSelectorName), originalImplementation, "v@:@");
}
AspectLog(@"Aspects: %@ is now aspect aware.", NSStringFromClass(klass));
}
核心转发函数处理¶
上面一切准备就绪,那么怎么触发之前添加的切面block呢,首先我们梳理下整个流程
- 方法 m1 的 IMP 指向了 _objc_msgForward ,调用 m1 则会自动触发消息转发机制
- 替换 forwardInvocation: ,把它的 IMP 指向
__ASPECTS_ARE_BEING_CALLED__
方法,消息转发时触发的就是__ASPECTS_ARE_BEING_CALLED__
。
上面操作可以直接看出调用方法 m1 则会直接触发 ASPECTS_ARE_BEING_CALLED 方法,而 ASPECTS_ARE_BEING_CALLED 方法就是处理切面block用和原有函数的调用时机,详细看下面实现步骤
- 根据调用的 selector ,获取容器对象 AspectsContainer ,这里面存储了这个类或对象的所有切面信息
- AspectInfo 会存储当前的参数信息,用于传递
- 首先触发函数调用前的block,存储在容器的 beforeAspects 对象中
- 接下来如果存在替换原有函数的block,即 insteadAspects 不为空,则触发它,如果不存在则调用原来的函数
- 触发函数调用后的block,存在在容器的 afterAspects 对象中
// This is the swizzled forwardInvocation: method.
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];
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)];
}
总结¶
Aspects的核心原理是利用了消息转发机制,通过替换消息转发方法来实现切面的分发调用。