简体   繁体   中英

Objective C Block Not Equal to Its Own Copy

In my app, I associate an NSTimer with a block passed to a method; the block is also added to a an array of blocks. When the timer fires, its associated block is called and should be removed from the array. So my setup looks like this:

@interface MyObject : NSObject

@property(strong, nonatomic) NSMutableArray *allBlocks;

- (void)myMethodWithBlock:(void(^)(void))block;
- (void)timerFired:(NSTimer *)timer;

@end

@implementation MyObject

- (id)init
{
    self = [super init];
    if (self)
    {
        self.allBlocks = [NSMutableArray array];
    }
    return self;
}

- (void)myMethodWithBlock:(void(^)(void))block
{
    [NSTimer scheduledTimerWithTimeInterval:5.0f
                                     target:self
                                   selector:@selector(timerFired:)
                                   userInfo:block
                                    repeats:NO];
    [self.allBlocks addObject:block];
}

- (void)timerFired:(NSTimer *)timer
{
    void(^block)(void) = timer.userInfo;
    [self.allBlocks removeObject:block];
    block();
}

@end

My problem is that when timerFired: is called, the block is (sometimes) not removed. Why?

The problem here is that NSTimer copies the block assigned to userInfo , but the block passed to myMethodWithBlock: is probably an instance of NSStackBlock , which is not equal to its copies .

Let's consider three scenarios, where myObject is an instance of MyObject :

// A
void(^myBlock)(void) = ^{
    NSLog(@"1");
};
[myObject myMethodWithBlock:myBlock];

// B
int one = 1;
void(^myBlock)(void) = ^{
    NSLog(@"%d", one);
};
[myObject myMethodWithBlock:myBlock];

// C
int one = 1;
[myObject myMethodWithBlock:^{
    NSLog(@"%d", one);
};];
  • In A , the block captures no variables from its context; the block will be an instance of NSGlobalBlock , which simply returns itself when copied.
  • In B , the block captures the variable one ; it will be an instance of NSMallocBlock , which also returns itself when copied.
  • In C , the block again captures the variable one , but is also not assigned to a variable before being passed to myMethodWithBlock: . In this case, the block is an instance of NSStackBlock , which returns an instance of NSMallocBlock when copied.

The result of this is that in a situation like scenario C, the NSStackBlock will be added to allBlocks , while an NSMallocBlock will be assigned to the timer's userInfo . When the timer fires, removeObject: does nothing since the block assigned to the timer isn't equal to any of the blocks in the array.

The solution is to always copy the block before storing it in the array. This way, the same block instance will be stored in the array and assigned to the timer:

- (void)myMethodWithBlock:(void(^)(void))block
{
    block = [block copy];
    [NSTimer scheduledTimerWithTimeInterval:5.0f
                                     target:self
                                   selector:@selector(timerFired:)
                                   userInfo:block
                                    repeats:NO];
    [self.allBlocks addObject:block];
}

A clearer approach is to mark the blocks with something whose isEqual: behavior is well known and more readable, like an NSNumber ...

// keep state so these can be made unique
@property(nonatomic, assign) NSInteger blockIndex;
// change the blocks collection to record blocks' associations with numbers
@property(nonatomic, strong) NSMutableDictionary *allBlocks;

// in myMethod...
NSNumber *nextIndex = [NSNumber numberWithInt:++self.blockIndex];
self.allBlocks[nextIndex] = block;

// pass userInfo:nextIndex when you schedule the timer

Now the timer context never has a block, copied or otherwise. Then, when the timer fires...

- (void)timerFired:(NSTimer *)timer {

    NSNumber *index = timer.userInfo;

    void(^block)(void) = self.allBlocks[index]; 
    [self.allBlocks removeObjectForKey:index];

    block();
}

I think it will be safest to compare apples to apples.

- (void)myMethodWithBlock:(void(^)(void))block
{
    NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:5.0f
                                                      target:self
                                                    selector:@selector(timerFired:)
                                                    userInfo:block
                                                     repeats:NO];
    [self.allBlocks addObject:timer.userInfo];
}

- (void)timerFired:(NSTimer *)timer
{
    [self.allBlocks removeObject:timer.userInfo];

    void(^block)(void) = timer.userInfo;
    block();
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM