OCMock 源碼學習筆記

背景

使用 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

NSProxy

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

YYWeakProxyYYKit 中提供的工具, 用於持有一個 weak 對象, 一般用來解決 NSTimerCADisplayLink 循環引用的問題. 好比咱們常常會在對象內使用 NSTimer, 該對象強引用着 NSTimer, 而該對象在做爲 target 時就又會被 NSTimer 強引用着, 就構成了循環引用, 致使都沒法釋放.

點這裏查看所有源碼

簡單介紹一下 YYWeakProxy 是如何實現的, 首先使用初始化方法, 弱引用着 target 對象.

- (instancetype)initWithTarget:(id)target {
    _target = target;
    return self;
}
複製代碼

經過實現 forwardingTargetForSelector: 方法來將消息轉發給 _taget, 充當了橋樑, 破除了如 NSTimertarget 的強引用.

- (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, 那麼沒有辦法處理消息, 則會致使發生崩潰.

因此這裏就是隨便返回了一個方法簽名, 直接返回 NSObjectinit 的方法簽名, 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

第一行, 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, 咱們比較經常使用到的就是比較 niceOCMClassMock.

OCMStub

整個 OCMStub 是最核心的點, 其餘的 ExpectReject 原理大都一致, 一點一點看.

enableXXFeature

展開

OCMStub([configMock enableXXFeature]).andReturn(YES);
複製代碼

先從這行代碼來看起, 先看 OCMStub 的展開, 我稍微整理了一下, 代碼以下:

({
    [OCMMacroState beginStubMacro];
    OCMStubRecorder *recorder = ((void *)0);
    @try{
        [configMock enableXXFeature];
    } @finally {
        recorder = [OCMMacroState endStubMacro];
    }
    recorder;
});
複製代碼

其中上下兩個 beginend 的方法就是爲了增長一個 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];
}
複製代碼

Stub

關鍵在中間一行 [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 也須要處理消息轉發機制.

recordermethodSignatureForSelector: 中, 先按照實例方法去獲取 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 的過程就理解了. 在簡單整理一下對象間的關係, 方便理解.

對象間關係-1

mock 對象持有一個 invocationMatcher 對象的數組, 每個 invocationMatcher 對象表示一次的 Stub(或者是 Expect 等), 還記錄着該方法是個類方法仍是實例方法.

每個 invocationMatcher 持有 invocation 對象, 用於進行在調用的時候, 和調用的 invocation 進行匹配, 以及參數校驗等邏輯.

在 Stub 流程中, 這個 recorder 對象至關於一個流程管理者, 記錄了該流程的信息, 再 Stub 語句完整結束後, 其實就被釋放了, 後面在看.

andReturn

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 是一個編譯器指令, 返回一個類型內部進行表示的字符串, 好比這裏使用的 YESBOOL 類型, 內部字符串表示就是 "B", 更深刻的, 更方便對類型進行判斷和處理, 關於 @encode 推薦閱讀這篇文章

Type Encodings

因此, 總體邏輯簡單來講實際上就是將這個返回值經過 NSValue 進行包裝, 能夠理解爲

recorder._addReturn(_nsval);
複製代碼

這個 _addReturn() 是一個 block, 傳入一個 NSValue, 返回自身方便鏈式編寫. 本質上就是根據返回值的類型, 是基本類型仍是對象使用不一樣的 ValueProvider 進行包裝. 基本類型使用 OCMBoxedReturnValueProvider, 對象則使用 OCMReturnValueProvider.

在來看剛剛的對象間關係:

對象間關係-2

這時就增長了 ValueProviders 的邏輯, 每個 invocationMatcher 持有多個. 由於不只僅能夠 andReturn 指定返回值, 例如還能夠 andDo 指定一個 block , 在方法被調用後執行等等. 不過感受 OCMock 此處的處理還能夠再完善一下, 這些相似於的 ValueProviders 都聽從一個 ValueProviders 的協議, 而後協議要求實現 handleInvocation:, 不過既然是人家內部的邏輯, 也無所謂啦.

調用過程

調用過程當中其實是沒有 recorder 的, 在 OCMStub 整行代碼結束後就被釋放啦. 對象關係就變成這樣了:

對象間關係-3

真正調用的是對 configMockOCClassMockObject 進行調用如 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]];
    }
}
複製代碼

sharedRemoteConfig

看明白了實例方法其實是經過 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

對於 stopMocking 方法的調用不是必須的, 在 mock 對象釋放掉的時候 dealloc 中會先調用 stopMocking, 其中乾的事就是打掃戰場, 因爲設置了 mock 對象的元類爲動態建立的子類的元類, 因此須要還原

object_setClass(mockedClass, originalMetaClass);
複製代碼

而後刪除掉動態建立的子類, 選擇使用動態建立的子類做爲元類而且添加方法, 而不是直接修改元類中的方法, 也是爲了最後還原比較容易, 直接釋放掉便可.

最後

對於其餘的 OCMPartialMockOCMProtocolMock 等, 基本原理也都類似, 就再也不記錄了, 關於 OCMock 大致的原理基本弄清楚了, 其實還有不少細節還得隨着繼續學習再加深理解, 歡迎交流👏 .

References

相關文章
相關標籤/搜索