Aspects 源码解读
1.Aspects简介
Aspects是一种面向切面编程,相对于继承而已,无需改动目标源码文件,做到无侵入式,千言万语不如看code明显:
[testController aspect_hookSelector:NSSelectorFromString(@"dealloc") withOptions:AspectPositionBefore usingBlock:^(id<AspectInfo> info) {
NSLog(@"Controller is about to be deallocated: %@", [info instance]);
} error:NULL];
实例中hook了testController的dealloc方法,不用重写dealloc方法,即可完成打印。
2.Aspects原理
2.1 runtime
Aspects是利用runtime原理对一个类的SEL进行消息转发,最终实现切片编程。
首先要看一下OC对象发送一个消息是如何进行消息发送的,消息发送的流程可参考如下图2-1-1

我们以一个具体demo为例说明runtime的调用顺序
#import "TestObject.h"
#import <objc/runtime.h>
@interface TestProxy:NSObject
- (void)printHelloWorld;
@end
@implementation TestProxy
- (void)printHelloWorld{
NSLog(@"🍎Hello World🍎");
}
@end
@interface TestObject ()
{
TestProxy *mProxy;
}
@end
@implementation TestObject
- (id)init
{
if (self = [super init]) {
mProxy = TestProxy.new;
}
return self;
}
- (BOOL)respondsToSelector:(SEL)aSelector{
if ([super respondsToSelector:aSelector]) {
return YES;
}
else
{
/* Here, test whether the aSelector message can *
* be forwarded to another object and whether that *
* object can respond to it. Return YES if it can. */
}
return NO;
}
//动态添加printHelloWorld实现
static void dynamicAddPrintHelloWorldIMP(id self, SEL _cmd){
NSLog(@"🍉dynamic print Hello World🍉");
}
+ (BOOL)resolveInstanceMethod:(SEL)sel{
NSLog(@"=======resolveInstanceMethod:%@========",NSStringFromSelector(sel));
//#pragma clang diagnostic push
//#pragma clang diagnostic ignored "-Wundeclared-selector"
// if (sel == @selector(printHelloWorld)) {
//#pragma clang diagnostic pop
// class_addMethod(self, sel, (IMP)dynamicAddPrintHelloWorldIMP, "v@:");
// return YES;
// }
return [super resolveInstanceMethod:sel];
}
- (id)forwardingTargetForSelector:(SEL)aSelector{
NSLog(@"=======forwardingTargetForSelector=======");
//#pragma clang diagnostic push
//#pragma clang diagnostic ignored "-Wundeclared-selector"
// if (aSelector == @selector(printHelloWorld) && [mProxy respondsToSelector:@selector(printHelloWorld)]) {
//#pragma clang diagnostic pop
// return mProxy;
// }
return [super forwardingTargetForSelector:aSelector];
}
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{
NSLog(@"=====methodSignatureForSelector=========");
//1.返回动态添加方法签名
if (aSelector == @selector(printHelloWorld)) {
NSMethodSignature *signature = [NSMethodSignature signatureWithObjCTypes:"v@:"];
return signature;
}
//2.返回代理对象方法签名
// if (aSelector == @selector(printHelloWorld)) {
// return [TestProxy instanceMethodSignatureForSelector:aSelector];
// }
return [super methodSignatureForSelector:aSelector];
}
- (void)forwardInvocation:(NSInvocation *)anInvocation{
NSLog(@"=====forwardInvocation=========");
//1.动态添加
if (anInvocation.selector == @selector(printHelloWorld)) {
__unused BOOL addDynamicMethod = class_addMethod([self class], anInvocation.selector, (IMP)dynamicAddPrintHelloWorldIMP, "v@:");
[anInvocation invokeWithTarget:self];
}
else
{
[super forwardInvocation:anInvocation];
}
//2.代理对象转发
// if ([mProxy respondsToSelector:anInvocation.selector]) {
// [anInvocation invokeWithTarget:mProxy];
// }
// else
// {
// [super forwardInvocation:anInvocation];
// }
}
@end
我们初始化TestObject,然后按照如下调用方式调用
- (void)testRuntime{
TestObject *test = [[TestObject alloc] init];
[test performSelector:@selector(printHelloWorld) withObject:nil];
}
test送法消息printHelloWorld,然后查看consle可以看到类似如下信息

TestObject没有实现方法printHelloWorld,找不到IMP,所以会依次进入
resolveInstanceMethod
->forwardingTargetForSelector
->forwardInvocation
进行补救操作,实例中我在forwardInvocation
中使用了两种方案对printHelloWorld进行补救,一种是利用class_addMethod
动态添加IMP指向SEL,然后invoke即可,另一种是利用TestProxy进行转发,TestProxy中实现了printHellowrold方法。读者可以通过修改上述代码观察消息转发的整个流程。
实际上XCode本身也可以查看消息的转发机制,具体的方法如下:首先开启调试模式、打印出所有运行时发送的消息,可以在console里执行下面的方法:
(void)instrumentObjcMessageSends(YES);
或者断点暂停程序运行,并在 gdb 中输入下面的命令:
call (void)instrumentObjcMessageSends(YES)
下面演示图片

该方法只能在模拟器上查看,运行时发送的所有消息都会打印到 /tmp/msgSend-xxxx 文件里了,具体的目录在/private/tmp中,如下图路径

打开文件可以看到,类似如下
- TestObject NSObject performSelector:withObject:
+ TestObject NSObject resolveInstanceMethod:
+ TestObject NSObject resolveInstanceMethod:
- TestObject NSObject forwardingTargetForSelector:
- TestObject NSObject forwardingTargetForSelector:
- TestObject NSObject methodSignatureForSelector:
- TestObject NSObject methodSignatureForSelector:
- TestObject NSObject class
- TestObject NSObject doesNotRecognizeSelector:
- TestObject NSObject doesNotRecognizeSelector:
- TestObject NSObject class
- __NSCFConstantString __NSCFString _fastCStringContents:
- __NSCFString NSObject isProxy
- __NSCFString NSObject respondsToSelector:
- __NSCFString NSObject class
+ __NSCFString NSObject resolveInstanceMethod:
+ __NSCFString NSObject resolveInstanceMethod:
- __NSCFString __NSCFString isNSString__
- __NSCFString NSObject isNSCFConstantString__
- __NSCFString __NSCFString _fastCStringContents:
简要了解了消息转发机器,我们就可以看到有三个阶段可供我们hook原始的SEL函数,这三个分别是
resolvedInstanceMethod:
该阶段适合动态添加实现响应的IMPforwardingTargetForSelector:
该阶段适合将SEL动态转发给代理对象实现forwardInvocation:
该阶段灵活性高,上述两种都可实现
那么Aspects为什么选择forwardInvocation:
呢,更重要的原因是如何获取SEL的参数,原先在32位机器上可通过va_list取出参数列表,然后直接调用即可,可参考这里JSPatch-实现原理详解,arm64下va_list的结构改变了,但是我们可通过forwardInvocation
拿到具体的NSInvocation,NSInvocation中可以轻松拿到SEL,argumentArgs等等,能拿到具体参数就可以动态生成IMP和函数签名了。
2.2 源码解析
Aspects的API及其简洁,如下:

AspectOptions简洁明了,提供是重写SEL还是在执行原始SEL之前或者之后

提供了静态和实例方法,静态方法hook整个程序中所有的该类,实例方法只hook本实例。block即是根据options状态插入的回调函数。
核心函数是aspect_add

aspect_performLocked
保证hook线程安全,首先执行的是aspect_isSelectorAllowedAndTrack
,这个函数很简单,主要有一下几个方面:
1.判断SEL是否在黑名单中@"retain", @"release", @"autorelease", @"forwardInvocation:"不能被hook,dealloc不能在执行之后hook
2.如果hook的是类,那么类的继承关系,同一个方法只能被hook一次,防止父类已经hook
符合条件可以hook的SEL进入首先会生成一个AspectsContainer,Container关联重命名的SEL(添加Aspect前缀而已)生成

顾名思义,Container是一个容器的,点进去查看简单明了

Conatiner包含了hook三种状态(之前,替换,之后)的数组NSArray<AspectsIdentifier *>,AspectsIdentifier则包含了我们hook对象的selector和hook的回调block以前block签名,AspectsIdentifier定义如下:

AspectsIdentifier是跟踪每一次生成切面的id,最终在对象调用hook的selector后会转发到对象中的
__ASPECTS_ARE_BEING_CALLED__
中,根据container取出对应的AspectsIdentifier对象,利用NSInvocation根据AspectsIdentifier的options状态调用block.详细查看AspectIdentifier的构造函数,会看到一件有意思的事情

首先根据传入的block生成block签名,为啥要生成block签名呢?因为最终我们要主动调用block只能通过NSInvocation,通过NSInvocation调用block就必须要知道block的方法签名,如何获取block的方法签名呢,这里就不在累述了,有兴趣的可以看这个NSInvocation动态调用任意block.
AspectsIndentifier被添加到Container中,然后就会执行到Aspect最为关键的核心函数
aspect_prepareClassAndHookSelector
了,作者的注释了写明改函数的作用就是修改原始class,然后去拦截消息转发。
static void aspect_prepareClassAndHookSelector(NSObject *self, SEL selector, NSError **error) {
NSCParameterAssert(selector);
//hook self,动态创建子类,动态将子类的forwardInvocation转发到__aspects_forwardInvocation中
Class klass = aspect_hookClass(self, error);
Method targetMethod = class_getInstanceMethod(klass, selector);
IMP targetMethodIMP = method_getImplementation(targetMethod);
//判断targetMethodIMP不是消息转发
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]) {
//动态生成的子类没有SEL:aliasSelector,动态添加aliasSelector,并将函数指针指向到原始的父类selector的IMP,这样动态子类遍有了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);
}
//动态子类kclss转发原始seletor到forwardInovcation->执行__ASPECTS_ARE_BEING_CALLED__中
// We use forwardInvocation to hook in.
class_replaceMethod(klass, selector, aspect_getMsgForwardIMP(self, selector), typeEncoding);
AspectLog(@"Aspects: Installed hook for -[%@ %@].", klass, NSStringFromSelector(selector));
}
}
这个函数主要做了三件事情:
1.
aspect_hookClass
hook self,动态创建子类,动态将子类的forwardInvocation
转发到__aspects_forwardInvocation
中,__aspects_forwardInvocation
指向的函数指针为__ASPECTS_ARE_BEING_CALLED__
,并将子类通过object_setClass(self, subclass)
设置为自身(这里的思想类似KVO,有兴趣的可以看看如何自己动手实现 KVO)
2.获取动态生成子类kclss的selector的方法签名IMP,生成aliasSelector,由于子类kclss没有SEL:aliasSelector,所以给子类动态添加aliasSelector,并将函数指针指向到原始的selector的IMP,这样动态子类便有了aliasSelector
3.动态子类kclss使用aspect_getMsgForwardIMP
替换原始seletor到forwardInovcation
->执行__ASPECTS_ARE_BEING_CALLED__
中.aspect_getMsgForwardIMP
中判断是arm64
设备使用_objc_msgForward转发SEL,否则使用``,关于它们的区别可以参考这篇文章你真的会判断 _objc_msgForward_stret 吗
通过上述三个步骤,hook的SEL会转发到C函数__ASPECTS_ARE_BEING_CALLED__
中。
关于aspect_hookClass
比较有意思,这个函数我们仔细看一下详细实现
static Class aspect_hookClass(NSObject *self, NSError **error) {
NSCParameterAssert(self);
//class
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. hook的是类直接forwardInvocation即可
}else if (class_isMetaClass(baseClass)) {
return aspect_swizzleClassInPlace((Class)self);
// Probably a KVO'ed class. Swizzle in place. Also swizzle meta classes in place. KVO 也 forwardInvocation
}else if (statedClass != baseClass) {
return aspect_swizzleClassInPlace(baseClass);
}
// Default case. Create dynamic subclass.
const char *subclassName = [className stringByAppendingString:AspectsSubclassSuffix].UTF8String;
//获取isa指针
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);
aspect_hookedGetClass(subclass, statedClass);
aspect_hookedGetClass(object_getClass(subclass), statedClass);
objc_registerClassPair(subclass);
}
object_setClass(self, subclass);
return subclass;
}
通过源码我们可以看出aspect_hookClass
主要逻辑如下:
1.为了区分hook的是Class还是instance,是Class或者KVO直接使用原始类,swizzle
forwardInvocation
的IMP,如果是instance变量,则创建子类,并将子类的isa指针指向self,让外界看起来自身没有变化,然后swizzleforwardInvocation
的IMP
2..class
当 target 是 Instance 则返回 Class,当 target 是 Class 则返回自身,objc_getClass
是isa指针的指向
来到最后一步我们详细看一下这个函数的源码
// 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.
// 如果有任何 insteadAspects 就直接替换了
BOOL respondsToAlias = YES;
if (objectContainer.insteadAspects.count || classContainer.insteadAspects.count) {
aspect_invoke(classContainer.insteadAspects, info);
aspect_invoke(objectContainer.insteadAspects, info);
}else {
//否则正常执行hook的selector,因为selector已被重命名,找到可以执行的aliasSelector的原始class
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)];
}
该函数我们做如下解释:
1.通过aliasSelector获取的实例container和类container,关联self,生成
AspectInfo
,AspectInfo
中存储的主要是NSInvocation信息,有了Info之后就可以遍历Container对应的beforeAspects
,insteadAspects
,afterAspects
数组中的AspectIdentifier去invokeWithInfo
执行对象hook的block了
2.看一下宏定义aspect_invoke(aspects, info)
,简单的遍历数组执行block而已
3.根据hook的option依次执行(1).首先执行before hooks (2).然后看是否有insteadAspects,如果有则说明需要替换到原始SEL,执行自己传入的block即可,否则正常执行hook的selector,因为selector已被重命名,找到可以执行的aliasSelector的原始class,通过最原始的NSInvocation调用invoke执行到IMP (3).最后在执行AfterAspects
4.第三步执行完毕后通常是如果本身hook的SEL就不存在,则抛出异常
最后我们总结Aspects的整个流程:

2.3 总结
Aspects源码虽然很短,但是其中包含了runtime的精华思想,通过熟悉源码可以熟知obj消息发送流程,block内部实现原理,方法签名,oc对象struct本质等等。本文介绍了首先oc消息转发的流程,然后深入Aspects源码介绍了Aspects实现的核心原理。
网友评论