NSTimer Category + Blocks implementation to replace selector

江枫思渺然 提交于 2019-12-01 00:05:28

Your major flaw besides the wrong target is your use of a static variable. You won't be able to support beyond a single timer.

Using block as argument to the invoked method.

@interface NSTimer (AdditionsPrivate) // Private stuff
- (void)theBlock:(VoidBlock)voidBlock;
@end


@implementation NSTimer (NSTimer_Additions)

+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)theSeconds repeats:(BOOL)repeats actions:(VoidBlock)actions {
    NSInvocation * invocation = [NSInvocation invocationWithMethodSignature:[self instanceMethodSignatureForSelector:@selector(theBlock:)]];
    NSTimer * timer = [NSTimer scheduledTimerWithTimeInterval:theSeconds
                                                   invocation:invocation
                                                      repeats:repeats];
    [invocation setTarget:timer];
    [invocation setSelector:@selector(theBlock:)];

    Block_copy(actions);
    [invocation setArgument:&actions atIndex:2];
    Block_release(actions);

    return timer;
}


- (void)theBlock:(VoidBlock)voidBlock {
    voidBlock();
}

@end

The problem with using associative references was the leak as there was no good point to release the block.


Earlier Approach using associative references

You can use associative references to attach the block to that particular instance of NSTimer.

@implementation NSTimer (NSTimer_Additions)

+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)theSeconds repeats:(BOOL)repeats actions:(VoidBlock)actions {
    NSInvocation * invocation = [NSInvocation invocationWithMethodSignature:[self instanceMethodSignatureForSelector:@selector(theBlock)]];
    NSTimer * timer = [NSTimer scheduledTimerWithTimeInterval:theSeconds
                                                   invocation:invocation
                                                      repeats:repeats];
    [invocation setTarget:timer];
    [invocation setSelector:@selector(theBlock)];

    objc_setAssociatedObject(timer, @"Block", actions, OBJC_ASSOCIATION_COPY);

    return timer;
}


- (void)theBlock {
    VoidBlock _voidBlock = (VoidBlock)objc_getAssociatedObject(self, @"Block");
    _voidBlock();
}

@end

What about leveraging userInfo to carry your block? (this is done with ARC)

void (^callback)(void) = ^{
    NSLog(@"do stuff");
}

NSTimer *timer = [NSTimer timerWithTimeInterval:10.0 target:self selector:@selector(handleTimeout:) userInfo:[NSDictionary dictionaryWithObject:[callback copy] forKey:@"block"] repeats:NO];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];

And then add the static selector of:

+ (void)handleTimeout:(NSTimer *)timer
{
    void (^callback)(void) = [timer.userInfo objectForKey:@"block"];
    callback();

    [timer invalidate];
    timer = nil;
};

This should work:

NSTimer* timer = [[NSTimer alloc] initWithFireDate:[NSDate date] 
                                          interval:theSeconds
                                            target:timer
                                          selector:@selector(theBlock) 
                                          userInfo:nil 
                                           repeats:repeats];

The problem is that you're setting the target of the new NSTimer instance to be self. However, in the context of + scheduleTimerWithTimeInterval:repeats:actions: (notice the +), self is NSTimer, and not (as you probably thought) your newly-created NSTimer instance.

As you can see from the error message, your app is crashing because NSTimer doesn't respond to the class method + theBlock, which is of course correct since you only defined the instance method - theBlock.

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!