iOS優秀第三方源碼解析(2、深刻理解Aspects源碼)

本篇是筆者解讀源碼項目 iOS-Framework-Analysis 的第二篇,今年計劃完成10個優秀第三方框架解讀,歡迎 star 和筆者一塊兒解讀這些優秀框架的背後思想。該篇詳細的源碼註釋已上傳 Aspects源碼註釋,若有須要請自取,如有什麼不足之處,敬請告知 🐝🐝。java

前言

AOP(Aspect-oriented programming) 也稱之爲 「面向切面編程」, 是一種經過預編譯方式和運行期動態代理實現程序功能的統一維護的一種技術,通俗點將就是相似切片的方式,統一注入代碼片斷而不須要修改原有代碼邏輯,相比於繼承等方式,代碼的耦合度更低。在java的Spring框架中應用普遍,而在iOS最火的AOP框架非 Aspects 莫屬。git

初識Aspects

Aspects 是一個輕量級的 AOP框架,提供了實例和類方法對類中方法進行 Hook,可在原先方法 運行前/運行中/運行後 插入自定義的代碼片斷。其原理是把全部的方法調用指向 _objc_msgForward ,並處理原方法的參數列表和返回值,最後修改 forwardInvocation 方法使用 NSInvocation 去動態調用。相比於直接使用 Method Swizzling 交換原方法和新方法的 IMP 指針,Aspects 在內部作了更多的安全處理,使用起來更加可靠。github

關於使用 Method Swizzling 存在的問題可查看 iOS 界的毒瘤:Method Swizzleobjective-c

申明

#import <Foundation/Foundation.h>

typedef NS_OPTIONS(NSUInteger, AspectOptions) {
    AspectPositionAfter   = 0,            /// Called after the original implementation (default)
    AspectPositionInstead = 1,            /// Will replace the original implementation.
    AspectPositionBefore  = 2,            /// Called before the original implementation.
    
    AspectOptionAutomaticRemoval = 1 << 3 /// Will remove the hook after the first execution.
};

/// Opaque Aspect Token that allows to deregister the hook.
/// 用於註銷Hook
@protocol AspectToken <NSObject>

/// Deregisters an aspect.
/// @return YES if deregistration is successful, otherwise NO.
- (BOOL)remove;

@end

/// 主要是所Hook方法的信息,用於校驗block兼容性,後續觸發block時會做爲block的首個參數
@protocol AspectInfo <NSObject>

/// The instance that is currently hooked.
- (id)instance;

/// The original invocation of the hooked method.
- (NSInvocation *)originalInvocation;

/// All method arguments, boxed. This is lazily evaluated.
- (NSArray *)arguments;

@end

/// Aspects利用消息轉發機制l來Hook消息,是存在性能開銷的,不要在頻繁調用的方法裏去使用Aspects,主要用在view/controller的代碼中
@interface NSObject (Aspects)


/// 在調用指定類的某個方法以前/過程當中/以後執行一段block代碼
/// block的第一個參數固定爲id<AspectInfo>`, 因此要Hook的方法若是有參數,則第一個參數必須爲對象,不然在比對簽名時或校驗不過
+ (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;

@end


typedef NS_ENUM(NSUInteger, AspectErrorCode) {
    AspectErrorSelectorBlacklisted,                   /// Selectors like release, retain, autorelease are blacklisted.
    AspectErrorDoesNotRespondToSelector,              /// Selector could not be found.
    AspectErrorSelectorDeallocPosition,               /// When hooking dealloc, only AspectPositionBefore is allowed.
    AspectErrorSelectorAlreadyHookedInClassHierarchy, /// Statically hooking the same method in subclasses is not allowed.
    AspectErrorFailedToAllocateClassPair,             /// The runtime failed creating a class pair.
    AspectErrorMissingBlockSignature,                 /// The block misses compile time signature info and can't be called.
    AspectErrorIncompatibleBlockSignature,            /// The block signature does not match the method or is too large.

    AspectErrorRemoveObjectAlreadyDeallocated = 100   /// (for removing) The object hooked is already deallocated.
};

extern NSString *const AspectErrorDomain;

複製代碼

使用方式比較簡單,其建立 NSObject 的分類寫入 Aspects 的相關方法,分別爲類對象和實例對象提供調用方法,在須要 Hook 的地方調用便可。編程

另外分別定義了 AspectTokenAspectInfo 兩個協議,AspectToken 實現了移除方法,AspectInfo 記錄了原方法的信息,做爲 block 的一個參數返回給使用者。數組

源碼解讀

內部定義

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
複製代碼

Aspects 對象的環境,包含被 Hook 的實例、調用方法和參數,並遵照AspectInfo 協議。安全

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
複製代碼

Aspect 標識,包含一次完整 Aspect 的全部內容,會做爲block 第一個參數,內部實現了remove方法,須要使用時遵照 AspectToken 協議便可。框架

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 是一個對象或者類的全部的 Aspects 的容器,每次注入Aspects時會將其按照 option 裏的時機放到對應數組中,方便後續的統一管理(例如移除)。ide

經過 objc_setAssociatedObject 給 NSObject 注 AspectsContainer 屬性,內部含有三個數組,對應關係以下。函數

NSArray *beforeAspects -> AspectPositionBefore

NSArray *insteadAspects -> AspectPositionInstead

NSArray *afterAspects -> AspectPositionAfter
複製代碼

AspectTracker

@interface AspectTracker : NSObject
- (id)initWithTrackedClass:(Class)trackedClass;
@property (nonatomic, strong) Class trackedClass;
@property (nonatomic, readonly) NSString *trackedClassName;
@property (nonatomic, strong) NSMutableSet *selectorNames;
//用於標記其全部子類有Hook的方法 示例:[HookingSelectorName: (AspectTracker1,AspectTracker2...)]
@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 過類都有一個對應 AspectTracker,以 <Class : AspectTracker *> 形式存儲在 swizzledClassesDict 字典中,用於追蹤記錄類中 Hook 的方法。

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

內部定義的 block 結構體,用於轉換外部 block ,與下面 block 源碼定義很類似。

// 從block源碼(libclosure)可知
 struct Block_layout {
 void *isa;
 int flags;
 int reserved;
 void (*invoke)(void *, ...);
 struct Block_descriptor *descriptor;

};
struct Block_descriptor {
    unsigned long int reserved;
    unsigned long int size;
    void (*copy)(void *dst, void *src);
    void (*dispose)(void *);
};
 // 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
 };
複製代碼

調用流程

兩個 API 的內部都是調用 aspect_add 函數,咱們直接從該函數入手,看做者是如何設計實現的。

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)) {
            //- 獲取混寫方法容器
            AspectsContainer *aspectContainer = aspect_getContainerForObject(self, selector);
            //- 建立方法標示
            identifier = [AspectIdentifier identifierWithSelector:selector object:self options:options block:block error:error];
            if (identifier) {
                //- 根據標示將方法放在對應容器中
                [aspectContainer addAspect:identifier withOptions:options];

                // Modify the class to allow message interception.
                //  **關鍵:真正實現Aspect的方法**
                aspect_prepareClassAndHookSelector(self, selector, error);
            }
        }
    });
    return identifier;
}
複製代碼

咱們先用一張流程圖畫下都作了些什麼事情。

aspects_0.jpg

前置準備步驟

爲了實現 Hook 注入,須要先作些準備工做,包括:

  • 校驗當前方法是否能夠被 Hook,例如 retain、release、 forwardInvocation 等方法都是禁止被 Hook 的。
  • 獲取類中的 AspectsContainer 容器
  • 將方法信息等封裝成 AspectIdentifier,其中有比較嚴格的參數兼容判斷,具體可看 aspect_isCompatibleBlockSignature 函數
  • 將 AspectIdentifier 放入對應容器中

實現都比較易懂,這裏就不累述了,詳細可看 Aspects源碼註釋

關鍵實現aspect_prepareClassAndHookSelector

static void aspect_prepareClassAndHookSelector(NSObject *self, SEL selector, NSError **error) {
    NSCParameterAssert(selector);
    //- 傳入self獲得其指向的類
    //- 若是是類對象則Hook其forwardInvocation方法,將Container內的方法注入進去,在將class/metaClass返回
    //- 若是是示例對象,則經過動態建立子類的方式返回新建立的子類
    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);
        //- 建立新的方法aspects_xxxx,方法的實現爲原方法的實現,目的是保存原來方法的實現
        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);
        }
        //- 修改原方法的實現,將其替換爲_objc_msgForward或_objc_msgForward_stret形式觸發,從而使調用時能進入消息轉發機制forwardInvocation
        // We use forwardInvocation to hook in.
        class_replaceMethod(klass, selector, aspect_getMsgForwardIMP(self, selector), typeEncoding);
        AspectLog(@"Aspects: Installed hook for -[%@ %@].", klass, NSStringFromSelector(selector));
    }
}
複製代碼

首先經過 aspect_hookClass 獲取目標類,並替換 forwardInvocation方法注入 Hook 代碼,而後將原方法的實現替換爲 _objc_msgForward 或 _objc_msgForward_stret 形式觸發,從而使調用時能進入消息轉發機制調用 forwardInvocation。

獲取目標類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);

    //  判斷是否已子類化過(類後綴爲_Aspects_)
	if ([className hasSuffix:AspectsSubclassSuffix]) {
		return baseClass;

        //  若self是類對象或元類對象,則混寫self(替換forwardInvocation方法)
	}else if (class_isMetaClass(baseClass)) {
        return aspect_swizzleClassInPlace((Class)self);
        //  statedClass!=baseClass,且不知足上述兩個條件,則說明是KVO模式下的實例對象,要混寫其metaClass
	}else if (statedClass != baseClass) {
        return aspect_swizzleClassInPlace(baseClass);
	}

    //  上述狀況都不知足,則說明是實例對象
    //  採用動態建立子類向其注入方法,最後替換實例對象的isa指針使其指向新建立的子類來實現Aspects
    
    //  拼接_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) {
            NSString *errrorDesc = [NSString stringWithFormat:@"objc_allocateClassPair failed to allocate class %s.", subclassName];
            AspectError(AspectErrorFailedToAllocateClassPair, errrorDesc);
            return nil;
        }
        //  改寫subclass的forwardInvocation方法,插入Aspects
		aspect_swizzleForwardInvocation(subclass);
        //  改寫subclass的.class方法,使其返回self.class
		aspect_hookedGetClass(subclass, statedClass);
        //  改寫subclass.isa的.class方法,使其返回self.class
		aspect_hookedGetClass(object_getClass(subclass), statedClass);
        //  註冊子類
		objc_registerClassPair(subclass);
	}
    //  更改isa指針
	object_setClass(self, subclass);
	return subclass;
}
複製代碼

aspect_hookClass 分別對實例對象和類對象作了不一樣處理。首先經過 self.classobjc_getClass(self) 的值來判斷當前對象的環境,分爲四種場景,分別是 子類化過的實例對象、類對象和元類對象 、 KVO模式下的實例對象和實例對象。對於子類化過的實例對象直接返回其類便可;類對象、元類對象和 KVO模式下的實例對象調用 aspect_swizzleClassInPlace 替換 forwardInvocation 的實現;如果實例對象,則建立以 _Aspects_ 結尾的子類,再替換 forwardInvocation 的實現和實例對象 isa 指針。

關於 self.classobjc_getClass(self) 這裏稍微補充下:

  • self.class: 當self是實例對象的時候,返回的是類對象,不然則返回自身 。

  • object_getClass: 得到的是 isa 的指針。

  • 當 self 是實例對象時,self.class 和 object_getClass(self) 相同,都是指向其類,當 self 爲類對象時,self.class 是自身類,object_getClass(self) 則是其 metaClass。

真正調用APECTS_ARE_BEING_CALLED

//  交換後的__aspects_forwardInvocation:方法實現
static void __ASPECTS_ARE_BEING_CALLED__(__unsafe_unretained NSObject *self, SEL selector, NSInvocation *invocation) {
    NSCParameterAssert(self);
    NSCParameterAssert(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) {
        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 no hooks are installed, call original implementation (usually to throw an exception)
    if (!respondsToAlias) {
        invocation.selector = originalSelector;
        SEL originalForwardInvocationSEL = NSSelectorFromString(AspectsForwardInvocationSelectorName);
        if ([self respondsToSelector:originalForwardInvocationSEL]) {
            ((void( *)(id, SEL, NSInvocation *))objc_msgSend)(self, originalForwardInvocationSEL, invocation);
        }else {
            [self doesNotRecognizeSelector:invocation.selector];
        }
    }

    // Remove any hooks that are queued for deregistration.
    [aspectsToRemove makeObjectsPerformSelector:@selector(remove)];
}
複製代碼

作完前面步驟後,當調用目標方法時,就是走到替換的 __ASPECTS_ARE_BEING_CALLED__ 方法中,按調用時機從 AspectsContainer 獲取 Aspects 注入。

移除aspect_remove

移除的邏輯比較清晰,這裏就用圖描述下具體都作了什麼,配合代碼註釋使用更佳。

aspects_1.jpg

總結

Aspects 不管從功能性仍是安全性上均可以稱得上是很是優秀的 AOP 庫,調用接口簡單明瞭,內部考慮了不少異常場景,每一個類的功能職責拆分得很細,很是推薦讀者根據 Aspects源碼註釋 再細看一遍。

一些問題

Aspects 也不是完美的,從執行函數到 objc_msgForward 須要通過多個消息轉發,並且須要額外的內存開銷去構建 Invocation, 所以不適合處理頻繁調用的方法
另外 Appects 的實現方式比較完全,將調用都轉移到 objc_msgForward 中,致使與其餘 Hook 方式不兼容,例如對同一個函數前後使用 Aspects 和 class_replaceMethod,會致使 class_replaceMethod 獲取的原方法爲 objc_msgForward,致使異常甚至崩潰。

至此,今年 iOS優秀開源框架解析 的第二篇結束 🎉🎉。

相關文章
相關標籤/搜索