iOS開發中的AOP利器 - Aspects 原始碼分析(一)

lyuf發表於2018-09-12

AOP簡介

AOP全名為 Aspect Oriented Programming- 面向切面程式設計。AOPOOPObject-Oriented Programing - 物件導向程式設計)的補充和完善。 OOP引入封裝繼承多型等概念來建立一種物件層次結構,用以模擬公共行為的一個集合。當我們需要為分散的物件引入公共行為的時候,OOP則顯得無能為力。也就是說,OOP允許你定義從上到下的關係,但並不適合定義從左到右的關係。例如日誌功能。日誌程式碼往往水平地散佈在所有物件層次中,而與它所散佈到的物件的核心功能毫無關係。對於其他型別的程式碼,如安全性、異常處理和透明的持續性也是如此。這種散佈在各處的無關的程式碼被稱為橫切(cross-cutting)程式碼,在OOP設計中,它導致了大量程式碼的重複,而不利於各個模組的重用。

AOP技術其實是對OOP設計的物件,利用一種稱為“橫切”的技術,剖解開封裝的物件內部,並將那些影響了多個類的公共行為封裝到一個可重用模組,並將其名為“Aspect”,即方面。所謂“方面”,簡單地說,就是將那些與業務無關,卻為業務模組所共同呼叫的邏輯或責任封裝起來,便於減少系統的重複程式碼,降低模組間的耦合度,並有利於未來的可操作性和可維護性。

---- 想了解更詳細的AOP思想可以檢視這邊文章 《團隊開發框架實戰—面向切面的程式設計 AOP》 ,以上總結也是摘自這篇文章。

舉個例子,我們需要統計使用者的行為,看下使用者對app的興趣分佈熱點,此時通常需要在多個控制器的 viewWillAppear:方法中加如處理統計的程式碼,這些程式碼都是與業務邏輯無關的,而且分散在多個模組中,此時我們可利用AOP技術,把這些重複、分散的程式碼提取出來成為一個獨立的模組。這樣既減少了系統的重複程式碼,也降低了模組的耦合度。好處還是十分明顯的。

Aspects初步認識及使用

這是iOS開發中實現AOP的一個輕量級框架 , 它就提供了兩個介面實現AOP ,這兩個方法都是 NSObject的分類方法

//為某個所有類 物件的selector 進行切面  新增AOP實現
+ (id<AspectToken>)aspect_hookSelector:(SEL)selector
                           withOptions:(AspectOptions)options
                            usingBlock:(id)block
                                 error:(NSError **)error;

//為某個物件的selector 進行切面  新增AOP實現
- (id<AspectToken>)aspect_hookSelector:(SEL)selector
                           withOptions:(AspectOptions)options
                            usingBlock:(id)block
                                 error:(NSError **)error;
複製程式碼

例如我要為所有繼承自UIViewController的物件的viewWillAppear:新增切面實現

[UIViewController aspect_hookSelector:@selector(viewDidAppear:) withOptions:AspectPositionAfter usingBlock:^(id<AspectInfo> aspectInfo  , BOOL animated ){
   NSLog(@"成功進行了切面");
 }error:NULL];
複製程式碼

在程式執行完上面這句程式碼後,你就成功的對所有控制器的viewWillAppear :方法hook, 在方法執行完原實現後,都會執行上面的Block中的列印 ,方法中返回的協議<AspectToken>物件,可用於移除你新增的hook(呼叫 協議物件的 -remove方法)。而Aspect的例項方法- (id<AspectToken>)aspect_hookSelector:(SEL)selector withOptions:(AspectOptions)options usingBlock:(id)block error:(NSError **)error; 是對單一物件的某個方法進行hook , 效果跟例子中的類方法差不多,這裡就不在過多介紹。

Aspects內部實現分析

Apects框架涉及的底層只是比較多,建議先了解清楚一下幾隻知識點後再看原始碼實現可能會比較容易理解。

  • Block的內部結構及實現原理
  • runtime的訊息轉發機制
  • OC中物件的 isa 指標 + KVO的實現原理

由於這個框架的設計中,物件間關係的關係比較複雜,這裡我先簡單介紹一下,Aspects中定義的類的作用。

AspectInfo1. 作為公有協議(面向介面呼叫) : 為外界提供了一個協議(抽象介面),用於在定義block時作為其第一個引數的遵循協議, 方便訪問物件的相關資料。2. 作為私有物件 : 在原方法selector發起呼叫時 , 作為封裝呼叫引數(實參,用NSInvocation包裝)的物件

AspectIdentifier :用於封裝定義hook時傳進來的block,方法的呼叫者target , selector ,切面時機選項(AspectOptions - 位移列舉) , 以及在呼叫完新增hook方法時作為返回值返回給呼叫者 ,遵守協議AspectToken 用於移除hook , 執行hook事件的執行處理

AspectTracker : 用於追蹤或記錄曾經hook過的Class(不包活例項物件的hook) , 以防止對同一個集繼承體系的Class對同一個例項方法進行重複hook

AspectsContainer : 用三陣列(beforeAspects , insteadAspects , afterAspects)分別記錄對應時機進行 hook 的標識物件 AspectIdentifier ,為hook 提供資料儲存及支援。

AspectToken : 用於移除hook的一個協議(只有一個方法 :-remove) , AspectIdentifier就是遵循該協議的類

先大概瞭解框架進行hook時對類的處理巨集觀處理圖解,更有利於對細節處理的理解及分析。 下圖是對某個Class的Selector進行了hook處理後的類內變化情況。

Aspects分析.png

Asepcts的核心步驟:把要進行hook的selector的IMP直接更換為runtime中的訊息轉發的IMP (_objc_msgForward_objc_msgForward_stret) , 讓外界呼叫改selector的時候直接進入到訊息轉發 (注意:這裡的原理與熱修復框架JSPatch的原理是一樣的,因此這兩個框架的共存是有問題的),從而呼叫到方法-forwardInvocation:方法中,此時Class的-forwardInvocation :的實現已經被框架替換為自定義函式 __ASPECTS_ARE_BEING_CALLED__ , 從而成功進行hook處理。

下面我們來分析Asepcts的具體實現 我們先看兩個新增hook處理的方法 , 其中兩個都是NSObjct的分類方法:

//NSObject的類方法
+ (id<AspectToken>)aspect_hookSelector:(SEL)selector
                      withOptions:(AspectOptions)options
                       usingBlock:(id)block
                            error:(NSError **)error;

//NSObject的例項方法
- (id<AspectToken>)aspect_hookSelector:(SEL)selector
                      withOptions:(AspectOptions)options
                       usingBlock:(id)block
                            error:(NSError **)error;
複製程式碼

其中這兩個方法都呼叫了下面函式

static id aspect_add(id self, SEL selector, AspectOptions options, id block, NSError * __autoreleasing *error) 
複製程式碼

上面函式的 形參 id self ,為什麼可以同時接受例項方法的物件和類方法中Class呢?其實理解這個問題的實質我覺得需要理解OC中對物件的定義。我們看來看下下面有關物件定義的原始碼:

其實OC中Class也是物件 ,我們可以看看他們三個(id , Class,NSObject *)在runtime中的定義

// Class其實是 結構體 objc_class * 的指標 
typedef struct objc_class *Class;

// id其實是 結構體 objc_object * 的指標 別名
typedef struct objc_object *id;

//OC的基類 NSObject 的宣告 - 可以理解為其實可以看做是首地址為指向  objc_class *  (Isa)指標的記憶體 都可以看做是物件
@interface NSObject <NSObject> {
     Class isa  OBJC_ISA_AVAILABILITY;
}

struct objc_object {
private:
    isa_t isa;
}

struct objc_class : objc_object {
    // Class ISA;
    Class superclass;
    cache_t cache;             // formerly cache pointer and vtable
    class_data_bits_t bits;    // class_rw_t * plus custom rr/alloc flags
}
複製程式碼

通過上面的原始碼我們可以總結出幾點:

  • Class其實是 結構體 objc_class * 的指標 別名
  • id其實是 結構體 objc_object * 的指標 別名
  • 結構體objc_object與OC中的 NSObject 的首地址都是指向 isa ,可以理解為首地址為ISA指標的記憶體都可以稱為物件。
  • objc_class 是繼承自 objc_object ,也就是說,OC中 Class 也是一個物件。因此框架中無論是hook的例項方法還是hook的類方法,都可以統一把 呼叫的物件( ClassNSObject * ) 傳參 給了id 型別。

新增hook的實現

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

    __block AspectIdentifier *identifier = nil;
    //自璇鎖 , 保證block中的執行緒安全
    aspect_performLocked(^{
        if (aspect_isSelectorAllowedAndTrack(self, selector, options, error)) {
            //型別懶載入 利用runtime 的屬性關聯 新增屬性  __aspects__selector -> AspectsContainer
            AspectsContainer *aspectContainer = aspect_getContainerForObject(self, selector);
            //生成hook的對應標識 AspectIdentifier
            identifier = [AspectIdentifier identifierWithSelector:selector object:self options:options block:block error:error];
            if (identifier) {
                
                // 新增 identifier 到 aspectContainer 的相應陣列
                [aspectContainer addAspect:identifier withOptions:options];

                // Modify the class to allow message interception.
                aspect_prepareClassAndHookSelector(self, selector, error);
            }
        }
    });
    return identifier;
}
複製程式碼

上面方法中的加鎖 aspect_performLocked 函式,保證了block中的資源在多執行緒下讀取安全 ,實現如下

static void aspect_performLocked(dispatch_block_t block) {
    static OSSpinLock aspect_lock = OS_SPINLOCK_INIT;
    OSSpinLockLock(&aspect_lock);
    block();
    OSSpinLockUnlock(&aspect_lock);
}
複製程式碼

注意 :但是OSSpinLock這個鎖系統提示已經過期了,而且這個鎖在多執行緒中如果執行緒的優先順序不同,會造成鎖無法釋放等問題,詳細可以看下這篇文章不再安全的 OSSpinLock

新增hook工作之一 : 檢查能否 hook

aspect_isSelectorAllowedAndTrack 檢查hook的可行性 , 並利用AspectTracker 處理防止對一個類的某個方法進行重複hook 。這個方法會分別過濾掉不能hook的黑名單方法 (retain , release ,autorelease ,forwardInvocation: ,retain ), 如果是對整個類的某個selector進行 hook (發生在呼叫Aspects框架的類方法進行 hook), 還會進行一個額外的處理 ,利用AspectTracker檢查 、記錄並追蹤Classhook 情況,在一個Class 第一次被hook時,在其向上的繼承關係中都會在全域性的容器中儲存下hook的記錄具體的實現 ,為了避免在一個繼承關係鏈中重複對同一個selector進行hook ,具體可以看下這段程式碼邏輯及註釋

//檢視 self 的 isa 指標是否是metaClass , 這下面的處理都是針對 對 整個class的所有例項物件的例項方法進行hook
if (class_isMetaClass(object_getClass(self))) {
    Class klass = [self class];
    //全域性變數記錄所有被hook的Class(系統或自定義的類)
    NSMutableDictionary *swizzledClassesDict = aspect_getSwizzledClassesDict();
    //獲取類
    Class currentClass = [self class];
    
    do {
        AspectTracker *tracker = swizzledClassesDict[currentClass];
        if ([tracker.selectorNames containsObject:selectorName]) { //證明曾經 對selector hook 過

            //判斷要hook的方法 , 在對應的子類是否有hook過同一個selector ,子類hook過了 ,就不能再對父類hook
            // Find the topmost class for the log.
            if (tracker.parentEntry) {  //證明子類已經對selector hook過了 ,下面的邏輯主要是找出具體那個子類被hook , 該類的 parentEntry = nil
                AspectTracker *topmostEntry = tracker.parentEntry;
                while (topmostEntry.parentEntry) {
                    topmostEntry = topmostEntry.parentEntry;
                }
                NSString *errorDescription = [NSString stringWithFormat:@"Error: %@ already hooked in %@. A method can only be hooked once per class hierarchy.", selectorName, NSStringFromClass(topmostEntry.trackedClass)];
                AspectError(AspectErrorSelectorAlreadyHookedInClassHierarchy, errorDescription);
                return NO;
            }else if (klass == currentClass) { //這裡表示以前對class的selector進行過hook , 現在從新在該類中對selector定義hook事件
                // hook的已經是最頂曾的類了(oc中的子類 例如:UIButton , UIImagView ),進行行過hook,因此會  沒有 parentEntry , 這裡並沒有執行下面的while語句
                // Already modified and topmost!
                return YES;
            }
        }
        
    }while ((currentClass = class_getSuperclass(currentClass)));

    // 執行到這裡證明 selector 可以 hook , 在整個向上的繼承體系中(父類)生成hook的記錄  對應關係 : AspectTracker -> 進行hook的Class + selectorName
    currentClass = klass;
    AspectTracker *parentTracker = nil; //實際新增hook的類 沒有這個parentTracker
    do {
        AspectTracker *tracker = swizzledClassesDict[currentClass];
        if (!tracker) {
            tracker = [[AspectTracker alloc] initWithTrackedClass:currentClass parent:parentTracker];
            swizzledClassesDict[(id<NSCopying>)currentClass] = tracker;
        }
        [tracker.selectorNames addObject:selectorName];
        // All superclasses get marked as having a subclass that is modified.
        parentTracker = tracker;
    }while ((currentClass = class_getSuperclass(currentClass)));
}
複製程式碼

新增hook工作之二 : hook定義的引數儲存 + Block 有效性驗證

//類似懶載入 利用runtime 的屬性關聯 新增屬性  __aspects__selector -> AspectsContainer
AspectsContainer *aspectContainer = aspect_getContainerForObject(self, selector);
//生成hook的對應標識 AspectIdentifier
identifier = [AspectIdentifier identifierWithSelector:selector object:self options:options block:block error:error];
if (identifier) {
    
    // 新增 identifier 到 aspectContainer 的相應陣列
    [aspectContainer addAspect:identifier withOptions:options];

    // Modify the class to allow message interception.
    aspect_prepareClassAndHookSelector(self, selector, error);
}
複製程式碼

如果允許hook , 一個 hook的定義對應一個 AspectIdentifier。一個物件(類也是物件)的所有hook都存放在這個物件通過runtime的物件關聯繫結的屬性中 ,該屬性型別為AspectsContainer,根據hook定義時傳進來的options引數分別加入到 AspectContainer 對應的陣列

  1. beforeAspects - selecter執行前進行的hook處理
  2. insteadAspects - 替換調selecter執行hook處理
  3. afterAspects - selecter執行後進行的hook處理

注意:我們看下AspectContainer中的屬性宣告 , 三個陣列都是宣告為 atomic ,來保證多線層的讀取安全。這也是框架作者在開始時就提示我們,建議不要對呼叫頻繁的方法進行hook的原因之一。

// AspectContainer陣列屬性宣告
@property (atomic, copy) NSArray *beforeAspects;
@property (atomic, copy) NSArray *insteadAspects;
@property (atomic, copy) NSArray *afterAspects;
複製程式碼

AspectIdentifier 這個類在初始化時還做了selector 和 執行hookBlock的引數校驗

AspectIdentifier的初始化方法:主要是驗證完blockhook sel ector的引數型別是否符合要求後,才完成初始化的操作 , 如果不符合要求直接返nil結束初始化,程式碼實現如下:

+ (instancetype)identifierWithSelector:(SEL)selector object:(id)object options:(AspectOptions)options block:(id)block error:(NSError **)error {
    NSCParameterAssert(block);
    NSCParameterAssert(selector);
    NSMethodSignature *blockSignature = aspect_blockMethodSignature(block, error); // TODO: check signature compatibility, etc.
    if (!aspect_isCompatibleBlockSignature(blockSignature, object, selector, error)) {
        return nil;
    }

    //selector 與 block 引數匹配後 生成 AspectIdentifier (引數的個數、型別一樣)
    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;
}
複製程式碼

AspectIdentifier初始化方法中呼叫的獲取Block簽名字元的函式:這個函式主要是根據BlockBlock在編譯成C語言後其實是一個結構體)的內部結構,操作指標的位移數來獲取到簽名引數字串,並色很生成 NSMethodSignature返回。 更詳細的原理 ,可以看下我之前寫的《淺析Block的內部結構 及其 如何利用 NSInvocation 進行呼叫》

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];
}
複製程式碼

Blockhook selector的引數校驗函式:主要是獲取selector引數數量方法簽名字串跟Block的簽名字串比較它們是否一致,這裡解析一下for迴圈為什麼是從2開始遍歷的

  • block執行呼叫時所傳的引數: 0 . block本身(encodeType = @?) 1 . 其他自定義的引數(這裡的Block 索引為1 的位置為 id aspectInfo)

  • selector 執行呼叫時所傳的引數: 0.id object 方法呼叫者 1.selector 方法本省 2 .其他自定義的引數

所以這裡要校對的是自定義引數的是否一致,這裡Block的前兩個引數分別是Block本身,以及一個id<ApsectInfo>型別的物件。所以從第二個索引開始比較自定義引數的型別。

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, that's 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:@"Blog signature %@ doesn't match %@.", blockSignature, methodSignature];
        AspectError(AspectErrorIncompatibleBlockSignature, description);
        return NO;
    }
    return YES;
}
複製程式碼

新增hook工作之三 : 方法的交換處理

static void aspect_prepareClassAndHookSelector(NSObject *self, SEL selector, NSError **error) {
    NSCParameterAssert(selector);
    
    //獲取klass 獲取進行hook處理的Class,主要是替換 forwardInvocation:方法的 IMP 。
    Class klass = aspect_hookClass(self, error);
    
    //獲取原來方法的IMP
    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.
        //selector的IMP替換為 訊息轉發的IMP
        //aspects__selector的IMP替換為 最初selector的IMP
        
        const char *typeEncoding = method_getTypeEncoding(targetMethod);
        SEL aliasSelector = aspect_aliasForSelector(selector);
        if (![klass instancesRespondToSelector:aliasSelector]) { //判斷klass 是否能響應aliasSelector ,不能的話就新增aliasSelector方法 , 實現為原來selector的實現IMP
            __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. 讓原來的selector 直接進入訊息轉發 forwardInvocaction:
        class_replaceMethod(klass, selector, aspect_getMsgForwardIMP(self, selector), typeEncoding);
        AspectLog(@"Aspects: Installed hook for -[%@ %@].", klass, NSStringFromSelector(selector));
    }
}
複製程式碼

首先獲取到要hookClass , 然後判斷要hookselectorIMP是不是進入 訊息轉發的 IMP 是的話就預設已經完成了框架中進行hook的準備工作了。如果不是的話繼續進行 if程式碼塊裡的處理邏輯

  1. 新增方法,名字為 aspects__selector(selector為要hook的方法名) , 使其 IMP指向selectorIMP
  2. selectorIMP指向訊息轉發的IMP, 這是外界呼叫這個 selector直接進入訊息轉發,從而呼叫到被處理過的 forwardInvocation: 經過處理後 ,外界呼叫selector是就可以進入了訊息轉發的 forwardInvocation:方法

接下來看下獲取hook Class時的程式碼實現

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

    // Already subclassed
    if ([className hasSuffix:AspectsSubclassSuffix]) { //className 如果有 _Aspects_  字首 , 以前hook做的例項物件
        return baseClass;

        // We swizzle a class object, not a single object.
    }else if (class_isMetaClass(baseClass)) { //self 是 Class
        return aspect_swizzleClassInPlace((Class)self);
    
        // Probably a KVO'ed class. Swizzle in place. Also swizzle meta classes in place.
        //測試 :對UILabel物件進行kvo後  class == UILabel , get_class == NSKVONotifying_UILabel
        //Aspect在gitHub上的issues上有人已經解決了KVO衝突的方案  https://github.com/steipete/Aspects/pull/115

    }else if (statedClass != baseClass) { //self 是 被KVO的物件 , 需要把 NSKVONotifying_ClassName 的 forwardInvocation: 替換處理
        return aspect_swizzleClassInPlace(baseClass);
    }

    //self是普通object , 建立一個 aspect__字首的子類 , 並把 self的 isa 指向新的子類
    //不用吧新建的class加入全域性class中記錄
    // Default case. Create dynamic subclass.
    const char *subclassName = [className stringByAppendingString:AspectsSubclassSuffix].UTF8String;
    Class subclass = objc_getClass(subclassName);
    //從來沒有建立過這個類的話,就從新建立。建立過的話,在runtime中會有記錄,Class類似單例
    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;
        }

        //讓新建立的類  forwardInvocation -> __ASPECTS_ARE_BEING_CALLED__ ,  __aspects_forwardInvocation -> originalImplementation(forwardInvocation)
        aspect_swizzleForwardInvocation(subclass);
        aspect_hookedGetClass(subclass, statedClass);
        aspect_hookedGetClass(object_getClass(subclass), statedClass);
        objc_registerClassPair(subclass);
    }

    //修改isa指標
    object_setClass(self, subclass);
    return subclass;
}
複製程式碼

這個函式其實也是Aspect比較核心的部分,我們來詳細分析一下方法接下來做的事情

分支else if (class_isMetaClass(baseClass) 證明self 是一個類(Class, 外界呼叫的是類方法,對整一個類進行hook),呼叫return aspect_swizzleClassInPlace((Class)self); ,隨後呼叫到下面兩個函式

static Class aspect_swizzleClassInPlace(Class klass) {
    NSCParameterAssert(klass);
    NSString *className = NSStringFromClass(klass);

    _aspect_modifySwizzledClasses(^(NSMutableSet *swizzledClasses) {
        if (![swizzledClasses containsObject:className]) {
            aspect_swizzleForwardInvocation(klass);
            [swizzledClasses addObject:className];
        }
    });
    return klass;
}

static NSString *const AspectsForwardInvocationSelectorName = @"__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@:@");
    }
    AspectLog(@"Aspects: %@ is now aspect aware.", NSStringFromClass(klass));
}
複製程式碼

上面兩個函式是把傳進來的Class做相應處理處理

  1. 修改 Class 的方法列表 forwardInvocation: 方法的IMP 指向自定義函式 ASPECTS_ARE_BEING_CALLED

  2. 新增 __aspects_forwardInvocation 方法 , 其IMP 原來的 forwardInvocation指向的IMP

  3. 修改完後把 ClassName 存放到全域性的集合中 記錄 證明這個Class已經是修改過了 訊息轉發IMP了 , 避免以後重複對一個Class進行hook時重複做上面 1 ,2的步驟

如果執行到分支else if (statedClass != baseClass) , 證明self是一個例項物件 , 並且這個例項物件是先被新增了KVO處理 ,再呼叫Aspects框架新增hook處理的物件 。 注意 :Apsects現在是不支援例項物件先被KVO,再新增hook處理的。程式會提示unrecognized selector sent to instance 然後崩掉 , 框架作者在Demo的測試程式碼中也要相關說明 , 也有人在github上給作者提了issue 詳細可以點選這裡檢視

如果上面那幾個if else都沒有返回到hook class的話,證明要hook的物件是一個普通例項物件 ,不是一個 Class 。接下來將要類似實現KVO的處理。

  1. 嘗試獲取一個類 (名為 :_ Aspects _例項物件的類名) ,如果系統沒有的話就生成這個類,並且讓這個類繼承自例項物件的類

  2. 呼叫函式 aspect_swizzleForwardInvocation,做訊息轉發方法IMP自定義處理(同上面3個步驟一樣)

  3. 呼叫 aspect_hookedGetClass(subclass, statedClass); aspect_hookedGetClass(object_getClass(subclass), statedClass);修改對應例項物件 和 類 的 class 方法 返回物件hook之前Class。保持其行為與hook之前保持一致,從而不影響到外界的使用。

4.呼叫objc_registerClassPair(subclass); object_setClass(self, subclass); 把新生成的Class註冊到系統中 ,並且把例項物件的isa指向新的Class

通過生成一個新的Class,並修改例項物件的isa指向新 Class , 這樣處理的目的是,既為單個例項物件實現了hook處理 , 也不會影響到其他同類的例項物件 。其實KVO也是通過同樣得原理實現的。

hook具體怎麼執行在第二文章分析

相關文章