iOS - 关于NSTimer的循环引用
ChaMuriel
8年前
<h2>现象</h2> <p>在当前控制器(ViewController)的view上添加了一个自定义的view(LXFTimerView),</p> <p>LXFTimerView在成功创建出来后添加了定时器NSTimer并加入RunLoop开始工作,</p> <p>当在当前控制器里将LXFTimerView移除掉后,定时器还在工作,而且LXFTimerView里的dealloc并没有调用</p> <p><img src="https://simg.open-open.com/show/c71207dd9f172d082eb2b800184f5098.gif"></p> <p>现象</p> <h2>代码</h2> <p>LXFTimerView.m</p> <pre> <code class="language-objectivec">#import "LXFTimerView.h" @interface LXFTimerView() /** 定时器 */ @property(nonatomic, weak) NSTimer *timer; @end @implementation LXFTimerView - (instancetype)initWithFrame:(CGRect)frame { if (self = [super initWithFrame:frame]) { [self addTimer]; } return self; } - (void)dealloc { NSLog(@"LXFTimerView - dealloc"); [self removeTimer]; } #pragma mark - 定时器方法 /** 添加定时器方法 */ - (void)addTimer { // 创建定时器 if (self.timer) { return; } self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(log) userInfo:nil repeats:YES]; [[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSRunLoopCommonModes]; } /** 移除定时器 */ - (void)removeTimer { [self.timer invalidate]; self.timer = nil; } - (void)log { NSLog(@"定时器 -- %s", __func__); } @end</code></pre> <p>ViewController.m</p> <pre> <code class="language-objectivec">#import "ViewController.h" #import "LXFTimerView.h" @interface ViewController () /** timerView */ @property(nonatomic, weak) LXFTimerView *timerView; @end @implementation ViewController - (void)viewDidLoad { [super viewDidLoad]; LXFTimerView *timerView = [[LXFTimerView alloc] initWithFrame:CGRectMake(0, 0, self.view.bounds.size.width, 200)]; timerView.backgroundColor = [UIColor orangeColor]; self.timerView = timerView; [self.view addSubview:timerView]; } - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event { [self.timerView removeFromSuperview]; } @end</code></pre> <h2>引用关系</h2> <p><img src="https://simg.open-open.com/show/46e60c86e4aeb0722879f18be5b5f342.png"></p> <p>引用关系</p> <p>问题就出在LXFTimerView与NSTimer之间,在创建定时器时执行</p> <pre> <code class="language-objectivec">[NSTimer scheduledTimerWithTimeInterval: target: selector: userInfo: repeats:];</code></pre> <p>会将LXFTimerView进行强引用,什么?我怎么知道?看下图</p> <p><img src="https://simg.open-open.com/show/e414e65084caab79f1f6239b6c338866.png"></p> <p>NSTimer</p> <p>翻译:定时器保持着对target的强引用,直到定时器作废</p> <p>那为什么LXFTimerView中的timer属性要用weak?? 不用着急,下面即将揭晓~</p> <h2>解决方案</h2> <p>让定时器指着另一个对象,让那个对象来执行LXFTimerView中需要执行的方法。</p> <p>引用关系如下图所示</p> <p><img src="https://simg.open-open.com/show/05eafd3308d5c766998016ba23b02bfe.png"></p> <p>LXFWeakTarget</p> <p>创建一个继承于NSObject的类 LXFWeakTarget,并提供一个创建定时器的方法(苹果官方的方法,对scheduledTimerWithTimeInterval进行转到定义操作【就是command+左键】就可以得到)</p> <p>LXFWeakTarget.h</p> <pre> <code class="language-objectivec">#import <Foundation/Foundation.h> @interface LXFWeakTarget : NSObject + (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo; @end</code></pre> <pre> <code class="language-objectivec">#import "LXFWeakTarget.h" @interface LXFWeakTarget() @property(nonatomic, weak) id target; @property(nonatomic, assign) SEL selector; @end @implementation LXFWeakTarget + (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo { // 创建当前类的对象 LXFWeakTarget *object = [[LXFWeakTarget alloc] init]; object.target = aTarget; object.selector = aSelector; return [NSTimer scheduledTimerWithTimeInterval:ti target:object selector:@selector(execute:) userInfo:userInfo repeats:yesOrNo]; } - (void)execute:(id)obj { [self.target performSelector:self.selector withObject:obj]; } @end</code></pre> <p>在LXFTimerView.m中导入LXFWeakTarget的头文件</p> <pre> <code class="language-objectivec">#import "LXFWeakTarget.h"</code></pre> <p>将创建定时器的类改为 LXFWeakTarget</p> <pre> <code class="language-objectivec">self.timer = [LXFWeakTarget scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(log) userInfo:nil repeats:YES];</code></pre> <p>现在再来执行一下程序</p> <p><img src="https://simg.open-open.com/show/06dce838884c04bb28fdd9c85d3b8932.gif"></p> <p>执行dealloc</p> <h2>最后缕下思路</h2> <ul> <li>我们用一个LXFWeakTarget来替LXFTimerView执行一些操作。</li> <li>当没有被定时器强引用的LXFTimerView从父控件上被移除时,就会执行dealloc方法,LXFTimerView被销毁。</li> <li>将定时器作废并设为nil,这样定时器对LXFWeakTarget的引用也没有了,LXFWeakTarget也会被销毁。</li> </ul> <p>好,那“为什么LXFTimerView中的timer属性要用weak”这个问题就不用多加解析了吧。</p> <p> </p> <p>来自:http://www.jianshu.com/p/0c5056b16c6b</p> <p> </p>