記憶體管理系列文章
記憶體管理—MRC時代的手動記憶體管理
記憶體管理—weak的實作原理
記憶體管理——autorelease原理分析
記憶體管理——定時器問題
iOS程式的記憶體布局
CADisplayLink、NSTimer的循環引用問題
CADisplayLink
是
QuartzCore
架構下的的一種定時器,用在跟畫圖相關的處理當中。
NSTimer
大家應該很熟悉,是我們最常用的定時器。這兩種定時器分别提供如下兩個API
這兩個API裡面都有
target
參數,該
target
會被
CADisplayLink/NSTimer
強引用。如果
CADisplayLink
或者
NSTimer
作為屬性被一個視圖控制器VC強引用,當我們在調用上述兩個API的時候,
target
參數傳VC,這樣VC和
CADisplayLink/NSTimer
之間便會形成引用循環,無法釋放,造成記憶體洩漏。圖示如下
NSTimer的解決方案1
通過使用别的API來添加
NSTimer
,如
并且将
self
通過
__weak typeof(self) weakSelf == self;
包裝成弱指針,傳入其中即可。
NSTimer的解決方案2
通過增加一個中間代理對象來打破引用循環。請看下圖
如上圖所示,在
timer
和
VC
之間增加一個代理對象
otherObject
,
timer
的強指針
target
指向
otherObject
,
otherObject
的弱指針
target
指向
VC
,這樣就成功打破了引用循環。我們之是以需要借助第三者來破環,是因為
NSTimer
并非開源,我們無法修改其内部
target
的強弱性。是以隻能通過一個自定義的代理對象來做一層引用中轉,最終打破引用循環。
現在還有一個細節需要處理,增加代理對象
otherObject
之前,是由
timer
通過
target
直接調用
VC
裡面的定時器方法的。現在中間多了一層
otherObject
,該如何實作定時器方法的調用呢?其實方法蠻多的,相信大家都能想出一些解決方案。這裡就直接推薦一種比較巧妙的方法——通過消息轉發。如下圖
因為代理對象的本質目的,就是打破引用循環,并且傳遞方法,了解OC消息機制的原理前提下,你應該很好了解消息轉發的作用,正好可以巧妙的用在這個場景下。請好好體會一下。
下面是一份代碼案例
#import "ViewController.h"
#import "CLProxy.h"
@interface ViewController ()
//@property (nonatomic, strong) CADisplayLink *link;
@property (nonatomic, strong) NSTimer *timer;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
//CADisplayLink用來保證調用頻率和螢幕的刷幀頻率一緻,60FPS
// self.link = [CADisplayLink displayLinkWithTarget:[CLProxy proxyWithTarget:self] selector:@selector(linkTest)];
// [self.link addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSDefaultRunLoopMode];
self.timer = [NSTimer scheduledTimerWithTimeInterval:1 target:[CLProxy proxyWithTarget:self] selector:@selector(timerTest) userInfo:nil repeats:YES];
}
//- (void)linkTest {
// NSLog(@"%s",__func__);
//}
- (void)timerTest {
NSLog(@"%s",__func__);
}
-(void)dealloc {
NSLog(@"%s",__func__);
}
@end
****************????代理類CLProxy???
**************** CLProxy.h ****************
#import <Foundation/Foundation.h>
@interface CLProxy : NSObject
+(instancetype)proxyWithTarget: (id)target;
@property (weak, nonatomic) id target;
@end
**************** CLProxy.m ****************
#import "CLProxy.h"
@implementation CLProxy
+(instancetype)proxyWithTarget: (id)target {
CLProxy *proxy = [[CLProxy alloc] init];
proxy.target = target;
return proxy;
}
-(id)forwardingTargetForSelector:(SEL)aSelector {
return self.target;
}
@end
該方案同樣适用于
CADisplayLink
,不再贅述。
認識NSProxy
大家可能看到過一個類叫
NSProxy
,但應該很少能用到,這是一個非常特殊的類。我們來對比一下它和
NSObject
的定義的對比
@interface NSProxy <NSObject> {
Class isa;
}
@interface NSObject <NSObject> {
Class isa ;
}
你可以看到,
NSProxy
和
NSObject
是同一層級的,是以也可以吧
NSProxy
了解成一個基類。他們都遵守
<NSObject>
協定,他們都沒有父類。
那麼
NSProxy
是幹嘛用的呢?其實它就是專門用來解決通過中間對象轉發消息的問題的。
這裡先貼出案例代碼
#import "ViewController.h"
#import "CLProxy2.h"
@interface ViewController ()
@property (nonatomic, strong) NSTimer *timer;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.timer = [NSTimer scheduledTimerWithTimeInterval:1 target:[CLProxy2 proxyWithTarget:self] selector:@selector(timerTest) userInfo:nil repeats:YES];
}
- (void)timerTest {
NSLog(@"%s",__func__);
}
-(void)dealloc {
NSLog(@"%s",__func__);
[self.timer invalidate];
}
@end
****************????代理類CLProxy???
**************** CLProxy2.h ****************
#import <Foundation/Foundation.h>
@interface CLProxy2 : NSProxy
+(instancetype)proxyWithTarget: (id)target;
@property (weak, nonatomic) id target;
@end
**************** CLProxy2.m ****************
#import "CLProxy2.h"
@implementation CLProxy2
+(instancetype)proxyWithTarget: (id)target {
//NSProxy對象不需要調用init,因為它本來就沒有init方法,直接alloc之後就可以使用
CLProxy2 *proxy = [CLProxy2 alloc];
proxy.target = target;
return proxy;
}
@end
CLProxy2
繼承自
NSProxy
,首先還是按照跟之前的案例的套路一樣,将
VC
,
timer
和
CLProxy2
連結起來,我們先不在
CLProxy2
對消息做任何處理,看一下會有什麼情況,結果是報錯資訊
可以看出,向
CLProxy2
對象發送一個它沒有實作的方法(消息),最後會調用
methodSignatureForSelector
方法。如果你很熟悉**【OC消息機制】**的話,對繼承自
NSObject
的類的執行個體對象發送消息,如果該對象沒有實作對應的方法的話,出現的報錯将是
也就是經典的
unrecognized selector sent to instance
。
這是怎麼回事呢?其實
NSProxy
接受到消息之後的處理流程如下
-
[proxyObj message]
- (1)到
的類對象裡面尋找對應的方法,找到就調用proxyObj
- (2)嘗試進入父類對象遞歸查找方法(省略該步驟)
- (3)找不到方法,嘗試進行方法動态解析(省略該步驟)
- (4)嘗試調用
進行消息轉發`(省略該步驟)forwardingTargetForSelector
- (5)嘗試調用
+methodSignatureForSelector
進行消息轉發。forwardInvocation
是以可以發現,相比較完整的消息機制流程,
NSProxy
的處理過程中,省略了(2)、(3)、(4)步驟。是以它相比于
NSObject
,效率更高,我們的今天所讨論的代理對象傳遞消息問題,正好可以通過
NSProxy
來解決,提升效率。根絕第(5)步驟,我們隻需要在子類裡面實作
methodSignatureForSelector
+
forwardInvocation
這兩個方法即可,上面的
CLProxy2.m
代碼修改如下即可
#import "CLProxy2.h"
@implementation CLProxy2
+(instancetype)proxyWithTarget: (id)target {
//NSProxy對象不需要調用init,因為它本來就沒有init方法,直接alloc之後就可以使用
CLProxy2 *proxy = [CLProxy2 alloc];
proxy.target = target;
return proxy;
}
-(NSMethodSignature *)methodSignatureForSelector:(SEL)sel {
return [self.target methodSignatureForSelector:sel];
}
-(void)forwardInvocation:(NSInvocation *)invocation {
invocation.target = self.target;
[invocation invoke];
}
@end
以後碰到類似的通過中間對象傳遞消息的場景,最為推薦的就是利用
NSProxy
來實作。
标題如果别人問你CADisplayLink、NSTimer是否準時?
相信答案大家都會說:不準時。但是不準時的原因未必每個人都清楚。那這裡就來簡單梳理一下。
CADisplayLink
、
NSTimer
底層都是靠RunLoop來實作的,也就是可以把它們了解成RunLoop所需要處理的事件。我們知道RunLoop可以拿來重新整理UI,處理定時器(
CADisplayLink
、
NSTimer
),處理點選滑動事件等非常多的事情。這裡,就需要來了解一下RunLoop是如何觸發
NSTimer
任務的。RunLoop每循環一圈,都會處理一定的事件,會消耗一定的時間,但是具體耗時多少這個是無法确定的。
假如你開啟一個
timer
,隔1秒觸發定時器事件,RunLoop會開始累計每一圈循環的用時,當時間累計夠1秒,就會觸發定時器事件。你有興趣的話,是可以在RunLoop的源碼裡面找到時間累加相關代碼的。可以借助下圖來加深了解
如果RunLoop在某一圈任務過于繁重,就可能出現如下情況
是以
CADisplayLink
、
NSTimer
是無法保證準時性的。
GCD定時器
GCD的定時器是直接跟系統核心挂鈎,不依賴于RunLoop機制,是以時間是相當精準的。GCD定時器的使用非常簡單,如下所示
@interface ViewController ()
@property (nonatomic, strong) dispatch_source_t timer;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
//初始化定時器
self.timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_main_queue());
//開始時間
dispatch_time_t startTime = dispatch_time(DISPATCH_TIME_NOW, 3.0*NSEC_PER_SEC);
//間隔時間
uint64_t intervalTime = 1.0;
//誤差時間
uint64_t leewayTime = 0;
//設定定時器時間
dispatch_source_set_timer(self.timer, startTime, 1.0 * NSEC_PER_SEC, 0 * NSEC_PER_SEC);
//設定定時器回調事件
dispatch_source_set_event_handler(self.timer, ^{
//定時器事件代碼
NSLog(@"GCD定時器事件");
//如果定時器不需要重複,可以在這裡取消定時器
dispatch_source_cancel(self.timer);
});
//運作定時器
dispatch_resume(self.timer);
}
GCD計時器細節:我們之前在RunLoop一章中讨論過使用被界面滑動事件阻塞的問題,置于相同的場景下(GCD定時器放主線程),GCD定時器是不會受到UI界面滑動的印象的,其根本原因就是在于GCD定時器跟RunLoop是沒有關系的,它們是兩套獨立的機制,是以GCD的定時器不會受到
NSTimer
的限制。大家可以自己通過代碼體會一下。
RunLoopMode
另外需要注意一下,ARC環境下,GCD裡面的建立的一些對象都是不需要銷毀的。GCD已經幫我們做好了記憶體管理相關的事情。