小碼哥iOS學習筆記第二十三天: 內存管理-定時器

1、CADisplayLink

  • CADisplayLink: 使用頻率和屏幕的刷新頻率保持一致, 60FPSoop

  • 設置程序的界面結構以下圖所示, 其中橙色的界面就是ViewControllerui

  • ViewController中有以下代碼, ViewController有一個屬性CADisplayLink *displayLink
#import "ViewController.h"

@interface ViewController ()
@property (nonatomic, strong) CADisplayLink *displayLink;
@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    self.displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(displayLinkTest)];
    [self.displayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
}

- (void)displayLinkTest
{
    NSLog(@"%s", __func__);
}

- (void)dealloc
{
    NSLog(@"%s", __func__);
    [self.displayLink invalidate];
}

@end
複製代碼
  • 運行程序, 進入ViewController後能夠看到控制檯不停的打印

  • 即使退出控制器, 也能夠看到-dealloc根本沒有執行, 定時器依然在不停的調用
  • 此時, 就造成了ViewController-CADisplayLink的循環引用, 相似下圖

解決循環引用

  • 可使用一箇中間對象來解決循環引用問題

  • Proxy中代碼以下, 使用便利構造器建立Proxy對象, 同時存儲target
  • Proxy不實現任何target調用的方法, 而是使用消息轉發的方式, 將消息轉發給target, 這樣不論定時器調用任何方法, 都能交給target去執行
#import "Proxy.h"

@interface Proxy ()
@property (nonatomic, weak) id target;
@end

@implementation Proxy

+ (instancetype)proxyWithTarget:(id)target
{
    Proxy *proxy = [[Proxy alloc] init];
    proxy.target = target;
    return proxy;
}

- (id)forwardingTargetForSelector:(SEL)aSelector
{
    return self.target;
}
@end
複製代碼
  • 此時ViewController中代碼以下, CADisplayLink綁定[Proxy proxyWithTarget:self], 調用-displayLinkTest方法
  • 當運行程序時, 由於Proxy沒有實現-displayLinkTest方法, 此時Proxy就會經過消息轉發, 將displayLinkTest轉交給target去執行
#import "ViewController.h"
#import "Proxy.h"

@interface ViewController ()
@property (nonatomic, strong) CADisplayLink *displayLink;
@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    self.displayLink = [CADisplayLink displayLinkWithTarget:[Proxy proxyWithTarget:self] selector:@selector(displayLinkTest)];
    [self.displayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
}

- (void)displayLinkTest
{
    NSLog(@"%s", __func__);
}

- (void)dealloc
{
    NSLog(@"%s", __func__);
    [self.displayLink invalidate];
}

@end
複製代碼
  • 運行程序, 進入ViewController界面後, 能夠看到控制檯不停的打印, 當點擊返回按鈕, 退出ViewController後, 就會調用ViewController-dealloc方法, 中止定時器

  • 這樣, 就解決了ViewController-CADisplayLink的循環引用問題

2、NSTimer

  • NSTimerCADisplayLink相似, 也會形成循環引用問題
#import "ViewController.h"

@interface ViewController ()
@property (nonatomic, strong) NSTimer *timer;
@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    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
複製代碼
  • 執行程序, 進入ViewController能夠看到每一秒打印一次, 退出ViewController後打印也不會中止

  • 此時的循環結構以下圖

解決循環引用問題

  • CADisplayLink同樣, 使用中間對象Proxy便可
#import "ViewController.h"
#import "Proxy.h"

@interface ViewController ()
@property (nonatomic, strong) NSTimer *timer;
@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:[Proxy proxyWithTarget:self] selector:@selector(timerTest) userInfo:nil repeats:YES];
}

- (void)timerTest
{
    NSLog(@"%s", __func__);
}

- (void)dealloc
{
    NSLog(@"%s", __func__);
    [self.timer invalidate];
}

@end
複製代碼
  • 運行程序, 進入ViewController後控制檯持續打印, 退出ViewController後, 定時器中止

  • 此時的內存結構以下

3、NSProxy

  • NSProxy是與NSObject同級別的類, NSProxy的定義是下面這段代碼
@interface NSProxy <NSObject> {
    Class	isa;
}

+ (id)alloc;
+ (id)allocWithZone:(nullable NSZone *)zone NS_AUTOMATED_REFCOUNT_UNAVAILABLE;
+ (Class)class;

- (void)forwardInvocation:(NSInvocation *)invocation;
- (nullable NSMethodSignature *)methodSignatureForSelector:(SEL)sel NS_SWIFT_UNAVAILABLE("NSInvocation and related APIs not available");
- (void)dealloc;
- (void)finalize;
@property (readonly, copy) NSString *description;
@property (readonly, copy) NSString *debugDescription;
+ (BOOL)respondsToSelector:(SEL)aSelector;

- (BOOL)allowsWeakReference NS_UNAVAILABLE;
- (BOOL)retainWeakReference NS_UNAVAILABLE;

// - (id)forwardingTargetForSelector:(SEL)aSelector;

@end
複製代碼
  • NSProxy沒有任何的父類, 與NSObject同樣遵照<NSObject>協議
  • NSProxy是用來作消息轉發的類, 若是本身沒有實現目標方法, 那麼就會馬上進入消息轉發

一、使用NSProxy解決定時器內存管理問題

  • 定義BWProxy繼承自NSProxy, 並實現下列方法
@interface BWProxy : NSProxy

@property (nonatomic, weak) id target;
+ (instancetype)proxyWithTarget:(id)target;

@end

@implementation BWProxy

+ (instancetype)proxyWithTarget:(id)target
{
    // NSProxy沒有init方法, 只須要調用alloc建立對象便可
    BWProxy *proxy = [BWProxy alloc];
    proxy.target = target;
    return proxy;
}
@end
複製代碼
  • ViewController使用BWProxy替代上面的Proxy
#import "ViewController.h"
#import "BWProxy.h"

@interface ViewController ()

@property (nonatomic, strong) NSTimer *timer;

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:[BWProxy proxyWithTarget:self] selector:@selector(timerTest) userInfo:nil repeats:YES];
}

- (void)timerTest
{
    NSLog(@"%s", __func__);
}

- (void)dealloc
{
    NSLog(@"%s", __func__);
    [self.timer invalidate];
}

@end
複製代碼
  • 執行程序, 進入ViewController能夠看到, 有下面的報錯

  • 能夠看到, 報錯信息是-[NSProxy methodSignatureForSelector:] called!, 並非找不到timerTest方法
  • 咱們能夠在BWProxy中加入-methodSignatureForSelector:-forwardInvocation:兩個方法, 實現消息轉發來解決崩潰的問題
#import <Foundation/Foundation.h>

@interface BWProxy : NSProxy
@property (nonatomic, weak) id target;
+ (instancetype)proxyWithTarget:(id)target;
@end

@implementation BWProxy

+ (instancetype)proxyWithTarget:(id)target
{
    // NSProxy沒有init方法, 只須要調用alloc建立對象便可
    BWProxy *proxy = [BWProxy alloc];
    proxy.target = target;
    return proxy;
}

- (nullable NSMethodSignature *)methodSignatureForSelector:(SEL)sel
{
    return [self.target methodSignatureForSelector:sel];
}

- (void)forwardInvocation:(NSInvocation *)invocation
{
    [invocation invokeWithTarget:self.target];
}

@end
複製代碼
  • 運行程序, 進入ViewController後, 再次退出, 能夠看到NSTimer中止, ViewController被釋放

  • CADisplayLinkNSTimer同樣, 這裏就再也不贅述

二、-isKindOfClass:

  • 使用上面的ProxyBWProxy, 實現下面的代碼
Proxy *proxy1 = [Proxy proxyWithTarget:self];
BWProxy *proxy2 = [BWProxy proxyWithTarget:self];
NSLog(@"%d", [proxy1 isKindOfClass:[ViewController class]]);
NSLog(@"%d", [proxy2 isKindOfClass:[ViewController class]]);
複製代碼
  • 能夠看到控制檯的打印以下

  • proxy1的基類是NSObject, 因此打印爲0
  • proxy2其實是進行了消息轉發, 將isKindOfClass:轉發給了target, 也就是ViewController, 因此打印是1
  • GUNStep中也能夠看到實現過程

4、GCD定時器

  • NSTimer依賴於RunLoop,若是RunLoop的任務過於繁重,可能會致使NSTimer不許時
  • GCD定時器不依賴於RunLoop, 會更加的準時
#import "ViewController.h"

@interface ViewController ()
@property (nonatomic, strong) dispatch_source_t timer;
@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    // 獲取主隊列
    dispatch_queue_t queue = dispatch_get_main_queue();
    // 建立定時器, 在主線程中調用
    dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
    // 2秒後執行
    NSTimeInterval start = 2.0;
    // 執行間隔1秒
    NSTimeInterval interval = 1.0;
    // 設置定時器
    dispatch_source_set_timer(timer,
                              dispatch_time(DISPATCH_TIME_NOW, start * NSEC_PER_SEC),
                              interval * NSEC_PER_SEC,
                              0);
    // 設置回調
    __weak typeof(self) weakSelf = self;
    dispatch_source_set_event_handler(timer, ^{
        [weakSelf timerTest];
    });
    // 啓動定時器
    dispatch_resume(timer);
    self.timer = timer;
}

- (void)timerTest
{
    NSLog(@"%s", __func__);
}

- (void)dealloc
{
    NSLog(@"%s", __func__);
}

@end
複製代碼
  • 運行程序, 進入ViewController, 能夠看到定時器的打印, 退出ViewController能夠看到-dealloc被調用, 定時器中止

相關文章
相關標籤/搜索