一、CADisplayLink
-
CADisplayLink
: 使用频率和屏幕的刷新频率保持一致, 60FPS -
设置程序的界面结构如下图所示, 其中橙色的界面就是
ViewController
- 在
ViewController
中有如下代码,ViewController
有一个属性CADisplayLink *displayLink
#import "ViewController.h"
@interface ViewController ()
@property (nonatomic, strong) CADisplayLink *displayLink;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(displayLinkTest)];
[self.displayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
}
- (void)displayLinkTest
{
NSLog(@"%s", __func__);
}
- (void)dealloc
{
NSLog(@"%s", __func__);
[self.displayLink invalidate];
}
@end
- 运行程序, 进入
ViewController
后可以看到控制台不停的打印
- 即便退出控制器, 也可以看到
-dealloc
根本没有执行, 定时器依然在不停的调用 - 此时, 就形成了
ViewController-CADisplayLink
的循环引用, 类似下图
解决循环引用
- 可以使用一个中间对象来解决循环引用问题
Proxy
中代码如下, 使用便利构造器
创建Proxy
对象, 同时存储target
Proxy
不实现任何target
调用的方法, 而是使用消息转发的方式, 将消息转发给target
, 这样不论定时器
调用任何方法, 都能交给target
去执行
#import "Proxy.h"
@interface Proxy ()
@property (nonatomic, weak) id target;
@end
@implementation Proxy
+ (instancetype)proxyWithTarget:(id)target
{
Proxy *proxy = [[Proxy alloc] init];
proxy.target = target;
return proxy;
}
- (id)forwardingTargetForSelector:(SEL)aSelector
{
return self.target;
}
@end
- 此时
ViewController
中代码如下,CADisplayLink
绑定[Proxy proxyWithTarget:self]
, 调用-displayLinkTest
方法 - 当运行程序时, 因为
Proxy
没有实现-displayLinkTest
方法, 此时Proxy
就会通过消息转发, 将displayLinkTest
转交给target
去执行
#import "ViewController.h"
#import "Proxy.h"
@interface ViewController ()
@property (nonatomic, strong) CADisplayLink *displayLink;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.displayLink = [CADisplayLink displayLinkWithTarget:[Proxy proxyWithTarget:self] selector:@selector(displayLinkTest)];
[self.displayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
}
- (void)displayLinkTest
{
NSLog(@"%s", __func__);
}
- (void)dealloc
{
NSLog(@"%s", __func__);
[self.displayLink invalidate];
}
@end
- 运行程序, 进入
ViewController
界面后, 可以看到控制台不停的打印, 当点击返回按钮, 退出ViewController
后, 就会调用ViewController
的-dealloc
方法, 停止定时器
- 这样, 就解决了
ViewController-CADisplayLink
的循环引用问题
二、NSTimer
NSTimer
与CADisplayLink
类似, 也会造成循环引用问题
#import "ViewController.h"
@interface ViewController ()
@property (nonatomic, strong) NSTimer *timer;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(timerTest) userInfo:nil repeats:YES];
}
- (void)timerTest
{
NSLog(@"%s", __func__);
}
- (void)dealloc
{
NSLog(@"%s", __func__);
[self.timer invalidate];
}
@end
- 执行程序, 进入
ViewController
可以看到每一秒打印一次, 退出ViewController
后打印也不会停止
- 此时的循环结构如下图
解决循环引用问题
- 与
CADisplayLink
一样, 使用中间对象Proxy
即可
#import "ViewController.h"
#import "Proxy.h"
@interface ViewController ()
@property (nonatomic, strong) NSTimer *timer;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:[Proxy proxyWithTarget:self] selector:@selector(timerTest) userInfo:nil repeats:YES];
}
- (void)timerTest
{
NSLog(@"%s", __func__);
}
- (void)dealloc
{
NSLog(@"%s", __func__);
[self.timer invalidate];
}
@end
- 运行程序, 进入
ViewController
后控制台持续打印, 退出ViewController
后, 定时器停止
- 此时的内存结构如下
三、NSProxy
NSProxy
是与NSObject
同级别的类,NSProxy
的定义是下面这段代码
@interface NSProxy <NSObject> {
Class isa;
}
+ (id)alloc;
+ (id)allocWithZone:(nullable NSZone *)zone NS_AUTOMATED_REFCOUNT_UNAVAILABLE;
+ (Class)class;
- (void)forwardInvocation:(NSInvocation *)invocation;
- (nullable NSMethodSignature *)methodSignatureForSelector:(SEL)sel NS_SWIFT_UNAVAILABLE("NSInvocation and related APIs not available");
- (void)dealloc;
- (void)finalize;
@property (readonly, copy) NSString *description;
@property (readonly, copy) NSString *debugDescription;
+ (BOOL)respondsToSelector:(SEL)aSelector;
- (BOOL)allowsWeakReference NS_UNAVAILABLE;
- (BOOL)retainWeakReference NS_UNAVAILABLE;
// - (id)forwardingTargetForSelector:(SEL)aSelector;
@end
NSProxy
没有任何的父类, 与NSObject
一样遵守<NSObject>
协议NSProxy
是用来做消息转发的类, 如果自己没有实现目标方法, 那么就会立刻进入消息转发
1、使用NSProxy
解决定时器内存管理问题
- 定义
BWProxy
继承自NSProxy
, 并实现下列方法
@interface BWProxy : NSProxy
@property (nonatomic, weak) id target;
+ (instancetype)proxyWithTarget:(id)target;
@end
@implementation BWProxy
+ (instancetype)proxyWithTarget:(id)target
{
// NSProxy没有init方法, 只需要调用alloc创建对象即可
BWProxy *proxy = [BWProxy alloc];
proxy.target = target;
return proxy;
}
@end
- 在
ViewController
使用BWProxy
替代上面的Proxy
#import "ViewController.h"
#import "BWProxy.h"
@interface ViewController ()
@property (nonatomic, strong) NSTimer *timer;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:[BWProxy proxyWithTarget:self] selector:@selector(timerTest) userInfo:nil repeats:YES];
}
- (void)timerTest
{
NSLog(@"%s", __func__);
}
- (void)dealloc
{
NSLog(@"%s", __func__);
[self.timer invalidate];
}
@end
- 执行程序, 进入
ViewController
可以看到, 有下面的报错
- 可以看到, 报错信息是
-[NSProxy methodSignatureForSelector:] called!
, 并不是找不到timerTest
方法 - 我们可以在
BWProxy
中加入-methodSignatureForSelector:
和-forwardInvocation:
两个方法, 实现消息转发
来解决崩溃的问题
#import <Foundation/Foundation.h>
@interface BWProxy : NSProxy
@property (nonatomic, weak) id target;
+ (instancetype)proxyWithTarget:(id)target;
@end
@implementation BWProxy
+ (instancetype)proxyWithTarget:(id)target
{
// NSProxy没有init方法, 只需要调用alloc创建对象即可
BWProxy *proxy = [BWProxy alloc];
proxy.target = target;
return proxy;
}
- (nullable NSMethodSignature *)methodSignatureForSelector:(SEL)sel
{
return [self.target methodSignatureForSelector:sel];
}
- (void)forwardInvocation:(NSInvocation *)invocation
{
[invocation invokeWithTarget:self.target];
}
@end
- 运行程序, 进入
ViewController
后, 再次退出, 可以看到NSTimer
停止,ViewController
被释放
CADisplayLink
与NSTimer
一样, 这里就不再赘述
2、-isKindOfClass:
- 使用上面的
Proxy
和BWProxy
, 实现下面的代码
Proxy *proxy1 = [Proxy proxyWithTarget:self];
BWProxy *proxy2 = [BWProxy proxyWithTarget:self];
NSLog(@"%d", [proxy1 isKindOfClass:[ViewController class]]);
NSLog(@"%d", [proxy2 isKindOfClass:[ViewController class]]);
- 可以看到控制台的打印如下
proxy1
的基类是NSObject
, 所以打印为0
proxy2
实际上是进行了消息转发, 将isKindOfClass:
转发给了target
, 也就是ViewController
, 所以打印是1
- 在
GUNStep
中也可以看到实现过程
四、GCD定时器
NSTimer
依赖于RunLoop
,如果RunLoop
的任务过于繁重,可能会导致NSTimer
不准时GCD
定时器不依赖于RunLoop
, 会更加的准时
#import "ViewController.h"
@interface ViewController ()
@property (nonatomic, strong) dispatch_source_t timer;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// 获取主队列
dispatch_queue_t queue = dispatch_get_main_queue();
// 创建定时器, 在主线程中调用
dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
// 2秒后执行
NSTimeInterval start = 2.0;
// 执行间隔1秒
NSTimeInterval interval = 1.0;
// 设置定时器
dispatch_source_set_timer(timer,
dispatch_time(DISPATCH_TIME_NOW, start * NSEC_PER_SEC),
interval * NSEC_PER_SEC,
0);
// 设置回调
__weak typeof(self) weakSelf = self;
dispatch_source_set_event_handler(timer, ^{
[weakSelf timerTest];
});
// 启动定时器
dispatch_resume(timer);
self.timer = timer;
}
- (void)timerTest
{
NSLog(@"%s", __func__);
}
- (void)dealloc
{
NSLog(@"%s", __func__);
}
@end
- 运行程序, 进入
ViewController
, 可以看到定时器的打印, 退出ViewController
可以看到-dealloc
被调用, 定时器停止