静下心来读源码之Aspects

2,393 阅读11分钟

前言

最近找工作受挫,无头苍蝇一般,挣扎焦虑的状态实在是难受。决心改变这样的状态并且提高自己,那就从最简单的静下心来细扣优秀源码开始吧。

Aspects简介

Aspects是一个轻量级的面向切面编程(AOP)的库。它主要提供了三个切入点:before(在原始的方法前执行)/instead(替换原始的方法执行)/after(在原始的方法后执行,默认),通过Runtime消息转发实现Hook。它支持Hook某个实例对象的方法。并且它的内部考虑到了大量的可能触发的问题并进行相应的处理来确保安全。相比于单纯交换两个IMP的Method Swizzling优势还是很明显的。

带着问题看源码

阅读源码前还是要自己先去试用一下,一般在这个试用的过程当中你或多或少的都是会有一些疑问的。带着这些疑问去阅读源码的时候你就可能会有一些针对性。从某个具体的细节问题切入进去比单纯泛泛的看源码的效果来的好。我这里抛砖引玉的提两个问题。

1.Aspects是如何Hook某个特定实例对象的方法的

上面这张对象的内存布局的图我想大家应该见过,我们都知道实例对象的方法列表都是存在类对象里的,并且类对象其实是一个单例对象。那么当不同实例对象调用相同方法的时候最后找到方法其实是一样的。

2.Aspects如何Hook类方法

Aspects提供了两个方法,一个对象方法一个是类方法。

+ (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;

看了一些文章的介绍都是说-号的方法是Hook实例方法的,+方法是Hook类方法的,但是这里有个疑问Aspects其实提供的是三种功能的Hook

  • Hook某个特定实例对象的某个对象方法
  • Hook所有实例对象的某个对象方法
  • Hook类方法

三种功能对应的是两个方法,咋做区分呢。(其实了解对象内存布局的,应该马上能反应过来要怎么操作)

宏观粗略的感受一下代码主要逻辑结构

一般常见的源码分析的文章喜欢从某个接口切入,从上往下的看整个代码的执行过程,然后最后在得出一个结论或者框架图。但是我感觉这样的方式对应读者来说相对是不太友好的,有的时候读者还没对整个框架大致了解,这时候一大推的源码贴上来读者是一脸蒙蔽的。我这里先对整个框架的结构大致做一层介绍,省略了一些细节具体的过程。

通过上面这张大致的流程图,我们知道最后的方法调用都是会走消息转发,并且forwardInvocation的IMP已经指向了我们新写的方法,所以最后的before/instead/after的逻辑都是在我们新写的方法里了。

回头看疑问

看我上面大致的流程图,然后结合自己的runtime的知识再回过头来看看上面的两个问题。(如果对runtime不太熟悉的推荐霜神的博客链接1 链接2 链接3

问题1

从上面的流程图中我们看到,当Hook实例对象的时候其实是创建了一个新的class,然后让当前实例对象的isa指向了这个新类。所以和未被Hook的实例对象的isa指向的其实是两个类对象了。并且原先类对象并未做任何处理。

问题2

这个问题其实就更简单了,我们都知道其实类方法是存在metaClass里的,所以想要Hook类对象,拿到metaClass就可以了。

实现细节

[xxx class]object_getClass(xxx)的差别需要注意一下

  • [xxx class]xxx是实例对象的时候返回的是类对象,其它情况返回的是自己。
  • object_getClass(xxx)返回的是当前对象isa指向的对象

OK,现在我们大致已经对整个流程有了一点点了解了。接下来我们就需要去深挖一些细节了。Aspects内部的注释还是非常全的~

1.协议介绍

AspectToken

@protocol AspectToken <NSObject>
//注销一个Hook
- (BOOL)remove;
@end

这是个协议,内部就一个remove方法。遵循这个协议需要实现remove方法去注销Hook。

AspectInfo

/// Hook的Block的第一个参数,遵循这个协议
@protocol AspectInfo <NSObject>

/// 当前Hook的对象
- (id)instance;

/// Hook的原方法的Invocation
- (NSInvocation *)originalInvocation;

/// 所有的方法参数
- (NSArray *)arguments;

@end

我们添加的Hook的block的第一个参数,遵循这个协议

2.类介绍

AspectInfo

AspectInfo协议遵循上面AspectInfo协议。三个属性和协议上一一对应。

@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

这里着重看一下arguments也就是方法的参数获取。内部的获取逻辑是originalInvocation调用了NSInvocation分类的方法- (NSArray *)aspects_arguments

- (NSArray *)aspects_arguments {
	NSMutableArray *argumentsArray = [NSMutableArray array];
	for (NSUInteger idx = 2; idx < self.methodSignature.numberOfArguments; idx++) {
		[argumentsArray addObject:[self aspect_argumentAtIndex:idx] ?: NSNull.null];
	}
	return [argumentsArray copy];
}

你可以看到上面方法的逻辑很简单就是遍历methodSignatureArguments,但是你肯定也注意到了idx是从2开始的。通过查看官方文档可以看到这么一句话。

A method signature consists of one or more characters for the method return type, followed by the string encodings of the implicit arguments self and _cmd, followed by zero or more explicit arguments

也就是说一个方法的签名是由返回值 + self + _cmd + 方法参数的encodings值组成但是这里方法参数是从3开始的,我们接下去看到获取到具体类型是通过- (void)getArgument:(void *)argumentLocation atIndex:(NSInteger)idx方法。OK,我们又在这个方法的文档里看到了这么一句话。

Indexes begin with 0. The implicit arguments self (of type id) and _cmd (of type SEL) are at indexes 0 and 1; explicit arguments begin at index 2.

我们发现0对应的是self并不是返回值。所以很显然了获取参数要从2开始啦。

总结:AspectInfo主要是对NSInvocation的保存和封装。

AspectIdentifier

@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

从这个类的初始化方法里我们能看出来,这个类主要是保存了Hook的一些信息,hook的执行时间方法参数等等的信息。这个类里需要关注的地方是怎么解析出传入Block的blockSignature。主要通过下面的方法。

static NSMethodSignature *aspect_blockMethodSignature(id block, NSError **error) {
    AspectBlockRef layout = (__bridge void *)block;
	if (!(layout->flags & AspectBlockFlagsHasSignature)) {
        NSString *description = [NSString stringWithFormat:@"The block %@ doesn't contain a type signature.", block];
        AspectError(AspectErrorMissingBlockSignature, description);
        return nil;
    }
	void *desc = layout->descriptor;
	desc += 2 * sizeof(unsigned long int);
	if (layout->flags & AspectBlockFlagsHasCopyDisposeHelpers) {
		desc += 2 * sizeof(void *);
    }
	if (!desc) {
        NSString *description = [NSString stringWithFormat:@"The block %@ doesn't has a type signature.", block];
        AspectError(AspectErrorMissingBlockSignature, description);
        return nil;
    }
	const char *signature = (*(const char **)desc);
	return [NSMethodSignature signatureWithObjCTypes:signature];
}

在上面的方法里我们注意到了AspectBlockRef这么一个结构体,定义如下。

typedef struct _AspectBlock {
	__unused Class isa;
	AspectBlockFlags flags;
	__unused int reserved;
	void (__unused *invoke)(struct _AspectBlock *block, ...);
	struct {
		unsigned long int reserved;
		unsigned long int size;
		// requires AspectBlockFlagsHasCopyDisposeHelpers
		void (*copy)(void *dst, const void *src);
		void (*dispose)(const void *);
		// requires AspectBlockFlagsHasSignature
		const char *signature;
		const char *layout;
	} *descriptor;
	// imported variables
} *AspectBlockRef;

在回去头去理一下方法逻辑。拿到descriptor的指针,对照结构体中signature的位置偏移2 * sizeof(unsigned long int)的位置,然后在判断是否包含Copy和Dispose函数(copy函数把Block从栈上拷贝到堆上,dispose函数是把堆上的函数在废弃的时候销毁掉。参考霜神的博客),包含的话再偏移2 * sizeof(void *)位置,最后拿到signature的位置。拿到blockSignature后续还对其进行了一下校验。

static BOOL aspect_isCompatibleBlockSignature(NSMethodSignature *blockSignature, id object, SEL selector, NSError **error) {
    NSCParameterAssert(blockSignature);
    NSCParameterAssert(object);
    NSCParameterAssert(selector);

    BOOL signaturesMatch = YES;
    NSMethodSignature *methodSignature = [[object class] instanceMethodSignatureForSelector:selector];
    if (blockSignature.numberOfArguments > methodSignature.numberOfArguments) {
        signaturesMatch = NO;
    }else {
        if (blockSignature.numberOfArguments > 1) {
            const char *blockType = [blockSignature getArgumentTypeAtIndex:1];
            if (blockType[0] != '@') {
                signaturesMatch = NO;
            }
        }
        // Argument 0 is self/block, argument 1 is SEL or id<AspectInfo>. We start comparing at argument 2.
        // The block can have less arguments than the method, thats ok.
        if (signaturesMatch) {
            for (NSUInteger idx = 2; idx < blockSignature.numberOfArguments; idx++) {
                const char *methodType = [methodSignature getArgumentTypeAtIndex:idx];
                const char *blockType = [blockSignature getArgumentTypeAtIndex:idx];
                // Only compare parameter, not the optional type data.
                if (!methodType || !blockType || methodType[0] != blockType[0]) {
                    signaturesMatch = NO; break;
                }
            }
        }
    }

    if (!signaturesMatch) {
        NSString *description = [NSString stringWithFormat:@"Block signature %@ doesn't match %@.", blockSignature, methodSignature];
        AspectError(AspectErrorIncompatibleBlockSignature, description);
        return NO;
    }
    return YES;
}

仔细走一遍上面的逻辑,主要是判断block有参数的情况下必须是 id< AspectInfo > + 原始方法的参数顺序(参数可以不全,但是顺序必须是对的)

总结:AspectIdentifier是一个Hook的具体内容。里面会包含了单个的 Hook 的具体信息,包括执行时机,要执行 block所需要用到的具体信息:包括方法签名、参数等等。

AspectsContainer

@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

总结:这个类还是很简单的,就是存了一些Hook的AspectIdentifier

AspectTracker

@interface AspectTracker : NSObject
- (id)initWithTrackedClass:(Class)trackedClass;
@property (nonatomic, strong) Class trackedClass;
@property (nonatomic, readonly) NSString *trackedClassName;
@property (nonatomic, strong) NSMutableSet *selectorNames;
@property (nonatomic, strong) NSMutableDictionary *selectorNamesToSubclassTrackers;
- (void)addSubclassTracker:(AspectTracker *)subclassTracker hookingSelectorName:(NSString *)selectorName;
- (void)removeSubclassTracker:(AspectTracker *)subclassTracker hookingSelectorName:(NSString *)selectorName;
- (BOOL)subclassHasHookedSelectorName:(NSString *)selectorName;
- (NSSet *)subclassTrackersHookingSelectorName:(NSString *)selectorName;
@end

总结:这个类主要的作用是追踪每个类Hook的selector情况。确保一条继承链上只有一个类Hook了这个方法。

3.具体流程

通过头文件看见公开的API就两个

+ (id<AspectToken>)aspect_hookSelector:(SEL)selector
                      withOptions:(AspectOptions)options
                       usingBlock:(id)block
                            error:(NSError **)error {
    return aspect_add((id)self, selector, options, block, error);
}

/// @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 {
    return aspect_add(self, selector, options, block, error);
}

这两个API最后走的都是同一个方法。

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(^{// 锁保证线程安全
        if (aspect_isSelectorAllowedAndTrack(self, selector, options, error)) {// 判断是否可以Hook
            // 根据方法拿到AspectIdentifier容器
            AspectsContainer *aspectContainer = aspect_getContainerForObject(self, selector);
            // 根据selector self options block 生成AspectIdentifier
            identifier = [AspectIdentifier identifierWithSelector:selector object:self options:options block:block error:error];
            if (identifier) {
                // 把生成AspectIdentifier添加进容器
                [aspectContainer addAspect:identifier withOptions:options];

                // 处理类
                aspect_prepareClassAndHookSelector(self, selector, error);
            }
        }
    });
    return identifier;
}

这里我们只关注aspect_prepareClassAndHookSelector这个方法。

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.
        class_replaceMethod(klass, selector, aspect_getMsgForwardIMP(self, selector), typeEncoding);
        AspectLog(@"Aspects: Installed hook for -[%@ %@].", klass, NSStringFromSelector(selector));
    }
}

我们先忽略Class klass = aspect_hookClass(self, error);来看一下下面的逻辑。相对还是比较直观的,主要的操作就是将当前的selector指向_objc_msgForward,那么当调用方法的时候会跳过前面的通过isa查找IMP的流程,直接就走消息转发了。最后我们来看一下aspect_hookClass这个核心方法。

static Class aspect_hookClass(NSObject *self, NSError **error) {
    NSCParameterAssert(self);
	Class statedClass = self.class;
	Class baseClass = object_getClass(self);
	NSString *className = NSStringFromClass(baseClass);

    // 类对象
	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);
    }

    // 实例对象
	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);
		aspect_hookedGetClass(subclass, statedClass);
		aspect_hookedGetClass(object_getClass(subclass), statedClass);
		objc_registerClassPair(subclass);
	}

	object_setClass(self, subclass);
	return subclass;
}

从方法的逻辑中我们看到

1.类对象

调用的是aspect_swizzleClassInPlace方法,这个方法主要的操作是class_replaceMethod(klass, @selector(forwardInvocation:), (IMP)__ASPECTS_ARE_BEING_CALLED__, "v@:@");也就是将消息转发的方法替换成我们自己的方法__ASPECTS_ARE_BEING_CALLED__

2.实例对象

如果你熟悉KVO的底层实现的话,你一定知道isa混写,也就是我们偷偷摸摸生成了一个新的类对象,然后我们对这个类对象做了和1相同的操作,并且我们Hook了class方法让外面看起来我们好像并没有做这个操作。最后我们将实例对象的isa指针指向了这个对象。你感兴趣的话,可以照着这个逻辑尝试下自己实现KVO。

最后理一下,当我们调用方法的时候就会直接走消息转发,消息转发的forwardInvocation已经替换成了我们的__ASPECTS_ARE_BEING_CALLED__。所以最后的具体执行逻辑就走这个方法里面了。

最后

我这里只是对Aspects做了一个很浅的介绍,希望能对大家有所帮助,也请大家多多指教~