天天看點

記憶體管理——定時器問題

記憶體管理系列文章

記憶體管理—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一章中讨論過使用

NSTimer

被界面滑動事件阻塞的問題,置于相同的場景下(GCD定時器放主線程),GCD定時器是不會受到UI界面滑動的印象的,其根本原因就是在于GCD定時器跟RunLoop是沒有關系的,它們是兩套獨立的機制,是以GCD的定時器不會受到

RunLoopMode

的限制。大家可以自己通過代碼體會一下。

另外需要注意一下,ARC環境下,GCD裡面的建立的一些對象都是不需要銷毀的。GCD已經幫我們做好了記憶體管理相關的事情。