Aspects 源码解读

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


runtime中SEL执行过程

我们以一个具体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可以看到类似如下信息

runtime_console

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)

下面演示图片


runtime_consloe2

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


runtime_msgsend

打开文件可以看到,类似如下

- 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:该阶段适合动态添加实现响应的IMP
  • forwardingTargetForSelector: 该阶段适合将SEL动态转发给代理对象实现
  • forwardInvocation: 该阶段灵活性高,上述两种都可实现
    那么Aspects为什么选择forwardInvocation:呢,更重要的原因是如何获取SEL的参数,原先在32位机器上可通过va_list取出参数列表,然后直接调用即可,可参考这里JSPatch-实现原理详解,arm64下va_list的结构改变了,但是我们可通过forwardInvocation拿到具体的NSInvocation,NSInvocation中可以轻松拿到SEL,argumentArgs等等,能拿到具体参数就可以动态生成IMP和函数签名了。

2.2 源码解析

Aspects的API及其简洁,如下:


aspect_1

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


Aspect_2

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

核心函数是aspect_add

aspect_3

aspect_performLocked保证hook线程安全,首先执行的是aspect_isSelectorAllowedAndTrack,这个函数很简单,主要有一下几个方面:

1.判断SEL是否在黑名单中@"retain", @"release", @"autorelease", @"forwardInvocation:"不能被hook,dealloc不能在执行之后hook
2.如果hook的是类,那么类的继承关系,同一个方法只能被hook一次,防止父类已经hook

符合条件可以hook的SEL进入首先会生成一个AspectsContainer,Container关联重命名的SEL(添加Aspect前缀而已)生成

aspect_4

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

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

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

首先根据传入的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,让外界看起来自身没有变化,然后swizzle forwardInvocation的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的整个流程:


Aspects流程图

2.3 总结

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

3.Refrence

1.AOP(面向切面编程) & Aspects 源码解析

推荐阅读更多精彩内容