面试题引发的思考:
Q: 使用CADisplayLink、NSTimer有什么注意点?
- 循环引用:
CADisplayLink、NSTimer会对target产生强引用,如果target又对自身产生强引用,那么就会引发 循环引用。 - 不准时:
CADisplayLink、NSTimer依赖于RunLoop,如果RunLoop的任务过于繁重,可能会导致CADisplayLink、NSTimer不准时。
Q: 使用CADisplayLink、NSTimer如何避免循环引用?
- 使用
scheduledTimerWithTimeInterval: repeats: block:方法; - 使用代理对象。
Q: 简述NSProxy?
NSProxy是专门用来做消息转发的类,相比NSObject类来说NSProxy更轻量级。- 通过
NSProxy可以帮助Objective-C间接的实现多重继承的功能。
1. 实例:
(1) NSTimer使用时产生循环引用
// TODO: ----------------- ViewController类 -----------------
@interface ViewController ()
// 循环引用问题:self对NSTimer对象产生强引用
@property (nonatomic, strong) NSTimer *timer;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// 循环引用问题:NSTimer对象对self产生强引用
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

由以上分析可知:
self对NSTimer对象产生强引用,而NSTimer对象又会对self产生强引用,此时会造成循环引用问题,导致无法NSTimer对象无法随着ViewController的释放而释放。
(2) CADisplayLink使用时产生循环引用
// TODO: ----------------- ViewController类 -----------------
@interface ViewController ()
// 循环引用问题:self对CADisplayLink对象产生强引用
@property (nonatomic, strong) CADisplayLink *link;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// 保证调用频率和屏幕的刷帧频率一致,60FPS
// 循环引用问题:CADisplayLink对象对self产生强引用
self.link = [CADisplayLink displayLinkWithTarget:self selector:@selector(linkTest)];
[self.link addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSDefaultRunLoopMode];
}
- (void)linkTest {
NSLog(@"%s", __func__);
}
- (void)dealloc {
NSLog(@"%s", __func__);
[self.link invalidate];
}
@end

由以上分析可知:
self对CADisplayLink对象产生强引用,而CADisplayLink对象又会对self产生强引用,此时会造成循环引用问题,导致无法CADisplayLink对象无法随着ViewController的释放而释放。
2. 解决方法探究:
(1) 解决方案一:使用block
// TODO: ----------------- ViewController类 -----------------
- (void)viewDidLoad {
[super viewDidLoad];
// weakSelf - block内部用的是弱指针,对外面的对象产生弱引用
// self - block内部用的是强指针,对外面的对象产生强引用
__weak typeof(self) weakSelf = self;
// weakSelf只是把地址赋值给target,而target在NSTimer内部是强引用
self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target: weakSelf selector:@selector(timerTest) userInfo:nil repeats:YES];
}

由以上分析可知:
直接使用弱指针是无法解决循环引用问题的,因为weakSelf只是把地址赋值给target,而target在NSTimer内部是强引用。而NSTimer是不开源的,无法修改成弱指针。
弱指针是针对block的方案,block内部用的是弱指针,对外面的对象产生弱引用。
所以可以使用以下方法避免循环引用:
// TODO: ----------------- ViewController类 -----------------
- (void)viewDidLoad {
[super viewDidLoad];
// 弱指针是针对block的方案
__weak typeof(self) weakSelf = self;
// NSTimer对象对block产生强引用
self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 repeats:YES block:^(NSTimer * _Nonnull timer) {
// block对self产生弱引用
[weakSelf timerTest];
}];
}
(2) 解决方案二:使用代理对象
1> 方案分析

由以上分析可知:
直接使用弱指针是无法解决循环引用问题的,因为weakSelf只是把地址赋值给target,而target在NSTimer内部是强引用。而NSTimer是不开源的,无法修改成弱指针。
如上图使用代理对象:
则self对NSTimer对象产生强引用,NSTimer对象对OtherObject对象产生强引用,而OtherObject对象对self产生弱引用,此时会避免循环引用,NSTimer对象会随着ViewController的释放而释放。
a> 对NSTimer使用代码如下:
// TODO: ----------------- ViewController类 -----------------
@interface ViewController ()
@property (nonatomic, strong) NSTimer *timer;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:[MYObjectProxy proxyWithTarget:self] selector:@selector(timerTest) userInfo:nil repeats:YES];
}
- (void)timerTest {
NSLog(@"%s", __func__);
}
- (void)dealloc {
NSLog(@"%s", __func__);
[self.timer invalidate];
}
@end
// TODO: ----------------- MYObjectProxy类 -----------------
@interface MYObjectProxy : NSObject
@property (nonatomic, weak) id target;
+ (instancetype)proxyWithTarget:(id)target;
@end
@implementation MYObjectProxy
+ (instancetype)proxyWithTarget:(id)target {
MYObjectProxy *proxy = [[MYObjectProxy alloc] init];
proxy.target = target;
return proxy;
}
// 消息转发
- (id)forwardingTargetForSelector:(SEL)aSelector {
// objc_msgSend(self.target, aSelector);
return self.target;
}
@end
b> 对CADisplayLink使用代码如下:
// TODO: ----------------- ViewController类 -----------------
@interface ViewController ()
// 没有block方案
@property (nonatomic, strong) CADisplayLink *link;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.link = [CADisplayLink displayLinkWithTarget:[MYObjectProxy proxyWithTarget:self] selector:@selector(linkTest)];
[self.link addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSDefaultRunLoopMode];
}
- (void)timerTest {
NSLog(@"%s", __func__);
}
- (void)dealloc {
NSLog(@"%s", __func__);
[self.timer invalidate];
}
@end
// TODO: ----------------- MYObjectProxy类 -----------------
@interface MYObjectProxy : NSObject
@property (nonatomic, weak) id target;
+ (instancetype)proxyWithTarget:(id)target;
@end
@implementation MYObjectProxy
+ (instancetype)proxyWithTarget:(id)target {
MYObjectProxy *proxy = [[MYObjectProxy alloc] init];
proxy.target = target;
return proxy;
}
// 消息转发
- (id)forwardingTargetForSelector:(SEL)aSelector {
// objc_msgSend(self.target, aSelector);
return self.target;
}
@end
2> 方案优化:使用NSProxy
源码如下:
// NSProxy声明
@interface NSProxy <NSObject> {
Class isa;
}
// NSObject声明
@interface NSObject <NSObject> {
Class isa;
}
由源码可知:
- NSProxy、NSObject两者都是基类;
- 两者区别在与方法调用执行流程不同。
MYObjectProxy继承自NSObject其方法调用执行流程:
objc_msgSend()的执行流程可以分为三个阶段:
- 消息发送阶段:负责从类及父类的缓存列表及方法列表查找方法;
- 动态解析阶段:如果消息发送阶段没有找到方法,则会进入动态解析阶段,负责动态的添加方法实现;
- 消息转发阶段:如果也没有实现动态解析方法,则会进行消息转发阶段,将消息转发给可以处理消息的接收者来处理;
- 报错:如果也没有实现消息转发方法,会报错
unrecognzied selector sent to instance。
MYProxy继承自NSProxy其方法调用执行流程:
- 直接进入消息转发阶段:将消息转发给可以处理消息的接收者来处理;
- 报错:如果也没有实现消息转发方法,会报错
unrecognzied selector sent to instance。
由以上结论可知:
NSProxy是专门用来做消息转发的类,相比NSObject类来说NSProxy更轻量级。- 通过
NSProxy可以帮助Objective-C间接的实现多重继承的功能。
对NSProxy使用代码如下:
// TODO: ----------------- ViewController类 -----------------
@interface ViewController ()
@property (nonatomic, strong) NSTimer *timer;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:[MYProxy proxyWithTarget:self] selector:@selector(timerTest) userInfo:nil repeats:YES];
}
- (void)timerTest {
NSLog(@"%s", __func__);
}
- (void)dealloc {
NSLog(@"%s", __func__);
[self.timer invalidate];
}
@end
// TODO: ----------------- MYProxy类 -----------------
@interface MYProxy : NSProxy
@property (nonatomic, weak) id target;
+ (instancetype)proxyWithTarget:(id)target;
@end
@implementation MYProxy
+ (instancetype)proxyWithTarget:(id)target {
// NSProxy对象不需要调用init,没有init方法
MYProxy *proxy = [MYProxy alloc];
proxy.target = target;
return proxy;
}
// 消息转发 - 效率高
- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel {
return [self.target methodSignatureForSelector:sel];
}
- (void)forwardInvocation:(NSInvocation *)invocation {
[invocation invokeWithTarget:self.target];
}
@end
3. 延伸:
Q: 以下代码输出为何为 “0 - 1” ?
// TODO: ----------------- ViewController类 -----------------
@interface ViewController ()
@property (nonatomic, strong) NSTimer *timer;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
ViewController *vc = [[ViewController alloc] init];
MYObjectProxy *objectProxy = [MYObjectProxy proxyWithTarget:vc];
MYProxy *proxy = [MYProxy proxyWithTarget:vc];
NSLog(@"%d - %d",
[objectProxy isKindOfClass:[ViewController class]],
[proxy isKindOfClass:[ViewController class]]);
}
objectProxy对象是MYObjectProxy类型,继承自NSObject;
则MYObjectProxy不是UIViewController类型及其子类,输出结果为“0”。
proxy对象是MYProxy类型,继承自NSProxy;
proxy对象调用isKindOfClass方法时进行消息转发,即调用target进行转发;
那么[proxy isKindOfClass:[ViewController class]]相当于[vc isKindOfClass:[ViewController class]];
则proxy是UIViewController类型及其子类,输出结果为“1”。