內(nèi)存管理剖析(二)——定時器問題

CADisplayLink复颈、NSTimer的循環(huán)引用問題

CADisplayLinkQuartzCore框架下的的一種定時器,用在跟畫圖相關(guān)的處理當(dāng)中。NSTimer大家應(yīng)該很熟悉枕赵,是我們最常用的定時器渴析。這兩種定時器分別提供如下兩個API

+ (CADisplayLink *)displayLinkWithTarget:(id)target selector:(SEL)sel;
+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo;

這兩個API里面都有target參數(shù)晚伙,該target會被CADisplayLink/NSTimer強引用。如果CADisplayLink或者NSTimer作為屬性被一個視圖控制器VC強引用俭茧,當(dāng)我們在調(diào)用上述兩個API的時候咆疗,target參數(shù)傳VC,這樣VC和CADisplayLink/NSTimer之間便會形成引用循環(huán)母债,無法釋放午磁,造成內(nèi)存泄漏。圖示如下

NSTimer/CADisplayLink產(chǎn)生循環(huán)引用

NSTimer的解決方案1
通過使用別的API來添加NSTimer毡们,如

 (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)ti invocation:(NSInvocation *)invocation repeats:(BOOL)yesOrNo;

并且將self通過__weak typeof(self) weakSelf == self;包裝成弱指針迅皇,傳入其中即可。

NSTimer的解決方案2
通過增加一個中間代理對象來打破引用循環(huán)衙熔。請看下圖


如上圖所示登颓,在timerVC之間增加一個代理對象otherObjecttimer的強指針target指向otherObject青责,otherObject的弱指針target指向VC挺据,這樣就成功打破了引用循環(huán)取具。我們之所以需要借助第三者來破環(huán),是因為NSTimer并非開源扁耐,我們無法修改其內(nèi)部target的強弱性暇检。因此只能通過一個自定義的代理對象來做一層引用中轉(zhuǎn),最終打破引用循環(huán)婉称。

現(xiàn)在還有一個細節(jié)需要處理块仆,增加代理對象otherObject之前,是由timer通過target直接調(diào)用VC里面的定時器方法的⊥醢担現(xiàn)在中間多了一層otherObject悔据,該如何實現(xiàn)定時器方法的調(diào)用呢?其實方法蠻多的俗壹,相信大家都能想出一些解決方案科汗。這里就直接推薦一種比較巧妙的方法——通過消息轉(zhuǎn)發(fā)。如下圖

代理對象的消息轉(zhuǎn)發(fā)

因為代理對象的本質(zhì)目的绷雏,就是打破引用循環(huán)头滔,并且傳遞方法,了解OC消息機制的原理前提下坤检,你應(yīng)該很好理解消息轉(zhuǎn)發(fā)的作用,正好可以巧妙的用在這個場景下期吓。請好好體會一下早歇。

下面是一份代碼案例

#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用來保證調(diào)用頻率和屏幕的刷幀頻率一致,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,但應(yīng)該很少能用到悬襟,這是一個非常特殊的類衅码。我們來對比一下它和NSObject的定義的對比

@interface NSProxy <NSObject> {
    Class   isa;
}

@interface NSObject <NSObject> {
    Class isa  ;
}

你可以看到,NSProxyNSObject是同一層級的脊岳,因此也可以吧NSProxy理解成一個基類逝段。他們都遵守<NSObject>協(xié)議,他們都沒有父類割捅。

那么NSProxy是干嘛用的呢奶躯?其實它就是專門用來解決通過中間對象轉(zhuǎn)發(fā)消息的問題的。

這里先貼出案例代碼

#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對象不需要調(diào)用init亿驾,因為它本來就沒有init方法嘹黔,直接alloc之后就可以使用
    CLProxy2 *proxy = [CLProxy2 alloc];
    proxy.target = target;
    return proxy;
    
}

@end

CLProxy2繼承自NSProxy,首先還是按照跟之前的案例的套路一樣,將VC儡蔓,timerCLProxy2鏈接起來郭蕉,我們先不在CLProxy2對消息做任何處理,看一下會有什么情況喂江,結(jié)果是報錯信息

2019-08-26 11:26:13.486949+0800 內(nèi)存管理[3407:219430] *** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '*** -[NSProxy methodSignatureForSelector:] called!'

可以看出召锈,向CLProxy2對象發(fā)送一個它沒有實現(xiàn)的方法(消息),最后會調(diào)用methodSignatureForSelector方法获询。如果你很熟悉【OC消息機制】的話涨岁,對繼承自NSObject的類的實例對象發(fā)送消息,如果該對象沒有實現(xiàn)對應(yīng)的方法的話吉嚣,出現(xiàn)的報錯將是

2019-08-26 11:31:01.254135+0800 內(nèi)存管理[3456:222524] *** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[CLProxy timerTest]: unrecognized selector sent to instance 0x600000d64210'

也就是經(jīng)典的unrecognized selector sent to instance梢薪。
這是怎么回事呢?其實NSProxy接受到消息之后的處理流程如下

  • [proxyObj message]
  • (1)到proxyObj的類對象里面尋找對應(yīng)的方法尝哆,找到就調(diào)用
  • (2)嘗試進入父類對象遞歸查找方法(省略該步驟)
  • (3)找不到方法秉撇,嘗試進行方法動態(tài)解析(省略該步驟)
  • (4)嘗試調(diào)用forwardingTargetForSelector進行消息轉(zhuǎn)發(fā)`(省略該步驟)
  • (5)嘗試調(diào)用methodSignatureForSelector+forwardInvocation進行消息轉(zhuǎn)發(fā)。

因此可以發(fā)現(xiàn)较解,相比較完整的消息機制流程畜疾,NSProxy的處理過程中,省略了(2)印衔、(3)、(4)步驟姥敛。所以它相比于NSObject奸焙,效率更高,我們的今天所討論的代理對象傳遞消息問題彤敛,正好可以通過NSProxy來解決与帆,提升效率。根絕第(5)步驟墨榄,我們只需要在子類里面實現(xiàn)methodSignatureForSelector+forwardInvocation這兩個方法即可玄糟,上面的CLProxy2.m代碼修改如下即可

#import "CLProxy2.h"

@implementation CLProxy2

+(instancetype)proxyWithTarget: (id)target {
//NSProxy對象不需要調(diào)用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來實現(xiàn)。

如果別人問你CADisplayLink之剧、NSTimer是否準時郭卫?

相信答案大家都會說:不準時。但是不準時的原因未必每個人都清楚背稼。那這里就來簡單梳理一下贰军。
CADisplayLinkNSTimer底層都是靠RunLoop來實現(xiàn)的蟹肘,也就是可以把它們理解成RunLoop所需要處理的事件词疼。我們知道RunLoop可以拿來刷新UI俯树,處理定時器(CADisplayLinkNSTimer)贰盗,處理點擊滑動事件等非常多的事情许饿。這里,就需要來了解一下RunLoop是如何觸發(fā)NSTimer任務(wù)的童太。RunLoop每循環(huán)一圈米辐,都會處理一定的事件,會消耗一定的時間书释,但是具體耗時多少這個是無法確定的翘贮。
假如你開啟一個timer,隔1秒觸發(fā)定時器事件爆惧,RunLoop會開始累計每一圈循環(huán)的用時狸页,當(dāng)時間累計夠1秒,就會觸發(fā)定時器事件扯再。你有興趣的話芍耘,是可以在RunLoop的源碼里面找到時間累加相關(guān)代碼的∠ㄗ瑁可以借助下圖來加深理解

NSTimer的準時觸發(fā)

如果RunLoop在某一圈任務(wù)過于繁重斋竞,就可能出現(xiàn)如下情況
NSTimer不準時情況

所以CADisplayLinkNSTimer是無法保證準時性的秃殉。

GCD定時器

GCD的定時器是直接跟系統(tǒng)內(nèi)核掛鉤坝初,不依賴于RunLoop機制,所以時間是相當(dāng)精準的钾军。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;
    //設(shè)置定時器時間
    dispatch_source_set_timer(self.timer, startTime, 1.0 * NSEC_PER_SEC, 0 * NSEC_PER_SEC);
    //設(shè)置定時器回調(diào)事件
    dispatch_source_set_event_handler(self.timer, ^{
        //定時器事件代碼
        NSLog(@"GCD定時器事件");
        //如果定時器不需要重復(fù),可以在這里取消定時器
        dispatch_source_cancel(self.timer);
    });
    //運行定時器
    dispatch_resume(self.timer);
    
}

GCD計時器細節(jié):我們之前在RunLoop一章中討論過使用NSTimer被界面滑動事件阻塞的問題吏恭,置于相同的場景下(GCD定時器放主線程)拗小,GCD定時器是不會受到UI界面滑動的印象的,其根本原因就是在于GCD定時器跟RunLoop是沒有關(guān)系的,它們是兩套獨立的機制樱哼,因此GCD的定時器不會受到RunLoopMode的約束哀九。大家可以自己通過代碼體會一下。

另外需要注意一下唇礁,ARC環(huán)境下勾栗,GCD里面的創(chuàng)建的一些對象都是不需要銷毀的。GCD已經(jīng)幫我們做好了內(nèi)存管理相關(guān)的事情盏筐。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末围俘,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌界牡,老刑警劉巖簿寂,帶你破解...
    沈念sama閱讀 222,252評論 6 516
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異宿亡,居然都是意外死亡常遂,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,886評論 3 399
  • 文/潘曉璐 我一進店門挽荠,熙熙樓的掌柜王于貴愁眉苦臉地迎上來克胳,“玉大人,你說我怎么就攤上這事圈匆∧恚” “怎么了?”我有些...
    開封第一講書人閱讀 168,814評論 0 361
  • 文/不壞的土叔 我叫張陵跃赚,是天一觀的道長笆搓。 經(jīng)常有香客問我,道長纬傲,這世上最難降的妖魔是什么满败? 我笑而不...
    開封第一講書人閱讀 59,869評論 1 299
  • 正文 為了忘掉前任,我火速辦了婚禮叹括,結(jié)果婚禮上算墨,老公的妹妹穿的比我還像新娘。我一直安慰自己汁雷,他們只是感情好米同,可當(dāng)我...
    茶點故事閱讀 68,888評論 6 398
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著摔竿,像睡著了一般。 火紅的嫁衣襯著肌膚如雪少孝。 梳的紋絲不亂的頭發(fā)上继低,一...
    開封第一講書人閱讀 52,475評論 1 312
  • 那天,我揣著相機與錄音稍走,去河邊找鬼袁翁。 笑死,一個胖子當(dāng)著我的面吹牛婿脸,可吹牛的內(nèi)容都是我干的粱胜。 我是一名探鬼主播,決...
    沈念sama閱讀 41,010評論 3 422
  • 文/蒼蘭香墨 我猛地睜開眼狐树,長吁一口氣:“原來是場噩夢啊……” “哼焙压!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,924評論 0 277
  • 序言:老撾萬榮一對情侶失蹤涯曲,失蹤者是張志新(化名)和其女友劉穎野哭,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體幻件,經(jīng)...
    沈念sama閱讀 46,469評論 1 319
  • 正文 獨居荒郊野嶺守林人離奇死亡拨黔,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,552評論 3 342
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了绰沥。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片篱蝇。...
    茶點故事閱讀 40,680評論 1 353
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖徽曲,靈堂內(nèi)的尸體忽然破棺而出零截,到底是詐尸還是另有隱情,我是刑警寧澤疟位,帶...
    沈念sama閱讀 36,362評論 5 351
  • 正文 年R本政府宣布瞻润,位于F島的核電站,受9級特大地震影響甜刻,放射性物質(zhì)發(fā)生泄漏绍撞。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 42,037評論 3 335
  • 文/蒙蒙 一得院、第九天 我趴在偏房一處隱蔽的房頂上張望傻铣。 院中可真熱鬧,春花似錦祥绞、人聲如沸非洲。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,519評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽两踏。三九已至,卻和暖如春兜喻,著一層夾襖步出監(jiān)牢的瞬間梦染,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,621評論 1 274
  • 我被黑心中介騙來泰國打工朴皆, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留帕识,地道東北人。 一個月前我還...
    沈念sama閱讀 49,099評論 3 378
  • 正文 我出身青樓遂铡,卻偏偏與公主長得像肮疗,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子扒接,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,691評論 2 361

推薦閱讀更多精彩內(nèi)容