面試題引發(fā)的思考:
Q: 使用CADisplayLink
抑诸、NSTimer
有什么注意點谍椅?
- 循環(huán)引用:
CADisplayLink
丸升、NSTimer
會對target
產(chǎn)生強引用勉盅,如果target
又對自身產(chǎn)生強引用佑颇,那么就會引發(fā) 循環(huán)引用。 - 不準時:
CADisplayLink
草娜、NSTimer
依賴于RunLoop挑胸,如果RunLoop的任務過于繁重,可能會導致CADisplayLink
宰闰、NSTimer
不準時茬贵。
Q: 使用CADisplayLink
、NSTimer
如何避免循環(huán)引用移袍?
- 使用
scheduledTimerWithTimeInterval: repeats: block:
方法解藻; - 使用代理對象。
Q: 簡述NSProxy
葡盗?
NSProxy
是專門用來做消息轉(zhuǎn)發(fā)的類螟左,相比NSObject
類來說NSProxy
更輕量級。- 通過
NSProxy
可以幫助Objective-C
間接的實現(xiàn)多重繼承的功能。
1. 實例:
(1) NSTimer
使用時產(chǎn)生循環(huán)引用
// TODO: ----------------- ViewController類 -----------------
@interface ViewController ()
// 循環(huán)引用問題:self對NSTimer對象產(chǎn)生強引用
@property (nonatomic, strong) NSTimer *timer;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// 循環(huán)引用問題:NSTimer對象對self產(chǎn)生強引用
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
對象產(chǎn)生強引用胶背,而NSTimer
對象又會對self
產(chǎn)生強引用巷嚣,此時會造成循環(huán)引用問題,導致無法NSTimer
對象無法隨著ViewController
的釋放而釋放钳吟。
(2) CADisplayLink
使用時產(chǎn)生循環(huán)引用
// TODO: ----------------- ViewController類 -----------------
@interface ViewController ()
// 循環(huán)引用問題:self對CADisplayLink對象產(chǎn)生強引用
@property (nonatomic, strong) CADisplayLink *link;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// 保證調(diào)用頻率和屏幕的刷幀頻率一致廷粒,60FPS
// 循環(huán)引用問題:CADisplayLink對象對self產(chǎn)生強引用
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
對象產(chǎn)生強引用,而CADisplayLink
對象又會對self
產(chǎn)生強引用砸抛,此時會造成循環(huán)引用問題评雌,導致無法CADisplayLink
對象無法隨著ViewController
的釋放而釋放树枫。
2. 解決方法探究:
(1) 解決方案一:使用block
// TODO: ----------------- ViewController類 -----------------
- (void)viewDidLoad {
[super viewDidLoad];
// weakSelf - block內(nèi)部用的是弱指針直焙,對外面的對象產(chǎn)生弱引用
// self - block內(nèi)部用的是強指針,對外面的對象產(chǎn)生強引用
__weak typeof(self) weakSelf = self;
// weakSelf只是把地址賦值給target砂轻,而target在NSTimer內(nèi)部是強引用
self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target: weakSelf selector:@selector(timerTest) userInfo:nil repeats:YES];
}
由以上分析可知:
直接使用弱指針是無法解決循環(huán)引用問題的奔誓,因為weakSelf
只是把地址賦值給target
,而target
在NSTimer
內(nèi)部是強引用搔涝。而NSTimer
是不開源的厨喂,無法修改成弱指針。
弱指針是針對block的方案庄呈,block內(nèi)部用的是弱指針蜕煌,對外面的對象產(chǎn)生弱引用。
所以可以使用以下方法避免循環(huán)引用:
// TODO: ----------------- ViewController類 -----------------
- (void)viewDidLoad {
[super viewDidLoad];
// 弱指針是針對block的方案
__weak typeof(self) weakSelf = self;
// NSTimer對象對block產(chǎn)生強引用
self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 repeats:YES block:^(NSTimer * _Nonnull timer) {
// block對self產(chǎn)生弱引用
[weakSelf timerTest];
}];
}
(2) 解決方案二:使用代理對象
1> 方案分析
由以上分析可知:
直接使用弱指針是無法解決循環(huán)引用問題的诬留,因為weakSelf
只是把地址賦值給target
斜纪,而target
在NSTimer
內(nèi)部是強引用。而NSTimer
是不開源的文兑,無法修改成弱指針盒刚。
如上圖使用代理對象:
則self
對NSTimer
對象產(chǎn)生強引用,NSTimer
對象對OtherObject
對象產(chǎn)生強引用绿贞,而OtherObject
對象對self
產(chǎn)生弱引用因块,此時會避免循環(huán)引用,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;
}
// 消息轉(zhuǎn)發(fā)
- (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;
}
// 消息轉(zhuǎn)發(fā)
- (id)forwardingTargetForSelector:(SEL)aSelector {
// objc_msgSend(self.target, aSelector);
return self.target;
}
@end
2> 方案優(yōu)化:使用NSProxy
源碼如下:
// NSProxy聲明
@interface NSProxy <NSObject> {
Class isa;
}
// NSObject聲明
@interface NSObject <NSObject> {
Class isa;
}
由源碼可知:
- NSProxy涡上、NSObject兩者都是基類;
- 兩者區(qū)別在與方法調(diào)用執(zhí)行流程不同拒名。
MYObjectProxy
繼承自NSObject
其方法調(diào)用執(zhí)行流程:
objc_msgSend()
的執(zhí)行流程可以分為三個階段:
- 消息發(fā)送階段:負責從類及父類的緩存列表及方法列表查找方法吩愧;
- 動態(tài)解析階段:如果消息發(fā)送階段沒有找到方法,則會進入動態(tài)解析階段靡狞,負責動態(tài)的添加方法實現(xiàn)耻警;
- 消息轉(zhuǎn)發(fā)階段:如果也沒有實現(xiàn)動態(tài)解析方法,則會進行消息轉(zhuǎn)發(fā)階段,將消息轉(zhuǎn)發(fā)給可以處理消息的接收者來處理甘穿;
- 報錯:如果也沒有實現(xiàn)消息轉(zhuǎn)發(fā)方法腮恩,會報錯
unrecognzied selector sent to instance
。
MYProxy
繼承自NSProxy
其方法調(diào)用執(zhí)行流程:
- 直接進入消息轉(zhuǎn)發(fā)階段:將消息轉(zhuǎn)發(fā)給可以處理消息的接收者來處理温兼;
- 報錯:如果也沒有實現(xiàn)消息轉(zhuǎn)發(fā)方法秸滴,會報錯
unrecognzied selector sent to instance
。
由以上結論可知:
NSProxy
是專門用來做消息轉(zhuǎn)發(fā)的類募判,相比NSObject
類來說NSProxy
更輕量級荡含。- 通過
NSProxy
可以幫助Objective-C
間接的實現(xiàn)多重繼承的功能。
對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對象不需要調(diào)用init届垫,沒有init方法
MYProxy *proxy = [MYProxy alloc];
proxy.target = target;
return proxy;
}
// 消息轉(zhuǎn)發(fā) - 效率高
- (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
對象調(diào)用isKindOfClass
方法時進行消息轉(zhuǎn)發(fā),即調(diào)用target
進行轉(zhuǎn)發(fā)登淘;
那么[proxy isKindOfClass:[ViewController class]]
相當于[vc isKindOfClass:[ViewController class]]
箫老;
則proxy
是UIViewController
類型及其子類,輸出結果為“1”黔州。