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

相关文章
相关标签/搜索