美文网首页
内存管理一:NSTimer

内存管理一:NSTimer

作者: 小心韩国人 | 来源:发表于2019-12-16 23:34 被阅读0次

NSTimer会对target产生强引用,如果target再对NSTimer产生强引用就会产生循环引用.我们直接用代码演示:

@interface ViewController ()
@property (nonatomic,strong)NSTimer *timer;
@end

@implementation ViewController

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

- (void)timerAction{
    NSLog(@".");
}

- (void)dealloc{
    
    [self.timer invalidate];
    self.timer = nil;
    
    NSLog(@"%s---%@...",__func__,self.obj);
}

@end

以上代码每秒中调用一次timerAction,即使已经退出当前控制器还会继续调用.虽然我们已经重写了dealloc方法,并且在dealloc方法内部调用了timerinvalidate方法,并且手动把timer置为nil.上述代码的dealloc是永远不会调用的,因为timerviewcontroller已经产生了循环引用.有人会想使用__weak修饰self不就可以了吗,像下面这样:

 __weak typeof(self)weakSelf = self;
self.timer = [NSTimer scheduledTimerWithTimeInterval:1 target:weakSelf selector:@selector(timerAction) userInfo:nil repeats:YES];

结果是这样仍然也解决不了问题,之前我们使用__weak是解决block的循环引用的.之所以能解决block的循环引用是因为blcok内部捕获的外部变量的引用关系取决于外部变量的修饰符.大家不要搞混淆了.而在NSTimer内部会强引用传进来的target.
那我们怎么解决这个问题呢?可以换一种初始化方法,使用带有block的初始化方法:

 self.timer = [NSTimer scheduledTimerWithTimeInterval:1 repeats:YES block:^(NSTimer * _Nonnull timer) {
            NSLog(@".");
    }];

这样就能解决循环引用的问题.补充一点,使用scheduled的方式创建的定时器会自动加入到当前线程的default mode模式下.不需要我们手动添加到runloop,其他方式创建的需要手动添加到runloop.我自己做了一个总结:

创建timer的两种方式
  • 还有一种计时器:CADisplayLink.这种计时器不用设置间隔时间,它会和屏幕的刷帧保持一样的频率调用方法,也就是60FPS (一秒钟60次,可能会不准确,如果说主线程有耗时的操作会影响到刷帧频率).
self.linkTimer = [CADisplayLink displayLinkWithTarget:self selector:@selector(timerAction)];
[self.linkTimer addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];

CADisplayLink没有block的创建方式,我们怎么解决循环引用呢?我们画图分析一下:

循环引用
造成循环引用的原因很简单,NStimer中的target强引用了ViewController,而ViewController中的timer属性又强引用了NStimer.所以就造成了循环引用.我们可以像下面这样再中间加上一层:
中间层

创建一个中间层,让NSTimer强引用这个中间层,中间层弱引用ViewController,就打破了之前的循环引用关系:

@interface TestProxy : NSObject

@property (nonatomic,weak)id target;
+ (id)proxyWithTarget:(id)target;

@end

@implementation TestProxy

+ (id)proxyWithTarget:(id)target{
    TestProxy *proxy = [[TestProxy alloc]init];
    proxy.target = target;
    return proxy;
}


- (id)forwardingTargetForSelector:(SEL)aSelector{
    return self.target;
}

@end


--------------------------------------------------------------

@interface ViewController ()

@property (nonatomic,strong)NSTimer *timer;
@property (nonatomic,strong)CADisplayLink *linkTimer;

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
//    __weak typeof(self)weakSelf = self;
//    self.timer = [NSTimer scheduledTimerWithTimeInterval:1 target:[TestProxy proxyWithTarget:self] selector:@selector(timerAction) userInfo:nil repeats:YES];
    
    
    //保证定时器的调用频率和屏幕的刷帧评率一样 60FPS
    self.linkTimer = [CADisplayLink displayLinkWithTarget:[TestProxy proxyWithTarget:self] selector:@selector(timerAction)];
    [self.linkTimer addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
    
}

- (void)timerAction{
    NSLog(@".");
}

- (void)dealloc{
    NSLog(@"%s",__func__);
    [self.linkTimer invalidate];
    self.linkTimer = nil;
}

@end

需要注意的是CADisplayLink也需要手动调用invalidate才能停止.
我们创建了一个中间层TestProxy类,这个类中有一个weak属性target.我们可以把ViewController处理的事情交给这个target处理,在TestProxy内部直接使用消息转发转发给真正处理的对象.
在OC中有一个专门用来处理这种去求的类NSProxy:

@interface NSProxy <NSObject> {
    Class   isa;
}

我们让NSProxyNSObject对比一下:

@interface NSObject <NSObject> {
    Class isa  OBJC_ISA_AVAILABILITY;
}

会发现这两个类非常的像,他们都没有继承任何类,都实现了< NSObject >协议.其实NSProxyNSObject一样都是基类.只不过NSProxy是专门用来做代理的类.我们看看这个类怎么用:

@interface FormalProxy : NSProxy

@property (nonatomic,weak)id target;
+ (id)proxyWithTarget:(id)target;
@end


@implementation FormalProxy
+ (id)proxyWithTarget:(id)target{
    //NSProxy 类中没有init方法,直接alloc就可以使用
    FormalProxy *proxy = [FormalProxy alloc];
    proxy.target = target;
    return proxy;
}
@end

我们在FormalProxy也设置一个target,然后FormalProxy调用ViewControllertimerAction方法,但是我们并没有在FormalProxy内部把这个消息转发处理.然后运行代码看看会发生什么:

NSProxy 的报错
可以看到NSProxy直接报methodSignatureForSelector:] called!找不到这个错误.
再看看如果是继承自NSObjectTestProxy如果没有消息转发会报什么错:
NSObject 报错
可以看到NSObject直接报unrecognized selector sent to instance这个经典的方法找不到的错误.
这就是NSProxyNSObject的区别.NSProxy首先查看自己的类中有没有这个方法,如果没有就不会走方法调用的三个阶段.而是直接走methodSignatureForSelectorforwardInvocation进入消息转发.所以我们要在FormalProxy中这样实现:
@interface FormalProxy : NSProxy

@property (nonatomic,weak)id target;
+ (id)proxyWithTarget:(id)target;

@end

@implementation FormalProxy

+ (id)proxyWithTarget:(id)target{
    //NSProxy 类中没有init方法,直接alloc就可以使用
    FormalProxy *proxy = [FormalProxy alloc];
    proxy.target = target;
    return proxy;
}

- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel{
    return [self.target methodSignatureForSelector:sel];
}

- (void)forwardInvocation:(NSInvocation *)invocation{
    [invocation invokeWithTarget:self.target];
}

@end

有人可能会说NSProxy代码比继承自NSObjectTestProxy代码更复杂一些,是不是TestProxy更方便呢?
当然不是,因为NSProxy是专门用来处理代理这种情况的.它的效率要比继承自NSObject的类更高.因为它不会像继承自NSObject的类那样检索方法.检索不到再进入方法解析.NSProxy会直接进入消息转发.

为了加深一下理解,我们做个小练习题:

TestProxy *objectProxy = [TestProxy proxyWithTarget:self];//继承自 NSObject
FormalProxy *formalProxy = [FormalProxy proxyWithTarget:self];//继承自 NSProxy
    
NSLog(@"%d",[objectProxy isKindOfClass:[self class]]);
NSLog(@"%d",[formalProxy isKindOfClass:[self class]]);

想想看会打印什么?直接运行一下看看结果:


结果

可以看到继承自NSObject的为false,而继承自NSProxy的为true.这是因为NSProxy直接把isKindOfClass转发给了ViewController处理,所以最后就是ViewController isKindOfClass [self class]结果就为true.

相关文章

网友评论

      本文标题:内存管理一:NSTimer

      本文链接:https://www.haomeiwen.com/subject/kivdnctx.html