面试题引发的思考:
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”。
网友评论