(長文預警)面向切面 Aspects 原始碼閱讀

我不言子不語兩相知發表於2018-02-10
前言

AOP(Aspect-oriented programming) 也稱之為 “面向切面程式設計”, 是一種通過預編譯方式和執行期動態代理實現程式功能的統一維護的一種技術。簡單來說可以做到 業務隔離解耦 等等效果。AOP 技術在__JAVA__ 的 Spring 框架中已經提供了非常全面成熟的解決方案。然而 iOS 等移動端在這方面的運用並不是很多,但是不妨礙它湧現出非常出色的三方庫,比如,我們接下來要說的三方庫 Aspects .

那麼我們什麼時候使用 AOP 比較適合呢

  • 當我們執行某種方法時候,對方法進行安全檢查 (例如,NSArray的陣列越界問題)
  • 對某些操作進行日誌記錄
  • 對購物車進行互動時候,根據使用者的操作,觸發建議或者提示
  • 激進一些,也可以用來去基類繼承,例如筆者的架構例項:NonBaseClass-MVVM-ReactiveObjc (乾貨帶程式碼)

大家會說,傳統的 OOP(Object Oriented Programming) 即物件導向程式設計,也完全能夠實現這些功能。 是的,沒錯,但是一個好的 OOP 架構應該是單一職責的,新增額外的切面需求意味著破壞了單一職責。例如,一個 Module 僅僅負責訂單業務,但是你其新增了安全檢查,日誌記錄,建議提示等等功能,這個 Module 會變得難以理解和維護,而且整個應用都將充斥著 日誌,安全檢查 等等邏輯,想想都頭皮發麻。

頭皮發麻

AOP 正是解決這一系列問題的良藥。

切面

__Aspects__基礎用法

__Aspects__使用起來也是非常簡單,只需要使用兩個簡單的介面,同時支援 類Hook例項Hook,提供了更細緻的操作

/// Adds a block of code before/instead/after the current `selector` for a specific class.
+ (id<AspectToken>)aspect_hookSelector:(SEL)selector
                      withOptions:(AspectOptions)options
                       usingBlock:(id)block
                            error:(NSError **)error;

/// Adds a block of code before/instead/after the current `selector` for a specific instance.
- (id<AspectToken>)aspect_hookSelector:(SEL)selector
                      withOptions:(AspectOptions)options
                       usingBlock:(id)block
                            error:(NSError **)error;
複製程式碼

例如,我們需要統計使用者進入某個 ViewController 的次數

[UIViewController aspect_hookSelector:@selector(viewWillAppear:) withOptions:AspectPositionAfter usingBlock:^(id<AspectInfo> aspectInfo, BOOL animated) {
    NSLog(@"View Controller %@ will appear animated: %tu", aspectInfo.instance, animated);
} error:NULL];
複製程式碼

從此,我們不必在基類中新增醜陋的程式碼了

Aspects架構剖析

架構

swizzledClassesDict 全域性字典

訪問介面

static NSMutableDictionary *aspect_getSwizzledClassesDict() {
    static NSMutableDictionary *swizzledClassesDict;
    static dispatch_once_t pred;
    dispatch_once(&pred, ^{
        swizzledClassesDict = [NSMutableDictionary new];
    });
    return swizzledClassesDict;
}
複製程式碼

儲存物件型別 <Class : AspectTracker *>
此全域性字典記錄了對Hook的類及其父類的追蹤資訊物件 AspectTracker

swizzledClasses 全域性集合

訪問介面

static void _aspect_modifySwizzledClasses(void (^block)(NSMutableSet *swizzledClasses)) {
    static NSMutableSet *swizzledClasses;
    static dispatch_once_t pred;
    dispatch_once(&pred, ^{
        swizzledClasses = [NSMutableSet new];
    });
    @synchronized(swizzledClasses) {
        block(swizzledClasses);
    }
}
複製程式碼

swizzledClasses 是一個全域性集合,儲存物件型別 被Hook的物件類名都會被儲存在此容器中

AspectBlockRef

Aspects 中的 AspectBlockRef 也就是我們使用介面中的引數 usingBlock 中的Block,在上述的例子中如下形式

^(id<AspectInfo> aspectInfo, BOOL animated) {
    NSLog(@"View Controller %@ will appear animated: %tu", aspectInfo.instance, animated);
}
複製程式碼

AspectBlockRef 的原始碼如下

typedef NS_OPTIONS(int, AspectBlockFlags) {
	AspectBlockFlagsHasCopyDisposeHelpers = (1 << 25),
	AspectBlockFlagsHasSignature          = (1 << 30)
};
/////////
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;
複製程式碼

看上去有點複雜,但是,我們一直使用的Block就是這樣的結構體,在這裡 AspectBlockRef 其實就是 ___GloablBlock__型別,AspectBlockRef 只是名字改了一下而已,本質上就是 Block 很幸運,蘋果開源了Block的相關程式碼 :Block實現原始碼傳送門 我們得以一窺究竟

/////////////////////// Block_private.h

// Values for Block_layout->flags to describe block objects
enum {
    BLOCK_DEALLOCATING =      (0x0001),  // runtime
    BLOCK_REFCOUNT_MASK =     (0xfffe),  // runtime
    BLOCK_NEEDS_FREE =        (1 << 24), // runtime
    BLOCK_HAS_COPY_DISPOSE =  (1 << 25), // compiler
    BLOCK_HAS_CTOR =          (1 << 26), // compiler: helpers have C++ code
    BLOCK_IS_GC =             (1 << 27), // runtime
    BLOCK_IS_GLOBAL =         (1 << 28), // compiler
    BLOCK_USE_STRET =         (1 << 29), // compiler: undefined if !BLOCK_HAS_SIGNATURE
    BLOCK_HAS_SIGNATURE  =    (1 << 30), // compiler
    BLOCK_HAS_EXTENDED_LAYOUT=(1 << 31)  // compiler
};

#define BLOCK_DESCRIPTOR_1 1
struct Block_descriptor_1 {
    uintptr_t reserved;
    uintptr_t size;
};

#define BLOCK_DESCRIPTOR_2 1
struct Block_descriptor_2 {
    // requires BLOCK_HAS_COPY_DISPOSE
    void (*copy)(void *dst, const void *src);
    void (*dispose)(const void *);
};

#define BLOCK_DESCRIPTOR_3 1
struct Block_descriptor_3 {
    // requires BLOCK_HAS_SIGNATURE
    const char *signature;
    const char *layout;     // contents depend on BLOCK_HAS_EXTENDED_LAYOUT
};

struct Block_layout {
    void *isa;
    volatile int32_t flags; // contains ref count
    int32_t reserved; 
    void (*invoke)(void *, ...);
    struct Block_descriptor_1 *descriptor;
    // imported variables
};
複製程式碼

其中 flags 代表著Block的運算元

//  AspectBlockRef
AspectBlockFlags flags;   
//  Block_private.h
volatile int32_t flags; // contains ref count
複製程式碼

Aspects 中只有兩種flag

AspectBlockFlagsHasCopyDisposeHelpers = (1 << 25),
AspectBlockFlagsHasSignature          = (1 << 30)
複製程式碼

對應 Block 定義列舉中的

BLOCK_HAS_COPY_DISPOSE =  (1 << 25), // compiler
BLOCK_HAS_SIGNATURE  =    (1 << 30), // compiler
複製程式碼
  • 當Block被Copy時 flag & AspectBlockFlagsHasCopyDisposeHelpers 為真時,Block佈局中將會新增 Block_descriptor_2
  • 當Block帶有方法簽名時 flag & AspectBlockFlagsHasSignature 為真時,Block佈局中存在Block_descriptor_3

invoke 函式指標也只是第一個引數由泛型變成了___AspectBlock__而已

//  AspectBlockRef
void (__unused *invoke)(struct _AspectBlock *block, ...);
//  Block_private.h
void (*invoke)(void *, ...);
複製程式碼

扯遠了,打住。

AspectToken

AspectToken

Aspects 內定義的協議,用來撤銷Hook

+ (id<AspectToken>)aspect_hookSelector:(SEL)selector
                      withOptions:(AspectOptions)options
                       usingBlock:(id)block
                            error:(NSError **)error;
複製程式碼

Hook方法會返回一個遵循 AspectToken 協議的方法,要取消對應的Hook,只需要呼叫代理物件的協議方法 remove就可以了

AspectInfo

AspectInfo

AspectInfo 物件遵循了 __AspectInfo__協議(同名協議),代表著切面資訊,也是上文 AspectBlockRef 中的首個引數

  • instance :當前被Hook的例項
  • originalInvocation :當前被Hook 原始的 NSInvocation 物件
  • arguments:所有方法的引數,一個計算屬性,被呼叫的時候才會有值

這裡特別提一下,引數是從 NSInvocation 中拿到的

NSMutableArray *argumentsArray = [NSMutableArray array];
for (NSUInteger idx = 2; idx < self.methodSignature.numberOfArguments; idx++) {
	[argumentsArray addObject:[self aspect_argumentAtIndex:idx] ?: NSNull.null];
}
複製程式碼

這裡引數下標是從2開始的,因為下標 0,1,已經分別對應了 __訊息接受物件__和 selector, 對於引數裝箱細節,可以細看 Aspects 的內部 NSInvocation 分類

AspectIdentifier

AspectIdentifier

實際上 AspectIdentifier 就是 aspect_hookSelector 函式的返回值

+ (id<AspectToken>)aspect_hookSelector:(SEL)selector
                      withOptions:(AspectOptions)options
                       usingBlock:(id)block
                            error:(NSError **)error;
複製程式碼

AspectIdentifier 提供了remove 方法的實現,然而我並沒有在原始碼中見到 AspectIdentifier 有宣告遵循 ____協議

以下方法對需要hook的類進行資訊封裝操作,方法內部對usingBlock引數中的Block進行適配檢查

+ (instancetype)identifierWithSelector:(SEL)selector object:(id)object options:(AspectOptions)options block:(id)block error:(NSError **)error 
{
    // 獲取Block的方法簽名
    NSMethodSignature *blockSignature =   aspect_blockMethodSignature(block, error); // TODO: check   signature compatibility, etc.
    // 相容性檢測
    if (!aspect_isCompatibleBlockSignature(blockSignature, object, selector, error)) {
        return nil;
    }
    // hook 資訊封裝
    AspectIdentifier *identifier = nil;
    if (blockSignature) {
        identifier = [AspectIdentifier new];
        identifier.selector = selector;
        identifier.block = block;
        identifier.blockSignature = blockSignature;
        identifier.options = options;
        identifier.object = object; // weak
    }
    return identifier;
}
複製程式碼
AspectsContainer

AspectsContainer

AspectsContainer 顧名思義,也就是切面的容器類,內部根據不同的option選項將 AspectIdentifier 放入不同的容器內

  • NSArray *beforeAspects 對應 AspectPositionBefore 選項
  • NSArray *insteadAspects 對應 AspectPositionInstead 選項
  • NSArray *afterAspects 對應 AspectPositionAfter 選項

要注意的是 __Asepcts__通過 aspect_getContainerForObject 方法從關聯的物件獲取

static AspectsContainer *aspect_getContainerForObject(NSObject *self, SEL selector) {
     // 獲取方法別名(原方法新增 aspects_ 字首)
    SEL aliasSelector = aspect_aliasForSelector(selector);
    AspectsContainer *aspectContainer = objc_getAssociatedObject(self, aliasSelector);
    if (!aspectContainer) {
        aspectContainer = [AspectsContainer new];
       // 以方法別名來關聯 AspectsContainer 容器物件
        objc_setAssociatedObject(self, aliasSelector, aspectContainer, OBJC_ASSOCIATION_RETAIN);
    }
    return aspectContainer;
}
複製程式碼
AspectTracker

AspectTracker

AspectTracker 代表著對切面的追蹤,儲存在全域性字典 swizzledClassesDict 中,從子類向上追蹤記錄資訊

  • selectorNames 記錄當前被追蹤的類需要hook的方法名
  • selectorNamesToSubclassTrackers 通過addSubclassTracker: hookingSelectorName記錄子類的 AspectTracker 物件
// Add the selector as being modified.
AspectTracker *subclassTracker = nil;
do {
    tracker = swizzledClassesDict[currentClass];
    if (!tracker) {
        tracker = [[AspectTracker alloc] initWithTrackedClass:currentClass];
        swizzledClassesDict[(id<NSCopying>)currentClass] = tracker;
    }
    if (subclassTracker) {
        [tracker addSubclassTracker:subclassTracker hookingSelectorName:selectorName];
    } else {
        [tracker.selectorNames addObject:selectorName];
    }

    // All superclasses get marked as having a subclass that is modified.
    subclassTracker = tracker;
}while ((currentClass = class_getSuperclass(currentClass)));
複製程式碼
Aspects核心解析

核心

方法入口

方法入口

+ (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);
}
複製程式碼

Aspects 既支援對類的 Hook,也支援對例項的Hook, 其核心在於 aspect_add 方法

static id aspect_add(id self, SEL selector, AspectOptions options, id block, NSError **error) {

    __block AspectIdentifier *identifier = nil;
    aspect_performLocked(^{
        // 判斷是否允許Hook類
        if (aspect_isSelectorAllowedAndTrack(self, selector, options, error)) {
            // 獲取關聯容器
            AspectsContainer *aspectContainer = aspect_getContainerForObject(self, selector);
            // hook資訊封裝成AspectIdentifier物件
            identifier = [AspectIdentifier identifierWithSelector:selector object:self options:options block:block error:error];
            if (identifier) {
                [aspectContainer addAspect:identifier withOptions:options];

                // hook 核心操作
                aspect_prepareClassAndHookSelector(self, selector, error);
            }
        }
    });
    return identifier;
}
複製程式碼

基本上 aspect_add 核心操作有三步

  • __aspect_isSelectorAllowedAndTrack__方法 判斷是否允許 Hook
  • hook資訊封裝成AspectIdentifier物件
  • aspect_prepareClassAndHookSelector 方法執行 Hook 操作(核心操作)

接下來一步步進行解析

aspect_isSelectorAllowedAndTrack 判斷是否允許Hook

1、黑名單過濾

static NSSet *disallowedSelectorList;
static dispatch_once_t pred;
dispatch_once(&pred, ^{
    disallowedSelectorList = [NSSet setWithObjects:@"retain", @"release", @"autorelease", @"forwardInvocation:", nil];
});

NSString *selectorName = NSStringFromSelector(selector);
if ([disallowedSelectorList containsObject:selectorName]) {
    ......
    return NO;
}
複製程式碼

特殊方法不允許hook

2、dealloc 方法hook只允許使用 AspectPositionBefore 選項

AspectOptions position = options&AspectPositionFilter;
if ([selectorName isEqualToString:@"dealloc"] && position != AspectPositionBefore) {
    ......
    return NO;
}
複製程式碼

3、過濾無法響應的方法

if (![self respondsToSelector:selector] && ![self.class instancesRespondToSelector:selector]) {
    return NO;
}
複製程式碼

4、例項和類過濾

在此之前插播下元類的概念

元類的概念

元類的定義:元類是類物件的類

還有需要注意的一點,class_isMetaClass(object_getClass(self)) 使用的是 object_getClass 方法 而不是類似 [obj class]的方法 兩者有何區別呢?

object_getClass [obj class]
例項物件 isa指標指向 isa指標指向
類物件 isa指標指向 物件本身
元類物件 isa指標指向 物件本身

好了,接下來我們看下過濾的程式碼

if (class_isMetaClass(object_getClass(self))) {
	......
}else{
	return YES;
}
複製程式碼

在這裡,筆者認為 class_isMetaClass(object_getClass(self)) 作用是判斷 hook 的物件是 例項 還是

NSObject *obj = [[NSObject alloc] init];
[obj aspect_hookSelector:@selector(copy) withOptions:AspectPositionAfter usingBlock:^(id <AspectInfo> info){
} error:nil];

Class getClass = object_getClass(self);
BOOL isMeta = class_isMetaClass(getClass); // 列印 NO
複製程式碼

如果是例項物件的話 getClass 也就是isa指標,指向了對應的類,然後 class_isMetaClass判斷自然是為NO

[NSObject aspect_hookSelector:@selector(copy) withOptions:AspectPositionAfter usingBlock:^(id <AspectInfo> info){

} error:nil];

Class getClass = object_getClass(self);
BOOL isMeta = class_isMetaClass(getClass); // 列印 YES
複製程式碼

如果是類物件的話 getClass 也就是isa指標,指向了對應的元類,然後 class_isMetaClass判斷自然是為YES

如果class_isMetaClass(object_getClass(self))返回YES,也就是說我們 Hook 的物件是類物件的話

Class klass = [self class];
NSMutableDictionary *swizzledClassesDict = aspect_getSwizzledClassesDict();
Class currentClass = [self class];

//  已經hook過的方法不再進行重複hook
AspectTracker *tracker = swizzledClassesDict[currentClass];
if ([tracker subclassHasHookedSelectorName:selectorName]) {
    return NO;
}

// 向上查詢父類
do {
    tracker = swizzledClassesDict[currentClass];
    if ([tracker.selectorNames containsObject:selectorName]) {
        if (klass == currentClass) {
            // 如果是已經遍歷到了父類頂層
            return YES;
        }
        //  已經hook過的方法不再進行重複hook
        return NO;
    }
} while ((currentClass = class_getSuperclass(currentClass)));

// 向上查詢父類,生成 AspectTracker 資訊
currentClass = klass; 
AspectTracker *subclassTracker = nil;
do {
    tracker = swizzledClassesDict[currentClass];
    if (!tracker) {
        tracker = [[AspectTracker alloc] initWithTrackedClass:currentClass];
        swizzledClassesDict[(id<NSCopying>)currentClass] = tracker;
    }
    if (subclassTracker) {
        [tracker addSubclassTracker:subclassTracker hookingSelectorName:selectorName];
    } else {
        [tracker.selectorNames addObject:selectorName];
    }

    // All superclasses get marked as having a subclass that is modified.
    subclassTracker = tracker;
}while ((currentClass = class_getSuperclass(currentClass)));
複製程式碼

所以,hook 例項方法不需要向上遍歷父類方法,這也符合直覺和邏輯

Aspects hook 之前做了非常健全的前置檢查,非常值得學習!

(長文預警)面向切面 Aspects 原始碼閱讀
OK,下一個tips

Hook 資訊封裝成AspectIdentifier物件

+ (instancetype)identifierWithSelector:(SEL)selector object:(id)object options:(AspectOptions)options block:(id)block error:(NSError **)error {

    NSMethodSignature *blockSignature = aspect_blockMethodSignature(block, error); // TODO: check signature compatibility, etc.
    if (!aspect_isCompatibleBlockSignature(blockSignature, object, selector, error)) {
        return nil;
    }

    AspectIdentifier *identifier = nil;
    if (blockSignature) {
        identifier = [AspectIdentifier new];
        identifier.selector = selector;
        identifier.block = block;
        identifier.blockSignature = blockSignature;
        identifier.options = options;
        identifier.object = object; // weak
    }
    return identifier;
}
複製程式碼

方法首先通過aspect_blockMethodSignature提取 Block 的方法簽名

static NSMethodSignature *aspect_blockMethodSignature(id block, NSError **error) {
    AspectBlockRef layout = (__bridge void *)block;
    // 通過判斷block的flags 來確定Block是否存在方法簽名
    if (!(layout->flags & AspectBlockFlagsHasSignature)) {
        return nil;
    }
    // 獲取 descriptor 
    void *desc = layout->descriptor;
    /* 計算記憶體地址偏移量來確定 signature 的位置 */
    // 加上 reserved 和 size的偏移量 (型別皆為unsigned long int 所以 乘以 2)
    desc += 2 * sizeof(unsigned long int);
    // 如果有 copy 和 dispose 的 flag 需要 加上對應偏移量
    if (layout->flags & AspectBlockFlagsHasCopyDisposeHelpers) {
        desc += 2 * sizeof(void *);
    }
    if (!desc) {
        return nil;
    }
    // 獲取到了真正的方法簽名
    const char *signature = (*(const char **)desc);
    return [NSMethodSignature signatureWithObjCTypes:signature];
}
複製程式碼

漲姿勢了,通過如上程式碼我們知道了如何獲得Block的方法簽名, 我們可以先理解消化本文解釋 AspectBlockRef 的記憶體佈局,然後再來理解這段程式碼

至於程式碼中的二級指標(*(const char **)desc)

// descriptor 是一個結構體指標,儲存的是結構體的起始地址
struct {
    const char *signature;
} *descriptor;
// 結構體指標 賦值給 泛型指標desc
void *desc = layout->descriptor;
//  當我們獲取到真正的方法簽名,此時 desc 已經指向了 signature 所在的地址
//  signature 的型別是 const char *,那麼 signature 的地址自然就是const char **咯
//  最後我們要取 desc 指標中的值,也就是 *obj 咯
const char ** obj = (const char **)desc;
const char *signature = *obj;
複製程式碼

恩,沒毛病,下一個Tips, 也是我們的重頭戲

aspect_prepareClassAndHookSelector 方法執行 Hook 操作

我們來看下aspect_prepareClassAndHookSelector的原始碼

static void aspect_prepareClassAndHookSelector(NSObject *self, SEL selector, NSError **error) {

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

        // We use forwardInvocation to hook in.
        class_replaceMethod(klass, selector, aspect_getMsgForwardIMP(self, selector), typeEncoding);
    }
}
複製程式碼

這裡原始碼中比較重要和核心的在於 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);

    // 已經子類化過了,也就是已經新增過子類字尾 “_Aspects_”
	if ([className hasSuffix:AspectsSubclassSuffix]) {
		return baseClass;
	}
    // 如果 Hook 的是 類物件,那麼混寫 類物件的ForwardInvocation方法 (類物件的指標指向本身)
    else if (class_isMetaClass(baseClass)) {
        return aspect_swizzleClassInPlace((Class)self);
    }
    // 如果 Hook 的是 一個 已經被kvo子類化的例項物件,我們需要混寫它的 metaClass 的ForwardInvocation方法
    else if (statedClass != baseClass) {
        return aspect_swizzleClassInPlace(baseClass);
    }

    // 混寫例項物件

   //  新增 預設字尾 _Aspects_ 
	const char *subclassName = [className stringByAppendingString:AspectsSubclassSuffix].UTF8String;
   //  獲取 新增預設字尾後的類
	Class subclass = objc_getClass(subclassName);

	if (subclass == nil) {
    // 如果還沒有動態建立過子類
		subclass = objc_allocateClassPair(baseClass, subclassName, 0);
		if (subclass == nil) {
            return nil;
        }
        // 混寫 forwardInvocation:方法
		aspect_swizzleForwardInvocation(subclass);
        // 替換 class 的 IMP 指標
        // subClass.class = statedClass
		aspect_hookedGetClass(subclass, statedClass);
        // subClass.isa.class = statedClass 
		aspect_hookedGetClass(object_getClass(subclass), statedClass);
        // 註冊新類
		objc_registerClassPair(subclass);
	}
    // 複寫 isa 指標,指向新的類
	object_setClass(self, subclass);
	return subclass;
}
複製程式碼

值得注意的是

// 如果 Hook 的是 類物件,那麼混寫 類物件 (類物件的指標指向本身)
Class baseClass = object_getClass(self);
else if (class_isMetaClass(baseClass)) {
    return aspect_swizzleClassInPlace((Class)self);
}
// 如果 Hook 的是 一個 已經被kvo子類化的例項物件,我們需要混寫它的 metaClass
else if (statedClass != baseClass) {
    return aspect_swizzleClassInPlace(baseClass);
}
複製程式碼

class_isMetaClass(baseClass)前面已經說過了,是用來判斷是否 hook 的是類物件,那麼 statedClass != baseClass 是什麼含義呢

首先我們得要先知道 KVO 實現的原理: 當觀察某物件 A 時,KVO 機制動態建立一個物件A當前類的子類,併為這個新的子類重寫了被觀察屬性 keyPath 的 setter 方法

從前文我們知道,例項物件 object_getClass[obj class] 的指向是一致的,當hook被KVO子類化的例項時候,例項物件的isa指標的指向 和 class 的指向才會不一致

我們還學會了動態建立一個類的流程

  • objc_allocateClassPair
  • class_addMethod
  • class_addIvar
  • objc_registerClassPair

小結:對類物件的hook是通過混寫類物件的 forwardInvocation 方法來實現,對例項物件的 hook 是通過子類化,然後混寫子類的 forwardInvocation 來實現的

emmmmm, 接下來我們聊一聊混寫__forwardInvocation__ 的一些細節

aspect_swizzleForwardInvocation 混寫 forwardInvocation

方法轉發流程
方法轉發流程,這裡就不贅述,Aspects 核心思想就是通過混寫 forwardInvocation 來實現切面執行自定義程式碼

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@:@");
    }
}
複製程式碼

上述程式碼將類的 forwardInvocation 方法 IMP 替換成 __ ASPECTS_ARE_BEING_CALLED __ ,原來的 IMP 與 AspectsForwardInvocationSelectorName 相關聯了

接下來我們看看 __ ASPECTS_ARE_BEING_CALLED __ 方法

static void __ASPECTS_ARE_BEING_CALLED__(__unsafe_unretained NSObject *self, SEL selector, NSInvocation *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.
    BOOL respondsToAlias = YES;
    if (objectContainer.insteadAspects.count || classContainer.insteadAspects.count) {
        // 執行 insteadAspects 中的操作
        aspect_invoke(classContainer.insteadAspects, info);
        aspect_invoke(objectContainer.insteadAspects, info);
    }else {
         // 正常執行方法 附加判斷是否能夠響應方法
        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 (!respondsToAlias) {
        invocation.selector = originalSelector;
        SEL originalForwardInvocationSEL = NSSelectorFromString(AspectsForwardInvocationSelectorName);

        // 如何hook物件不響應 aliasSelector 那麼執行原有的 forwardInvocation
        if ([self respondsToSelector:originalForwardInvocationSEL]) {
            ((void( *)(id, SEL, NSInvocation *))objc_msgSend)(self, originalForwardInvocationSEL, invocation);
        }else {
            // 如果類沒有實現 forwardInvocation 方法的話,將會丟擲 doesNotRecognizeSelector 錯誤
            [self doesNotRecognizeSelector:invocation.selector];
        }
    }

    // 移除 aspectsToRemove 中的 AspectIdentifier,並執行  AspectIdentifier 的 remove 方法
    [aspectsToRemove makeObjectsPerformSelector:@selector(remove)];
}
複製程式碼

上述程式碼執行了對應的 beforeAspects,insteadAspects,afterAspects 如果有insteadAspects 操作則執行insteadAspects 操作,否則執行 aliasSelector, 若不響應 aliasSelector, 那麼將執行 hook 之前的 forwardInvocation的方法,沒有實現 forwardInvocation那麼將丟擲 doesNotRecognizeSelector 異常

我們再來看看 aspect_invoke 執行切面的細節

aspect_invoke
#define aspect_invoke(aspects, info) \
for (AspectIdentifier *aspect in aspects) {\
    [aspect invokeWithInfo:info];\
    if (aspect.options & AspectOptionAutomaticRemoval) { \
        aspectsToRemove = [aspectsToRemove?:@[] arrayByAddingObject:aspect]; \
    } \
}
複製程式碼

巨集定義執行了__AspectIdentifier__ 的 invokeWithInfo:方法

- (BOOL)invokeWithInfo:(id<AspectInfo>)info {
    NSInvocation *blockInvocation = [NSInvocation invocationWithMethodSignature:self.blockSignature];
    NSInvocation *originalInvocation = info.originalInvocation;
    NSUInteger numberOfArguments = self.blockSignature.numberOfArguments;

    // 檢查引數是否適配,已經是第二次檢查了
    if (numberOfArguments > originalInvocation.methodSignature.numberOfArguments) {
        return NO;
    }

    // 設定引數
    if (numberOfArguments > 1) {
        [blockInvocation setArgument:&info atIndex:1];
    }
    
    // 遍歷 originalInvocation 引數,將引數值複製到  blockInvocation
	void *argBuf = NULL;
    for (NSUInteger idx = 2; idx < numberOfArguments; idx++) {
        const char *type = [originalInvocation.methodSignature getArgumentTypeAtIndex:idx];
		NSUInteger argSize;
		NSGetSizeAndAlignment(type, &argSize, NULL);
        
		if (!(argBuf = reallocf(argBuf, argSize))) {
			return NO;
		}
        
		[originalInvocation getArgument:argBuf atIndex:idx];
		[blockInvocation setArgument:argBuf atIndex:idx];
    }
    // 執行 blockInvocation
    [blockInvocation invokeWithTarget:self.block];
    
    if (argBuf != NULL) {
        free(argBuf);
    }
    return YES;
}
複製程式碼

這裡比較有意思的是 [blockInvocation setArgument:&info atIndex:1]; 按照正常來說 引數下標0 和 1 分別是 target, selector

(長文預警)面向切面 Aspects 原始碼閱讀

列印 blockInvocation 的方法簽名,發現引數下標為 1 型別 並不是 selector ,而是 id< AspectInfo > 有知道的大佬希望能夠指出提點下

完結,撒花

還有什麼?

  • aspect_remove 對Hook的清理操作,這裡就不贅述了

  • 有對實踐感興趣的話,可以看筆者粗淺的架構例項:NonBaseClass-MVVM-ReactiveObjc (乾貨帶程式碼)

  • 最後祝願大家新年快樂,過個好年,且行且珍惜,麼麼噠?

    (長文預警)面向切面 Aspects 原始碼閱讀

相關文章