NSTimer内存问题分析及优雅使用姿势

NSTimer特别容易出现内存泄露问题,这篇文章会分析一下为什么会出现内存泄露,以及如何优雅的解决这个问题。

NSTimer导致内存问题的原因分析

@interface ViewController ()
@property (nonatomic, strong) NSTimer *timer;
@end

@implementation ViewController
- (void)viewDidLoad
{
    _timer = [NSTimer timerWithTimeInterval:1 target:self selector:@selector(timerAction) userInfo:nil repeats:YES];
    [[NSRunLoop currentRunLoop] addTimer:_timer forMode:NSRunLoopCommonModes];
}

- (void)timerAction {}
@end

这段代码是常见的NSTimer使用方法,然而这段代码会有一个隐患,self无法正常被释放,除非在selfdealloc执行之前调用[_timer invalidate]self才能正常被释放。此时他们在内存中的引用关系如图

timer内存引用关系简图

我这个例子用的strong修饰timer,所以self强引用着timer, 同时timer也会强引用selftimer运行需要添加到当前线程所运行的runloop中,所以runloop也强引用着timer。当self被pop掉的时候,此时self始终被timer强引用着,从而无法在内存中释放,这也是为什么不能在selfdealloc方法中去做[_timer invalidate]的原因。

最笨的解决方法:在dealloc前手动去释放

上面这段代码,已经出现了内存泄露,只要app没被杀死,self会一直在内存中,那么有种最笨的方法就是手动去释放,例如可以在- (void)viewWillDisappear:(BOOL)animated方法中去做[_timer invalidate]操作,执行完invalidate后,此时runloop不在持有timer, timer也不在持有self,即图中的2和3箭头断开,此时没有任何对象引用着self,self的引用计数为0,从而能够在内存中被释放,self被释放后,timer此时也没对象引用着,即1箭头断开,从而timer也在内存中被释放。
当然这种方法也是会受到一定的业务限制的,例如self上继续push一个viewController的时候,runloop不在引用timer, timer不在引用self, 此时selftimer都在内存中,只不过timer不在运行着了。假如业务场景要求push的时候,timer正常工作,只有selfpop的时候才杀掉timer,此时这种处理方式就显得力不从心了。

那么有什么通用方案能让使用者不在担心内存问题吗

根据上面的分析,我们知道,真正导致self无法释放的原因是timer强引用着self,那么如果说timer不强引用self的话,self就能在pop的时候愉快的释放了,开发者也能够在selfdealloc方法中去做[_timer invalidate]操作杀死timer了。

weakSelf解决block循环引用思路影响,我做了如下尝试

@interface ViewController ()
@property (nonatomic, strong) NSTimer *timer;
@end

@implementation ViewController
- (void)viewDidLoad
{
    __weak typeof(self) weakSelf = self;
    _timer = [NSTimer timerWithTimeInterval:1 target:weakSelf selector:@selector(timerAction) userInfo:nil repeats:YES];
    [[NSRunLoop currentRunLoop] addTimer:_timer forMode:NSRunLoopCommonModes];
}

- (void)timerAction {}
@end

这段代码和上面的代码只有self和weakSelf的差别,此时内存中的引用关系如图


timer内存引用关系简图

注意虚线这里表示弱引用,self的引用计数不+1的。当self被pop的时候,由于没有其它对象引用着他,所以能够正常释放,从而能够正常执行selfdealloc方法,此时1箭头断开,在selfdealloc方法中执行[_timer invalidate],此时3和2断开,timer也从内存中被释放。看起来貌似合理,然而这种尝试是失败的。

上面貌似合理的理论其实是不成立的,weakSelfself之间并不是持有关系,weakSelfself指向的是同一片内存,只不过weakSelf指向内存的时候,引用计数没+1。无论传给timer的是weakSelf还是self,对于timer来说,他接收到的消息就是,他要去持有一片内存,这片内存地址是和self或者weakSelf一样的,至于是强持有还是弱持有,完全由他自己决定,而不是由传进去的变量是weak还是strong决定的。这里是有区别于block的循环引用的。

终极解决姿势

说道终极解决姿势得感谢github开源代码的各路豪杰,我也是在第三方库中看到的解决方案。这里其实就是通过消息转发机制巧妙的解决了这个问题。

@interface ViewController ()
@property (nonatomic, strong) NSTimer *timer;
@end

@implementation ViewController
- (void)viewDidLoad
{
    _timer = [NSTimer timerWithTimeInterval:1 target:[JLWeakProxy proxyWithTarget:self] selector:@selector(timerAction) userInfo:nil repeats:YES];
    [[NSRunLoop currentRunLoop] addTimer:_timer forMode:NSRunLoopCommonModes];
}

- (void)timerAction {}
@end

此时他们在内存中的引用关系如图


timer内存引用关系简图

这里的核心点就在JLWeakProxy上,timer的直接target对象其实是JLWeakProxy, 也就是说,timer其实是通过JLWeakProxy对象去执行selector方法的,只不过在JLWeakProxy内部,弱引用着self, 并且会把消息转发给self去执行selector方法。
从内存引用简图可以看出,popself的时候,self能够正常释放,从而dealloc方法能够正常被执行,在dealloc中做[_timer invalidate]操作,此时runloop不在引用timer, timer 也不在引用JLWeakProxy,从而内存也就都被释放了。

关于消息转发这里不做解释说明,感兴趣的可以看看我整理的iOS Runtime: 消息转发,下面贴出JLWeakProxy实现。

@interface JLWeakProxy : NSProxy

@property (nullable, nonatomic, weak, readonly) id target;

- (instancetype)initWithTarget:(id)target;

+ (instancetype)proxyWithTarget:(id)target;

@end

@implementation JLWeakProxy

- (instancetype)initWithTarget:(id)target {
    _target = target;
    return self;
}

+ (instancetype)proxyWithTarget:(id)target {
    return [[JLWeakProxy alloc] initWithTarget:target];
}

- (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)];
}

- (BOOL)respondsToSelector:(SEL)aSelector {
    return [_target respondsToSelector:aSelector];
}

- (BOOL)isEqual:(id)object {
    return [_target isEqual:object];
}

- (NSUInteger)hash {
    return [_target hash];
}

- (Class)superclass {
    return [_target superclass];
}

- (Class)class {
    return [_target class];
}

- (BOOL)isKindOfClass:(Class)aClass {
    return [_target isKindOfClass:aClass];
}

- (BOOL)isMemberOfClass:(Class)aClass {
    return [_target isMemberOfClass:aClass];
}

- (BOOL)conformsToProtocol:(Protocol *)aProtocol {
    return [_target conformsToProtocol:aProtocol];
}

- (BOOL)isProxy {
    return YES;
}

- (NSString *)description {
    return [_target description];
}

- (NSString *)debugDescription {
    return [_target debugDescription];
}

@end
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容