靜下心來讀源碼之Aspects

前言

最近找工做受挫,無頭蒼蠅通常,掙扎焦慮的狀態實在是難受。決心改變這樣的狀態而且提升本身,那就從最簡單的靜下心來細扣優秀源碼開始吧。ios

Aspects簡介

Aspects是一個輕量級的面向切面編程(AOP)的庫。它主要提供了三個切入點:before(在原始的方法前執行)/instead(替換原始的方法執行)/after(在原始的方法後執行,默認),經過Runtime消息轉發實現Hook。它支持Hook某個實例對象的方法。而且它的內部考慮到了大量的可能觸發的問題並進行相應的處理來確保安全。相比於單純交換兩個IMP的Method Swizzling優點仍是很明顯的。編程

帶着問題看源碼

閱讀源碼前仍是要本身先去試用一下,通常在這個試用的過程中你或多或少的都是會有一些疑問的。帶着這些疑問去閱讀源碼的時候你就可能會有一些針對性。從某個具體的細節問題切入進去比單純泛泛的看源碼的效果來的好。我這裏拋磚引玉的提兩個問題。安全

1.Aspects是如何Hook某個特定實例對象的方法的

上面這張對象的內存佈局的圖我想你們應該見過,咱們都知道實例對象的方法列表都是存在類對象裏的,而且類對象實際上是一個單例對象。那麼當不一樣實例對象調用相同方法的時候最後找到方法實際上是同樣的。

2.Aspects如何Hook類方法

Aspects提供了兩個方法,一個對象方法一個是類方法。bash

+ (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類方法

三種功能對應的是兩個方法,咋作區分呢。(其實瞭解對象內存佈局的,應該立刻能反應過來要怎麼操做)less

宏觀粗略的感覺一下代碼主要邏輯結構

通常常見的源碼分析的文章喜歡從某個接口切入,從上往下的看整個代碼的執行過程,而後最後在得出一個結論或者框架圖。可是我感受這樣的方式對應讀者來講相對是不太友好的,有的時候讀者還沒對整個框架大體瞭解,這時候一大推的源碼貼上來讀者是一臉矇蔽的。我這裏先對整個框架的結構大體作一層介紹,省略了一些細節具體的過程。ide

經過上面這張大體的流程圖,咱們知道最後的方法調用都是會走消息轉發,而且 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作了一個很淺的介紹,但願能對你們有所幫助,也請你們多多指教~

相關文章
相關標籤/搜索