Aspects 原理

Posted by Maqy on January 31, 2019

Aspects 是对AOP编程思想的一种实现,所谓AOP即面向切面编程,主要是实现一种对原有功能无侵害的一种编程方式,在iOS里只要是通过预编译和运行期代码修改来实现的一种编程方式。

其实主要用的就是runtime的那一套东西,动态创建类,增加方法和方法替换等方式来在原有的功能上新增自己想要做的事情。

一般主要做的事情包括日志,性能监控,异常处理,事务处理等。

下面介绍下Aspects的原理

使用

[AsceptDemo aspect_hookSelector:@selector(viewWillAppear:) withOptions:AspectPositionBefore usingBlock:^(id info){
    NSLog(@"Replaced viewWillAppear");
    NSLog(@"%@", info);
} error:&error];

其实调用上就只是这个方法的调用啦

/// Adds a block of code before/instead/after the current `selector` for a specific class.
///
/// @param block Aspects replicates the type signature of the method being hooked.
/// The first parameter will be `id<AspectInfo>`, followed by all parameters of the method.
/// These parameters are optional and will be filled to match the block signature.
/// You can even use an empty block, or one that simple gets `id<AspectInfo>`.
///
/// @note Hooking static methods is not supported.
/// @return A token which allows to later deregister the aspect.
+ (id<AspectToken>)aspect_hookSelector:(SEL)selector
                           withOptions:(AspectOptions)options
                            usingBlock:(id)block
                                 error:(NSError **)error;

通过options可以控制你是想插入在方法前,还是后还是直接替换掉。

我们通过runtime 打印出方法来看下Aspects内部究竟做了什么

test[4227:4188947] MethodName(0): viewWillAppear:
test[4227:4188947] MethodName(1): executor
test[4227:4188947] start resign
test[4227:4188947] MethodName(0): aspects__viewWillAppear:
test[4227:4188947] MethodName(1): forwardInvocation:
test[4227:4188947] MethodName(2): viewWillAppear:
test[4227:4188947] MethodName(3): executor

可以看到注册后比注册前多了两个方法aspects__viewWillAppear:forwardInvocation:,那么我们深入看下究竟做了什么吧。

原理

直接看方法的实现吧

static id aspect_add(id self, SEL selector, AspectOptions options, id block, NSError **error) {
    __block AspectIdentifier *identifier = nil;
    aspect_performLocked(^{
        if (aspect_isSelectorAllowedAndTrack(self, selector, options, error)) {
            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_performLocked实际上就是个锁,用来保证线程安全。

而实际内部做的事情就是,首先判断这个对象的selector是否可以hook,如果可以,才会给该对象创建一个container的关联对象,来保存所有的hook信息,也保存了方法的新的方法调用等,下面详细的讲下吧:

aspect_performLocked加锁

static void aspect_performLocked(dispatch_block_t block) {
    static OSSpinLock aspect_lock = OS_SPINLOCK_INIT;
    OSSpinLockLock(&aspect_lock);
    block();
    OSSpinLockUnlock(&aspect_lock);
}

我们知道Aspects是个公共库,既然是第三方库,那么就是给所有人使用的,肯定不能限制使用场景,那么就可能会有人在多线程中对hook同一个对象,那么就一定要保证线程安全。

aspect_getContainerForObject 容器

static AspectsContainer *aspect_getContainerForObject(NSObject *self, SEL selector) {
    NSCParameterAssert(self);
    SEL aliasSelector = aspect_aliasForSelector(selector);
    AspectsContainer *aspectContainer = objc_getAssociatedObject(self, aliasSelector);
    if (!aspectContainer) {
        aspectContainer = [AspectsContainer new];
        objc_setAssociatedObject(self, aliasSelector, aspectContainer, OBJC_ASSOCIATION_RETAIN);
    }
    return aspectContainer;
}

为了方便记录已经hook的方法,需要用一个属性来保存已经hook的方法,这里通过关联对象来记录,而实际上container内部有三个集合,分别存储对应AspectOptions的方法,也就是我们每hook一个方法就会生成一个AspectIdentifier,这个对象里存储了hook的方法名,及block等信息,然后会将这个对象存储在container的集合里,方便消息转发的时候调用。

aspect_prepareClassAndHookSelector HOOK

那么真正的hook做了什么呢?

如果你传入的是一个元类,那么他会直接swizzing他们的方法转发的方法

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));
}

接下来会将原有的方法替换掉原有实现,设置成转发类型,这样就不调用方法,而直接调用转发了

static IMP aspect_getMsgForwardIMP(NSObject *self, SEL selector) {
    IMP msgForwardIMP = _objc_msgForward;
#if !defined(__arm64__)
    Method method = class_getInstanceMethod(self.class, selector);
    const char *encoding = method_getTypeEncoding(method);
    BOOL methodReturnsStructValue = encoding[0] == _C_STRUCT_B;
    if (methodReturnsStructValue) {
        @try {
            NSUInteger valueSize = 0;
            NSGetSizeAndAlignment(encoding, &valueSize, NULL);

            if (valueSize == 1 || valueSize == 2 || valueSize == 4 || valueSize == 8) {
                methodReturnsStructValue = NO;
            }
        } @catch (__unused NSException *e) {}
    }
    if (methodReturnsStructValue) {
        msgForwardIMP = (IMP)_objc_msgForward_stret;
    }
#endif
    return msgForwardIMP;
}

之后就在转发的流程里做响应了。那么在转发流程里,就可以判断这个响应是前置,还是后置,或者替换响应了。

如果是前置或后置,可以用NSInvotion的invoke再次调用原有的实现。

Q 已经替换掉原有的实现了,还怎么响应呢?

if (!aspect_isMsgForwardIMP(targetMethodIMP)) {
        // 新增一个带前缀的方法,并将原有方法的实现拿过来
        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);
        }

        // 接下来继续走转发的流程
        class_replaceMethod(klass, selector, aspect_getMsgForwardIMP(self, selector), typeEncoding);
    }

这样实际上不管我们hook 的是某个对象的某个方法还是所有对象的某个方法,其本质走的都是转发,在转发消息里再调用原来的实现。

Q 如果只hook某个对象的某个方法呢?

如果只hook单个对象的方法,其实道理类似,流程大致如下:

1.创建对象的关联对象 container
2.给当前类加后缀并创建子类,将对象的类型设置成子类
3.添加带有前缀的指定的selector,并添加实现
4.将对应的方法设置成转发类型
5.在转发的响应方法里,通过将指定方法的响应设置成带有前缀的方法的响应

为了明白我简单的画个图:

如果我们有个对象A,hook了他的name方法,那么实际上内存结构里的类的结构大概是这样的

A has methods:
1. name
A_Aspects_ has methods:
1. name (forward)
2. _Aspects_name (normal)

A_Aspects_ 就是框架自动帮我们生成的类型,也会自动将我们的对象的isa设置成这个类型,当我们再次调用A的name方法的时候,实际上是调用的A_Aspects_name方法,又因为这个方法是直接转发的,所以会直接调用到forwardInvocation,在这里如果类型是A_Aspects_的,那么会将原有的方法加上前缀去invoke

Q 怎么不影响到父类的?

我们知道我们做方法替换的时候实际上操作的都是class里的method,实际上这些是类共用的,那怎么做到只hook某个对象的方法而不影响其他对象的呢?

看上边 A 的例子可以知道,实际上是生成了新的对象,而我们改变的是新的对象的class结构,原有的class的结构是不受影响的,所以不会影响到子类。

总结

Aspects 是一个老强大的AOP库了,在这里可以学到很多runtime的知识