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
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,491评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,856评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,745评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,196评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,073评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,112评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,531评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,215评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,485评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,578评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,356评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,215评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,583评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,898评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,174评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,497评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,697评论 2 335

推荐阅读更多精彩内容