靜下心來讀原始碼之Aspects

小和山吳彥祖發表於2018-03-21

前言

最近找工作受挫,無頭蒼蠅一般,掙扎焦慮的狀態實在是難受。決心改變這樣的狀態並且提高自己,那就從最簡單的靜下心來細扣優秀原始碼開始吧。

Aspects簡介

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

帶著問題看原始碼

閱讀原始碼前還是要自己先去試用一下,一般在這個試用的過程當中你或多或少的都是會有一些疑問的。帶著這些疑問去閱讀原始碼的時候你就可能會有一些針對性。從某個具體的細節問題切入進去比單純泛泛的看原始碼的效果來的好。我這裡拋磚引玉的提兩個問題。

1.Aspects是如何Hook某個特定例項物件的方法的

靜下心來讀原始碼之Aspects
上面這張物件的記憶體佈局的圖我想大家應該見過,我們都知道例項物件的方法列表都是存在類物件裡的,並且類物件其實是一個單例物件。那麼當不同例項物件呼叫相同方法的時候最後找到方法其實是一樣的。

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

三種功能對應的是兩個方法,咋做區分呢。(其實瞭解物件記憶體佈局的,應該馬上能反應過來要怎麼操作)

巨集觀粗略的感受一下程式碼主要邏輯結構

一般常見的原始碼分析的文章喜歡從某個介面切入,從上往下的看整個程式碼的執行過程,然後最後在得出一個結論或者框架圖。但是我感覺這樣的方式對應讀者來說相對是不太友好的,有的時候讀者還沒對整個框架大致瞭解,這時候一大推的原始碼貼上來讀者是一臉矇蔽的。我這裡先對整個框架的結構大致做一層介紹,省略了一些細節具體的過程。

靜下心來讀原始碼之Aspects
通過上面這張大致的流程圖,我們知道最後的方法呼叫都是會走訊息轉發,並且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做了一個很淺的介紹,希望能對大家有所幫助,也請大家多多指教~

相關文章