NSTimer的坑

定时器陷阱与解决方案

写在前面-------给大家推荐一个不错的招聘网站  www.joblai.com

还有一篇文章也不错,请大家看看

昨天下午工作的时候遇见一个这样的需求,网络请求失败后把请求数据保存到本地,并自动重发3次,时间间隔是10秒,如果3次后还失败的话,下一次启动这个接口的时候,把新数据和保存在本地的数据都要发送,刚开始以为没多少难度,不就是网络请求发送数据嘛,首先脑子里的第一反应就是用定时器,初始化定时器,然后触发相应的方法,设置请求的次数标志,超过3次停止定时器。事实却证明我还没有理解定时器......

  由于是老接口,不能修改,因为产品已经上线,修改会涉及到太多业务,所以只能客户端想办法处理。这样导致的问题就是新数据不能和旧数据一起整合在一起发送,得分两次发送。好吧,那就上吧,我就信心满满的上了。

     初始化定时器,遍历本地的数据,分别对应创建一个定时器使用下面的方法,加载到定时器数组

+ (NSTimer *)timerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(id)userInfo repeats:(BOOL)yesOrNo;

然后fire执行。OK,搞定。

bi..bi...bi...bi....bi.....bi......

  擦,定时器全乱了,10s内定时器没啥问题,10s后所以定时器都交替进行。。。这不是坑爹么。。。。

  吸了口气,喝了一杯水,扫了一眼定时器的代码,灵光一闪,会不会是fire用错了,初始化的时候不要立即执行,等初始化完毕的时候在从数组里面拿出定时器,请求成功或者失败三次后再拿出第二个定时器请求。哈哈哈哈哈哈,应该不会错了,就这么办。

bi....bi.....bi.....bi....bi........

  我了个去,稍微好一点了,20秒内的数据是正常的,后面的定时器又交替进行。。。。泥煤呀,甘都得。。。不过已经有进步了,至少20秒是正确的吧,再改改代码应该就可以了,所以立马想一下定时器的执行流程,后来发现会不会是多个定时器和一个定时器的运行是有区别的?因为自己之前基本上都是创建一个定时器就可以了,fire、invalidate使用。没办法,上SOF看看吧。后来才知道原来这两个方法初始化的定时器即使不用fire也会对应的NSTimeInterval后执行,fire只是让他们立即执行,把启动的时间提前到当前,就像一个演唱会本来打算10分钟后开始的,现在因为主唱提前10分钟到了会场,看见粉丝这么热情,提前开始了。

+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)ti invocation:(NSInvocation *)invocation repeats:(BOOL)yesOrNo;

+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(id)userInfo repeats:(BOOL)yesOrNo;

  可是问题又来了,那既然这样没办法控制定时器的执行,我这个功能岂不是没法做了,有没有什么办法可以控制定时器么,想执行的时候就执行,不想执行的时候就丢掉它。。。。

  查找资料的过程中还发现了几个初始化定时器的方法:两个类方法,一个实例方法。

复制代码
+ (NSTimer *)timerWithTimeInterval:(NSTimeInterval)ti invocation:(NSInvocation *)invocation repeats:(BOOL)yesOrNo;

+ (NSTimer *)timerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(id)userInfo repeats:(BOOL)yesOrNo;

- (id)initWithFireDate:(NSDate *)date interval:(NSTimeInterval)ti target:(id)t selector:(SEL)s userInfo:(id)ui repeats:(BOOL)rep;
复制代码

  这和上面的初始化方法有什么区别么,接着发现者两个类方法和实例方法是要手动添加到NSRunLoop代码执行的:

[[NSRunLoop currentRunLoop]addTimer:timer forMode:NSDefaultRunLoopMode];

哈哈哈,这不就是我想要的东东嘛(ˇˍˇ),yo yo check now!

修改定时器的方法,手动添加NSRunLoop执行,然后网络请求不变。。。。OK,搞定。。

bi..bi.....bi....bi......

无压力了。。。。测试一个for循环1000次,没发生什么错误。。。好吧,来个总结。

一直都习惯用最上面的两个方法初始化定时器,然后fire,并且fire的作用只是把定时器的时间提前了,这个是之前使用的时候没有去考虑的。。这种东东在一个定时器下面不会有什么问题,但是多个定时器的话基本上就悲剧。。不过在同一个地方使用多个定时器这样的设计方法我暂时也不知道合理不合理,可能也会有意想不到的的错误,比如内存暴涨,性能受影响之类的,这个暂时没有去考虑,如果你有更好的解决方法,可以交流交流。

// services/LocationService.uts import type { FormattedLocation, LocationSuccessResult } from '@/utils/location'; export class LocationService { private static instance: LocationService | null = null; private isRunning: boolean = false; private intervalMs: number = 3000; // 回调函数数组(支持多个监听者) private listeners: ((loc: FormattedLocation) => void)[] = []; public static getInstance(): LocationService { if (!this.instance) { this.instance = new LocationService(); } return this.instance; } start(): void { if (this.isRunning) { console.log("定位服务已在运行"); return; } this.isRunning = true; console.log("【LocationService】启动原生级定位循环"); // 使用原生循环(Android Handler / iOS NSTimer) this.nativeLoop(); } stop(): void { this.isRunning = false; this.nativeClear(); console.log("【LocationService】已停止"); } addListener(callback: (loc: FormattedLocation) => void): void { this.listeners.push(callback); } removeListener(callback: (loc: FormattedLocation) => void): void { const index = this.listeners.indexOf(callback); if (index !== -1) { this.listeners.splice(index, 1); } } private nativeLoop(): void { // 模拟原生循环(实际应使用平台特定实现) this.doLocation(); if (this.isRunning) { setTimeout(() => { this.nativeLoop(); // 递归调用,但需注意黑屏仍可能暂停 }, this.intervalMs); } } private nativeClear(): void { // 清理原生定时器(此处省略具体实现) } private doLocation(): void { uni.getLocation({ type: 'wgs84', success: (res: LocationSuccessResult) => { const formatted: FormattedLocation = { lat: res.latitude, lng: res.longitude, spd: res.speed ?? 0, acc: res.accuracy ?? 999, alt: res.altitude ?? 0 }; console.log(`📍 定位成功: ${formatted.lat.toFixed(6)}, ${formatted.lng.toFixed(6)}`); // 通知所有监听者 for (const cb of this.listeners) { try { cb(formatted); } catch (e) { console.error("回调执行出错", e); } } }, fail: (err: any) => { } }); } } 上方代码报错15:01:30.748 [plugin:uni:app-uts] 编译失败 15:01:30.748 ‌⁠error: 参数类型不匹配:实际类型为 'Function1<LocationSuccessResult, Unit>',预期类型为 'Function1<@ParameterName(...) GetLocationSuccess, Unit>?'。错误详情链接: https://doc.dcloud.net.cn/uni-app-x/uts/compiler-known-issues.html#error17‌ 15:01:30.748 at utils/LocationService.uts:66:15 15:01:30.748 64 | uni.getLocation({ 15:01:30.748 65 | type: 'wgs84', 15:01:30.748 66 | success: (res: LocationSuccessResult) => { 15:01:30.748 | ^ 15:01:30.748 67 | const formatted: FormattedLocation = { 15:01:30.748 68 | lat: res.latitude,⁠ 15:01:30.748 找不到名称“not”。参考: https://doc.dcloud.net.cn/uni-app-x/uts/compiler-known-issues.html#error18 15:01:30.748 at utils/LocationService.uts:13:9 15:01:30.748 11 | 15:01:30.748 12 | public static getInstance(): LocationService { 15:01:30.748 13 | if (!this.instance) { 15:01:30.748 | ^ 15:01:30.748 14 | this.instance = new LocationService(); 15:01:30.749 15 | }⁠ 15:01:30.795 代码编译报错
最新发布
11-25
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值