ios开发-解决定时器循环引用

一、NSTimer 的使用方法

系统提供了8个创建方法,6个类创建方法,2个实例化方法。
在这里插入图片描述
通过方法注释可以发现,其中这三个方法在创建后直接将timer添加到了当前runloop default mode,而不需要我们自己操作,当然这个runloop只是当前的runloop,模式是default mode:

+ (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 *)scheduledTimerWithTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block;

其它五种创建,不会自动添加到runloop,还需调用addTimer: forMode 添加到runloop。

-(void)createTimer0 {
    /**定时器会自动启动*/
    _timer = [NSTimer scheduledTimerWithTimeInterval:1.0f target:self selector:@selector(doSomething5) userInfo:nil repeats:YES];
}

-(void)createTimer00 {
    /**定时器不会自动启动,需要添加到RunLoop中*/
    _timer = [NSTimer timerWithTimeInterval:1.0f target:self selector:@selector(doSomething5) userInfo:nil repeats:YES];

}

二、NSTimer 和 VC 造成的循环引用

下面一个普通直接使用

#import "TestViewController.h"

@interface TestViewController ()

@property (nonatomic, strong) NSTimer *timer;

@property (nonatomic, assign) NSInteger  indexNum;

@end

@implementation TestViewController


- (void)viewDidLoad {
    [super viewDidLoad];
    self.indexNum = 0;
    self.timer = [NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(doSomething) userInfo:nil repeats:YES];
    [[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSRunLoopCommonModes];
}

- (void)doSomething{
    NSLog(@" ---当前indexNum: %ld----", (long)self.indexNum ++);
}

- (void)dealloc{
    NSLog(@"************ 走进dealloc ***************");
    [self.timer invalidate];
    self.timer = nil;
   
}
@end

进入页面后, 倒计时触发,定时器指定方法开始执行。 点击返回按钮后, 不会触发 dealloc 方法, 但控制台依然在打印indexNum值,说明内存无法释放。
结论:VC 强持有 timer, 而timer的创建方法中参数target使得timer持有VC,造成循环引用。视图控制器在自动调用dealloc前,会判断有没有未销毁的NSTimer对象,如果有就不调用dealloc方法。

设想:target中的self编程弱引用weakSelf可以吗?答案是不可以,即使设置为弱引用self,delloc依然不会走

weak关键字适用于block,当block引用了块外的变量时,会根据修饰变量的关键字来决定是强引用还是弱引用,如果变量使用weak关键字修饰,那block会对变量进行弱引用,如果没有__weak关键字,那就是强引用。
但是NSTimer的 scheduledTimerWithTimeInterval:target方法内部不会判断修饰target的关键字,所以这里传self 和 weakSelf是没区别的,其内部会对target进行强引用,还是会产生循环引用。

无论如何创建。都会将NSTimer 加入到当前的RunLoop当中。所以RunLoop就持有该timer。即VC和timer相互引用,Runloop同时也引用timer。

三、NSTimer和VC循环引用解决方案:

方案一:手动销毁定时器

1.在viewDidDisappear里面释放定时器

@implementation TestViewController

- (void)viewDidDisappear:(BOOL)animated {
    [super viewDidDisappear:animated];
    [self.timer invalidate];
    self.timer = nil;
}


- (void)viewDidLoad {
    [super viewDidLoad];
    self.indexNum = 0;
    self.timer = [NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(doSomething) userInfo:nil repeats:YES];
}

- (void)doSomething{
    NSLog(@" ---当前indexNum: %ld----", (long)self.indexNum ++);
}

- (void)dealloc{
    NSLog(@"************ 走进dealloc ***************");
   
}
@end

2.在特定的场景,比如点击某一个按钮,释放定时器

方案二:创建一个NSObject对象作为定时器target对象,主要原理:利用forwardingTargetForSelector做消息转发

这个方案的实现方式主要是加入了一个中间者proxy,使得timer不直接持有self,而是持有proxy,让proxy对象弱引用self来解决循环引用。当定时器回调的时候,通过消息转发机制,把消息重定向给self。
代码如下:

@interface MLProxy : NSObject

+ (instancetype)createProxyWeithTarget:(id)target;

- (id)forwardingTargetForSelector:(SEL)aSelector;

@end
#import "MLProxy.h"

@interface MLProxy ()

@property (nonatomic, weak) id target;

@end

@implementation MLProxy

+ (instancetype)createProxyWeithTarget:(id)target{
    MLProxy *proxy = [[self alloc] init];
    proxy.target = target;
    return proxy;
}

//消息重定向
- (id)forwardingTargetForSelector:(SEL)aSelector{
    return self.target;
}


@end
@interface FirstViewController ()


@property (nonatomic,strong)NSTimer *timer;
@property (nonatomic,strong)MLProxy *proxy;
@property (nonatomic, assign) NSInteger  indexNum;
@end

@implementation FirstViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    self.indexNum = 0;
    [self addTimer];
    
}

-(void)addTimer{
    
    self.proxy = [MLProxy createProxyWeithTarget:self];
    // NSTimer
    // 由于iOS的消息机制(objc_msgSend()), 系统会对self.proxy 发送一个scheduledTimerWithTimeInterval的消息由于MLProxyb并没有实现该方法,就会执行Runtime的消息转发机制。
    _timer = [NSTimer scheduledTimerWithTimeInterval:1 target:self.proxy selector:@selector(timerAction) userInfo:nil repeats:YES];
}

- (void)timerAction{
    NSLog(@"走到这里了");
    NSLog(@"self.class=%@",[self class]);
    NSLog(@" ---当前indexNum: %ld----", (long)self.indexNum ++);
}

- (void)dealloc{
    NSLog(@"************ 走进dealloc ***************");
    [_timer invalidate];
    _timer = nil;
}

方案三:NSProxy
NSProxy我理解的其实它就是一个消息重定向封装的一个抽象类,类似一个代理人、中间件。可以通过继承它,并重写下面这两个方法来实现消息转发到另一个实例:

- (void)forwardInvocation:(NSInvocation *)invocation;
- (nullable NSMethodSignature *)methodSignatureForSelector:(SEL)sel

代码如下:

@interface XTProxy : NSProxy

@property (nonatomic, weak) id target;

@end
#import "XTProxy.h"

@interface XTProxy ()

@end

@implementation XTProxy

- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel {
    return [self.target methodSignatureForSelector:sel];
}
- (void)forwardInvocation:(NSInvocation *)invocation {
    [invocation invokeWithTarget:self.target];
}
@interface ThirdViewController ()

@property (nonatomic,strong)XTProxy *proxy;
@property (nonatomic,strong)NSTimer *timer;
@property (nonatomic, assign) NSInteger  indexNum;

@end

@implementation ThirdViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    self.indexNum = 0;
    [self addTimer];
}

-(void)addTimer {
    self.proxy = [XTProxy alloc];
    self.proxy.target = self;
    _timer = [NSTimer scheduledTimerWithTimeInterval:1.0f target:self.proxy selector:@selector(timerAction) userInfo:nil repeats:YES];
    
}

- (void)timerAction{
    NSLog(@"走到这里了");
    NSLog(@"self.class=%@",[self class]);
    NSLog(@" ---当前indexNum: %ld----", (long)self.indexNum ++);
}

- (void)dealloc{
    NSLog(@"************ 走进dealloc ***************");
    [_timer invalidate];
    _timer = nil;
}

方案四:使用Apple提供的API类,但该方法仅限于ios10以上,有很多局限性

+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block API_AVAILABLE(macosx(10.12), ios(10.0), watchos(3.0), tvos(10.0));

代码如下:

@interface FourViewController ()

@property (nonatomic,strong)NSTimer *timer;
@property (nonatomic, assign) NSInteger  indexNum;

@end

@implementation FourViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    self.indexNum = 0;
    [self addTimer];
}

-(void)addTimer {
    __weak typeof(self) weakSelf = self;
    if (@available(iOS 10.0, *)) {
        _timer = [NSTimer scheduledTimerWithTimeInterval:1 repeats:YES block:^(NSTimer * _Nonnull timer) {
            [weakSelf timerAction];
        }];
    }else {
        NSLog(@"该方法不支持");
    }
    
}

- (void)timerAction{
    NSLog(@"走到这里了");
    NSLog(@"self.class=%@",[self class]);
    NSLog(@" ---当前indexNum: %ld----", (long)self.indexNum ++);
}

- (void)dealloc{
    NSLog(@"************ 走进dealloc ***************");
    [_timer invalidate];
    _timer = nil;
}


@end

四、NSTimer不准确的问题探究及解决:

NSTimer不准确原因:
1、NSTimer定时器依赖于RunLoop,我们知道RunLoop每循环一次的时间是基于任务量的,即每一次的时间都不一定相同,如果RunLoop的任务过于繁重,可能会导致NSTimer不准时。
2、模式的切换,当创建的timer被加入到NSDefaultRunLoopMode时,此时如果有滑动UIScrollView的操作,runLoop 的mode会切换为TrackingRunLoopMode,而MainRunLoop处于 UITrackingRunLoopMode 的模式下,是不会处理 NSDefaultRunLoopMode 的消息(因为它们的RunLoop Mode不一样),所以timer会暂时停止;

对于精度要求不高的场景我们使用NSTimer没太大影响,对于上面2的滑动UIScrollView的导致定时器停止的问题下面一行代码即可解决:
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];

解决方案:
使用GCD定时器,因为GCD定时器是基于系统内核的,所以不会受其他因素影响。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
iOS开发中上传图片可以采用以下步骤: 1.选择要上传的图片,可以使用系统提供的UIImagePickerController控制器,或者使用第三方库,例如TZImagePickerController。 2.将选中的图片转换为NSData格式。 3.使用NSURLSession或AFNetworking等网络库,将图片数据上传到服务器。 以下是一个简单的上传图片的示例代码: ``` // 选择图片 UIImagePickerController *imagePicker = [[UIImagePickerController alloc] init]; imagePicker.sourceType = UIImagePickerControllerSourceTypePhotoLibrary; imagePicker.delegate = self; [self presentViewController:imagePicker animated:YES completion:nil]; // 将选中的图片转换为NSData格式 - (void)imagePickerController:(UIImagePickerController *)picker didFinishPickingMediaWithInfo:(NSDictionary<UIImagePickerControllerInfoKey,id> *)info { UIImage *selectedImage = info[UIImagePickerControllerOriginalImage]; NSData *imageData = UIImageJPEGRepresentation(selectedImage, 0.5); // 上传图片到服务器 NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration defaultSessionConfiguration]; NSURLSession *session = [NSURLSession sessionWithConfiguration:configuration]; NSURL *url = [NSURL URLWithString:@"http://example.com/upload.php"]; NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url]; request.HTTPMethod = @"POST"; NSURLSessionUploadTask *uploadTask = [session uploadTaskWithRequest:request fromData:imageData completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) { // 处理服务器返回的响应 }]; [uploadTask resume]; [picker dismissViewControllerAnimated:YES completion:nil]; } ``` 其中,upload.php是服务器端接收图片的脚本文件。在服务器端,可以使用PHP等语言来处理上传的图片数据。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值