前言
在“Runtime病院”住院的后兩天,分析了一下AOP的實現(xiàn)原理。“出院”后,發(fā)現(xiàn)Aspect庫還沒有詳細(xì)分析,于是就有了這篇文章,今天就來說說iOS 是如何實現(xiàn)Aspect Oriented Programming。
目錄
- 1.Aspect Oriented Programming簡介
- 2.什么是Aspects
- 3.Aspects 中4個基本類 解析
- 4.Aspects hook前的準(zhǔn)備工作
- 5.Aspects hook過程詳解
- 6.關(guān)于Aspects的一些 “坑”
一.Aspect Oriented Programming簡介
面向切面的程序設(shè)計(aspect-oriented programming,AOP,又譯作面向方面的程序設(shè)計、觀點導(dǎo)向編程、剖面導(dǎo)向程序設(shè)計)是計算機(jī)科學(xué)中的一個術(shù)語,指一種程序設(shè)計范型。該范型以一種稱為側(cè)面(aspect,又譯作方面)的語言構(gòu)造為基礎(chǔ),側(cè)面是一種新的模塊化機(jī)制,用來描述分散在對象、類或函數(shù)中的橫切關(guān)注點(crosscutting concern)。
側(cè)面的概念源于對面向?qū)ο蟮某绦蛟O(shè)計的改進(jìn),但并不只限于此,它還可以用來改進(jìn)傳統(tǒng)的函數(shù)。與側(cè)面相關(guān)的編程概念還包括元對象協(xié)議、主題(subject)、混入(mixin)和委托。
AOP通過預(yù)編譯方式和運行期動態(tài)代理實現(xiàn)程序功能的統(tǒng)一維護(hù)的一種技術(shù)。
OOP(面向?qū)ο缶幊蹋┽槍I(yè)務(wù)處理過程的實體及其屬性和行為進(jìn)行抽象封裝,以獲得更加清晰高效的邏輯單元劃分。
AOP則是針對業(yè)務(wù)處理過程中的切面進(jìn)行提取,它所面對的是處理過程中的某個步驟或階段,以獲得邏輯過程中各部分之間低耦合性的隔離效果。
OOP和AOP屬于兩個不同的“思考方式”。OOP專注于對象的屬性和行為的封裝,AOP專注于處理某個步驟和階段的,從中進(jìn)行切面的提取。
舉個例子,如果有一個判斷權(quán)限的需求,OOP的做法肯定是在每個操作前都加入權(quán)限判斷。那日志記錄怎么辦?在每個方法的開始結(jié)束的地方都加上日志記錄。AOP就是把這些重復(fù)的邏輯和操作,提取出來,運用動態(tài)代理,實現(xiàn)這些模塊的解耦。OOP和AOP不是互斥,而是相互配合。
在iOS里面使用AOP進(jìn)行編程,可以實現(xiàn)非侵入。不需要更改之前的代碼邏輯,就能加入新的功能。主要用來處理一些具有橫切性質(zhì)的系統(tǒng)性服務(wù),如日志記錄、權(quán)限管理、緩存、對象池管理等。
二. 什么是Aspects
Aspects是一個輕量級的面向切面編程的庫。它能允許你在每一個類和每一個實例中存在的方法里面加入任何代碼。可以在以下切入點插入代碼:before(在原始的方法前執(zhí)行) / instead(替換原始的方法執(zhí)行) / after(在原始的方法后執(zhí)行,默認(rèn))。通過Runtime消息轉(zhuǎn)發(fā)實現(xiàn)Hook。Aspects會自動的調(diào)用super方法,使用method swizzling起來會更加方便。
這個庫很穩(wěn)定,目前用在數(shù)百款app上了。它也是PSPDFKit的一部分,PSPDFKit是一個iOS 看PDF的framework庫。作者最終決定把它開源出來。
三.Aspects 中4個基本類 解析
我們從頭文件開始看起。
1.Aspects.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.
};
在頭文件中定義了一個枚舉。這個枚舉里面是調(diào)用切片方法的時機(jī)。默認(rèn)是AspectPositionAfter在原方法執(zhí)行完之后調(diào)用。AspectPositionInstead是替換原方法。AspectPositionBefore是在原方法之前調(diào)用切片方法。AspectOptionAutomaticRemoval是在hook執(zhí)行完自動移除。
@protocol AspectToken <NSObject>
- (BOOL)remove;
@end
定義了一個AspectToken的協(xié)議,這里的Aspect Token是隱式的,允許我們調(diào)用remove去撤銷一個hook。remove方法返回YES代表撤銷成功,返回NO就撤銷失敗。
@protocol AspectInfo <NSObject>
- (id)instance;
- (NSInvocation *)originalInvocation;
- (NSArray *)arguments;
@end
又定義了一個AspectInfo協(xié)議。AspectInfo protocol是我們block語法里面的第一個參數(shù)。
instance方法返回當(dāng)前被hook的實例。originalInvocation方法返回被hooked方法的原始的invocation。arguments方法返回所有方法的參數(shù)。它的實現(xiàn)是懶加載。
頭文件中還特意給了一段注釋來說明Aspects的用法和注意點,值得我們關(guān)注。
/**
Aspects uses Objective-C message forwarding to hook into messages. This will create some overhead. Don't add aspects to methods that are called a lot. Aspects is meant for view/controller code that is not called a 1000 times per second.
Adding aspects returns an opaque token which can be used to deregister again. All calls are thread safe.
*/
Aspects利用的OC的消息轉(zhuǎn)發(fā)機(jī)制,hook消息。這樣會有一些性能開銷。不要把Aspects加到經(jīng)常被使用的方法里面。Aspects是用來設(shè)計給view/controller 代碼使用的,而不是用來hook每秒調(diào)用1000次的方法的。
添加Aspects之后,會返回一個隱式的token,這個token會被用來注銷hook方法的。所有的調(diào)用都是線程安全的。
關(guān)于線程安全,下面會詳細(xì)分析。現(xiàn)在至少我們知道Aspects不應(yīng)該被用在for循環(huán)這些方法里面,會造成很大的性能損耗。
@interface NSObject (Aspects)
/// Adds a block of code before/instead/after the current `selector` for a specific class.
///
/// @param block Aspects replicates the type signature of the method being hooked.
/// The first parameter will be `id<AspectInfo>`, followed by all parameters of the method.
/// These parameters are optional and will be filled to match the block signature.
/// You can even use an empty block, or one that simple gets `id<AspectInfo>`.
///
/// @note Hooking static methods is not supported.
/// @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;
/// 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
Aspects整個庫里面就只有這兩個方法。這里可以看到,Aspects是NSobject的一個extension,只要是NSObject,都可以使用這兩個方法。這兩個方法名字都是同一個,入?yún)⒑头祷刂狄惨粯樱ㄒ徊煌氖且粋€是加號方法一個是減號方法。一個是用來hook類方法,一個是用來hook實例方法。
方法里面有4個入?yún)ⅰ5谝粋€selector是要給它增加切面的原方法。第二個參數(shù)是AspectOptions類型,是代表這個切片增加在原方法的before / instead / after。第4個參數(shù)是返回的錯誤。
重點的就是第三個入?yún)lock。這個block復(fù)制了正在被hook的方法的簽名signature類型。block遵循AspectInfo協(xié)議。我們甚至可以使用一個空的block。AspectInfo協(xié)議里面的參數(shù)是可選的,主要是用來匹配block簽名的。
返回值是一個token,可以被用來注銷這個Aspects。
注意,Aspects是不支持hook 靜態(tài)static方法的
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;
這里定義了錯誤碼的類型。出錯的時候方便我們調(diào)試。
2.Aspects.m
#import "Aspects.h"
#import <libkern/OSAtomic.h>
#import <objc/runtime.h>
#import <objc/message.h>
#import <libkern/OSAtomic.h>導(dǎo)入這個頭文件是為了下面用到的自旋鎖。#import <objc/runtime.h> 和 #import <objc/message.h>是使用Runtime的必備頭文件。
typedef NS_OPTIONS(int, AspectBlockFlags) {
AspectBlockFlagsHasCopyDisposeHelpers = (1 << 25),
AspectBlockFlagsHasSignature = (1 << 30)
};
定義了AspectBlockFlags,這是一個flag,用來標(biāo)記兩種情況,是否需要Copy和Dispose的Helpers,是否需要方法簽名Signature 。
在Aspects中定義的4個類,分別是AspectInfo,AspectIdentifier,AspectsContainer,AspectTracker。接下來就分別看看這4個類是怎么定義的。
3. 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
AspectInfo對應(yīng)的實現(xiàn)
#pragma mark - AspectInfo
@implementation AspectInfo
@synthesize arguments = _arguments;
- (id)initWithInstance:(__unsafe_unretained id)instance invocation:(NSInvocation *)invocation {
NSCParameterAssert(instance);
NSCParameterAssert(invocation);
if (self = [super init]) {
_instance = instance;
_originalInvocation = invocation;
}
return self;
}
- (NSArray *)arguments {
// Lazily evaluate arguments, boxing is expensive.
if (!_arguments) {
_arguments = self.originalInvocation.aspects_arguments;
}
return _arguments;
}
AspectInfo是繼承于NSObject,并且遵循了AspectInfo協(xié)議。在其 - (id)initWithInstance: invocation:方法中,把外面?zhèn)鬟M(jìn)來的實例instance,和原始的invocation保存到AspectInfo類對應(yīng)的成員變量中。- (NSArray *)arguments方法是一個懶加載,返回的是原始的invocation里面的aspects參數(shù)數(shù)組。
aspects_arguments這個getter方法是怎么實現(xiàn)的呢?作者是通過一個為NSInvocation添加一個分類來實現(xiàn)的。
@interface NSInvocation (Aspects)
- (NSArray *)aspects_arguments;
@end
為原始的NSInvocation類添加一個Aspects分類,這個分類中只增加一個方法,aspects_arguments,返回值是一個數(shù)組,數(shù)組里面包含了當(dāng)前invocation的所有參數(shù)。
對應(yīng)的實現(xiàn)
#pragma mark - NSInvocation (Aspects)
@implementation NSInvocation (Aspects)
- (NSArray *)aspects_arguments {
NSMutableArray *argumentsArray = [NSMutableArray array];
for (NSUInteger idx = 2; idx < self.methodSignature.numberOfArguments; idx++) {
[argumentsArray addObject:[self aspect_argumentAtIndex:idx] ?: NSNull.null];
}
return [argumentsArray copy];
}
@end
- (NSArray *)aspects_arguments實現(xiàn)很簡單,就是一層for循環(huán),把methodSignature方法簽名里面的參數(shù),都加入到數(shù)組里,最后把數(shù)組返回。
關(guān)于獲取方法所有參數(shù)的這個- (NSArray *)aspects_arguments方法的實現(xiàn),有2個地方需要詳細(xì)說明。一是為什么循環(huán)從2開始,二是[self aspect_argumentAtIndex:idx]內(nèi)部是怎么實現(xiàn)的。
先來說說為啥循環(huán)從2開始。
Type Encodings作為對Runtime的補(bǔ)充,編譯器將每個方法的返回值和參數(shù)類型編碼為一個字符串,并將其與方法的selector關(guān)聯(lián)在一起。這種編碼方案在其它情況下也是非常有用的,因此我們可以使用@encode編譯器指令來獲取它。當(dāng)給定一個類型時,@encode返回這個類型的字符串編碼。這些類型可以是諸如int、指針這樣的基本類型,也可以是結(jié)構(gòu)體、類等類型。事實上,任何可以作為sizeof()操作參數(shù)的類型都可以用于@encode()。
在Objective-C Runtime Programming Guide中的Type Encoding一節(jié)中,列出了Objective-C中所有的類型編碼。需要注意的是這些類型很多是與我們用于存檔和分發(fā)的編碼類型是相同的。但有一些不能在存檔時使用。
注:Objective-C不支持long double類型。@encode(long double)返回d,與double是一樣的。
OC為支持消息的轉(zhuǎn)發(fā)和動態(tài)調(diào)用,Objective-C Method 的 Type 信息以 “返回值 Type + 參數(shù) Types” 的形式組合編碼,還需要考慮到 self
和 _cmd 這兩個隱含參數(shù):
- (void)tap; => "v@:"
- (int)tapWithView:(double)pointx; => "i@:d"
按照上面的表,我們可以知道,編碼出來的字符串,前3位分別是返回值Type,self隱含參數(shù)Type @,_cmd隱含參數(shù)Type :。
所以從第3位開始,是入?yún)ⅰ?/p>
假設(shè)我們以- (void)tapView:(UIView *)view atIndex:(NSInteger)index為例,打印一下methodSignature
(lldb) po self.methodSignature
<NSMethodSignature: 0x60800007df00>
number of arguments = 4
frame size = 224
is special struct return? NO
return value: -------- -------- -------- --------
type encoding (v) 'v'
flags {}
modifiers {}
frame {offset = 0, offset adjust = 0, size = 0, size adjust = 0}
memory {offset = 0, size = 0}
argument 0: -------- -------- -------- --------
type encoding (@) '@'
flags {isObject}
modifiers {}
frame {offset = 0, offset adjust = 0, size = 8, size adjust = 0}
memory {offset = 0, size = 8}
argument 1: -------- -------- -------- --------
type encoding (:) ':'
flags {}
modifiers {}
frame {offset = 8, offset adjust = 0, size = 8, size adjust = 0}
memory {offset = 0, size = 8}
argument 2: -------- -------- -------- --------
type encoding (@) '@'
flags {isObject}
modifiers {}
frame {offset = 16, offset adjust = 0, size = 8, size adjust = 0}
memory {offset = 0, size = 8}
argument 3: -------- -------- -------- --------
type encoding (q) 'q'
flags {isSigned}
modifiers {}
frame {offset = 24, offset adjust = 0, size = 8, size adjust = 0}
memory {offset = 0, size = 8}
number of arguments = 4,因為有2個隱含參數(shù)self和_cmd,加上入?yún)iew和index。
argument | return value | 0 | 1 | 2 | 3 |
---|---|---|---|---|---|
methodSignature | v | @ | : | @ | q |
第一個argument的frame {offset = 0, offset adjust = 0, size = 0, size adjust = 0}memory {offset = 0, size = 0},返回值在這里不占size。第二個argument是self,frame {offset = 0, offset adjust = 0, size = 8, size adjust = 0}memory {offset = 0, size = 8}。由于size = 8,下一個frame的offset就是8,之后是16,以此類推。
至于為何這里要傳遞2,還跟aspect_argumentAtIndex具體實現(xiàn)有關(guān)系。
再來看看aspect_argumentAtIndex的具體實現(xiàn)。這個方法還要感謝ReactiveCocoa團(tuán)隊,為獲取方法簽名的參數(shù)提供了一種優(yōu)雅的實現(xiàn)方式。
// Thanks to the ReactiveCocoa team for providing a generic solution for this.
- (id)aspect_argumentAtIndex:(NSUInteger)index {
const char *argType = [self.methodSignature getArgumentTypeAtIndex:index];
// Skip const type qualifier.
if (argType[0] == _C_CONST) argType++;
#define WRAP_AND_RETURN(type) do { type val = 0; [self getArgument:&val atIndex:(NSInteger)index]; return @(val); } while (0)
if (strcmp(argType, @encode(id)) == 0 || strcmp(argType, @encode(Class)) == 0) {
__autoreleasing id returnObj;
[self getArgument:&returnObj atIndex:(NSInteger)index];
return returnObj;
} else if (strcmp(argType, @encode(SEL)) == 0) {
SEL selector = 0;
[self getArgument:&selector atIndex:(NSInteger)index];
return NSStringFromSelector(selector);
} else if (strcmp(argType, @encode(Class)) == 0) {
__autoreleasing Class theClass = Nil;
[self getArgument:&theClass atIndex:(NSInteger)index];
return theClass;
// Using this list will box the number with the appropriate constructor, instead of the generic NSValue.
} else if (strcmp(argType, @encode(char)) == 0) {
WRAP_AND_RETURN(char);
} else if (strcmp(argType, @encode(int)) == 0) {
WRAP_AND_RETURN(int);
} else if (strcmp(argType, @encode(short)) == 0) {
WRAP_AND_RETURN(short);
} else if (strcmp(argType, @encode(long)) == 0) {
WRAP_AND_RETURN(long);
} else if (strcmp(argType, @encode(long long)) == 0) {
WRAP_AND_RETURN(long long);
} else if (strcmp(argType, @encode(unsigned char)) == 0) {
WRAP_AND_RETURN(unsigned char);
} else if (strcmp(argType, @encode(unsigned int)) == 0) {
WRAP_AND_RETURN(unsigned int);
} else if (strcmp(argType, @encode(unsigned short)) == 0) {
WRAP_AND_RETURN(unsigned short);
} else if (strcmp(argType, @encode(unsigned long)) == 0) {
WRAP_AND_RETURN(unsigned long);
} else if (strcmp(argType, @encode(unsigned long long)) == 0) {
WRAP_AND_RETURN(unsigned long long);
} else if (strcmp(argType, @encode(float)) == 0) {
WRAP_AND_RETURN(float);
} else if (strcmp(argType, @encode(double)) == 0) {
WRAP_AND_RETURN(double);
} else if (strcmp(argType, @encode(BOOL)) == 0) {
WRAP_AND_RETURN(BOOL);
} else if (strcmp(argType, @encode(bool)) == 0) {
WRAP_AND_RETURN(BOOL);
} else if (strcmp(argType, @encode(char *)) == 0) {
WRAP_AND_RETURN(const char *);
} else if (strcmp(argType, @encode(void (^)(void))) == 0) {
__unsafe_unretained id block = nil;
[self getArgument:&block atIndex:(NSInteger)index];
return [block copy];
} else {
NSUInteger valueSize = 0;
NSGetSizeAndAlignment(argType, &valueSize, NULL);
unsigned char valueBytes[valueSize];
[self getArgument:valueBytes atIndex:(NSInteger)index];
return [NSValue valueWithBytes:valueBytes objCType:argType];
}
return nil;
#undef WRAP_AND_RETURN
}
getArgumentTypeAtIndex:這個方法是用來獲取到methodSignature方法簽名指定index的type encoding的字符串。這個方法傳出來的字符串直接就是我們傳進(jìn)去的index值。比如我們傳進(jìn)去的是2,其實傳出來的字符串是methodSignature對應(yīng)的字符串的第3位。
由于第0位是函數(shù)返回值return value對應(yīng)的type encoding,所以傳進(jìn)來的2,對應(yīng)的是argument2。所以我們這里傳遞index = 2進(jìn)來,就是過濾掉了前3個type encoding的字符串,從argument2開始比較。這就是為何循環(huán)從2開始的原因。
_C_CONST是一個常量,用來判斷encoding的字符串是不是CONST常量。
#define _C_ID '@'
#define _C_CLASS '#'
#define _C_SEL ':'
#define _C_CHR 'c'
#define _C_UCHR 'C'
#define _C_SHT 's'
#define _C_USHT 'S'
#define _C_INT 'i'
#define _C_UINT 'I'
#define _C_LNG 'l'
#define _C_ULNG 'L'
#define _C_LNG_LNG 'q'
#define _C_ULNG_LNG 'Q'
#define _C_FLT 'f'
#define _C_DBL 'd'
#define _C_BFLD 'b'
#define _C_BOOL 'B'
#define _C_VOID 'v'
#define _C_UNDEF '?'
#define _C_PTR '^'
#define _C_CHARPTR '*'
#define _C_ATOM '%'
#define _C_ARY_B '['
#define _C_ARY_E ']'
#define _C_UNION_B '('
#define _C_UNION_E ')'
#define _C_STRUCT_B '{'
#define _C_STRUCT_E '}'
#define _C_VECTOR '!'
#define _C_CONST 'r'
這里的Type和OC的Type 是完全一樣的,只不過這里是一個C的char類型。
#define WRAP_AND_RETURN(type) do { type val = 0; [self getArgument:&val atIndex:(NSInteger)index]; return @(val); } while (0)
WRAP_AND_RETURN是一個宏定義。這個宏定義里面調(diào)用的getArgument:atIndex:方法是用來在NSInvocation中根據(jù)index得到對應(yīng)的Argument,最后return的時候把val包裝成對象,返回出去。
在下面大段的if - else判斷中,有很多字符串比較的函數(shù)strcmp。
比如說strcmp(argType, @encode(id)) == 0,argType是一個char,內(nèi)容是methodSignature取出來對應(yīng)的type encoding,和@encode(id)是一樣的type encoding。通過strcmp比較之后,如果是0,代表類型是相同的。
下面的大段的判斷就是把入?yún)⒍挤祷氐倪^程,依次判斷了id,class,SEL,接著是一大推基本類型,char,int,short,long,long long,unsigned char,unsigned int,unsigned short,unsigned long,unsigned long long,float,double,BOOL,bool,char *這些基本類型都會利用WRAP_AND_RETURN打包成對象返回。最后判斷block和struct結(jié)構(gòu)體,也會返回對應(yīng)的對象。
這樣入?yún)⒕投挤祷氐綌?shù)組里面被接收了。假設(shè)還是上面- (void)tapView:(UIView *)view atIndex:(NSInteger)index為例子,執(zhí)行完aspects_arguments,數(shù)組里面裝的的是:
(
<UIView: 0x7fa2e2504190; frame = (0 80; 414 40); layer = <CALayer: 0x6080000347c0>>",
1
)
總結(jié),AspectInfo里面主要是 NSInvocation 信息。將NSInvocation包裝一層,比如參數(shù)信息等。
4. AspectIdentifier
// Tracks a single aspect.
@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
對應(yīng)實現(xiàn)
#pragma mark - AspectIdentifier
@implementation AspectIdentifier
+ (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;
}
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;
}
- (BOOL)invokeWithInfo:(id<AspectInfo>)info {
NSInvocation *blockInvocation = [NSInvocation invocationWithMethodSignature:self.blockSignature];
NSInvocation *originalInvocation = info.originalInvocation;
NSUInteger numberOfArguments = self.blockSignature.numberOfArguments;
// Be extra paranoid. We already check that on hook registration.
if (numberOfArguments > originalInvocation.methodSignature.numberOfArguments) {
AspectLogError(@"Block has too many arguments. Not calling %@", info);
return NO;
}
// The `self` of the block will be the AspectInfo. Optional.
if (numberOfArguments > 1) {
[blockInvocation setArgument:&info atIndex:1];
}
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))) {
AspectLogError(@"Failed to allocate memory for block invocation.");
return NO;
}
[originalInvocation getArgument:argBuf atIndex:idx];
[blockInvocation setArgument:argBuf atIndex:idx];
}
[blockInvocation invokeWithTarget:self.block];
if (argBuf != NULL) {
free(argBuf);
}
return YES;
}
- (NSString *)description {
return [NSString stringWithFormat:@"<%@: %p, SEL:%@ object:%@ options:%tu block:%@ (#%tu args)>", self.class, self, NSStringFromSelector(self.selector), self.object, self.options, self.block, self.blockSignature.numberOfArguments];
}
- (BOOL)remove {
return aspect_remove(self, NULL);
}
@end
在instancetype方法中調(diào)用了aspect_blockMethodSignature方法。
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];
}
這個aspect_blockMethodSignature的目的是把傳遞進(jìn)來的AspectBlock轉(zhuǎn)換成NSMethodSignature的方法簽名。
AspectBlock的結(jié)構(gòu)如下
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;
這里定義了一個Aspects內(nèi)部使用的block類型。對系統(tǒng)的Block很熟悉的同學(xué)一眼就會感覺兩者很像。不熟悉的可以看看我之前分析Block的文章。文章里,用Clang把Block轉(zhuǎn)換成結(jié)構(gòu)體,結(jié)構(gòu)和這里定義的block很相似。
了解了AspectBlock的結(jié)構(gòu)之后,再看aspect_blockMethodSignature函數(shù)就比較清楚了。
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;
}
AspectBlockRef layout = (__bridge void *)block,由于兩者block實現(xiàn)類似,所以這里先把入?yún)lock強(qiáng)制轉(zhuǎn)換成AspectBlockRef類型,然后判斷是否有AspectBlockFlagsHasSignature的標(biāo)志位,如果沒有,報不包含方法簽名的error。
注意,傳入的block是全局類型的
(__NSGlobalBlock) __NSGlobalBlock = {
NSBlock = {
NSObject = {
isa = __NSGlobalBlock__
}
}
}
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;
}
desc就是原來block里面對應(yīng)的descriptor指針。descriptor指針往下偏移2個unsigned long int的位置就指向了copy函數(shù)的地址,如果包含Copy和Dispose函數(shù),那么繼續(xù)往下偏移2個(void *)的大小。這時指針肯定移動到了const char *signature的位置。如果desc不存在,那么也會報錯,該block不包含方法簽名。
const char *signature = (*(const char **)desc);
return [NSMethodSignature signatureWithObjCTypes:signature];
到了這里,就保證有方法簽名,且存在。最后調(diào)用NSMethodSignature的signatureWithObjCTypes方法,返回方法簽名。
舉例說明aspect_blockMethodSignature最終生成的方法簽名是什么樣子的。
[UIView aspect_hookSelector:@selector(UIView:atIndex:) withOptions:AspectPositionAfter usingBlock:^(id<AspectInfo> aspects, UIView *view, NSInteger index)
{
NSLog(@"按鈕點擊了 %ld",index);
} error:nil];
const char *signature最終獲得的字符串是這樣
(const char *) signature = 0x0000000102f72676 "v32@?0@\"<AspectInfo>\"8@\"UIView\"16q24"
v32@?0@"<AspectInfo>"8@"UIView"16q24是Block
^(id<AspectInfo> aspects, UIView *view, NSInteger index){
}
對應(yīng)的Type。void返回值的Type是v,32是offset,@?是block對應(yīng)的Type,@“<AspectInfo>”是第一個參數(shù),@"UIView"是第二個參數(shù),NSInteger對應(yīng)的Type就是q了。
每個Type后面跟的數(shù)字都是它們各自對應(yīng)的offset。把最終轉(zhuǎn)換好的NSMethodSignature打印出來。
<NSMethodSignature: 0x600000263dc0>
number of arguments = 4
frame size = 224
is special struct return? NO
return value: -------- -------- -------- --------
type encoding (v) 'v'
flags {}
modifiers {}
frame {offset = 0, offset adjust = 0, size = 0, size adjust = 0}
memory {offset = 0, size = 0}
argument 0: -------- -------- -------- --------
type encoding (@) '@?'
flags {isObject, isBlock}
modifiers {}
frame {offset = 0, offset adjust = 0, size = 8, size adjust = 0}
memory {offset = 0, size = 8}
argument 1: -------- -------- -------- --------
type encoding (@) '@"<AspectInfo>"'
flags {isObject}
modifiers {}
frame {offset = 8, offset adjust = 0, size = 8, size adjust = 0}
memory {offset = 0, size = 8}
conforms to protocol 'AspectInfo'
argument 2: -------- -------- -------- --------
type encoding (@) '@"UIView"'
flags {isObject}
modifiers {}
frame {offset = 16, offset adjust = 0, size = 8, size adjust = 0}
memory {offset = 0, size = 8}
class 'DLMenuView'
argument 3: -------- -------- -------- --------
type encoding (q) 'q'
flags {isSigned}
modifiers {}
frame {offset = 24, offset adjust = 0, size = 8, size adjust = 0}
memory {offset = 0, size = 8}
回到AspectIdentifier中繼續(xù)看instancetype方法,獲取到了傳入的block的方法簽名之后,又調(diào)用了aspect_isCompatibleBlockSignature方法。
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:@"Block signature %@ doesn't match %@.", blockSignature, methodSignature];
AspectError(AspectErrorIncompatibleBlockSignature, description);
return NO;
}
return YES;
}
這個函數(shù)的作用是把我們要替換的方法block和要替換的原方法,進(jìn)行對比。如何對比呢?對比兩者的方法簽名。
入?yún)elector是原方法。
if (blockSignature.numberOfArguments > methodSignature.numberOfArguments) {
signaturesMatch = NO;
}else {
if (blockSignature.numberOfArguments > 1) {
const char *blockType = [blockSignature getArgumentTypeAtIndex:1];
if (blockType[0] != '@') {
signaturesMatch = NO;
}
}
先比較方法簽名的參數(shù)個數(shù)是否相等,不等肯定是不匹配,signaturesMatch = NO。如果參數(shù)個數(shù)相等,再比較我們要替換的方法里面第一個參數(shù)是不是_cmd,對應(yīng)的Type就是@,如果不是,也是不匹配,所以signaturesMatch = NO。如果上面兩條都滿足,signaturesMatch = YES,那么就進(jìn)入下面更加嚴(yán)格的對比。
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;
}
}
}
這里循環(huán)也是從2開始的。舉個例子來說明為什么從第二位開始比較。還是用之前的例子。
[UIView aspect_hookSelector:@selector(UIView:atIndex:) withOptions:AspectPositionAfter usingBlock:^(id<AspectInfo> aspects, UIView *view, NSInteger index)
{
NSLog(@"按鈕點擊了 %ld",index);
} error:nil];
這里我要替換的原方法是UIView:atIndex:,那么對應(yīng)的Type是v@:@q。根據(jù)上面的分析,這里的blockSignature是之前調(diào)用轉(zhuǎn)換出來的Type,應(yīng)該是v@?@"<AspectInfo>"@"UIView"q。
argument | return value | 0 | 1 | 2 | 3 |
---|---|---|---|---|---|
methodSignature | v | @ | : | @ | q |
blockSignature | v | @? | @"<AspectInfo>" | @"UIView" | q |
methodSignature 和 blockSignature 的return value都是void,所以對應(yīng)的都是v。methodSignature的argument 0 是隱含參數(shù) self,所以對應(yīng)的是@。blockSignature的argument 0 是block,所以對應(yīng)的是@?。methodSignature的argument 1 是隱含參數(shù) _cmd,所以對應(yīng)的是:。blockSignature的argument 1 是<AspectInfo>,所以對應(yīng)的是@"<AspectInfo>"。從argument 2開始才是方法簽名后面的對應(yīng)可能出現(xiàn)差異,需要比較的參數(shù)列表。
最后
if (!signaturesMatch) {
NSString *description = [NSString stringWithFormat:@"Block signature %@ doesn't match %@.", blockSignature, methodSignature];
AspectError(AspectErrorIncompatibleBlockSignature, description);
return NO;
}
如果經(jīng)過上面的比較signaturesMatch都為NO,那么就拋出error,Block無法匹配方法簽名。
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;
如果這里匹配成功了,就會blockSignature全部都賦值給AspectIdentifier。這也就是為何AspectIdentifier里面有一個單獨的屬性NSMethodSignature的原因。
AspectIdentifier還有另外一個方法invokeWithInfo。
// Be extra paranoid. We already check that on hook registration.
if (numberOfArguments > originalInvocation.methodSignature.numberOfArguments) {
AspectLogError(@"Block has too many arguments. Not calling %@", info);
return NO;
}
注釋也寫清楚了,這個判斷是強(qiáng)迫癥患者寫的,到了這里block里面的參數(shù)是不會大于原始方法的方法簽名里面參數(shù)的個數(shù)的。
// The `self` of the block will be the AspectInfo. Optional.
if (numberOfArguments > 1) {
[blockInvocation setArgument:&info atIndex:1];
}
把AspectInfo存入到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))) {
AspectLogError(@"Failed to allocate memory for block invocation.");
return NO;
}
[originalInvocation getArgument:argBuf atIndex:idx];
[blockInvocation setArgument:argBuf atIndex:idx];
}
[blockInvocation invokeWithTarget:self.block];
這一段是循環(huán)把originalInvocation中取出參數(shù),賦值到argBuf中,然后再賦值到blockInvocation里面。循環(huán)從2開始的原因上面已經(jīng)說過了,這里不再贅述。最后把self.block賦值給blockInvocation的Target。
總結(jié),AspectIdentifier是一個切片Aspect的具體內(nèi)容。里面會包含了單個的 Aspect 的具體信息,包括執(zhí)行時機(jī),要執(zhí)行 block 所需要用到的具體信息:包括方法簽名、參數(shù)等等。初始化AspectIdentifier的過程實質(zhì)是把我們傳入的block打包成AspectIdentifier。
5. AspectsContainer
// Tracks all aspects for an object/class.
@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
對應(yīng)實現(xiàn)
#pragma mark - AspectsContainer
@implementation AspectsContainer
- (BOOL)hasAspects {
return self.beforeAspects.count > 0 || self.insteadAspects.count > 0 || self.afterAspects.count > 0;
}
- (void)addAspect:(AspectIdentifier *)aspect withOptions:(AspectOptions)options {
NSParameterAssert(aspect);
NSUInteger position = options&AspectPositionFilter;
switch (position) {
case AspectPositionBefore: self.beforeAspects = [(self.beforeAspects ?:@[]) arrayByAddingObject:aspect]; break;
case AspectPositionInstead: self.insteadAspects = [(self.insteadAspects?:@[]) arrayByAddingObject:aspect]; break;
case AspectPositionAfter: self.afterAspects = [(self.afterAspects ?:@[]) arrayByAddingObject:aspect]; break;
}
}
- (BOOL)removeAspect:(id)aspect {
for (NSString *aspectArrayName in @[NSStringFromSelector(@selector(beforeAspects)),
NSStringFromSelector(@selector(insteadAspects)),
NSStringFromSelector(@selector(afterAspects))]) {
NSArray *array = [self valueForKey:aspectArrayName];
NSUInteger index = [array indexOfObjectIdenticalTo:aspect];
if (array && index != NSNotFound) {
NSMutableArray *newArray = [NSMutableArray arrayWithArray:array];
[newArray removeObjectAtIndex:index];
[self setValue:newArray forKey:aspectArrayName];
return YES;
}
}
return NO;
}
- (NSString *)description {
return [NSString stringWithFormat:@"<%@: %p, before:%@, instead:%@, after:%@>", self.class, self, self.beforeAspects, self.insteadAspects, self.afterAspects];
}
@end
AspectsContainer比較好理解。addAspect會按照切面的時機(jī)分別把切片Aspects放到對應(yīng)的數(shù)組里面。removeAspects會循環(huán)移除所有的Aspects。hasAspects判斷是否有Aspects。
AspectsContainer是一個對象或者類的所有的 Aspects 的容器。所有會有兩種容器。
值得我們注意的是這里數(shù)組是通過Atomic修飾的。關(guān)于Atomic需要注意在默認(rèn)情況下,由編譯器所合成的方法會通過鎖定機(jī)制確保其原子性(Atomicity)。如果屬性具備nonatomic特質(zhì),則不需要同步鎖。
6. AspectTracker
@interface AspectTracker : NSObject
- (id)initWithTrackedClass:(Class)trackedClass;
@property (nonatomic, strong) Class trackedClass;
@property (nonatomic, readonly) NSString *trackedClassName;
@property (nonatomic, strong) NSMutableSet *selectorNames;
@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
對應(yīng)實現(xiàn)
@implementation AspectTracker
- (id)initWithTrackedClass:(Class)trackedClass {
if (self = [super init]) {
_trackedClass = trackedClass;
_selectorNames = [NSMutableSet new];
_selectorNamesToSubclassTrackers = [NSMutableDictionary new];
}
return self;
}
- (BOOL)subclassHasHookedSelectorName:(NSString *)selectorName {
return self.selectorNamesToSubclassTrackers[selectorName] != nil;
}
- (void)addSubclassTracker:(AspectTracker *)subclassTracker hookingSelectorName:(NSString *)selectorName {
NSMutableSet *trackerSet = self.selectorNamesToSubclassTrackers[selectorName];
if (!trackerSet) {
trackerSet = [NSMutableSet new];
self.selectorNamesToSubclassTrackers[selectorName] = trackerSet;
}
[trackerSet addObject:subclassTracker];
}
- (void)removeSubclassTracker:(AspectTracker *)subclassTracker hookingSelectorName:(NSString *)selectorName {
NSMutableSet *trackerSet = self.selectorNamesToSubclassTrackers[selectorName];
[trackerSet removeObject:subclassTracker];
if (trackerSet.count == 0) {
[self.selectorNamesToSubclassTrackers removeObjectForKey:selectorName];
}
}
- (NSSet *)subclassTrackersHookingSelectorName:(NSString *)selectorName {
NSMutableSet *hookingSubclassTrackers = [NSMutableSet new];
for (AspectTracker *tracker in self.selectorNamesToSubclassTrackers[selectorName]) {
if ([tracker.selectorNames containsObject:selectorName]) {
[hookingSubclassTrackers addObject:tracker];
}
[hookingSubclassTrackers unionSet:[tracker subclassTrackersHookingSelectorName:selectorName]];
}
return hookingSubclassTrackers;
}
- (NSString *)trackedClassName {
return NSStringFromClass(self.trackedClass);
}
- (NSString *)description {
return [NSString stringWithFormat:@"<%@: %@, trackedClass: %@, selectorNames:%@, subclass selector names: %@>", self.class, self, NSStringFromClass(self.trackedClass), self.selectorNames, self.selectorNamesToSubclassTrackers.allKeys];
}
@end
AspectTracker這個類是用來跟蹤要被hook的類。trackedClass是被追蹤的類。trackedClassName是被追蹤類的類名。selectorNames是一個NSMutableSet,這里會記錄要被hook替換的方法名,用NSMutableSet是為了防止重復(fù)替換方法。selectorNamesToSubclassTrackers是一個字典,key是hookingSelectorName,value是裝滿AspectTracker的NSMutableSet。
addSubclassTracker方法是把AspectTracker加入到對應(yīng)selectorName的集合中。removeSubclassTracker方法是把AspectTracker從對應(yīng)的selectorName的集合中移除。subclassTrackersHookingSelectorName方法是一個并查集,傳入一個selectorName,通過遞歸查找,找到所有包含這個selectorName的set,最后把這些set合并在一起作為返回值返回。
四. Aspects hook前的準(zhǔn)備工作
Aspects 庫中就兩個函數(shù),一個是針對類的,一個是針對實例的。
+ (id<AspectToken>)aspect_hookSelector:(SEL)selector
withOptions:(AspectOptions)options
usingBlock:(id)block
error:(NSError **)error {
return aspect_add((id)self, selector, options, block, error);
}
- (id<AspectToken>)aspect_hookSelector:(SEL)selector
withOptions:(AspectOptions)options
usingBlock:(id)block
error:(NSError **)error {
return aspect_add(self, selector, options, block, error);
}
兩個方法的實現(xiàn)都是調(diào)用同一個方法aspect_add,只是傳入的參數(shù)不同罷了。所以我們只要從aspect_add開始研究即可。
- aspect_hookSelector:(SEL)selector withOptions:(AspectOptions)options usingBlock:(id)block error:(NSError **)error
└── aspect_add(self, selector, options, block, error);
└── aspect_performLocked
├── aspect_isSelectorAllowedAndTrack
└── aspect_prepareClassAndHookSelector
這是函數(shù)調(diào)用棧。從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_prepareClassAndHookSelector(self, selector, error);
}
}
});
return identifier;
}
aspect_add函數(shù)一共5個入?yún)ⅲ谝粋€參數(shù)是self,selector是外面?zhèn)鬟M(jìn)來需要hook的SEL,options是切片的時間,block是切片的執(zhí)行方法,最后的error是錯誤。
aspect_performLocked是一個自旋鎖。自旋鎖是效率比較高的一種鎖,相比@synchronized來說效率高得多。
static void aspect_performLocked(dispatch_block_t block) {
static OSSpinLock aspect_lock = OS_SPINLOCK_INIT;
OSSpinLockLock(&aspect_lock);
block();
OSSpinLockUnlock(&aspect_lock);
}
如果對iOS中8大鎖不了解的,可以看以下兩篇文章
iOS 常見知識點(三):Lock
深入理解 iOS 開發(fā)中的鎖
但是自旋鎖也是有可能出現(xiàn)問題的:
如果一個低優(yōu)先級的線程獲得鎖并訪問共享資源,這時一個高優(yōu)先級的線程也嘗試獲得這個鎖,它會處于 spin lock 的忙等(busy-wait)狀態(tài)從而占用大量 CPU。此時低優(yōu)先級線程無法與高優(yōu)先級線程爭奪 CPU 時間,從而導(dǎo)致任務(wù)遲遲完不成、無法釋放 lock。不再安全的 OSSpinLock
OSSpinLock的問題在于,如果訪問這個所的線程不是同一優(yōu)先級的話,會有死鎖的潛在風(fēng)險。
這里暫時認(rèn)為是相同優(yōu)先級的線程,所以O(shè)SSpinLock保證了線程安全。也就是說aspect_performLocked是保護(hù)了block的線程安全。
現(xiàn)在就剩下aspect_isSelectorAllowedAndTrack函數(shù)和aspect_prepareClassAndHookSelector函數(shù)了。
接下來先看看aspect_isSelectorAllowedAndTrack函數(shù)實現(xiàn)過程。
static NSSet *disallowedSelectorList;
static dispatch_once_t pred;
dispatch_once(&pred, ^{
disallowedSelectorList = [NSSet setWithObjects:@"retain", @"release", @"autorelease", @"forwardInvocation:", nil];
});
先定義了一個NSSet,這里面是一個“黑名單”,是不允許hook的函數(shù)名。retain, release, autorelease, forwardInvocation:是不允許被hook的。
NSString *selectorName = NSStringFromSelector(selector);
if ([disallowedSelectorList containsObject:selectorName]) {
NSString *errorDescription = [NSString stringWithFormat:@"Selector %@ is blacklisted.", selectorName];
AspectError(AspectErrorSelectorBlacklisted, errorDescription);
return NO;
}
當(dāng)檢測到selector的函數(shù)名是黑名單里面的函數(shù)名,立即報錯。
AspectOptions position = options&AspectPositionFilter;
if ([selectorName isEqualToString:@"dealloc"] && position != AspectPositionBefore) {
NSString *errorDesc = @"AspectPositionBefore is the only valid position when hooking dealloc.";
AspectError(AspectErrorSelectorDeallocPosition, errorDesc);
return NO;
}
再次檢查如果要切片dealloc,切片時間只能在dealloc之前,如果不是AspectPositionBefore,也要報錯。
if (![self respondsToSelector:selector] && ![self.class instancesRespondToSelector:selector]) {
NSString *errorDesc = [NSString stringWithFormat:@"Unable to find selector -[%@ %@].", NSStringFromClass(self.class), selectorName];
AspectError(AspectErrorDoesNotRespondToSelector, errorDesc);
return NO;
}
當(dāng)selector不在黑名單里面了,如果切片是dealloc,且selector在其之前了。這時候就該判斷該方法是否存在。如果self和self.class里面都找不到該selector,會報錯找不到該方法。
if (class_isMetaClass(object_getClass(self))) {
Class klass = [self class];
NSMutableDictionary *swizzledClassesDict = aspect_getSwizzledClassesDict();
Class currentClass = [self class];
AspectTracker *tracker = swizzledClassesDict[currentClass];
if ([tracker subclassHasHookedSelectorName:selectorName]) {
NSSet *subclassTracker = [tracker subclassTrackersHookingSelectorName:selectorName];
NSSet *subclassNames = [subclassTracker valueForKey:@"trackedClassName"];
NSString *errorDescription = [NSString stringWithFormat:@"Error: %@ already hooked subclasses: %@. A method can only be hooked once per class hierarchy.", selectorName, subclassNames];
AspectError(AspectErrorSelectorAlreadyHookedInClassHierarchy, errorDescription);
return NO;
}
class_isMetaClass 先判斷是不是元類。接下來的判斷都是判斷元類里面能否允許被替換方法。
subclassHasHookedSelectorName會判斷當(dāng)前tracker的subclass里面是否包含selectorName。因為一個方法在一個類的層級里面只能被hook一次。如果已經(jīng)tracker里面已經(jīng)包含了一次,那么會報錯。
do {
tracker = swizzledClassesDict[currentClass];
if ([tracker.selectorNames containsObject:selectorName]) {
if (klass == currentClass) {
// Already modified and topmost!
return YES;
}
NSString *errorDescription = [NSString stringWithFormat:@"Error: %@ already hooked in %@. A method can only be hooked once per class hierarchy.", selectorName, NSStringFromClass(currentClass)];
AspectError(AspectErrorSelectorAlreadyHookedInClassHierarchy, errorDescription);
return NO;
}
} while ((currentClass = class_getSuperclass(currentClass)));
在這個do-while循環(huán)中,currentClass = class_getSuperclass(currentClass)這個判斷會從currentClass的superclass開始,一直往上找,直到這個類為根類NSObject。
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)));
經(jīng)過上面合法性hook判斷和類方法不允許重復(fù)替換的檢查后,到此,就可以把要hook的信息記錄下來,用AspectTracker標(biāo)記。在標(biāo)記過程中,一旦子類被更改,父類也需要跟著一起被標(biāo)記。do-while的終止條件還是currentClass = class_getSuperclass(currentClass)。
以上是元類的類方法hook判斷合法性的代碼。
如果不是元類,只要不是hook這"retain", "release", "autorelease", "forwardInvocation:"4種方法,而且hook “dealloc”方法的時機(jī)必須是before,并且selector能被找到,那么方法就可以被hook。
通過了selector是否能被hook合法性的檢查之后,就要獲取或者創(chuàng)建AspectsContainer容器了。
// Loads or creates the aspect container.
static AspectsContainer *aspect_getContainerForObject(NSObject *self, SEL selector) {
NSCParameterAssert(self);
SEL aliasSelector = aspect_aliasForSelector(selector);
AspectsContainer *aspectContainer = objc_getAssociatedObject(self, aliasSelector);
if (!aspectContainer) {
aspectContainer = [AspectsContainer new];
objc_setAssociatedObject(self, aliasSelector, aspectContainer, OBJC_ASSOCIATION_RETAIN);
}
return aspectContainer;
}
在讀取或者創(chuàng)建AspectsContainer之前,第一步是先標(biāo)記一下selector。
static SEL aspect_aliasForSelector(SEL selector) {
NSCParameterAssert(selector);
return NSSelectorFromString([AspectsMessagePrefix stringByAppendingFormat:@"_%@", NSStringFromSelector(selector)]);
}
在全局代碼里面定義了一個常量字符串
static NSString *const AspectsMessagePrefix = @"aspects_";
用這個字符串標(biāo)記所有的selector,都加上前綴"aspects_"。然后獲得其對應(yīng)的AssociatedObject關(guān)聯(lián)對象,如果獲取不到,就創(chuàng)建一個關(guān)聯(lián)對象。最終得到selector有"aspects_"前綴,對應(yīng)的aspectContainer。
得到了aspectContainer之后,就可以開始準(zhǔn)備我們要hook方法的一些信息。這些信息都裝在AspectIdentifier中,所以我們需要新建一個AspectIdentifier。
調(diào)用AspectIdentifier的instancetype方法,創(chuàng)建一個新的AspectIdentifier
+ (instancetype)identifierWithSelector:(SEL)selector object:(id)object options:(AspectOptions)options block:(id)block error:(NSError **)error
這個instancetype方法,只有一種情況會創(chuàng)建失敗,那就是aspect_isCompatibleBlockSignature方法返回NO。返回NO就意味著,我們要替換的方法block和要替換的原方法,兩者的方法簽名是不相符的。(這個函數(shù)在上面詳解過了,這里不再贅述)。方法簽名匹配成功之后,就會創(chuàng)建好一個AspectIdentifier。
[aspectContainer addAspect:identifier withOptions:options];
aspectContainer容器會把它加入到容器中。完成了容器和AspectIdentifier初始化之后,就可以開始準(zhǔn)備進(jìn)行hook了。通過options選項分別添加到容器中的beforeAspects,insteadAspects,afterAspects這三個數(shù)組
// Modify the class to allow message interception.
aspect_prepareClassAndHookSelector(self, selector, error);
小結(jié)一下,aspect_add干了一些什么準(zhǔn)備工作:
- 首先調(diào)用aspect_performLocked ,利用自旋鎖,保證整個操作的線程安全
- 接著調(diào)用aspect_isSelectorAllowedAndTrack對傳進(jìn)來的參數(shù)進(jìn)行強(qiáng)校驗,保證參數(shù)合法性。
- 接著創(chuàng)建AspectsContainer容器,利用AssociatedObject關(guān)聯(lián)對象動態(tài)添加到NSObject分類中作為屬性的。
- 再由入?yún)elector,option,創(chuàng)建AspectIdentifier實例。AspectIdentifier主要包含了單個的 Aspect的具體信息,包括執(zhí)行時機(jī),要執(zhí)行block 所需要用到的具體信息。
- 再將單個的 AspectIdentifier 的具體信息加到屬性AspectsContainer容器中。通過options選項分別添加到容器中的beforeAspects,insteadAspects,afterAspects這三個數(shù)組。
- 最后調(diào)用prepareClassAndHookSelector準(zhǔn)備hook。
由于簡書單篇文章字?jǐn)?shù)限制,無奈只能拆成2篇,下部分見下篇。