无法保留从另一个块中检索到的块

时间:2013-05-02 17:51:18

标签: objective-c automatic-ref-counting objective-c-blocks ocmock

我正在尝试为一个方法编写一个单元测试,该方法本身创建并将一个块传递给另一个对象(因此可以在以后调用)。此方法使用socket.io-objc向服务器发送请求。它将回调块传递给socketio的sendEvent:withData:andAcknowledge,当它从服务器收到响应时将调用它。这是我想测试的方法:

typedef void(^MyResponseCallback)(NSDictionary * response);

...

-(void) sendCommand:(NSDictionary*)dict withCallback:(MyResponseCallback)callback andTimeoutAfter:(NSTimeInterval)delay
{
    __block BOOL didTimeout = FALSE;
    void (^timeoutBlock)() = nil;

    // create the block to invoke if request times out
    if (delay > 0)
    {
        timeoutBlock = ^
        {
            // set the flag so if we do get a response we can suppress it
            didTimeout = TRUE;

            // invoke original callback with no response argument (to indicate timeout)
            callback(nil);
        };
    }

    // create a callback/ack to be invoked when we get a response
    SocketIOCallback cb = ^(id argsData)
    {
        // if the callback was invoked after the UI timed out, ignore the response. otherwise, invoke
        // original callback
        if (!didTimeout)
        {
            if (timeoutBlock != nil)
            {
                // cancel the timeout timer
                [NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(onRequestTimeout:) object:timeoutBlock];
            }

            // invoke original callback
            NSDictionary * responseDict = argsData;
            callback(responseDict);
        }
    };

    // send the event to the server
    [_socketIO sendEvent:@"message" withData:dict andAcknowledge:cb];

    if (timeoutBlock != nil)
    {
        // if a timeout value was specified, set up timeout now
        [self performSelector:@selector(onRequestTimeout:) withObject:timeoutBlock afterDelay:delay];
    }
}

-(void) onRequestTimeout:(id)arg
{
    if (nil != arg)
    {
        // arg is a block, execute it
        void (^callback)() = (void (^)())arg;
        callback();
    }
}

这一切似乎在正常运行时工作正常。当我运行我的单元测试(使用SenTestingKit和OCMock)时,我的问题出现了:

-(void)testSendRequestWithNoTimeout
{
    NSDictionary * cmd = [[NSDictionary alloc] initWithObjectsAndKeys:
                          @"TheCommand", @"msg", nil];
    __block BOOL callbackInvoked = FALSE;
    __block SocketIOCallback socketIoCallback = nil;
    MyResponseCallback requestCallback = ^(NSDictionary * response)
    {
        STAssertNotNil(response, @"Response dictionary is invalid");
        callbackInvoked = TRUE;
    };

    // expect controller to emit the message
    [[[_mockSocket expect] andDo:^(NSInvocation * invocation) {
        SocketIOCallback temp;
        [invocation getArgument:&temp atIndex:4];

        // THIS ISN'T WORKING AS I'D EXPECT
        socketIoCallback = [temp copy];

        STAssertNotNil(socketIoCallback, @"No callback was passed to socket.io sendEvent method");
    }] sendEvent:@"message" withData:cmd andAcknowledge:OCMOCK_ANY];

    // send command to dio
    [_ioController sendCommand:cmd withCallback:requestCallback];

    // make sure callback not invoked yet
    STAssertFalse(callbackInvoked, @"Response callback was invoked before receiving a response");

    // fake a response coming back
    socketIoCallback([[NSDictionary alloc] initWithObjectsAndKeys:@"msg", @"response", nil]);

    // make sure the original callback was invoked as a result
    STAssertTrue(callbackInvoked, @"Original requester did not get callback when msg recvd");
}

为了模拟响应,我需要捕获(并保留)由我正在测试的方法创建的块并传递给sendEvent。通过传递'andDo'参数到我的期望,我能够访问我正在寻找的块。但是,它似乎没有得到保留。因此,当sendEvent展开并且我去调用回调时,应该在块中捕获的所有值都显示为null。结果是,当我调用socketIoCallback时,测试崩溃,并且它访问最初作为块的一部分捕获的“回调”值(现在为零)。

我正在使用ARC,因此我希望“__block SocketIOCallback socketIoCallback”将保留值。我试图将块“-copy”到这个变量中,但它仍然没有保留在sendCommand的末尾。我该怎么做才能强制这个块保留足够长的时间来模拟响应?

注意:我已经尝试调用[invocation retainArguments],但是在测试完成后清理时会在objc_release中崩溃。

2 个答案:

答案 0 :(得分:2)

我终于能够重现这个问题了,我怀疑错误出现在这段代码中:

SocketIOCallback temp;
[invocation getArgument:&temp atIndex:4];

以这种方式提取块无法正常工作。我不确定为什么,但它可能与一些神奇的ARC在后台做的有关。如果您将代码更改为以下代码,它应该按照您的预期工作:

void *pointer;
[invocation getArgument:&pointer atIndex:4];
SocketIOCallback temp = (__brigde SocketIOCallback)pointer;

答案 1 :(得分:1)

@aLevelOfIndirection是正确的,因为它是调用getArgument:atIndex:

要理解原因,请记住SocketIOCallback是一个对象指针类型(它是块指针类型的typedef),它在ARC中隐式__strong。因此&tempSocketIOCallback __strong *类型,即它是指向强"的指针。当你将一个"指针传递给strong"对于一个函数,契约是如果函数替换指针指向的值(__strong),它必须1)释放现有值,2)保留新值。

但是,NSInvocation' getArgument:atIndex:对所指向的事物的类型一无所知。它需要一个void *参数,并简单地将所需的值按二进制方式复制到指针指向的位置。因此,简单来说,它会对temp进行ARC之前的非保留分配。它不保留指定的值。

但是,由于temp是一个强引用,ARC将假定它被保留,并在其范围的末尾释放它。这是一个过度释放,因此会崩溃。

理想情况下,编译器应该禁止在"指向强"之间的转换。和void *,所以这不会意外发生。

解决方案是不将"指针传递给强"到getArgument:atIndex:。您可以传递void *作为aLevelOfIndirection show:

void *pointer;
[invocation getArgument:&pointer atIndex:4];
SocketIOCallback temp = (__brigde SocketIOCallback)pointer;

或"指向未上报的":

SocketIOCallback __unsafe_unretained pointer;
[invocation getArgument:&pointer atIndex:4];
SocketIOCallback temp = pointer;

然后再分配回强引用。 (或者在后一种情况下,如果你小心的话,可以直接使用未保留的参考文献)