Why is my object's weak delegate property nil

2020-02-26 06:09发布

问题:

I have a pretty simple setup for this unit test. I have a class that has a delegate property:

@interface MyClass : NSObject
...
@property (nonatomic, weak) id<MyDelegateProtocol> connectionDelegate;
...
@end

and I set the delegate in my test:

- (void)testMyMethod_WithDelegate {
  id delegate = mockDelegateHelper(); // uses OCMock to create a mock object
  [[delegate expect] someMethod];
  myClassIvar.connectionDelegate = delegate;
  [myClass someOtherMethod];
  STAssertNoThrow([delegate verify], @"should have called someMethod on delegate.");
}

But the delegate is not actually set on line 3 of my unit test, so #someMethod is never called. When I change it to

myClassIvar.connectionDelegate = delegate;
STAssertNotNil(myClassIvar.connectionDelegate, @"delegate should not be nil");

it fails there. I'm using ARC, so my hunch was that the weak property was being deallocated. Sure enough, changing it to strong makes the STAssertNotNil pass. But I don't want to do that with a delegate, and I don't understand why that makes a difference here. From what I've read, all local references in ARC are strong, and STAssertNotNil(delegate) passes. Why is my weak delegate property nil when the same object in a local variable is not?

回答1:

This is a bug in the iOS runtime. The following discussion has more detail. In a nutshell, the iOS ARC runtime can't seem to handle weak references to proxies. The OSX runtime can.

http://www.mulle-kybernetik.com/forum/viewtopic.php?f=4&t=252

As far as I understand from the discussion a bug report has been filed with Apple. If anyone has a sensible idea for a workaround...



回答2:

I don't really know what's happening here, but OCMock returns an autoreleased NSProxy-descendant from the mockForProtocol: method, which I think is right. Maybe ARC has problems with NSProxies? Anyway, I've overcome this problem by declaring the variable __weak:

- (void)testMyMethod_WithDelegate {
  // maybe you'll also need this modifier inside the helper
  __weak id delegate = mockDelegateHelper(); 
  ...

It really doesn't need to be __strong (the default) in this case, as it's autoreleased and you're not keeping it around...



回答3:

A workaround is to use Partial Mocks.

@interface TestMyDelegateProtocolDelegate : NSObject <MyDelegateProtocol>
@end

@implementation TestMyDelegateProtocolDelegate
- (void)someMethod {}
@end


@implementation SomeTest {
- (void)testMyMethod_WithDelegate {
  id<MyDelegateProtocol> delegate = [[TestMyDelegateProtocolDelegate] alloc] init];
  id delegateMock = [OCMockObject partialMockForObject:delegate]
  [[[delegateMock expect] someMethod]
  myClassIvar.connectionDelegate = delegate;
  [myClass someOtherMethod];
  STAssertNoThrow([delegate verify], @"should have called someMethod on delegate.");
}
@end


回答4:

I am no ARC expert but my guess is that mockDelegateHelper() is returning a weak object. As a result delegate is nil before the second line of code executes. I would venture to guess that either the mockDelegateHelper() is the culprit or that OCMock is getting in the way with how it manipulates and creates objects.