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

內存管理傳送門🦋🦋🦋

內存管理剖析(一)—MRC時代的手動內存管理markdown

內存管理剖析(三)——iOS程序的內存佈局app

內存管理剖析(四)——autorelease原理分析框架

內存管理剖析(五)—— weak指針實現原理oop

CADisplayLink、NSTimer的循環引用問題

CADisplayLink QuartzCore框架下的的一種定時器,用在跟畫圖相關的處理當中。NSTimer 你們應該很熟悉,是咱們最經常使用的定時器。這兩種定時器分別提供以下兩個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參數,該target會被CADisplayLink/NSTimer強引用。若是CADisplayLink或者NSTimer做爲屬性被一個視圖控制器VC強引用,當咱們在調用上述兩個API的時候,target參數傳VC,這樣VC和CADisplayLink/NSTimer之間便會造成引用循環,沒法釋放,形成內存泄漏。圖示以下NSTimer/CADisplayLink產生循環引用post

NSTimer的解決方案1 經過使用別的API來添加NSTimer,如ui

(NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)ti invocation:(NSInvocation *)invocation repeats:(BOOL)yesOrNo;
複製代碼

而且將self經過__weak typeof(self) weakSelf == self;包裝成弱指針,傳入其中便可。atom

NSTimer的解決方案2 經過增長一箇中間代理對象來打破引用循環。請看下圖 如上圖所示,在timerVC之間增長一個代理對象otherObjecttimer的強指針target指向otherObjectotherObject的弱指針target指向VC,這樣就成功打破了引用循環。咱們之因此須要藉助第三者來破環,是由於NSTimer並不是開源,咱們沒法修改其內部target的強弱性。所以只能經過一個自定義的代理對象來作一層引用中轉,最終打破引用循環。spa

如今還有一個細節須要處理,增長代理對象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  ;
}
複製代碼

你能夠看到,NSProxyNSObject是同一層級的,所以也能夠吧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,首先仍是按照跟以前的案例的套路同樣,將VCtimerCLProxy2連接起來,咱們先不在CLProxy2對消息作任何處理,看一下會有什麼狀況,結果是報錯信息

2019-08-26 11:26:13.486949+0800 內存管理[3407:219430] *** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '*** -[NSProxy methodSignatureForSelector:] called!'`
複製代碼

能夠看出,向CLProxy2對象發送一個它沒有實現的方法(消息),最後會調用methodSignatureForSelector 方法。若是你很熟悉 【OC消息機制】 的話,對繼承自NSObject的類的實例對象發送消息,若是該對象沒有實現對應的方法的話,出現的報錯將是

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

也就是經典的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是否準時?

相信答案你們都會說:不許時。可是不許時的緣由未必每一個人都清楚。那這裏就來簡單梳理一下。 CADisplayLinkNSTimer底層都是靠RunLoop來實現的,也就是能夠把它們理解成RunLoop所須要處理的事件。咱們知道RunLoop能夠拿來刷新UI,處理定時器(CADisplayLinkNSTimer),處理點擊滑動事件等很是多的事情。這裏,就須要來了解一下RunLoop是如何觸發NSTimer任務的。RunLoop每循環一圈,都會處理必定的事件,會消耗必定的時間,可是具體耗時多少這個是沒法肯定的。 假如你開啓一個timer,隔1秒觸發定時器事件,RunLoop會開始累計每一圈循環的用時,當時間累計夠1秒,就會觸發定時器事件。你有興趣的話,是能夠在RunLoop的源碼裏面找到時間累加相關代碼的。能夠藉助下圖來加深理解NSTimer的準時觸發 若是RunLoop在某一圈任務過於繁重,就可能出現以下狀況NSTimer不許時狀況

因此CADisplayLinkNSTimer是沒法保證準時性的。

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已經幫咱們作好了內存管理相關的事情。

內存管理傳送門🦋🦋🦋

內存管理剖析(一)—MRC時代的手動內存管理

內存管理剖析(三)——iOS程序的內存佈局

內存管理剖析(四)——autorelease原理分析

內存管理剖析(五)—— weak指針實現原理

相關文章
相關標籤/搜索