iOS Aspects原始碼剖析

hejunm發表於2017-12-21

##Aspects用來幹什麼?

Aspect是一個簡潔高效的用於使iOS支援AOP(面向切面程式設計)的框架。官方描述的很清楚,大致意思如下:你可以使用Aspect為每一個類或者類的例項的某個方法插入一段程式碼,切入點可以選擇before(在原方法執行前執行)/instead(替換原方法)/after(原方法執行完之後執行)。

Think of Aspects as method swizzling on steroids. It allows you to add code to existing methods per class or per instance, whilst thinking of the insertion point e.g. before/instead/after. Aspects automatically deals with calling super and is easier to use than regular method swizzling.

  • 本博文基於 v1.4.2 版本原始碼進行分析。

##技術儲備 Aspect是在Runtime的基礎上構建的。在學習Aspect前,你需要搞清楚下面的概念: ###1. NSMethodSignature 和 NSInvocation 使用NSMethodSignatureNSInvocation 不僅可以完成對method的呼叫,也可以完成block的呼叫。在Aspect中,正是運用NSMethodSignature,NSInvocation 實現了對block的統一處理。不清楚沒關係,先搞清楚NSMethodSignatureNSInvocation的使用方法及如何使用他們執行method 或 block。

####物件呼叫method程式碼示例 一個例項物件可以通過三種方式呼叫其方法。

- (void)test{
    
//type1
    [self printStr1:@"hello world 1"];
    
//type2
    [self performSelector:@selector(printStr1:) withObject:@"hello world 2"];
    
//type3
    //獲取方法簽名
    NSMethodSignature *sigOfPrintStr = [self methodSignatureForSelector:@selector(printStr1:)];
    
    //獲取方法簽名對應的invocation
    NSInvocation *invocationOfPrintStr = [NSInvocation invocationWithMethodSignature:sigOfPrintStr];
    
    /**
    設定訊息接受者,與[invocationOfPrintStr setArgument:(__bridge void * _Nonnull)(self) atIndex:0]等價
    */
    [invocationOfPrintStr setTarget:self];
    
    /**設定要執行的selector。與[invocationOfPrintStr setArgument:@selector(printStr1:) atIndex:1] 等價*/
    [invocationOfPrintStr setSelector:@selector(printStr1:)];
    
    //設定引數 
    NSString *str = @"hello world 3";
    [invocationOfPrintStr setArgument:&str atIndex:2];
    
    //開始執行
    [invocationOfPrintStr invoke];
}

- (void)printStr1:(NSString*)str{
    NSLog(@"printStr1  %@",str);
}
複製程式碼

在呼叫test方法時,會分別輸出:

2017-01-11 15:20:21.642 AspectTest[2997:146594] printStr1  hello world 1
2017-01-11 15:20:21.643 AspectTest[2997:146594] printStr1  hello world 2
2017-01-11 15:20:21.643 AspectTest[2997:146594] printStr1  hello world 3
複製程式碼

type1和type2是我們常用的,這裡不在贅述,我們來說說type3。 NSMethodSignatureNSInvocationFoundation框架為我們提供的一種呼叫方法的方式,經常用於訊息轉發。

####NSMethodSignature概述

NSMethodSignature用於描述method的型別資訊:返回值型別,及每個引數的型別。 可以通過下面的方式進行建立:

@interface NSObject 
//獲取例項方法的簽名
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector;
//獲取類方法的簽名
+ (NSMethodSignature *)instanceMethodSignatureForSelector:(SEL)aSelector;
@end

-------------
//使用ObjCTypes建立方法簽名
@interface NSMethodSignature
+ (nullable NSMethodSignature *)signatureWithObjCTypes:(const char *)types;
@end
複製程式碼

使用NSObject的例項方法和類方法建立NSMethodSignature很簡單,不說了。我們撩一撩signatureWithObjCTypes。 在OC中,每一種資料型別可以通過一個字元編碼來表示(Objective-C type encodings)。例如字元‘@’代表一個object, 'i'代表int。 那麼,由這些字元組成的字元陣列就可以表示方法型別了。舉個例子:上面提到的printStr1:對應的ObjCTypes 為 v@:@。

  • ’v‘ : void型別,第一個字元代表返回值型別
  • ’@‘ : 一個id型別的物件,第一個引數型別
  • ’:‘ : 對應SEL,第二個引數型別
  • ’@‘ : 一個id型別的物件,第三個引數型別,也就是- (void)printStr1:(NSString*)str中的str。

printStr1:本來是一個引數,ObjCTypes怎麼成了三個引數?要理解這個還必須理解OC中的訊息機制。一個method對應的結構體如下,ObjCTypes中的引數其實與IMP method_imp 函式指標指向的函式的引數相一致。相關內容有很多,不瞭解的可以參考這篇文章方法與訊息

typedef struct objc_method *Method;
struct objc_method {
    SEL method_name                 OBJC2_UNAVAILABLE;  // 方法名
    char *method_types                  OBJC2_UNAVAILABLE;
    IMP method_imp                      OBJC2_UNAVAILABLE;  // 方法實現
}
複製程式碼

####NSInvocation概述

就像示例程式碼所示,我們可以通過+ (NSInvocation *)invocationWithMethodSignature:(NSMethodSignature *)sig;建立出NSInvocation物件。接下來你設定各個引數資訊, 然後呼叫invoke進行呼叫。執行結束後,通過- (void)getReturnValue:(void *)retLoc;獲取返回值。 這裡需要注意,對NSInvocation物件設定的引數個數及型別和獲取的返回值的型別要與建立物件時使用的NSMethodSignature物件代表的引數及返回值型別向一致,否則cresh。

####使用NSInvocation呼叫block 下面展示block 的兩種呼叫方式

- (void)test{

    void (^block1)(int) = ^(int a){
         NSLog(@"block1 %d",a);
    };
    
    //type1
    block1(1);
    
    //type2
    //獲取block型別對應的方法簽名。
    NSMethodSignature *signature = aspect_blockMethodSignature(block1);
    NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:signature];
    [invocation setTarget:block1];
    int a=2;
    [invocation setArgument:&a atIndex:1];
    [invocation invoke];
}
複製程式碼

type1 就是常用的方法,不再贅述。看一下type2。 type2和上面呼叫method的type3用的一樣的套路,只是引數不同:由block生成的NSInvocation物件的第一個引數是block本身,剩下的為 block自身的引數。

由於系統沒有提供獲取block的ObjCTypes的api,我們必須想辦法找到這個ObjCTypes,只有這樣才能生成NSMethodSignature物件! ####block的資料結構 & 從資料結構中獲取 ObjCTypes oc是一門動態語言,通過編譯 oc可以轉變為c語言。經過編譯後block對應的資料結構是struct。(block中技術點還是挺過的,推薦一本書“Objective-C 高階程式設計”)

//程式碼來自 Aspect
// Block internals.
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;
複製程式碼

在此結構體中 const char *signature 欄位就是我們想要的。通過下面的方法獲取signature並建立NSMethodSignature物件。

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

###2. method swizzling 在Objective-C中呼叫一個方法,其實是向一個物件傳送訊息。每個類都有一個方法列表,存放著selector的名字和方法實現的對映關係。IMP有點類似函式指標,指向Method具體的實現。

selector-imp.png
通過 method swizzling這種黑科技,你可以改變selector和方法實現的對映關係。
swizzled-imp
此時當執行[objc selectorC]時,實際呼叫的是 IMPn指標指向的函式。

具體實現程式碼如下:

程式碼來源: https://github.com/hejunm/iOS-Tools

@implementation HJMSwizzleTools:NSObject
+ (void)hjm_swizzleWithClass:(Class)processedClass originalSelector:(SEL)originSelector swizzleSelector:(SEL)swizzlSelector{
    
    Method originMethod = class_getInstanceMethod(processedClass, originSelector);
    Method swizzleMethod = class_getInstanceMethod(processedClass, swizzlSelector);
    
    //當processedClass實現originSelector時,didAddMethod返回false,否則返回true. 如果當前類沒有實現originSelector而父類實現了,這是直接使用method_exchangeImplementations會swizzle父類的originSelector。這樣會出現很大的問題。
    BOOL didAddMethod = class_addMethod(processedClass, originSelector, method_getImplementation(swizzleMethod), method_getTypeEncoding(swizzleMethod));
    
    if (didAddMethod) {
        class_replaceMethod(processedClass, swizzlSelector, method_getImplementation(originMethod), method_getTypeEncoding(originMethod));
    }else{
        method_exchangeImplementations(originMethod, swizzleMethod);
    }
}
@end
複製程式碼

可以這樣使用

+ (void)load{
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        [HJMSwizzleTools hjm_swizzleWithClass:self originalSelector:@selector(viewDidLoad) swizzleSelector:@selector(swizzleViewDidLoad)];
    });
}

//被替換了。。
- (void)viewDidLoad {
    [super viewDidLoad];
}

//現在系統會呼叫這個方法
- (void)swizzleViewDidLoad {
    NSLog(@"do something");
}
複製程式碼

###3. 訊息轉發流程 在Objective-C中呼叫一個方法,其實是向一個物件傳送訊息。如果這個訊息沒有對應的實現時就會進行訊息轉發。轉發流程圖如下:

forwardMethod.png

下面用程式碼演示一遍

  • resolveInstanceMethod

當根據selector沒有找到對應的method時,首先會呼叫這個方法,在該方法中你可以為一個類新增一個方法。並返回yes。下面的程式碼只是宣告瞭runTo方法,沒有實現。

//Car.h
@interface Car : NSObject
- (void)runTo:(NSString *)place;
@end

//Car.m
@implementation Car
+ (BOOL)resolveInstanceMethod:(SEL)sel{
    if (sel == @selector(runTo:)) {
        class_addMethod(self, sel, (IMP)dynamicMethodIMPRunTo, "v@:@");
        return YES;
    }
    return [super resolveInstanceMethod:sel];
}
//動態新增的@selector(runTo:) 對應的實現
static void dynamicMethodIMPRunTo(id self, SEL _cmd,id place){
    NSLog(@"dynamicMethodIMPRunTo %@",place);
}
@end
複製程式碼
  • forwardingTargetForSelector

如果resolveInstanceMethod沒有實現,返回No,或者沒有動態新增方法的話,就會執行forwardingTargetForSelector。 在這裡你可以返回一個能夠執行這個selector的物件otherTarget,接下來訊息會重新傳送到這個otherTarget。

//Person.h
@interface Person : NSObject
- (void)runTo:(NSString *)place;
@end

//Person.m
@implementation Person
- (void)runTo:(NSString *)place;{
    NSLog(@"person runTo %@",place);
}
@end

//Car.h
@interface Car : NSObject
- (void)runTo:(NSString *)place;
@end

//Car.m
@implementation Car
- (id)forwardingTargetForSelector:(SEL)aSelector{
    //將訊息轉發給Person的例項
    if (aSelector == @selector(runTo:)){
        return [[Person alloc]init];
    }
    return [super forwardingTargetForSelector:aSelector];
}
@end
複製程式碼
  • forwardInvocation

如果上面兩種情況沒有執行,就會執行通過forwardInvocation進行訊息轉發。

@implementation Car
- (NSMethodSignature*)methodSignatureForSelector:(SEL)aSelector{
    //判斷selector是否為需要轉發的,如果是則手動生成方法簽名並返回。
    if (aSelector == @selector(runTo:)){
        return [NSMethodSignature signatureWithObjCTypes:"v@:@"];
    }
    return [super forwardingTargetForSelector:aSelector];
}

- (void)forwardInvocation:(NSInvocation *)anInvocation{
    //判斷待處理的anInvocation是否為我們要處理的
    if (anInvocation.selector == @selector(runTo:)){
    		
    }else{
    }
}
@end
複製程式碼

在NSInvocation物件中儲存著我們呼叫一個method的所有資訊。可以看下其屬性和方法:

  • methodSignature 含有返回值型別,引數個數及每個引數的型別 等資訊。
  • - (void)getArgument:(void *)argumentLocation atIndex:(NSInteger)idx;獲取呼叫method時傳的引數
  • - (void)setArgument:(void *)argumentLocation atIndex:(NSInteger)idx; 設定第index引數。
  • - (void)invoke; 開始執行
  • - (void)getReturnValue:(void *)retLoc; 獲取返回值

下面的程式碼演示如何獲取呼叫method時所傳的各引數值

- (void)forwardInvocation:(NSInvocation *)anInvocation{
    if (anInvocation.selector == @selector(runTo:)){
        void *argBuf = NULL;
        NSUInteger numberOfArguments = anInvocation.methodSignature.numberOfArguments;
        for (NSUInteger idx = 2; idx < numberOfArguments; idx++) {
            const char *type = [anInvocation.methodSignature getArgumentTypeAtIndex:idx];
            NSUInteger argSize;
            NSGetSizeAndAlignment(type, &argSize, NULL);
            if (!(argBuf = reallocf(argBuf, argSize))) {
                NSLog(@"Failed to allocate memory for block invocation.");
                return ;
            }
            
            [anInvocation getArgument:argBuf atIndex:idx];
            //現在argBuf 中儲存著第index 引數的值。 你可以使用這些值進行其他處理,例如為block中各引數賦值,並呼叫。
        }
    }else{
        
    }
}
複製程式碼

####通過手動觸發訊息轉發(method已經實現) 前面所描述的訊息轉發都是在selector沒有對應實現時自動進行的,我們稱之為自動訊息轉發。現在有個需求:即使Car類實現了 runTo:,執行[objOfCar runTo:@"shangHai"]; 時也進行訊息轉發(手動觸發),如何實現? 實現方法如下:利用 method swizzling 將selector的實現改變為_objc_msgForward或者_objc_msgForward_stret。在調selector時就會進行訊息轉發 看下面的程式碼:

//對 runTo: 進行訊息轉發
@implementation Car

//進行 method swizzling。此時呼叫runTo:就會進行訊息轉發
+ (void)load{
    SEL selector = @selector(runTo:);
    Method targetMethod = class_getInstanceMethod(self.class, @selector(selector));
    const char *typeEncoding = method_getTypeEncoding(targetMethod);
    IMP targetMethodIMP = _objc_msgForward;
    class_replaceMethod(self.class, selector, targetMethodIMP, typeEncoding);
}

- (void)runTo:(NSString *)place{
    NSLog(@"car runTo %@",place);
}

//訊息轉發,呼叫這個方法。anInvocation中儲存著呼叫方法時傳遞的引數資訊
- (void)forwardInvocation:(NSInvocation *)anInvocation{
    if (anInvocation.selector == @selector(runTo:)){
    
    }else{
        
    }
}
複製程式碼

上面提到了_objc_msgForward或者_objc_msgForward_stret, 該如何選擇?首先兩者都是進行訊息轉發的,大概是這樣:如果轉發的訊息的返回值是struct型別,就使用_objc_msgForward_stret,否則使用_objc_msgForward參考資料。簡單引用JSPatch作者的解釋

大多數CPU在執行C函式時會把前幾個引數放進暫存器裡,對 obj_msgSend 來說前兩個引數固定是 self / _cmd,它們會放在暫存器上,在最後執行完後返回值也會儲存在暫存器上,取這個暫存器的值就是返回值。普通的返回值(int/pointer)很小,放在暫存器上沒問題,但有些 struct 是很大的,暫存器放不下,所以要用另一種方式,在一開始申請一段記憶體,把指標儲存在暫存器上,返回值往這個指標指向的記憶體寫資料,所以暫存器要騰出一個位置放這個指標,self / _cmd 在暫存器的位置就變了。objc_msgSend 不知道 self / _cmd 的位置變了,所以要用另一個方法 objc_msgSend_stret 代替。原理大概就是這樣。在 NSMethodSignature 的 debugDescription 上打出了是否 special struct,只能通過這字串判斷。所以最終的處理是,在非 arm64 下,是 special struct 就走 _objc_msgForward_stret,否則走 _objc_msgForward。

根據selector返回值型別獲取_objc_msgForward或者_objc_msgForward_stret 的程式碼如下:

//程式碼來自Aspect
static IMP aspect_getMsgForwardIMP(NSObject *self, SEL selector) {
    IMP msgForwardIMP = _objc_msgForward;
#if !defined(__arm64__)
    Method method = class_getInstanceMethod(self.class, selector);
    const char *encoding = method_getTypeEncoding(method);
    BOOL methodReturnsStructValue = encoding[0] == _C_STRUCT_B;
    if (methodReturnsStructValue) {
        @try {
            NSUInteger valueSize = 0;
            NSGetSizeAndAlignment(encoding, &valueSize, NULL);

            if (valueSize == 1 || valueSize == 2 || valueSize == 4 || valueSize == 8) {
                methodReturnsStructValue = NO;
            }
        } @catch (__unused NSException *e) {}
    }
    if (methodReturnsStructValue) {
        msgForwardIMP = (IMP)_objc_msgForward_stret;
    }
#endif
    return msgForwardIMP;
}
複製程式碼

##Aspects 原始碼 一直在思考如何使用文字清晰的描述出Aspects的實現原理,最後決定使用在原始碼上新增註釋的形式呈現。自己偷個懶。 Aspects 原始碼剖析

##Aspects 使用場景

相關文章