原文链接
最近希望在业务中完成一套根据 AOP 的埋点计划,调研过程中,我花了些时刻阅览了一下 Aspects 的源码,关于 Aspects 规划有了一些更深入的了解。因而,经过本文记载我在阅览源码后的一些收成和考虑,以供后续进行回忆。
概述
Aspects 是一款轻量且简易的面向切面编程的结构,其根据 Objective-C Runtime 原理完成。Aspects 答应咱们对 类的一切实例的实例办法 或 单个实例的实例办法 增加额外的代码,而且支撑设置代码的履行机遇,包含:before
、instead
、after
三种。
留意:Aspects 无法为类办法供给面向切面编程的才能。
方针类型 | 方针办法类型 | Aspects 是否支撑 hook | hook 作用 |
---|---|---|---|
类方针(UIViewController) | 类办法(“+”最初的办法) | 不支撑 | – |
类方针(UIViewController) | 实例办法(“-”最初的办法) | 支撑 | 对类的一切实例方针收效 |
实例方针(vc) | 类办法(“+”最初的办法) | 不支撑 | – |
实例方针(vc) | 实例办法(“-”最初的办法) | 支撑 | 对单个实例方针收效 |
这儿咱们提出第一个问题:为什么 Aspects 仅支撑对实例办法进行 hook?
另一方面,Aspects 的作者在结构的 README 中明晰表明不要在出产环境中运用 Aspects。这儿咱们提出第二个问题:在项目中运用 Aspects 进行 hook 是否有什么坑?
基础
Aspects 巧妙利用了 Objective-C 的音讯传递和音讯转发机制,完成了一套与 KVO 类似的技术计划。为了能够愈加明晰地了解 Aspects 的规划,这儿咱们简略地回忆一下 Objective-C 的音讯传递和音讯转发机制。
音讯传递
Objective-C 是一门动态言语,其 办法调用 在底层的完成是 音讯传递(Message Passing)。本质上,音讯发送是 沿着一条引证链顺次查找不同的方针,判别该方针是否能够处理音讯。在 Objective-C 中,一切都是方针,包含类、元类,音讯就是在这些方针之间进行传递的。
因而,咱们需要了解这些方针之间的联系。下图所示,为 Objective-C 方针在内存中的引证联系图。
在 Objective-C 中,涉及音讯传递的办法首要有两种:实例办法、类办法。下面,咱们来别离介绍。
实例办法
关于实例办法,音讯传递时,依据当时实例方针的 isa
指针,找到其所属的类方针,并在类方针的办法列表中查找。假如找到,则履行;不然,依据 superclass
指针,找到类方针的超类方针,并在超类方针的办法列表中查找,以此类推,如下所示。
类办法
虽然 Aspects 不支撑 hook 类办法,可是为了便利进行对照,这儿咱们也介绍一下类办法的查找。
关于类办法,音讯传递时,依据当时类方针的 isa
指针,找到其所属的元类方针,并在元类方针的办法列表中查找。假如找到,则履行;不然,依据 superclass
指针,找到元类方针的元超类方针,并在元超类方针的办法列表中查找,以此类推,如下所示。
音讯转发
假如音讯传递无法找到能够处理音讯的方针,那么,Objective-C runtime 将进入音讯转发(Message Forwarding)。
音讯转发包含三个阶段:
- 动态音讯解析
- 备用接收者
- 完整音讯转发
动态音讯解析
当方针接收到不知道音讯时,首要会调用所属类的实例办法 + (BOOL)resolveInstanceMethod:(SEL)sel
或类办法 + (BOOL)resolveClassMethod:(SEL)sel
。咱们能够在办法内部动态增加一个“处理办法”,经过 class_addMethod
函数动态增加到类中。比方:
void dynamicMethodIMP(id self, SEL _cmd) {
// 办法完成
}
+ (BOOL)resolveInstanceMethod:(SEL)aSEL {
if (aSEL == @selector(resolveThisMethodDynamically)) {
class_addMethod([self class], aSEL, (IMP) dynamicMethodIMP, "v@:");
return YES;
}
return [super resolveInstanceMethod:aSEL];
}
备用接收者
假如上一步无法处理音讯,则 runtime 会持续调用 forwardingTargetForSelector:
办法。
假如一个方针完成了这个办法,并回来一个非 nil
(也不能是 self
) 的方针,则这个方针会作为音讯的新接收者,音讯会被分发到这个方针。比方:
- (id)forwardingTargetForSelector:(SEL)aSelector {
NSString * selString = NSStringFromSelector(aSelector);
if ([selString isEqualToString:@"walk"]) {
return self.otherObject;
}
return [super forwardingTargetForSelector:aSelector];
}
这一步适宜于咱们只想将音讯转发到另一个能处理该音讯的方针上。但这一步无法对音讯进行处理,如操作音讯的参数和回来值。
完整音讯转发
假如在上一步还不能处理不知道音讯,则唯一能做的就是启用完整的音讯转发机制了。
这步调用 methodSignatureForSelector:
进行办法签名,这能够将函数的参数类型和回来值进行封装。假如回来 nil
,则阐明音讯无法处理并报错 unrecognized selector sent to instance
。
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
if ([NSStringFromSelector(aSelector) isEqualToString:@"testInstanceMethod"]){
return [NSMethodSignature signatureWithObjcTypes:"v@:"];
}
return [super methodSignatureForSelector: aSelector];
}
假如回来 methodSignature
,则进入 forwardInvocation
。方针会创立一个表明音讯的 NSInvocation
方针,把与没有处理的音讯有关的全部细节都封装在 anInvocation
中,包含 selector
,target
,参数。在这个办法中能够修正完成办法,修正呼应方针等,假如办法调用成功,则完毕。假如依然不能正确呼应音讯,则报错 unrecognized selector sent to instance
。
- (void)forwardInvovation:(NSInvocation)anInvocation {
[anInvocation invokeWithTarget:_helper];
[anInvocation setSelector:@selector(run)];
[anInvocation invokeWithTarget:self];
}
中心原理
Aspects 的中心原理首要包含三个部分:
-
注册相关方针:在 hook 实例办法时,均会注册相关方针
AspectsContainer
。 - 创立动态类:只要在 hook 实例方针的实例办法时,才会创立动态类。
- 中心办法交流:在 hook 实例办法时,均会对中心办法的完成进行交流。
注册相关方针
当 hook 实例办法时,Aspects 会为 实例方针 或 类方针 注册相关方针 AspectsContainer
。AspectsContainer
保存了用户 hook 的方针办法、履行闭包、闭包参数、履行机遇等信息。下图所示,为 AspectsContainer
引证联系图。
相关方针注册的方针分两种状况,这种规划策略是有原因的:
- 在实例方针中注册相关方针,能够完成让每个实例方针独自办理 aspects,然后确保实例之间相互不影响。
- 在类方针中注册相关方针,能够完成让类的每个实例方针同享 aspects,然后完成影响一切实例方针。
创立动态类
当且仅当 hook 实例方针的实例办法时,Aspects 会为实例的所属类 TestClass
创立一个子类 TestClass_Aspects_
(一起创立对应的元类),并修正实例的 isa
指针,使其指向 TestClass_Aspects_
子类,一起 hook TestClass_Aspects_
的 class
办法,使其回来实例的所属类 TestClass
,如下图所示。
整体的完成办法与 KVO 原理一致,尤其是修正动态类 class
办法的完成,使得在外部看来,实例的所属类并没有产生任何变化。
这儿,咱们能够考虑一下第三个问题:为什么在 hook 实例方针的实例办法时要创立动态类?
中心办法交流
当 hook 实例办法时,最重要的一步是对 动态创立的类方针(下文简称:动态类方针) 或 原始承继链中的类方针(下文简称:方针类方针) 的两个中心办法与 Aspects 供给的办法进行交流。这两个办法别离是:方针 selector 和 forwardInvocation:
。详细的交流逻辑如下图所示。
Aspects 会将方针 selector 的完成设置为 Aspects 供给的 aspect_getMsgForwardIMP
办法的回来值。aspects_getMsgForwardIMP
的回来值本质上是一个能够直接触发音讯转发机制的办法。愈加特殊的地方在于,这儿会直接进入音讯转发的最终一步 forwardInvocation:
。
与此一起,Aspects 会将动态类方针或方针类方针的 forwardInvocation:
的完成设置为 Aspects 供给的 __ASPECTS_ARE_BEING_CALLED__
办法完成。__ASPECTS_ARE_BEING_CALLED__
内部会从 实例方针 或 类方针 中取出相关方针 AspectsContainer
,并依据其所保存的 hook 信息履行闭包和方针 selector 的原始完成。
留意:关于中心办法交流,Aspects 支撑幂等。即假如对同一个实例办法 hook 屡次,Aspects 会确保对这两个办法只交流一次。
详细完成
下面,咱们来经过源码,详细剖析一下 Aspects 中的规划细节。
数据结构
首要,简要介绍一下 Aspects 定义的数据结构,首要包含三种数据结构:
AspectsContainer
AspectIdentifier
AspectInfo
AspectsContainer
如下所示,为 AspectsContainer
的数据结构定义。AspectsContainer
是 Aspects 一切信息的根容器,其包含了三个数组,用于保存三种类型的 AspectIdentifier
。
-
beforeAspects
:用于保存履行机遇为AspectPositionBefore
的AspectIdentifier
。 -
insteadAspects
:用于保存履行机遇为AspectPositionInstead
的AspectIdentifier
。 -
afterAspects
:用于保存履行机遇为AspectPositionAfter
的AspectIdentifier
。
除此之外,AspectsContainer
还供给了关于数组进行增删操作的办法。
// Tracks all aspects for an object/class.
@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
AspectIdentifer
如下所示,为 AspectIdentifier
的数据结构定义。AspectIdentifier
是用于表明一个 aspect 的相关信息,其包含了方针 selector、履行闭包、闭包签名、方针方针、履行机遇等。
// Tracks a single aspect.
@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;
@property (nonatomic, strong) id block;
@property (nonatomic, strong) NSMethodSignature *blockSignature;
@property (nonatomic, weak) id object;
@property (nonatomic, assign) AspectOptions options;
@end
AspectInfo
如下所示,为 AspectInfo
的数据结构定义。AspectInfo
的作用是保存方针 selector 的原始完成的履行环境。因为方针 selector 会被交流办法完成,因而 originalInvocation
的 selector
其实是 Aspects 交流的 selector,即 aspects__SEL
。
@interface AspectInfo : NSObject <AspectInfo>
- (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
代码流程
如下所示,Aspects 对外供给两个接口,别离用于 hook 类办法和实例办法,即增加 aspect。
/// Adds a block of code before/instead/after the current `selector` for a specific class.
+ (id<AspectToken>)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<AspectToken>)aspect_hookSelector:(SEL)selector
withOptions:(AspectOptions)options
usingBlock:(id)block
error:(NSError **)error;
两者的内部完成都只调用了同一个办法 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(^{
// 判别是否答应 add aspect
// 假如答应,会顺带构建 tracker 链。
if (aspect_isSelectorAllowedAndTrack(self, selector, options, error)) {
// 加载或创立 container,每个 selector 对应一个 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;
}
在 aspect_add
办法内部完成中,首要经过 aspect_isSelectorAllowedAndTrack
办法判别是否答应增加 aspect。假如答应,则初始化 AspectsContainer
,并将其设置成实例方针或类方针的相关方针。一个 selector 对应一个 container,一个实例方针或类方针可包含多个 container。最终经过 aspect_prepareClassAndHookSelector
履行中心办法交流,关于实例方针,还会创立动态类。
aspect_isSelectorAllowedAndTrack
Aspects 经过 aspect_isSelectorAllowedAndTrack
办法来判别是否答应增加 aspect,假如答应则进行追寻。详细完成逻辑如下所示。
static BOOL aspect_isSelectorAllowedAndTrack(NSObject *self, SEL selector, AspectOptions options, NSError **error) {
// part 1
// 静态变量,作为黑名单
static NSSet *disallowedSelectorList;
static dispatch_once_t pred;
dispatch_once(&pred, ^{
// 不答应增加 aspect 的办法黑名单
disallowedSelectorList = [NSSet setWithObjects:@"retain", @"release", @"autorelease", @"forwardInvocation:", nil];
});
// 查看办法是否归于黑名单
NSString *selectorName = NSStringFromSelector(selector);
if ([disallowedSelectorList containsObject:selectorName]) {
NSString *errorDescription = [NSString stringWithFormat:@"Selector %@ is blacklisted.", selectorName];
AspectError(AspectErrorSelectorBlacklisted, errorDescription);
return NO;
}
// 关于 dealloc 办法,只答应在 before 阶段进行 hook
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;
}
// 不能 hook 不存在的实例办法
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;
}
// part 2
// 假如 hook 方针是类方针,必须确保类承继链上,只答应对一个办法进行一次 hook
if (class_isMetaClass(object_getClass(self))) {
Class klass = [self class];
NSMutableDictionary *swizzledClassesDict = aspect_getSwizzledClassesDict();
Class currentClass = [self class];
// 查看承继链中是否 hook 过方针类办法
do {
AspectTracker *tracker = swizzledClassesDict[currentClass];
// 顺次遍历超类直到根类,依据类对应的 track 进行判别
// 假如类办法现已 hook 过,则进一步判别
if ([tracker.selectorNames containsObject:selectorName]) {
if (tracker.parentEntry) {
// 假如父类中 hook 过,则不答应 hook
// 一起查找最顶层 tracker,打印日志
AspectTracker *topmostEntry = tracker.parentEntry;
while (topmostEntry.parentEntry) {
topmostEntry = topmostEntry.parentEntry;
}
NSString *errorDescription = [NSString stringWithFormat:@"Error: %@ already hooked in %@. A method can only be hooked once per class hierarchy.", selectorName, NSStringFromClass(topmostEntry.trackedClass)];
AspectError(AspectErrorSelectorAlreadyHookedInClassHierarchy, errorDescription);
return NO;
} else if (klass == currentClass) {
// 假如当时类的办法现已 hook 过,则答应 hook
return YES;
}
}
} while ((currentClass = class_getSuperclass(currentClass)));
// 假如承继链上没有类对方针办法hook过,则答应 hook,并记载 tracker
// Add the selector as being modified.
currentClass = klass;
AspectTracker *parentTracker = nil;
do {
AspectTracker *tracker = swizzledClassesDict[currentClass];
if (!tracker) {
tracker = [[AspectTracker alloc] initWithTrackedClass:currentClass parent:parentTracker];
swizzledClassesDict[(id<NSCopying>)currentClass] = tracker;
}
[tracker.selectorNames addObject:selectorName];
// All superclasses get marked as having a subclass that is modified.
parentTracker = tracker;
}while ((currentClass = class_getSuperclass(currentClass)));
}
return YES;
}
aspect_isSelectorAllowedAndTrack
的内部逻辑能够分为两部分:办法黑名单查看、方针类型查看。
关于办法黑名单查看,可细分为三个步骤:
- 判别方针办法是不是
retain
、release
、autorelease
等,假如是,则不答应 hook。 - 假如方针办法是
dealloc
,则只答应 hookbefore
机遇,其他机遇,则不答应 hook。 - 进一步承认 hook 的方针办法是否存在,假如不存在,则不答应 hook。
关于方针类型查看,假如方针类型是实例方针,则答应 hook。假如方针类型是类方针,则进一步判别。依据方针类方针,遍历承继链,关于承继链中的每一个类方针,从大局字典 swizzledClassesDict
中读取对应的追寻器 AspectTracker
。依据追寻器的记载,咱们能够处理两种状况:
- 假如方针类没有 hook 过方针办法,但其父类 hook 过,则不答应 hook。
- 假如方针类 hook 过父类办法,但其子类没有 hook 过,则答应 hook。
如下图所示,为追寻器工作原理示意图。
当对 SubClass
类方针 hook 实例办法 SEL01
时,Aspects 会从 SubClass
类方针开始,遍历其承继链,读取承继链上的每一个类方针所对应的追寻器(假如没有则创立),将方针办法 SEL01
保存至其内部的 selectorNames
数组中作为记载。
后续,假如对 Class
类方针 hook 实例办法 SEL01
时,因为其子类 SubClass
现已 hook 过同名办法,则不答应 Class
对其再次 hook。依据音讯传递的原理,对 Class
进行 hook 是不会收效的,因为子类 SubClass
会在音讯传递链中提早回来 SEL01
。所以,Aspects 的规划不答应在这种状况下再次 hook 同名办法。
当然,假如对 Class
类方针 hook 实例办法 SEL02
时,因为一切其子类均没有 hook 过同名办法,因而答应 Class
对其再次 hook。
本质上,Aspects 利用了 正向的类方针承继链 和 反向的追寻器链,经过大局字典 swizzledClassDict
进行绑定,形成了一个双向链表,便于判别是否答应对类方针的实例办法进行 hook。
aspect_prepareClassAndHookSelector
如下所示,为 aspect_prepareClassAndHookSelector
的完成逻辑。
static void aspect_prepareClassAndHookSelector(NSObject *self, SEL selector, NSError **error) {
NSCParameterAssert(selector);
// Aspects_Class_
Class klass = aspect_hookClass(self, error);
// 读取 Aspects_Class_ 的 selector 办法
Method targetMethod = class_getInstanceMethod(klass, selector);
IMP targetMethodIMP = method_getImplementation(targetMethod);
if (!aspect_isMsgForwardIMP(targetMethodIMP)) {
// IMP 不能是 _objc_msgForward 或 _objc_msgForward_stret
const char *typeEncoding = method_getTypeEncoding(targetMethod);
// aspects__SEL
SEL aliasSelector = aspect_aliasForSelector(selector);
if (![klass instancesRespondToSelector:aliasSelector]) {
// 假如不存在 aspects__SEL,即没有被交流过,则新增一个 aspects__SEL 办法,其完成指向 selector IMP
__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);
}
// 将 selector 办法的完成指向 _objc_msgForward,然后直接触发音讯转发
class_replaceMethod(klass, selector, aspect_getMsgForwardIMP(self, selector), typeEncoding);
AspectLog(@"Aspects: Installed hook for -[%@ %@].", klass, NSStringFromSelector(selector));
}
}
其中 aspect_hookClass
将判别方针类型,假如是实例方针,则创立一个动态类方针回来;假如是类方针,则回来对应的类方针。
根据 aspect_hookClass
回来的方针,Aspects 将修正该方针的两个办法,使其指向 Aspects 的两个办法完成,即上述咱们介绍的 中心办法交流。
在 aspect_prepareClassAndHookSelector
的完成中,Aspects 会在进行办法交流之前进行查看,避免重复交流,然后完成幂等。
aspect_hookClass
如下所示,为 aspect_hookClass
的完成逻辑。
static Class aspect_hookClass(NSObject *self, NSError **error) {
NSCParameterAssert(self);
Class statedClass = self.class; // 其所声明的类
Class baseClass = object_getClass(self);// isa
NSString *className = NSStringFromClass(baseClass);
if ([className hasSuffix:AspectsSubclassSuffix]) {
// 假如是实例方针,且实例方针现已 hook 过办法,即其 isa 指向的是动态类方针 Aspects_Class_,则直接复用动态类方针
return baseClass;
} else if (class_isMetaClass(baseClass)) {
// 假如是类方针,则将该类方针 forwardInvocation: 的完成设置为 _aspects_forwardInvocation:。
// 办法交流完成后,回来该类方针。
return aspect_swizzleClassInPlace((Class)self);
} else if (statedClass != baseClass) {
// 假如是被 KVO 的实例方针。
// baseClass 为 KVO 所创立的动态类,则直接对 KVO 创立的动态类方针进行办法交流,交流 forwardInvocation: 与 _aspects_forwardInvocation: 的办法完成。
return aspect_swizzleClassInPlace(baseClass);
}
// 假如是实例方针,且实例方针未 hook 过办法,则创立动态子类 Aspects_Class_
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;
}
// 将动态类方针的 forwardInvocation: 与 _aspects_forwardInvocation: 进行办法交流
aspect_swizzleForwardInvocation(subclass);
// 将动态类方针的 class 设置成 statedClass
aspect_hookedGetClass(subclass, statedClass);
// 将动态类方针的元类的 class 设置成 stateClass
aspect_hookedGetClass(object_getClass(subclass), statedClass);
objc_registerClassPair(subclass);
}
// 设置 isa 指针,指向 subclass
object_setClass(self, subclass);
return subclass;
}
aspect_hookClass
办法首要用于 挑选对哪个方针的方针办法履行 hook,这儿面包含了 4 种详细的状况,顺次为:
- 假如方针方针是实例方针,且实例方针从前 hook 过办法,则直接回来已创立的动态类方针。
- 假如方针方针是类方针,则对类方针的
forwardInvocation:
办法的完成设置为 Aspects 供给的_aspects_forwardInvocation:
,并回来该类方针。 - 假如方针方针是被 KVO 的方针,则直接复用 KVO 所创立的动态类,并对动态类方针的
forwardInvocation:
办法的完成设置为 Aspects 供给的_aspects_forwardInvocation:
,并回来 KVO 的动态类方针。 - 假如方针方针是实例方针,且实例方针没有 hook 过办法,则创立一个动态类方针
Aspects_Class_
,一起包含元类方针,并对动态类方针的forwardInvocation:
办法履行办法交流,而且设置动态类与原始类之间的联系,最终回来动态类方针。
相关问题
本节,咱们将来介绍上文所提出的几个问题。
问题一:为什么 Aspects 仅支撑对实例办法进行 hook?
在 Aspects 的完成中,在判别能够增加 aspect 的逻辑中,会经过 aspect_isCompatibleBlockSignature
办法来判别 block 与 selector 的办法签名是否匹配,如下所示。其中,它会经过类的 instanceMethodSignatureForSelector
办法获取 selector 的办法签名。关于类办法,经过这种办法必定回来 nil
,然后导致判别条件无法满意,因而无法 hook 类办法。
static BOOL aspect_isCompatibleBlockSignature(NSMethodSignature *blockSignature, id object, SEL selector, NSError **error) {
NSCParameterAssert(blockSignature);
NSCParameterAssert(object);
NSCParameterAssert(selector);
BOOL signaturesMatch = YES;
// 关于类办法,经过 instanceMethodSignatureForSelector: 读取必定回来 nil
NSMethodSignature *methodSignature = [[object class] instanceMethodSignatureForSelector:selector];
if (blockSignature.numberOfArguments > methodSignature.numberOfArguments) {
signaturesMatch = NO;
} else {
...
}
...
}
问题二:在项目中运用 Aspects 进行 hook 是否有什么坑?
假如咱们真正了解了 Aspects 的规划原理,很容易理解为什么作者不推荐在出产环境中运用 Aspects。事实上,在实践的项目开发中,咱们经常会用到对已有办法进行 hook。当然,咱们能够确保自己写的代码只运用 Aspects 进行 hook,可是咱们无法确认引进的第三方库是否运用其他办法对办法进行 hook。那么,这时候埋下了不知道的风险。
如上图所示,假如咱们对 SEL
与 bcq_SEL
进行了 swizzle。那么,bcq_SEL
的完成将指向 SEL
的完成 aspect_getMsgForwardIMP
;SEL
的完成将指向 bcq_SEL
的完成 bcq_IMP
。
在有些状况下,比方:hook viewWillAppear:
办法。bcq_IMP
里会再次调用 bcq_SEL
,然后再次调用原始完成。这时候,咱们调用 SEL
,它最终仍然会调用 aspect_getMsgForwardIMP
,Aspects 的设置不受影响。
可是有些状况下,bcq_IMP
的内部逻辑可能只在特定条件下调用原始完成,其他条件下调用自定义完成。这时候,咱们调用 SEL
,在某些条件下将不会触发 aspect_getMsgForwardIMP
,最终导致 Aspects 的设置不收效。
清楚明了,在出产环境在运用 Aspects 确实可能会出现不确认的反常问题。因而,作者不建议咱们在出产环境中运用 Aspects。
问题三:为什么在 hook 实例方针的实例办法时要创立动态类?
关于实例方针的实例办法,咱们显然不能直接 hook 承继链中的类方针,不然将影响类的一切实例的实例办法。因而,Aspects 挑选了一种类似于 KVO 的规划,动态创立一个子类,并将实例方针的 isa
指针指向动态子类。动态子类的 class
办法则指向实例方针的声明类,然后是外部看来没有任何变化。
这种做法,为实例方针独自拓荒了一条承继链分支,如下图所示。只要被 hook 的实例方针才会走这条分支承继链,因而不影响其他实例。
假如对同一个类的多个实例进行 Aspects,那么会怎么样?从上图中,咱们也能猜到,Aspects 会复用动态子类。只不过 hook 的闭包由各个实例方针自己办理罢了。
总结
经过剖析 Aspects 的源码及其规划原理,咱们一起加深了关于 Objective-C Runtime 的了解。从中,咱们也了解到 Aspects 的局限性,引进需谨慎。
在 Aspects 中,咱们看到了许多 Objective-C 的黑魔法 API,比方:
-
_objc_msgForward
/_objc_msgForward_stret
:直接触发forwardInvocation:
-
objc_allocateClassPair
:动态创立类方针和元类方针 -
objc_registerClassPair
:注册类方针和元类方针 -
object_setClass
:设置isa
指针指向。
除此之外,Aspects 运用了十分底层的办法完成了闭包的参数查看与匹配,这一块十分值得咱们深入学习,后续有时机咱们再来研究一下。
最终,向作者表达一下敬意!假如对 Objective-C 底层原理没有如此深刻的了解,一般人是写不出来这样的结构的!
参阅
- Aspects
- Objective-C Runtime 音讯传递与转发
- Friday Q&A 2009-03-27: Objective-C Message Forwarding
- 面向切面编程之 Aspects 源码解析及使用
- object_getClass(obj)与[obj class]的差异
- Type Encodings
- Aspects相关&调用流程浅析
- Aspects源码剖析