從 Aspects 原始碼中我學到了什麼?

Lision發表於2018-01-18

前言

AOP (Aspect-oriented programming) 譯為 “面向切面程式設計”,是通過預編譯方式和執行期動態代理實現程式功能統一維護的一種技術。利用 AOP 可以對業務邏輯的各個部分進行隔離,從而使得業務邏輯各部分之間的耦合度降低,提高程式的可重用性,同時提高了開發的效率。

Emmmmm...AOP 目前是較為熱門的一個話題,儘管你也許沒有聽說過它,但是你的專案中可能已經滲入了它,例如:使用者統計(不新增一行程式碼即實現對所有 ViewController 的跟蹤日誌)。

對於 iOS 開發者而言,無外乎 Swift 和 Objective-C 兩種主流開發語言:

  • Swift 受限於 ABI 尚未穩定,動態性依賴 dynamic 修飾符,在 Runtime 沒有留給我們太多的發揮空間(前幾日新增了 swift-5.0-branch 分支,寫這篇文章時看了一眼 181 commits behind master ?)。
  • Objective-C 在動態性上相對 Swift 具有無限大的優勢,這幾年 Objective-C Runtime 相關文章多如牛毛,相信現在的 iOSer 都具備一定的 Runtime 相關知識。

Aspects 作為 Objective-C 語言編寫的 AOP 庫,適用於 iOS 和 Mac OS X,使用體驗簡單愉快,已經在 GitHub 摘得 5k+ Star。Aspects 內部實現比較健全,考慮到了 Hook 安全方面可能發生的種種問題,非常值得我們學習。

Note: 本文內引用 Aspects 原始碼版本為 v1.4.2,要求讀者具備一定的 Runtime 知識。

索引

  • AOP 簡介
  • Aspects 簡介
  • Aspects 結構剖析
  • Aspects 核心程式碼剖析
  • 優秀 AOP 庫應該具備的特質
  • 總結

AOP 簡介

從 Aspects 原始碼中我學到了什麼?

執行時,動態地將程式碼切入到類的指定方法、指定位置上的程式設計思想就是面向切面的程式設計。

AOP (Aspect-oriented programming),即 “面向切面程式設計” 是一種程式設計正規化,或者說是一種程式設計思想,它解決了 OOP (Object-oriented programming) 的延伸問題。

什麼時候需要使用 AOP

光是給個概念可能初次接觸 AOP 的人還是無法 Get 到其中微妙,拿我們前言中舉的例子?,假設隨著我們所在的公司逐步發展,之前第三方的使用者頁面統計已經不能滿足需求了,公司要求實現一個我們自己的使用者頁面統計。

嘛~ 我們來理一下 OOP 思想下該怎麼辦?

  • 一個熟悉 OOP 思想的程式猿會理所應當的想到要把使用者頁面統計這一任務放到 ViewController 中;
  • 考慮到一個個的手動新增統計程式碼要死人(而且還會漏,以後新增 ViewController 也要手動加),於是想到了 OOP 思想中的繼承;
  • 不巧由於專案久遠,所有的 ViewController 都是直接繼承自系統類 UIViewController(笑),此時選擇抽一個專案 RootViewController,替換所有 ViewController 繼承 RootViewController;
  • 然後在 RootViewController 的 viewWillAppear:viewWillDisappear: 方法加入時間統計程式碼,記錄 ViewController 以及 Router 傳參。

你會想,明明 OOP 也能解決問題是不是?不要急,再假設你們公司有多個 App,你被抽調至基礎技術組專門給這些 App 寫通用元件,要把之前實現過的使用者頁面統計重新以通用的形式實現,提供給你們公司所有的 App 使用。

MMP,使用標準 OOP 思想貌似無解啊...這個時候就是 AOP 的用武之地了。

這裡簡單給個思路:Hook UIViewController 的 viewWillAppear:viewWillDisappear: 方法,在原方法執行之後記錄需要統計的資訊上報即可。

Note: 簡單通過 Method Swizzling 來 Hook 不是不可以,但是有很多安全隱患!

Aspects 簡介

從 Aspects 原始碼中我學到了什麼?

Aspects 是一個使用起來簡單愉快的 AOP 庫,使用 Objective-C 編寫,適用於 iOS 與 Mac OS X。

Aspects 內部實現考慮到了很多 Hook 可能引發的問題,筆者在看原始碼的過程中摳的比較細,真的是受益匪淺。

Aspects 簡單易用,作者通過在 NSObject (Aspects) 分類中暴露出的兩個介面分別提供了對例項和 Class 的 Hook 實現:

@interface NSObject (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;

@end
複製程式碼

Aspects 支援例項 Hook,相較其他 Objective-C AOP 庫而言可操作粒度更小,適合的場景更加多樣化。作為使用者無需進行更多的操作即可 Hook 指定例項或者 Class 的指定 SEL,AspectOptions 引數可以指定 Hook 的點,以及是否執行一次之後就撤銷 Hook。

Aspects 結構剖析

從 Aspects 原始碼中我學到了什麼?

Emmmmm...儘管 Aspects 只有不到千行的原始碼,但是其內部實現考慮到了很多 Hook 相關的安全問題和其他細節,對比其他 Objective-C AOP 開源專案來說 Aspects 更為健全,所以我自己在扒 Aspects 原始碼時也看的比較仔細。

Aspects 內部結構

Aspects 內部定義了兩個協議:

  • AspectToken - 用於登出 Hook
  • AspectInfo - 嵌入 Hook 中的 Block 首位引數

此外 Aspects 內部還定義了 4 個類:

  • AspectInfo - 切面資訊,遵循 AspectInfo 協議
  • AspectIdentifier - 切面 ID,應該遵循 AspectToken 協議(作者漏掉了,已提 PR)
  • AspectsContainer - 切面容器
  • AspectTracker - 切面跟蹤器

以及一個結構體:

  • AspectBlockRef - 即 _AspectBlock,充當內部 Block

如果你扒一遍原始碼,還會發現兩個內部靜態全域性變數:

  • static NSMutableDictionary *swizzledClassesDict;
  • static NSMutableSet *swizzledClasses;

現在你也許還不能理解為什麼要定義這麼多東西,別急~ 我們後面都會分析到。

Aspects 協議

按照上面列出的順序,先來介紹一些 Aspects 宣告的協議。

AspectToken

AspectToken 協議旨在讓使用者可以靈活的登出之前新增過的 Hook,內部規定遵守此協議的物件須實現 remove 方法。

/// 不透明的 Aspect Token,用於登出 Hook
@protocol AspectToken <NSObject>

/// 登出一個 aspect.
/// 返回 YES 表示登出成功,否則返回 NO
- (BOOL)remove;

@end
複製程式碼

AspectInfo

AspectInfo 協議旨在規範對一個切面,即 aspect 的 Hook 內部資訊的紕漏,我們在 Hook 時新增切面的 Block 第一個引數就遵守此協議。

/// AspectInfo 協議是我們塊語法的第一個引數。
@protocol AspectInfo <NSObject>

/// 當前被 Hook 的例項
- (id)instance;

/// 被 Hook 方法的原始 invocation
- (NSInvocation *)originalInvocation;

/// 所有方法引數(裝箱之後的)惰性執行
- (NSArray *)arguments;

@end
複製程式碼

Note: 裝箱是一個開銷昂貴操作,所以用到再去執行。

Aspects 內部類

接著協議,我們下面詳細介紹一下 Aspects 的內部類。

AspectInfo

Note: AspectInfo 在這裡是一個 Class,其遵守上文中講到的 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
複製程式碼

Note: 關於裝箱,對於提供一個 NSInvocation 就可以拿到其 arguments 這一點上,ReactiveCocoa 團隊提供了很大貢獻(細節見 Aspects 內部 NSInvocation 分類)。

AspectInfo 比較簡單,參考 ReactiveCocoa 團隊提供的 NSInvocation 引數通用方法可將引數裝箱為 NSValue,簡單來說 AspectInfo 扮演了一個提供 Hook 資訊的角色。

AspectIdentifier

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

Note: AspectIdentifier 實際上是新增切面的 Block 的第一個引數,其應該遵循 AspectToken 協議,事實上也的確如此,其提供了 remove 方法的實現。

AspectIdentifier 內部需要注意的是由於使用 Block 來寫 Hook 中我們加的料,這裡生成了 blockSignature,在 AspectIdentifier 初始化的過程中會去判斷 blockSignature 與入參 objectselector 得到的 methodSignature 的相容性,相容性判斷成功才會順利初始化。

AspectsContainer

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

AspectsContainer 作為切面的容器類,關聯指定物件的指定方法,內部有三個切面佇列,分別容納關聯指定物件的指定方法中相對應 AspectOption 的 Hook:

  • NSArray *beforeAspects; - AspectPositionBefore
  • NSArray *insteadAspects; - AspectPositionInstead
  • NSArray *afterAspects; - AspectPositionAfter

為什麼要說關聯呢?因為 AspectsContainer 是在 NSObject 分類中通過 AssociatedObject 方法與當前要 Hook 的目標關聯在一起的。

Note: 關聯目標是 Hook 之後的 Selector,即 aliasSelector(原始 SEL 名稱加 aspects_ 字首對應的 SEL)。

AspectTracker

AspectTracker 類定義:

@interface AspectTracker : NSObject

- (id)initWithTrackedClass:(Class)trackedClass parent:(AspectTracker *)parent;

@property (nonatomic, strong) Class trackedClass;
@property (nonatomic, strong) NSMutableSet *selectorNames;
@property (nonatomic, weak) AspectTracker *parentEntry;

@end
複製程式碼

AspectTracker 作為切面追蹤器,原理大致如下:

// Add the selector as being modified.
currentClass = klass;
AspectTracker *parentTracker = nil;
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)));
複製程式碼

Note: 聰明的你應該已經注意到了全域性變數 swizzledClassesDict 中的 value 對應著 AspectTracker 指標。

嘛~ 就是說 AspectTracker 是從下而上追蹤,最底層的 parentEntrynil,父類的 parentEntry 為子類的 tracker

Aspects 內部結構體

AspectBlockRef

AspectBlockRef,即 struct _AspectBlock,其定義如下:

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

Emmmmm...沒什麼特別的,大家應該比較眼熟吧。

Note: __unused 巨集定義實際上是 __attribute__((unused)) GCC 定語,旨在告訴編譯器“如果我沒有在後面使用到這個變數也別警告我”。

嘛~ 想起之前自己挖的坑還沒有填,事實上自己也不知道什麼時候填(笑):

  • 之前挖坑說要寫一篇文章記錄一些閱讀原始碼時發現的程式碼書寫技巧
  • 之前挖坑說要封裝一個 WKWebView 給群裡的兄弟參考

不要急~ 你瞧倫家不是都記得嘛(至於什麼時候填坑嘛就...咳咳)

Aspects 靜態全域性變數

static NSMutableDictionary *swizzledClassesDict;

static NSMutableDictionary *swizzledClassesDict; 在 Aspects 中扮演著已混寫類字典的角色,其內部結構應該是這樣的:

<Class : AspectTracker *>
複製程式碼

Aspects 內部提供了專門訪問這個全域性字典的方法:

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

這個全域性變數可以簡單理解為記錄整個 Hook 影響的 Class 包含其 SuperClass 的追蹤記錄的全域性字典。

static NSMutableSet *swizzledClasses;

static NSMutableSet *swizzledClasses; 在 Aspects 中擔當記錄已混寫類的角色,其內部結構如下:

<NSStringFromClass(Class)>
複製程式碼

Aspects 內部提供一個用於修改這個全域性變數內容的方法:

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

Note: 注意 @synchronized(swizzledClasses)

這個全域性變數記錄了 forwardInvocation: 被混寫的的類名稱。

Note: 注意在用途上與 static NSMutableDictionary *swizzledClassesDict; 區分理解。

Aspects 核心程式碼剖析

從 Aspects 原始碼中我學到了什麼?

嘛~ Aspects 的整體實現程式碼不超過一千行,而且考慮的情況也比較全面,非常值得大家花時間去讀一下,這裡我只準備給出自己對其核心程式碼的理解。

Hook Class && Hook Instance

Aspects 不光支援 Hook Class 還支援 Hook Instance,這提供了更小粒度的控制,配合 Hook 的撤銷功能可以更加靈活精準的做我們想做的事~

Aspects 為了能區別 Class 和 Instance 的邏輯,實現了名為 aspect_hookClass 的方法,我認為其中的實現值得我用一部分篇幅來單獨講解,也覺得讀者們有必要花點時間理解這裡的實現邏輯。

static Class aspect_hookClass(NSObject *self, NSError **error) {
    // 斷言 self
    NSCParameterAssert(self);
    // class
    Class statedClass = self.class;
    // isa
    Class baseClass = object_getClass(self);
    NSString *className = NSStringFromClass(baseClass);
    
    // 已經子類化過了
    if ([className hasSuffix:AspectsSubclassSuffix]) {
        return baseClass;
        // 我們混寫了一個 class 物件,而非一個單獨的 object
    }else if (class_isMetaClass(baseClass)) {
        // baseClass 是元類,則 self 是 Class 或 MetaClass,混寫 self
        return aspect_swizzleClassInPlace((Class)self);
        // 可能是一個 KVO'ed class。混寫就位。也要混寫 meta classes。
    }else if (statedClass != baseClass) {
        // 當 .class 和 isa 指向不同的情況,混寫 baseClass
        return aspect_swizzleClassInPlace(baseClass);
    }
    
    // 預設情況下,動態建立子類
    // 拼接子類字尾 AspectsSubclassSuffix
    const char *subclassName = [className stringByAppendingString:AspectsSubclassSuffix].UTF8String;
    // 嘗試用拼接字尾的名稱獲取 isa
    Class subclass = objc_getClass(subclassName);
    
    // 找不到 isa,代表還沒有動態建立過這個子類
    if (subclass == nil) {
        // 建立一個 class pair,baseClass 作為新類的 superClass,類名為 subclassName
        subclass = objc_allocateClassPair(baseClass, subclassName, 0);
        if (subclass == nil) { // 返回 nil,即建立失敗
            NSString *errrorDesc = [NSString stringWithFormat:@"objc_allocateClassPair failed to allocate class %s.", subclassName];
            AspectError(AspectErrorFailedToAllocateClassPair, errrorDesc);
            return nil;
        }
        
        // 混寫 forwardInvocation:
        aspect_swizzleForwardInvocation(subclass);
        // 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;
}
複製程式碼

Note: 其實這裡的難點就在於對 .classobject_getClass 的區分。

  • .class 當 target 是 Instance 則返回 Class,當 target 是 Class 則返回自身
  • object_getClass 返回 isa 指標的指向

Note: 動態建立一個 Class 的完整步驟也是我們應該注意的。

  • objc_allocateClassPair
  • class_addMethod
  • class_addIvar
  • objc_registerClassPair

嘛~ 難點和重點都講完了,大家結合註釋理解其中的邏輯應該沒什麼困難了,有什麼問題可以找我一起交流~

Hook 的實現

在上面 aspect_hookClass 方法中,不僅僅是返回一個要 Hook 的 Class,期間還做了一些細節操作,不論是 Class 還是 Instance,都會呼叫 aspect_swizzleForwardInvocation 方法,這個方法沒什麼難點,簡單貼一下程式碼讓大家有個印象:

static void aspect_swizzleForwardInvocation(Class klass) {
    // 斷言 klass
    NSCParameterAssert(klass);
    // 如果沒有 method,replace 實際上會像是 class_addMethod 一樣
    IMP originalImplementation = class_replaceMethod(klass, @selector(forwardInvocation:), (IMP)__ASPECTS_ARE_BEING_CALLED__, "v@:@");
    // 拿到 originalImplementation 證明是 replace 而不是 add,情況少見
    if (originalImplementation) {
        // 新增 AspectsForwardInvocationSelectorName 的方法,IMP 為原生 forwardInvocation:
        class_addMethod(klass, NSSelectorFromString(AspectsForwardInvocationSelectorName), originalImplementation, "v@:@");
    }
    AspectLog(@"Aspects: %@ is now aspect aware.", NSStringFromClass(klass));
}
複製程式碼

上面的方法就是把要 Hook 的目標 Class 的 forwardInvocation: 混寫了,混寫之後 forwardInvocation: 的具體實現在 __ASPECTS_ARE_BEING_CALLED__ 中,裡面能看到 invoke 標識位的不同是如何實現的,還有一些其他的實現細節:

// 巨集定義,以便於我們有一個更明晰的 stack trace
#define aspect_invoke(aspects, info) \
for (AspectIdentifier *aspect in aspects) {\
    [aspect invokeWithInfo:info];\
    if (aspect.options & AspectOptionAutomaticRemoval) { \
        aspectsToRemove = [aspectsToRemove?:@[] arrayByAddingObject:aspect]; \
    } \
}

static void __ASPECTS_ARE_BEING_CALLED__(__unsafe_unretained NSObject *self, SEL selector, NSInvocation *invocation) {
    // __unsafe_unretained NSObject *self 不解釋了
    // 斷言 self, invocation
    NSCParameterAssert(self);
    NSCParameterAssert(invocation);
    // 從 invocation 可以拿到很多東西,比如 originalSelector
    SEL originalSelector = invocation.selector;
    // originalSelector 加字首得到 aliasSelector
    SEL aliasSelector = aspect_aliasForSelector(invocation.selector);
    // 用 aliasSelector 替換 invocation.selector
    invocation.selector = aliasSelector;
    
    // Instance 的容器
    AspectsContainer *objectContainer = objc_getAssociatedObject(self, aliasSelector);
    // Class 的容器
    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 { // 否則正常執行
        // 遍歷 invocation.target 及其 superClass 找到例項可以響應 aliasSelector 的點 invoke
        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);

    // 如果沒有 hook,則執行原始實現(通常會丟擲異常)
    if (!respondsToAlias) {
        invocation.selector = originalSelector;
        SEL originalForwardInvocationSEL = NSSelectorFromString(AspectsForwardInvocationSelectorName);
        // 如果可以響應 originalForwardInvocationSEL,表示之前是 replace method 而非 add method
        if ([self respondsToSelector:originalForwardInvocationSEL]) {
            ((void( *)(id, SEL, NSInvocation *))objc_msgSend)(self, originalForwardInvocationSEL, invocation);
        }else {
            [self doesNotRecognizeSelector:invocation.selector];
        }
    }

    // 移除 aspectsToRemove 佇列中的 AspectIdentifier,執行 remove
    [aspectsToRemove makeObjectsPerformSelector:@selector(remove)];
}
#undef aspect_invoke
複製程式碼

Note: aspect_invoke 巨集定義的作用域。

  • 程式碼實現對應了 Hook 的 AspectOptions 引數的 Before,Instead 和 After。
  • aspect_invokeaspectsToRemove 是一個 NSArray,裡面容納著需要被銷戶的 Hook,即 AspectIdentifier(之後會呼叫 remove 移除)。
  • 遍歷 invocation.target 及其 superClass 找到例項可以響應 aliasSelector 的點 invoke 實現程式碼。

Block Hook

Aspects 讓我們在指定 Class 或 Instance 的特定 Selector 執行時,根據 AspectOptions 插入我們自己的 Block 做 Hook,而這個 Block 內部有我們想要的有關於當前 Target 和 Selector 的資訊,我們來看一下 Aspects 是怎麼辦到的:

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

    // 偏執。我們已經在 hook 註冊的時候檢查過了,(不過這裡我們還要檢查)。
    if (numberOfArguments > originalInvocation.methodSignature.numberOfArguments) {
        AspectLogError(@"Block has too many arguments. Not calling %@", info);
        return NO;
    }

    // block 的 `self` 將會是 AspectInfo。可選的。
    if (numberOfArguments > 1) {
        [blockInvocation setArgument:&info atIndex:1];
    }
    
    // 簡歷引數分配記憶體 argBuf 然後從 originalInvocation 取 argument 賦值給 blockInvocation
    void *argBuf = NULL;
    for (NSUInteger idx = 2; idx < numberOfArguments; idx++) {
        const char *type = [originalInvocation.methodSignature getArgumentTypeAtIndex:idx];
		NSUInteger argSize;
		NSGetSizeAndAlignment(type, &argSize, NULL);
		
		// reallocf 優點,如果建立記憶體失敗會自動釋放之前的記憶體,講究
		if (!(argBuf = reallocf(argBuf, argSize))) {
            AspectLogError(@"Failed to allocate memory for block invocation.");
			return NO;
		}
        
		[originalInvocation getArgument:argBuf atIndex:idx];
		[blockInvocation setArgument:argBuf atIndex:idx];
    }
    
    // 執行
    [blockInvocation invokeWithTarget:self.block];
    
    // 釋放 argBuf
    if (argBuf != NULL) {
        free(argBuf);
    }
    return YES;
}
複製程式碼

考慮兩個問題:

  • [blockInvocation setArgument:&info atIndex:1]; 為什麼要在索引 1 處插入呢?
  • for (NSUInteger idx = 2; idx < numberOfArguments; idx++) 為什麼要從索引 2 開始遍歷引數呢?

嘛~ 如果你對 Block 的 Runtime 結構以及執行過程下斷點研究一下就全都明白了,感興趣的同學有疑問可以聯絡我(與真正勤奮好學的人交流又有誰會不樂意呢?笑~)

優秀 AOP 庫應該具備的特質

從 Aspects 原始碼中我學到了什麼?

  • 良好的使用體驗
  • 可控粒度小
  • 使用 Block 做 Hook
  • 支援撤銷 Hook
  • 安全性

良好的使用體驗

Aspects 使用 NSObject + Categroy 的方式提供介面,非常巧妙的涵蓋了 Instance 和 Class。

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

Note: 其實介面這裡對於 block 的引數自動補全可以更進一步,不過 Aspects 當初是沒有辦法做到的,單從介面設計這塊已經很優秀了。

可控粒度小

Aspects 不僅支援大部分 AOP 框架應該做到的對於 Class 的 Hook,還支援粒度更小的 Instance Hook,而其在內部實現中為了支援 Instance Hook 所做的程式碼也非常值得我們參考和學習(已在上文 Aspects 核心程式碼剖析 處單獨分析)。

為使用者提供更為自由的 Hook 方式以達到更加精準的控制是每個使用者樂於見到的事。

使用 Block 做 Hook

Aspects 使用 Block 來做 Hook 應該考慮到了很多東西,支援使用者通過在 Block 中獲取到相關的資訊,書寫自己額外的操作就可以實現 Hook 需求。

支援撤銷 Hook

Aspects 還支援撤銷之前做的 Hook 以及已混寫的 Method,為了實現這個功能 Aspects 設計了全域性容器,把 Hook 和混寫用全域性容器做記錄,讓一切都可以復原,這不正是我們想要的嗎?

安全性

嘛~ 我們在學習 Runtime 的時候,就應該看到過不少文章講解 Method Swizzling 要注意的安全性問題,由於用到了大量 Runtime 方法,加上 AOP 是面向整個切面的,所以一單發現問題就會比較嚴重,設計的面會比較廣,而且難以除錯。

Note: 我們不能因為容易造成問題就可以迴避 Method Swizzling,就好比大學老師講到遞迴時強調容易引起迴圈呼叫,很多人就在內心迴避使用遞迴,甚至於非常適合使用遞迴來寫的演算法題(這裡指遞迴來寫會易讀寫、易維護)只會用複雜的方式來思考。

總結

  • 文章簡單介紹了 AOP 的概念,希望能給各位讀者對 AOP 思想的理解提供微薄的幫助。
  • 文章系統的剖析了 Aspects 開源庫的內部結構,希望能讓大家在瀏覽 Aspects 原始碼時快速定位程式碼位置,找到核心內容。
  • 文章重點分析了 Aspects 的核心程式碼,提煉了一些筆者認為值得注意的點,但願可以在大家扒原始碼時提供一些指引。
  • 文章結尾總結了 Aspects 作為一個比較優秀

文章寫得比較用心(是我個人的原創文章,轉載請註明 lision.me/),如果發現錯誤會優先在我的 個人部落格 中更新。如果有任何問題歡迎在我的微博 @Lision 聯絡我~


補充~ 我建了一個技術交流微信群,想在裡面認識更多的朋友!如果各位同學對文章有什麼疑問或者工作之中遇到一些小問題都可以在群裡找到我或者其他群友交流討論,期待你的加入喲~

從 Aspects 原始碼中我學到了什麼?

Emmmmm..由於微信群人數過百導致不可以掃碼入群,所以請掃描上面的二維碼關注公眾號進群。

相關文章