使用 XCTest + OCMock 寫單元測試也有一段時間了. 一直沒了解 OCMock 究竟是怎麼實現的, 因此就想找個時間讀讀源碼, 揭開 OCMock 的神祕面紗. 在閱讀源碼時發現比較核心的機制就是 NSProxy + 消息轉發, 因此在看源碼以前, 先簡單複習一下相關知識.git
先來看看消息轉發, Objective-C 的消息機制就不贅述了, 在 objc_msgSend
時, 若是對象的和其父類一直到根類都沒有在方法緩存和方法列表中找到對應的方法就會發生這樣的錯誤: unrecognized selector sent to instance
, 可是在崩潰前, 會有消息轉發的機制來嘗試挽救.github
第一步, 首先會調用 forwardingTargetForSelector:
方法獲取一個能夠處理該 Selector
的對象. 對該對象從新進行發送消息, 若是返回爲 nil
, 則走第二步.數組
第二步, 調用 methodSignatureForSelector:
方法來得到方法簽名 NSMethodSignature
, 包含 Selector
和 參數的信息, 用於生成 NSInvocation
, 若是返回爲 nil
, 則拋出 doesNotRecognizeSelector
異常.緩存
第三步, 調用 forwardInvocation:
對 NSInvocation
進行處理, 若是自己, 父類一直到根類都沒有處理, 則仍是會拋出 doesNotRecognizeSelector
異常.bash
簡單整理消息轉發到機制就是這樣, 更深的原理推薦閱讀楊蕭玉大神的這篇文章: Objective-C 消息發送與轉發機制原理.ide
An abstract superclass defining an API for objects that act as stand-ins for other objects or for objects that don’t exist yet.工具
在文檔中的解釋是這樣的, NSProxy 是一個抽象的父類(說根類更爲合適), 用於定義對象的 API, 能夠充當其餘對象或者已經不存在的對象的替身.單元測試
在 iOS 中的根類是 NSObject 和 NSProxy, NSObject 便是根類也是協議, NSProxy 也實現了該協議, 而且做爲一個抽象類, 它並不提供初始化方法, 若是接收到它沒有響應的消息時會拋出異常, 因此, 須要使用子類繼承實現初始化方法, 而後經過重寫 forwardInvocation:
和methodSignatureForSelector:
方法來處理它自己未實現的消息處理.學習
這裏列出兩個常常會使用到的小 Tips.測試
YYWeakProxy
是 YYKit
中提供的工具, 用於持有一個 weak
對象, 一般用來解決 NSTimer
和 CADisplayLink
循環引用的問題. 好比咱們常常會在對象內使用 NSTimer
, 該對象強引用着 NSTimer
, 而該對象在做爲 target
時就又會被 NSTimer
強引用着, 就構成了循環引用, 致使都沒法釋放.
簡單介紹一下 YYWeakProxy
是如何實現的, 首先使用初始化方法, 弱引用着 target
對象.
- (instancetype)initWithTarget:(id)target {
_target = target;
return self;
}
複製代碼
經過實現 forwardingTargetForSelector:
方法來將消息轉發給 _taget
, 充當了橋樑, 破除了如 NSTimer
對 target
的強引用.
- (id)forwardingTargetForSelector:(SEL)selector {
return _target;
}
複製代碼
而後這裏又另外實現了這兩個方法, 這是爲了什麼呢?
- (void)forwardInvocation:(NSInvocation *)invocation {
void *null = NULL;
[invocation setReturnValue:&null];
}
- (NSMethodSignature *)methodSignatureForSelector:(SEL)selector {
return [NSObject instanceMethodSignatureForSelector:@selector(init)];
}
複製代碼
由於 target
是弱引用的, 若是釋放了, 就會被置爲 nil
, 轉發方法 forwardingTargetForSelector:
就至關於返回了 nil
, 那麼沒有辦法處理消息, 則會致使發生崩潰.
因此這裏就是隨便返回了一個方法簽名, 直接返回 NSObject
的 init
的方法簽名, invocation
並未調用 invoke
只是返回 nil
, 至關於此時發送什麼消息都會返回 nil
, 不會崩潰.
在 objc 中是不能多繼承的, 可是咱們可使用 NSProxy 來模擬多繼承的效果, 其實將上面的例子的 target
變成一個數組來持有多個 target
.
而後將方法按照 respondsToSelector:
誰能處理, 來轉發給各個 target
就能夠實現多繼承了, 比較簡單, 這裏用最簡單的方法實現以下:
- (id)forwardingTargetForSelector:(SEL)selector {
__block id target = nil;
[self.tagets enumerateObjectsUsingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
if ([obj respondsToSelector:selector]) {
target = obj;
*stop = YES;
}
}];
return target;
}
複製代碼
接下來進入正題, 來開始看一下 OCMock
的核心源碼實現.
我準備從一個常常會用到的例子來一點一點閱讀 OCMock 的源碼實現.
在單元測試中, 常常須要屏蔽掉外界因素的干擾, 好比方法中依賴的外部方法的結果, 在咱們的項目中, 大量的使用了下發的開關配置, 好比下面這行代碼來判斷是否開啓某個功能.
BOOL enableXX = [[RemoteConfig sharedRemoteConfig] enableXXFeature];
複製代碼
使用 OCMock 來 Mock 該結果的方式以下:
// Setup
id configMock = OCMClassMock([RemoteConfig class]);
OCMStub([configMock sharedRemoteConfig]).andReturn(configMock);
OCMStub([configMock enableXXFeature]).andReturn(YES);
// Assert
...
// Teardown
[configMock stopMocking];
複製代碼
第一行建立一個 RemoteConfig
類的 mock 對象, 命名爲 configMock
;
第二行 mock 掉 [configMock sharedRemoteConfig]
的類方法, 而且 andReturn
添加返回值爲該 mock 對象. 這樣經過 [RemoteConfig sharedRemoteConfig]
就能夠永遠返回一個 mock 的對象, 接下來只要在對這個 mock 的對象的 enableXXFeature
方法添加一個返回值就能夠實現 mock 開關了;
第三行, mock 掉 [configMock enableXXFeature]
的實例方法而且 andReturn
添加返回值恆定爲 YES
.
OCMock
使用了大量的宏定義, 那麼就經過 Xcode
提供的 Preprocess
的功能來一步一步看看究竟是怎麼回事吧.
第一行, OCMClassMock
宏展開後以下:
id configMock = [OCMockObject niceMockForClass:[RemoteConfig class]];
複製代碼
這個 OCMockObject
就是咱們剛剛說到的 NSProxy
的一個子類, 來實現消息轉發, niceMockForClass
其實就是調用了
+ (id)mockForClass:(Class)aClass {
return [[[OCClassMockObject alloc] initWithClass:aClass] autorelease];
}
複製代碼
只不過設置了一個 isNice
的實例變量, 而且標記爲 YES
, 這個不影響核心原理的理解, 簡單說一下, OCMock 中使用 OCMStrictClassMock
能夠進行一個嚴格的 mock, 若是調用沒有 Stub
住的方法時, 就會崩潰, 而這個 OCMClassMock
就是 nice
的, 沒有 Stub
的方法會進行一下保護, 不會產生崩潰, 比較 nice
, 咱們比較經常使用到的就是比較 nice
的 OCMClassMock
.
整個 OCMStub
是最核心的點, 其餘的 Expect
和 Reject
原理大都一致, 一點一點看.
OCMStub([configMock enableXXFeature]).andReturn(YES);
複製代碼
先從這行代碼來看起, 先看 OCMStub 的展開, 我稍微整理了一下, 代碼以下:
({
[OCMMacroState beginStubMacro];
OCMStubRecorder *recorder = ((void *)0);
@try{
[configMock enableXXFeature];
} @finally {
recorder = [OCMMacroState endStubMacro];
}
recorder;
});
複製代碼
其中上下兩個 begin
和 end
的方法就是爲了增長一個 OCMStubRecorder
標記, 而且存放在當前線程的字典中. 代碼以下:
+ (void)beginStubMacro {
OCMStubRecorder *recorder = [[[OCMStubRecorder alloc] init] autorelease];
OCMMacroState *macroState = [[OCMMacroState alloc] initWithRecorder:recorder];
[NSThread currentThread].threadDictionary[OCMGlobalStateKey] = macroState;
[macroState release];
}
+ (OCMStubRecorder *)endStubMacro {
NSMutableDictionary *threadDictionary = [NSThread currentThread].threadDictionary;
OCMMacroState *globalState = threadDictionary[OCMGlobalStateKey];
OCMStubRecorder *recorder = [(OCMStubRecorder *)[globalState recorder] retain];
[threadDictionary removeObjectForKey:OCMGlobalStateKey];
return [recorder autorelease];
}
複製代碼
關鍵在中間一行 [configMock enableXXFeature]
的調用, 存在這個 OCMStubRecorder
標記時, 會在消息轉發的 forwardingTargetForSelector:
這個方法中進行處理, 記錄 configMock
對象的同時, 返回這個 recorder
對象進行處理.
- (id)forwardingTargetForSelector:(SEL)aSelector {
if([OCMMacroState globalState] != nil) {
OCMRecorder *recorder = [[OCMMacroState globalState] recorder];
[recorder setMockObject:self];
return recorder;
}
return nil;
}
複製代碼
因此便理解了上面爲何要將 recorder
對象放入當前線程的字典中, 是爲了一樣是這樣一行代碼 [configMock enableXXFeature]
, 在是否有 recorder
時, 能夠有兩種大相徑庭的處理路線, 非常巧妙. 即在定義 Stub
時, 能夠交給 recorder
去處理, 而在真正調用該方法時, 能夠由這個 mock 的對象按照消息轉發接下來的流程處理.
這個 recorder
對象是 OCMStubRecorder
類型, 繼承自 OCMRecorder
, 而 OCMRecorder
又繼承自 NSProxy
. 因此這個 recorder
也須要處理消息轉發機制.
recorder
在 methodSignatureForSelector:
中, 先按照實例方法去獲取 mock 對象的方法簽名, 若是沒有的話再按照類方法去獲取方法簽名, 若是獲取到則在 invocationMatcher
記錄標記一下, 是類方法
, 仍是沒獲取到就會返回 nil
了, 按照消息轉發機制, 則會拋出 doesNotRecognizeSelector
異常.
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
if([invocationMatcher recordedAsClassMethod])
return [mockedClass methodSignatureForSelector:aSelector];
NSMethodSignature *signature = [mockObject methodSignatureForSelector:aSelector];
if(signature == nil) {
if([mockedClass respondsToSelector:aSelector]) {
// 標記一下證實該 Selector 是類方法, 標記到 invocationMatcher 上
[self classMethod];
// 從新調用這個方法取方法前面, 這樣就會被前兩行返回
signature = [self methodSignatureForSelector:aSelector];
}
}
return signature;
}
複製代碼
前面兩行的意思是若是已經被標記爲類方法了, 則直接返回類方法的方法簽名.
再來看 forwardInvocation:
處理的方法, 我按照繼承關係整理了一下方便閱讀:
- (void)forwardInvocation:(NSInvocation *)anInvocation {
[anInvocation setTarget:nil];
[invocationMatcher setInvocation:anInvocation];
[mockObject addStub:invocationMatcher];
}
複製代碼
其目的就是經過 setTarget:nil
來禁止這個 invocation
調用, 用 invocationMatcher
來記錄而且管理一下這個 invocation
, 而後把這個 invocationMatcher
傳遞給 mockObject
就是咱們上面記錄過的 configMock
對象.
在 addStub:
方法中, 若是是實例方法只是將這個 invocationMatcher
保存到了一個數組中, 若是是類方法等下再看 Stub sharedRemoteConfig
這個類方法時再看.
這樣整個 OCMStub
的過程就理解了. 在簡單整理一下對象間的關係, 方便理解.
mock 對象持有一個 invocationMatcher
對象的數組, 每個 invocationMatcher
對象表示一次的 Stub(或者是 Expect 等), 還記錄着該方法是個類方法仍是實例方法.
每個 invocationMatcher
持有 invocation
對象, 用於進行在調用的時候, 和調用的 invocation
進行匹配, 以及參數校驗等邏輯.
在 Stub 流程中, 這個 recorder
對象至關於一個流程管理者, 記錄了該流程的信息, 再 Stub 語句完整結束後, 其實就被釋放了, 後面在看.
OCMStub
其實是返回了 OCMStubRecorder
這個對象. 在這個對象中記錄須要的方法返回值. 展開後以下:
recorder._andReturn(({
__typeof__((YES)) _val = ((YES));
NSValue *_nsval = [NSValue value:&_val withObjCType:@encode(__typeof__(_val))];
if (OCMIsObjectType(@encode(__typeof(_val)))) {
objc_setAssociatedObject(_nsval, "OCMAssociatedBoxedValue", *(__unsafe_unretained id *) (void *) &_val, OBJC_ASSOCIATION_RETAIN);
}
_nsval;
}));
複製代碼
補充說明一下, @encode
是一個編譯器指令, 返回一個類型內部進行表示的字符串, 好比這裏使用的 YES
是 BOOL
類型, 內部字符串表示就是 "B"
, 更深刻的, 更方便對類型進行判斷和處理, 關於 @encode
推薦閱讀這篇文章
因此, 總體邏輯簡單來講實際上就是將這個返回值經過 NSValue
進行包裝, 能夠理解爲
recorder._addReturn(_nsval);
複製代碼
這個 _addReturn()
是一個 block
, 傳入一個 NSValue
, 返回自身方便鏈式編寫. 本質上就是根據返回值的類型, 是基本類型仍是對象使用不一樣的 ValueProvider
進行包裝. 基本類型使用 OCMBoxedReturnValueProvider
, 對象則使用 OCMReturnValueProvider
.
在來看剛剛的對象間關係:
這時就增長了 ValueProviders
的邏輯, 每個 invocationMatcher
持有多個. 由於不只僅能夠 andReturn
指定返回值, 例如還能夠 andDo
指定一個 block
, 在方法被調用後執行等等. 不過感受 OCMock
此處的處理還能夠再完善一下, 這些相似於的 ValueProviders
都聽從一個 ValueProviders
的協議, 而後協議要求實現 handleInvocation:
, 不過既然是人家內部的邏輯, 也無所謂啦.
調用過程當中其實是沒有 recorder
的, 在 OCMStub
整行代碼結束後就被釋放啦. 對象關係就變成這樣了:
真正調用的是對 configMock
即 OCClassMockObject
進行調用如 enableXXFeature
方法的過程就像前面說過的, 因爲沒有了 recorder
, forwardingTargetForSelector:
會返回 nil
接下來的消息轉發流程回去獲取方法簽名, 而後在 forwardInvocation:
中處理.
- (void)forwardInvocation:(NSInvocation *)anInvocation {
@try
{
if([self handleInvocation:anInvocation] == NO)
[self handleUnRecordedInvocation:anInvocation];
} @catch(NSException *e) {
...
}
}
複製代碼
核心步驟在 handleInvocation:
中, 整理以下
- (BOOL)handleInvocation:(NSInvocation *)anInvocation {
// 1. 記錄 `invocation` 用於實現 `Expect` 的校驗邏輯
@synchronized(invocations) {
[anInvocation retainObjectArgumentsExcludingObject:self];
[invocations addObject:anInvocation];
}
// 2. 取剛剛 `addStub:` 中記錄的 `invocationMatcher` 進行匹配
OCMInvocationStub *stub = nil;
@synchronized(stubs) {
for(stub in stubs) {
if([stub matchesInvocation:anInvocation])
break;
}
[stub retain];
}
if(stub == nil)
return NO;
// ...expectaion 相關邏輯省略
// 3. 這個 stub 就是 `invocationMatcher`, 交由它處理.
@try {
[stub handleInvocation:anInvocation];
} @finally {
[stub release];
}
return YES;
}
複製代碼
invocationMatcher
的處理邏輯以下:
- (void)handleInvocation:(NSInvocation *)anInvocation {
NSMethodSignature *signature = [recordedInvocation methodSignature];
NSUInteger n = [signature numberOfArguments];
for(NSUInteger i = 2; i < n; i++) {
id recordedArg = [recordedInvocation getArgumentAtIndexAsObject:i];
id passedArg = [anInvocation getArgumentAtIndexAsObject:i];
if([recordedArg isProxy])
continue;
if([recordedArg isKindOfClass:[NSValue class]])
recordedArg = [OCMArg resolveSpecialValues:recordedArg];
if(![recordedArg isKindOfClass:[OCMArgAction class]])
continue;
[recordedArg handleArgument:passedArg];
}
// 4. 經過記錄的 `ValueProvider` 交給它去處理
[invocationActions makeObjectsPerformSelector:@selector(handleInvocation:) withObject:anInvocation];
}
複製代碼
以 OCMBoxedReturnValueProvider
爲例子, 處理邏輯以下
- (void)handleInvocation:(NSInvocation *)anInvocation {
const char *returnType = [[anInvocation methodSignature] methodReturnType];
NSUInteger returnTypeSize = [[anInvocation methodSignature] methodReturnLength];
char valueBuffer[returnTypeSize];
NSValue *returnValueAsNSValue = (NSValue *)returnValue;
// 5. 將返回值設置到 `invocation` 中 `[anInvocation setReturnValue:valueBuffer]`
if([self isMethodReturnType:returnType compatibleWithValueType:[returnValueAsNSValue objCType]]) {
[returnValueAsNSValue getValue:valueBuffer];
[anInvocation setReturnValue:valueBuffer];
} else if([returnValueAsNSValue getBytes:valueBuffer objCType:returnType]) {
[anInvocation setReturnValue:valueBuffer];
} else {
[NSException raise:NSInvalidArgumentException
format:@"Return value cannot be used for method; method signature declares '%s' but value is '%s'.", returnType, [returnValueAsNSValue objCType]];
}
}
複製代碼
這樣就完成了整個調用過程, 其中若是沒有找到匹配的方法等等緣由則會判斷若是不是 isNice
則會拋出異常.
- (void)handleUnRecordedInvocation:(NSInvocation *)anInvocation {
if(isNice == NO) {
[NSException raise:NSInternalInconsistencyException format:@"%@: unexpected method invoked: %@ %@", [self description], [anInvocation invocationDescription], [self _stubDescriptions:NO]];
}
}
複製代碼
看明白了實例方法其實是經過 mock 對象進行消息轉發進行處理, 而後獲取指望的結果並返回的, 那類方法又是如何實現 mock 的呢?
關鍵就在初始化時作了一個準備工做 prepareClassForClassMethodMocking
和剛剛 addStub:
的處理上, 一個一個看.
prepareClassForClassMethodMocking
用註釋總結整理以下:
- (void)prepareClassForClassMethodMocking
{
// 1. 排除一些會引發錯誤的類 `NSString` / `NSArray` / `NSManagedObject`
if([[mockedClass class] isSubclassOfClass:[NSString class]] || [[mockedClass class] isSubclassOfClass:[NSArray class]])
return;
if([mockedClass isSubclassOfClass:objc_getClass("NSManagedObject")])
return;
// 2. 若是以前有對該類進行的 mock 未中止則中止
id otherMock = OCMGetAssociatedMockForClass(mockedClass, NO);
if(otherMock != nil)
[otherMock stopMockingClassMethods];
OCMSetAssociatedMockForClass(self, mockedClass);
// 3. 動態建立一個 mock 的類(例子裏是 `RemoteConfig` )的子類.
classCreatedForNewMetaClass = OCMCreateSubclass(mockedClass, mockedClass);
originalMetaClass = object_getClass(mockedClass);
id newMetaClass = object_getClass(classCreatedForNewMetaClass);
// 4. 建立一個空方法 `initializeForClassObject`, 做爲子類的 `initialize` 方法, 以便排除 mock 類 `initialize` 中特殊邏輯的影響.
Method myDummyInitializeMethod = class_getInstanceMethod([self mockObjectClass], @selector(initializeForClassObject));
const char *initializeTypes = method_getTypeEncoding(myDummyInitializeMethod);
IMP myDummyInitializeIMP = method_getImplementation(myDummyInitializeMethod);
class_addMethod(newMetaClass, @selector(initialize), myDummyInitializeIMP, initializeTypes);
// 5. `object_setClass(mockedClass, newMetaClass)` 設置 mock 的類的 Class 爲新建立的子類的元類.
object_setClass(mockedClass, newMetaClass);
// 6. 爲其元類添加一個 `+ (void)forwardInvocation:` 的實現 `forwardInvocationForClassObject:` 以即可以對類方法進行消息轉發.
Method myForwardMethod = class_getInstanceMethod([self mockObjectClass], @selector(forwardInvocationForClassObject:));
IMP myForwardIMP = method_getImplementation(myForwardMethod);
class_addMethod(newMetaClass, @selector(forwardInvocation:), myForwardIMP, method_getTypeEncoding(myForwardMethod));
// 7. 遍歷該元類的方法列表, 對其自身的方法(非 `NSObject` 繼承來的) 方法執行 `setupForwarderForClassMethodSelector:`
NSArray *methodBlackList = @[@"class", @"forwardingTargetForSelector:", @"methodSignatureForSelector:", @"forwardInvocation:", @"isBlock",
@"instanceMethodForwarderForSelector:", @"instanceMethodSignatureForSelector:"];
[NSObject enumerateMethodsInClass:originalMetaClass usingBlock:^(Class cls, SEL sel) {
if((cls == object_getClass([NSObject class])) || (cls == [NSObject class]) || (cls == object_getClass(cls)))
return;
NSString *className = NSStringFromClass(cls);
NSString *selName = NSStringFromSelector(sel);
if(([className hasPrefix:@"NS"] || [className hasPrefix:@"UI"]) &&
([selName hasPrefix:@"_"] || [selName hasSuffix:@"_"]))
return;
if([methodBlackList containsObject:selName])
return;
@try
{
[self setupForwarderForClassMethodSelector:sel];
}
@catch(NSException *e)
{
// ignore for now
}
}];
}
複製代碼
addStub:
的特殊邏輯實際上也是執行了 setupForwarderForClassMethodSelector:
, 該方法進行了排重. 實現以下:
- (void)setupForwarderForClassMethodSelector:(SEL)selector {
SEL aliasSelector = OCMAliasForOriginalSelector(selector);
if(class_getClassMethod(mockedClass, aliasSelector) != NULL)
return;
Method originalMethod = class_getClassMethod(mockedClass, selector);
IMP originalIMP = method_getImplementation(originalMethod);
const char *types = method_getTypeEncoding(originalMethod);
Class metaClass = object_getClass(mockedClass);
IMP forwarderIMP = [originalMetaClass instanceMethodForwarderForSelector:selector];
class_addMethod(metaClass, aliasSelector, originalIMP, types);
class_replaceMethod(metaClass, selector, forwarderIMP, types);
}
複製代碼
添加一個 ocmock_replaced_原方法名
, 將該方法指向原來方法的方法指針, 而且將原來方法指向到一個不存在的方法上, 以即可以走消息轉發, 也就是剛剛添加的 forwardInvocationForClassObject:
在 forwardInvocationForClassObject:
方法真正調用時, 也是調用了 handleInvocation:
, 便統一了消息轉發的流程, 實現了對類方法的 mock, 不一樣的是對於沒有匹配到的方法直接執行了 invocation
,
對於 stopMocking
方法的調用不是必須的, 在 mock 對象釋放掉的時候 dealloc
中會先調用 stopMocking
, 其中乾的事就是打掃戰場, 因爲設置了 mock 對象的元類爲動態建立的子類的元類, 因此須要還原
object_setClass(mockedClass, originalMetaClass);
複製代碼
而後刪除掉動態建立的子類, 選擇使用動態建立的子類做爲元類而且添加方法, 而不是直接修改元類中的方法, 也是爲了最後還原比較容易, 直接釋放掉便可.
對於其餘的 OCMPartialMock
和 OCMProtocolMock
等, 基本原理也都類似, 就再也不記錄了, 關於 OCMock 大致的原理基本弄清楚了, 其實還有不少細節還得隨着繼續學習再加深理解, 歡迎交流👏 .