面试题

内存管理 之 定时器

2019-01-03  本文已影响7人  ychen3022
1、NSTimer和CADisplayLink定时器的用法

CADisplayLink是一个能让我们以和屏幕刷新率同步的频率将特定的内容画到屏幕上的定时器类。CADisplayLink以特定模式注册到runloop后, 每当屏幕显示内容刷新结束的时候,runloop就会向CADisplayLink指定的target发送一次指定的selector消息,CADisplayLink类对应的selector就会被调用一次。
CADisplayLink使用场合相对专一,适合做UI的不停重绘,比如自定义动画引擎或者视频播放的渲染。
NSTimer的使用范围要广泛的多,各种需要单次或者循环定时处理的任务都可以使用。

#import "ViewController.h"

@interface ViewController ()

@property(nonatomic,strong)NSTimer *timer1;
@property(nonatomic,strong)NSTimer *timer2;
@property(nonatomic,strong)CADisplayLink *displayerLink;

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    //NStimer的用法
    //1. selector
    self.timer1 = [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(runTest) userInfo:nil repeats:YES];
    
    //2.block的用法
    self.timer2 = [NSTimer scheduledTimerWithTimeInterval:1.0 repeats:YES block:^(NSTimer * _Nonnull timer) {
        [self runTest];
    }];

    //3.CADisplayLink的用法
    //无需设置时间间隔,因为它保证调用频率和UI刷帧的频率一致,一般来说是一秒钟60次(60PFS)
    self.displayerLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(runTest)];
    [self.displayerLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
}

-(void)runTest{
    NSLog(@"111");
}


-(void)dealloc{
    //页面销毁时,注销掉定时器
    [self.timer1 invalidate];
    [self.timer2 invalidate];
    [self.displayerLink invalidate];
}
@end
2、定时器强引用问题

在开发过程中,不知道你们有没有遇到过这样的情况:在某个页面中创建定时器,该页面退出栈时,按理应该调用其dealloc方法,但结果dealloc中的方法都没有执行。
我曾经遇到这个问题,最终查明是因为定时器NSTimer和ViewController之间强引用了,导致ViewController无法释放。
下面,我们就来说说这种情况该怎么解决。
情况1 : 使用NSTimer的block时,可以使用弱指针(弱指针可以解决block情况下的强引用)

@implementation ViewController
- (void)viewDidLoad {
    [super viewDidLoad];
    //使用若指针,可以解决定时器强引用的问题,
    //但是它的适用场景仅限于这种block的方式
    __weak typeof(self) weakSelf = self;
    self.timer2 = [NSTimer scheduledTimerWithTimeInterval:1.0 repeats:YES block:^(NSTimer * _Nonnull timer) {
        [weakSelf runTest];
    }];
}

-(void)runTest{
    NSLog(@"111");
}

-(void)dealloc{
    //页面销毁时,注销掉定时器
    [self.timer2 invalidate];
}
@end

情况2 : 使用NSTimer和CADisplayLink的selector时,需要使用消息转发机制

#pragma mark -MJProxy.h文件

#import <Foundation/Foundation.h>
//MJProxy可用作消息转发,MJProxy和NSObject属于同级别的基类
@interface MJProxy : NSProxy

@property(weak,nonatomic) id target;//注意:weak修饰

+(instancetype)proxyWithTarget:(id)target;

@end


==========================
#pragma mark -MJProxy.m文件

#import "MJProxy.h"

@implementation MJProxy

+(instancetype)proxyWithTarget:(id)target{
    //NSProxy没有init方法,无需调用init
    MJProxy *proxy = [MJProxy alloc];
    proxy.target = target;
    return proxy;
}

//在给程序添加消息转发功能以前,必须覆盖两个方法,即methodSignatureForSelector:和forwardInvocation:

//methodSignatureForSelector:的作用在于为另一个类实现的消息创建一个有效的方法签名,必须实现
- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel{
    return [self.target methodSignatureForSelector:sel];
}

//forwardInvocation:将选择器转发给一个真正实现了该消息的对象
- (void)forwardInvocation:(NSInvocation *)invocation{
    [invocation invokeWithTarget:self.target];
}
@end
//ViewController.m文件

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    //NSTimer
    self.timer1 = [NSTimer scheduledTimerWithTimeInterval:1.0 target:[MJProxy proxyWithTarget:self] selector:@selector(runTest) userInfo:nil repeats:YES];
  
    //CADisplayLink
    self.displayerLink = [CADisplayLink displayLinkWithTarget:[MJProxy proxyWithTarget:self] selector:@selector(runTest)];
    [self.displayerLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
}

-(void)runTest{
    NSLog(@"111");
}

-(void)dealloc{
    //页面销毁时,注销掉定时器
    [self.timer1 invalidate];
    [self.displayerLink invalidate];
}
@end

当然,我们也可以不使用NSProxy,使用NSObject也可以做到

#pragma mark -MJTranspond.h文件

#import <Foundation/Foundation.h>

@interface MJTranspond : NSObject

@property(nonatomic,weak)id target;//注意使用weak修饰

+ (instancetype)proxyWithTarget:(id)target;

@end


==============================
#pragma mark -MJTranspond.m文件

#import "MJTranspond.h"

@implementation MJTranspond

+ (instancetype)proxyWithTarget:(id)target{
    MJTranspond *transpond = [[MJTranspond alloc] init];
    transpond.target = target;
    return transpond;
}

//forwardingTargetForSelector:系统就会在运行时调用这个方法,只要这个方法返回的不是nil或self,也会重启消息发送的过程,把这消息转发给其他对象来处理。
- (id)forwardingTargetForSelector:(SEL)aSelector{
    return self.target;
}

@end

//ViewController.m文件

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    //NSTimer
    self.timer1 = [NSTimer scheduledTimerWithTimeInterval:1.0 target:[MJTranspond proxyWithTarget:self] selector:@selector(runTest) userInfo:nil repeats:YES];
    
    //CADisplayLink
    self.displayerLink = [CADisplayLink displayLinkWithTarget:[MJTranspond proxyWithTarget:self] selector:@selector(runTest)];
    [self.displayerLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
}

-(void)runTest{
    NSLog(@"111");
}


-(void)dealloc{
    //页面销毁时,注销掉定时器
    [self.timer1 invalidate];
    [self.displayerLink invalidate];
}
@end
3、总结
   UIViewController *vc = [[UIViewController alloc] init];
    
    MJProxy *proxy = [MJProxy proxyWithTarget:vc];
    MJTranspond *transpond = [MJTranspond proxyWithTarget:vc];
    
    NSLog(@"%d",[proxy isKindOfClass:[UIViewController class]]);
    NSLog(@"%d",[transpond isKindOfClass:[UIViewController class]]);

以上这段代码建立在上文MJProxy文件和MJTranspond文件的基础之上,
打印结果分别是1和0。
这表明MJTranspond继承自NSObject,它确实不属于UIViewController这种类型,所以打印为0不奇怪。
而MJProxy继承自NSProxy,但是它底层对isKindOfClass方法进行了消息转发,所以是MJProxy的target(ViewController)真正执行isKindOfClass这个方法的,才会打印1。
所以我们做消息转发的时候,首选NSProxy,NSObject会经历方法搜索的过程。

上一篇下一篇

猜你喜欢

热点阅读