资讯动态

【OC】NSTimer

发布时间:2026/10/27 13:09:55 来源:尧图企业网站定制
NSTimer文章目录NSTimer为什么定时器停不下来NSTimer 基础RunLoopNSTimer 的循环引用实战无限轮播图的完整 Timer 方案为什么定时器停不下来我曾经做过一个定时器按钮但是里面有这样一个问题连续点击几次「开始」按钮之后点「结束」按钮怎么都停不下来我们先来看一下代码-(void)startTimer{self.timer[NSTimer scheduledTimerWithTimeInterval:1.0target:selfselector:selector(tick)userInfo:nil repeats:YES];}-(void)stopTimer{[self.timer invalidate];self.timernil;}根本原因是每次点击「开始」都会创建一个新的 timer 并加入 RunLoop但self.timer只保存最后一个引用前面创建的 timer 失去了引用但是它还活着还在 RunLoop 里继续跑invalidate只能停掉最后一个之前的全部失控直观一点的看就是连续点击开始按钮就会变成un loop 里有 timerA、timerB、timerCself.timerView 只指向 timerC这时点击“停止”只能停掉 timerC前面的 timerA、timerB 还在运行解决方案就是开始之前先判断是否已经存在-(void)startTimer{if(self.timerself.timer.isValid)return;// 已经有在跑的 timer 就直接返回不重复创建self.timer[NSTimer scheduledTimerWithTimeInterval:1.0target:selfselector:selector(tick)userInfo:nil repeats:YES];}这个 Bug 背后隐藏了 NSTimer 最重要的两个知识点timer 依赖 RunLoop 运行timer 的生命周期管理NSTimer 基础NSTimer 是一个注册在 RunLoop 上的时间源本质上就是告诉系统每隔多少秒帮我调一次这个方法两种创建方式// 方式一自动加入当前 RunLoop 的 DefaultMode最常用[NSTimer scheduledTimerWithTimeInterval:1.0target:selfselector:selector(tick)userInfo:nil repeats:YES];// 方式二手动创建手动加入 RunLoop可以指定 ModeNSTimer*timer[NSTimer timerWithTimeInterval:1.0target:selfselector:selector(tick)userInfo:nil repeats:YES];[[NSRunLoop mainRunLoop]addTimer:timer forMode:NSRunLoopCommonModes];repeats 参数repeats:YES// 重复触发需要手动 invalidate 停止repeats:NO// 只触发一次触发后自动从 RunLoop 移除并释放timer 的生命周期创建 → 加入 RunLoop → 触发回调 → invalidate → 从 RunLoop 移除 → 释放invalidate之后 timer 永久失效不能重新激活要重新用必须重新创建invalidate和self.timerView nilself.timerView nil只是把“我们手里的引用”清掉 代表 timer 停了 如果还有别的强引用timer 仍然活着但是[timer invalidate]让 timer 失效 是从 run loop 中移除 之后不再触发回调 如果没有别的强引用timer 才会释放所以即使你丢了对旧 timer 的引用旧 timer 也可能还活着因为run loop 持有 timer timer 持有 target:self 可能形成这种关系RunLoop → NSTimer → ViewController所以问题不只是“停不下来”还可能是 旧 timer 无法释放 ViewController 也可能因为被 timer 持有而不能释放时间不精确NSTimer 不是严格精确的RunLoop 每跑一圈才检查一次 timer 有没有到时间如果这一圈处理了耗时任务timer 会延迟触发所以 NSTimer 适合做轮播这种对精度要求不高的场景不适合做精确计时RunLoopRunLoop 是一个事件循环本质是一个 while 循环while(有事件){处理事件// 会检查timer有没有到时间有没有触摸事件有没有网络回调// 如果某件事处理很耗时间这一个循环就会很耗时间// timer 到点了但还没到检查的时机就会延迟触发}// 没事件就休眠有事件就唤醒没有RunLoop的话程序执行完main函数就直接退出了RunLoop 让程序一直活着等待触摸事件、定时器、网络回调这些事件进来RunLoop 的五种模式RunLoop 同一时间只能跑在一种模式下这种模式下只处理注册在这个模式里的事件源模式说明NSDefaultRunLoopMode默认模式空闲时跑这个UITrackingRunLoopMode手指滑动时切换到这个NSRunLoopCommonModes伪模式同时标记 Default 和 TrackingUIInitializationRunLoopMode启动时用启动完切回 DefaultGSEventReceiveRunLoopMode系统内部使用滑动时定时器为什么暂停scheduledTimerWithTimeInterval默认把 timer 加在NSDefaultRunLoopMode里// 正常状态RunLoop 跑在 DefaultMode → timer 正常触发// 手指开始滑动RunLoop 切换到 TrackingMode → timer 只注册在 DefaultMode 里 → 不触发RunLoop 只处理 TrackingMode 里的事件 → 定时器暂停// 手指滑动时系统需要以极高的频率处理触摸事件保证滑动流畅不卡顿如果这时候还要同时处理 DefaultMode 里的所有事件网络回调、timer、各种通知会抢占资源导致滑动卡顿// 所以系统把滑动单独放在 TrackingMode 里切换过去之后只专心处理触摸事件其他事情都先放下保证滑动帧率// 手指松开后系统检测到触摸结束RunLoop 从 TrackingMode 切回 DefaultModetimer 重新在当前模式下被检测到继续正常触发轮播就恢复了这就是无限轮播图在手动滑动时自动轮播会暂停一停下来又继续的原因CommonModes 解决方案NSRunLoopCommonModes不是真正的模式是一个标记把 timer 同时注册到 Default 和 Tracking 两个模式里不管 RunLoop 跑在哪个模式都会触发NSTimer*timer[NSTimer timerWithTimeInterval:2.0target:selfselector:selector(nextToPage)userInfo:nil repeats:YES];[[NSRunLoop mainRunLoop]addTimer:timer forMode:NSRunLoopCommonModes];self.timertimer;NSTimer 的循环引用为什么会循环引用property(nonatomic,strong)NSTimer*timer;// self 强持有 timerself.timer[NSTimer scheduledTimerWithTimeInterval:1.0target:self// timer 强持有 selfselector:selector(tick)userInfo:nil repeats:YES];引用链两个互相强持有引用计数永远不为 0dealloc永远走不到timer 永远不释放内存泄漏。为什么 weakSelf 无效很多人第一反应是用 weakSelf__weaktypeof(self)weakSelfself;self.timer[NSTimer scheduledTimerWithTimeInterval:1.0target:weakSelf// 以为这样就行了...];但这是无效的weakSelf只是一个弱引用变量但把它传给target:的那一刻NSTimer 内部会对这个对象做一次retain变成强引用弱引用在传递的过程中就失效了target:weakSelf和target:self对 NSTimer 来说没有区别都是强持有解决方案一TimerProxy 中间对象核心思路是让 timer 持有一个中间对象 proxyproxy 弱引用 self把循环打断interfaceTimerProxy:NSObjectproperty(nonatomic,weak)id target;(instancetype)proxyWithTarget:(id)target;endimplementationTimerProxy(instancetype)proxyWithTarget:(id)target{TimerProxy*proxy[[TimerProxy alloc]init];proxy.targettarget;returnproxy;}-(void)nextToPage{// target 是 weakself 释放后 target 变 nil// [nil nextToPage] 在 OC 里不会崩溃直接忽略[self.target performSelector:selector(nextToPage)];}end// ViewController.m-(void)startTimer{TimerProxy*proxy[TimerProxy proxyWithTarget:self];self.timer[NSTimer scheduledTimerWithTimeInterval:1.0target:proxy selector:selector(nextToPage)userInfo:nil repeats:YES];}用户离开页面时self引用计数归零没有对象强持有self调用dealloc去销毁定时器timer释放然后proxy释放由于proxy.target本来就是weakself释放时自动变nil⚠️但是这只是一个示范展示一下思路因为它只适用于nextToPage一个函数有很大的局限性解决方案二NSProxy推荐苹果提供了专门用于消息转发的基类NSProxy比手写 TimerProxy 更通用不需要手动写每个转发方法interfaceWeakProxy:NSProxyproperty(nonatomic,weak)id target;(instancetype)proxyWithTarget:(id)target;endimplementationWeakProxy(instancetype)proxyWithTarget:(id)target{WeakProxy*proxy[WeakProxy alloc];// NSProxy 没有 init直接 allocproxy.targettarget;returnproxy;}// 所有发给 proxy 的消息自动转发给 target不需要手动写每个方法// 告诉系统这个方法的签名是什么样的// 假如系统收到消息proxy nextToPage-系统问proxy你认得 nextToPage 方法吗-proxy 说我不认识但我的 target 可能认识 - 系统调用 methodSignatureForSelector:-proxy 向 target 问方法签名-target 提供签名-系统拿到签名准备转发-(NSMethodSignature*)methodSignatureForSelector:(SEL)sel{return[self.target methodSignatureForSelector:sel];}// 真正执行消息转发-(void)forwardInvocation:(NSInvocation*)invocation{if(self.target){[invocation invokeWithTarget:self.target];}}end// ViewController.m-(void)viewDidLoad{[superviewDidLoad];// 创建 proxyWeakProxy*proxy[WeakProxy proxyWithTarget:self];self.timer1[NSTimer scheduledTimerWithTimeInterval:1.0target:proxy selector:selector(nextToPage)userInfo:nil repeats:YES];}NSProxy 的优势是通用性任何方法都不需要额外处理自动转发给 target解决方案三block我们直接使用另外一个函数创建NSTimer// iOS 10 的 Block 方式 timer 不会循环引用-(void)startTimer{__weaktypeof(self)weakSelfself;// 之前那个方法的timer 的 target 会强引用self// 但是在 block 内部使用 weakSelfblock捕获的就会是弱引用的selfself.timer[NSTimer scheduledTimerWithTimeInterval:1.0repeats:YES block:^(NSTimer*_Nonnull timer){// __strong typeof(weakSelf) strongSelf weakSelf;// 也可以加入 strongSelf 保证执行安全[weakSelf updateTime];}];}实战无限轮播图的完整 Timer 方案结合前面所有知识点把三个问题一次性解决-(void)startAutoScroll{// 防止重复创建if(self.timerself.timer.isValid)return;// 用 NSProxy 解决循环引用WeakProxy*proxy[WeakProxy proxyWithTarget:self];// 手动加入 RunLoop指定 CommonModes 防止滑动时暂停self.timer[NSTimer timerWithTimeInterval:2.0target:proxy selector:selector(nextToPage)userInfo:nil repeats:YES];[[NSRunLoop mainRunLoop]addTimer:self.timer forMode:NSRunLoopCommonModes];}-(void)stopAutoScroll{[self.timer invalidate];self.timernil;}-(void)dealloc{[selfstopAutoScroll];}但其实对于用户滑动的这个情况如果不使用NSRunLoopCommonModes用户滑动的时候自动轮播暂停是ok的但是停止滑动后由于过了几秒定时器重新启动会把之前错过的事件补上所以就会出现很快滑动几张的情况如果使用NSRunLoopCommonModes用户滑动和自动轮播同时存在虽然时间上不太会重叠但是用户可能在慢慢滑的时候定时器起作用突然跳到下一页也会影响用户体验最好的处理办法是在用户滑动的时候停止定时器检测到用户一段时间没有动作再重启定时器-(void)restartAutoScrollAfterDelay{// NSObject 的方法用来取消之前用 performSelector:afterDelay: 延迟调用的方法[NSObject cancelPreviousPerformRequestsWithTarget:selfselector:selector(startAutoScroll)object:nil];[selfperformSelector:selector(startAutoScroll)withObject:nil afterDelay:2.0];}-(void)viewWillAppear:(BOOL)animated{[superviewWillAppear:animated];[selfstartAutoScroll];}-(void)viewWillDisappear:(BOOL)animated{[superviewWillDisappear:animated];[selfstopAutoScroll];[NSObject cancelPreviousPerformRequestsWithTarget:selfselector:selector(startAutoScroll)object:nil];}-(void)scrollViewWillBeginDragging:(UIScrollView*)scrollView{[selfstopAutoScroll];[NSObject cancelPreviousPerformRequestsWithTarget:selfselector:selector(startAutoScroll)object:nil];}-(void)scrollViewDidEndDragging:(UIScrollView*)scrollView willDecelerate:(BOOL)decelerate{// decelerate 表示手指松开后 scrollView 是否还会继续减速滑动if(!decelerate){// decelerate NO松手后直接停了这里重启自动轮播[selfrestartAutoScrollAfterDelay];}}

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑