From 6e52009ff08b70623048fe97de2fa2a17198f541 Mon Sep 17 00:00:00 2001 From: Matthew Chen Date: Tue, 9 May 2017 14:38:49 -0400 Subject: [PATCH] =?UTF-8?q?Rework=20the=20=E2=80=9Cdisappearing=20messages?= =?UTF-8?q?=E2=80=9D=20logic.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit // FREEBIE --- src/Devices/OWSReadReceiptsProcessor.m | 4 +- src/Devices/OWSRecordTranscriptJob.m | 1 - src/Messages/Interactions/TSMessage.m | 1 - src/Messages/OWSDisappearingMessagesJob.h | 23 ++- src/Messages/OWSDisappearingMessagesJob.m | 181 ++++++++++++++---- src/Messages/OWSIncomingMessageReadObserver.m | 9 +- src/Messages/OWSMessageSender.m | 10 +- src/Messages/TSMessagesManager.h | 1 - src/Messages/TSMessagesManager.m | 8 +- src/Util/NSTimer+OWS.h | 19 ++ src/Util/NSTimer+OWS.m | 69 +++++++ 11 files changed, 257 insertions(+), 69 deletions(-) create mode 100644 src/Util/NSTimer+OWS.h create mode 100644 src/Util/NSTimer+OWS.m diff --git a/src/Devices/OWSReadReceiptsProcessor.m b/src/Devices/OWSReadReceiptsProcessor.m index a41bcc6d6..30d070ef7 100644 --- a/src/Devices/OWSReadReceiptsProcessor.m +++ b/src/Devices/OWSReadReceiptsProcessor.m @@ -17,7 +17,6 @@ NSString *const OWSReadReceiptsProcessorMarkedMessageAsReadNotification = @interface OWSReadReceiptsProcessor () @property (nonatomic, readonly) NSArray *readReceipts; -@property (nonatomic, readonly) OWSDisappearingMessagesJob *disappearingMessagesJob; @property (nonatomic, readonly) TSStorageManager *storageManager; @end @@ -34,7 +33,6 @@ NSString *const OWSReadReceiptsProcessorMarkedMessageAsReadNotification = _readReceipts = [readReceipts copy]; _storageManager = storageManager; - _disappearingMessagesJob = [[OWSDisappearingMessagesJob alloc] initWithStorageManager:storageManager]; return self; } @@ -85,7 +83,7 @@ NSString *const OWSReadReceiptsProcessorMarkedMessageAsReadNotification = [TSIncomingMessage findMessageWithAuthorId:readReceipt.senderId timestamp:readReceipt.timestamp]; if (message) { [message markAsReadFromReadReceipt]; - [self.disappearingMessagesJob setExpirationForMessage:message expirationStartedAt:readReceipt.timestamp]; + [OWSDisappearingMessagesJob setExpirationForMessage:message expirationStartedAt:readReceipt.timestamp]; // If it was previously saved, no need to keep it around any longer. [readReceipt remove]; [[NSNotificationCenter defaultCenter] diff --git a/src/Devices/OWSRecordTranscriptJob.m b/src/Devices/OWSRecordTranscriptJob.m index be351c83e..2a6bcea7b 100644 --- a/src/Devices/OWSRecordTranscriptJob.m +++ b/src/Devices/OWSRecordTranscriptJob.m @@ -4,7 +4,6 @@ #import "OWSRecordTranscriptJob.h" #import "OWSAttachmentsProcessor.h" -#import "OWSDisappearingMessagesJob.h" #import "OWSIncomingSentMessageTranscript.h" #import "OWSMessageSender.h" #import "TSInfoMessage.h" diff --git a/src/Messages/Interactions/TSMessage.m b/src/Messages/Interactions/TSMessage.m index 42fc86072..280ae1162 100644 --- a/src/Messages/Interactions/TSMessage.m +++ b/src/Messages/Interactions/TSMessage.m @@ -4,7 +4,6 @@ #import "TSMessage.h" #import "NSDate+millisecondTimeStamp.h" -#import "OWSDisappearingMessagesJob.h" #import "TSAttachment.h" #import "TSAttachmentPointer.h" #import "TSThread.h" diff --git a/src/Messages/OWSDisappearingMessagesJob.h b/src/Messages/OWSDisappearingMessagesJob.h index 64e6f1cda..a12acc3d9 100644 --- a/src/Messages/OWSDisappearingMessagesJob.h +++ b/src/Messages/OWSDisappearingMessagesJob.h @@ -1,5 +1,6 @@ -// Created by Michael Kirk on 9/23/16. -// Copyright © 2016 Open Whisper Systems. All rights reserved. +// +// Copyright (c) 2017 Open Whisper Systems. All rights reserved. +// NS_ASSUME_NONNULL_BEGIN @@ -10,15 +11,13 @@ NS_ASSUME_NONNULL_BEGIN @interface OWSDisappearingMessagesJob : NSObject -- (instancetype)init NS_UNAVAILABLE; -- (instancetype)initWithStorageManager:(TSStorageManager *)storageManager NS_DESIGNATED_INITIALIZER; ++ (instancetype)sharedJob; -- (void)run; -- (void)setExpirationsForThread:(TSThread *)thread; -- (void)setExpirationForMessage:(TSMessage *)message; -- (void)setExpirationForMessage:(TSMessage *)message expirationStartedAt:(uint64_t)expirationStartedAt; -- (void)runBy:(uint64_t)millisecondTimestamp; +- (instancetype)init NS_UNAVAILABLE; ++ (void)setExpirationsForThread:(TSThread *)thread; ++ (void)setExpirationForMessage:(TSMessage *)message; ++ (void)setExpirationForMessage:(TSMessage *)message expirationStartedAt:(uint64_t)expirationStartedAt; /** * Synchronize our disappearing messages settings with that of the given message. Useful so we can @@ -31,9 +30,13 @@ NS_ASSUME_NONNULL_BEGIN * @param contactsManager * Provides the contact name responsible for any configuration changes in an info message. */ -- (void)becomeConsistentWithConfigurationForMessage:(TSMessage *)message ++ (void)becomeConsistentWithConfigurationForMessage:(TSMessage *)message contactsManager:(id)contactsManager; +// Clean up any messages that expired since last launch immediately +// and continue cleaning in the background. +- (void)startIfNecessary; + @end NS_ASSUME_NONNULL_END diff --git a/src/Messages/OWSDisappearingMessagesJob.m b/src/Messages/OWSDisappearingMessagesJob.m index 485ffa0a5..e542b8e7c 100644 --- a/src/Messages/OWSDisappearingMessagesJob.m +++ b/src/Messages/OWSDisappearingMessagesJob.m @@ -1,26 +1,46 @@ -// Created by Michael Kirk on 9/23/16. -// Copyright © 2016 Open Whisper Systems. All rights reserved. +// +// Copyright (c) 2017 Open Whisper Systems. All rights reserved. +// #import "OWSDisappearingMessagesJob.h" #import "ContactsManagerProtocol.h" #import "NSDate+millisecondTimeStamp.h" +#import "NSTimer+OWS.h" #import "OWSDisappearingConfigurationUpdateInfoMessage.h" #import "OWSDisappearingMessagesConfiguration.h" #import "OWSDisappearingMessagesFinder.h" #import "TSIncomingMessage.h" #import "TSMessage.h" +#import "TSStorageManager.h" NS_ASSUME_NONNULL_BEGIN @interface OWSDisappearingMessagesJob () +// This property should only be accessed on the serialQueue. @property (nonatomic, readonly) OWSDisappearingMessagesFinder *disappearingMessagesFinder; -@property (atomic) uint64_t scheduledAt; + +// These three properties should only be accessed on the main thread. +@property (nonatomic) BOOL hasStarted; +@property (nonatomic, nullable) NSTimer *timer; +@property (nonatomic, nullable) NSDate *timerScheduleDate; @end +#pragma mark - + @implementation OWSDisappearingMessagesJob ++ (instancetype)sharedJob +{ + static OWSDisappearingMessagesJob *sharedJob = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + sharedJob = [[self alloc] initWithStorageManager:[TSStorageManager sharedManager]]; + }); + return sharedJob; +} + - (instancetype)initWithStorageManager:(TSStorageManager *)storageManager { self = [super init]; @@ -28,12 +48,23 @@ NS_ASSUME_NONNULL_BEGIN return self; } - _scheduledAt = ULLONG_MAX; _disappearingMessagesFinder = [[OWSDisappearingMessagesFinder alloc] initWithStorageManager:storageManager]; + OWSSingletonAssert(); + return self; } ++ (dispatch_queue_t)serialQueue +{ + static dispatch_queue_t queue = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + queue = dispatch_queue_create("org.whispersystems.disappearing.messages", DISPATCH_QUEUE_SERIAL); + }); + return queue; +} + - (void)run { uint64_t now = [NSDate ows_millisecondTimeStamp]; @@ -46,18 +77,17 @@ NS_ASSUME_NONNULL_BEGIN return; } - DDLogDebug(@"%@ removing message which expired at: %lld", self.tag, message.expiresAt); + DDLogDebug(@"%@ Removing message which expired at: %lld", self.tag, message.expiresAt); [message remove]; expirationCount++; }]; - DDLogDebug(@"%@ removed %u expired messages", self.tag, expirationCount); + DDLogDebug(@"%@ Removed %u expired messages", self.tag, expirationCount); } - (void)runLoop { - // allow next runAt to schedule. - self.scheduledAt = ULLONG_MAX; + DDLogVerbose(@"%@ Run", self.tag); [self run]; @@ -69,23 +99,19 @@ NS_ASSUME_NONNULL_BEGIN unsigned int delaySeconds = (10 * 60); // 10 minutes. DDLogDebug( @"%@ No more expiring messages. Setting next check %u seconds into the future", self.tag, delaySeconds); - [self runBy:now + delaySeconds * 1000]; + [self runByDate:[NSDate ows_dateWithMillisecondsSince1970:now + delaySeconds * 1000]]; return; } uint64_t nextExpirationAt = [nextExpirationTimestampNumber unsignedLongLongValue]; - uint64_t runByMilliseconds; - if (nextExpirationAt < now + 1000) { - DDLogWarn(@"%@ Next run requested at %llu, which is too soon. Delaying by 1 sec to prevent churn", - self.tag, - nextExpirationAt); - runByMilliseconds = now + 1000; - } else { - runByMilliseconds = nextExpirationAt; - } + [self runByDate:[NSDate ows_dateWithMillisecondsSince1970:MAX(nextExpirationAt, now)]]; +} - DDLogVerbose(@"%@ Requesting next expiration to run by: %llu", self.tag, nextExpirationAt); - [self runBy:runByMilliseconds]; ++ (void)setExpirationForMessage:(TSMessage *)message +{ + dispatch_async(self.serialQueue, ^{ + [[self sharedJob] setExpirationForMessage:message]; + }); } - (void)setExpirationForMessage:(TSMessage *)message @@ -104,6 +130,13 @@ NS_ASSUME_NONNULL_BEGIN [self setExpirationForMessage:message expirationStartedAt:[NSDate ows_millisecondTimeStamp]]; } ++ (void)setExpirationForMessage:(TSMessage *)message expirationStartedAt:(uint64_t)expirationStartedAt +{ + dispatch_async(self.serialQueue, ^{ + [[self sharedJob] setExpirationForMessage:message expirationStartedAt:expirationStartedAt]; + }); +} + - (void)setExpirationForMessage:(TSMessage *)message expirationStartedAt:(uint64_t)expirationStartedAt { if (!message.isExpiringMessage) { @@ -120,7 +153,14 @@ NS_ASSUME_NONNULL_BEGIN } // Necessary that the async expiration run happens *after* the message is saved with expiration configuration. - [self runBy:message.expiresAt]; + [self runByDate:[NSDate ows_dateWithMillisecondsSince1970:message.expiresAt]]; +} + ++ (void)setExpirationsForThread:(TSThread *)thread +{ + dispatch_async(self.serialQueue, ^{ + [[self sharedJob] setExpirationsForThread:thread]; + }); } - (void)setExpirationsForThread:(TSThread *)thread @@ -138,26 +178,14 @@ NS_ASSUME_NONNULL_BEGIN }]; } -- (void)runBy:(uint64_t)timestamp ++ (void)becomeConsistentWithConfigurationForMessage:(TSMessage *)message + contactsManager:(id)contactsManager { - // Prevent amplification. - if (timestamp >= self.scheduledAt) { - DDLogVerbose(@"%@ expiration already scheduled before %llu", self.tag, timestamp); - return; - } - - // Update Schedule - DDLogVerbose(@"%@ Scheduled expiration run at %llu", self.tag, timestamp); - self.scheduledAt = timestamp; - uint64_t millisecondsDelay = timestamp - [NSDate ows_millisecondTimeStamp]; - dispatch_after(dispatch_time(DISPATCH_TIME_NOW, NSEC_PER_MSEC * millisecondsDelay), - dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), - ^{ - [self runLoop]; - }); + dispatch_async(self.serialQueue, ^{ + [[self sharedJob] becomeConsistentWithConfigurationForMessage:message contactsManager:contactsManager]; + }); } - - (void)becomeConsistentWithConfigurationForMessage:(TSMessage *)message contactsManager:(id)contactsManager { @@ -206,6 +234,83 @@ NS_ASSUME_NONNULL_BEGIN } } +- (void)startIfNecessary +{ + dispatch_async(dispatch_get_main_queue(), ^{ + if (self.hasStarted) { + return; + } + self.hasStarted = YES; + + [self runNow]; + }); +} + +- (void)runNow +{ + [self runByDate:[NSDate new] ignoreMinDelay:YES]; +} + +- (void)runByDate:(NSDate *)date +{ + [self runByDate:date ignoreMinDelay:NO]; +} + +- (void)runByDate:(NSDate *)date ignoreMinDelay:(BOOL)ignoreMinDelay +{ + OWSAssert(date); + + NSDateFormatter *dateFormatter = [NSDateFormatter new]; + dateFormatter.dateStyle = NSDateFormatterShortStyle; + dateFormatter.timeStyle = kCFDateFormatterMediumStyle; + + dispatch_async(dispatch_get_main_queue(), ^{ + // Don't run more often than once per second. + const NSTimeInterval kMinDelaySeconds = ignoreMinDelay ? 0.f : 1.f; + // Don't run less often than once per N minutes. + const NSTimeInterval kMaxDelaySeconds = 5 * 60.f; + NSTimeInterval delaySeconds + = MAX(kMinDelaySeconds, MIN(kMaxDelaySeconds, [date timeIntervalSinceDate:[NSDate new]])); + NSDate *timerScheduleDate = [NSDate dateWithTimeIntervalSinceNow:delaySeconds]; + if (self.timerScheduleDate && [timerScheduleDate timeIntervalSinceDate:self.timerScheduleDate] > 0) { + DDLogVerbose(@"%@ Request to run at %@ (%d sec.) ignored due to scheduled run at %@ (%d sec.)", + self.tag, + [dateFormatter stringFromDate:date], + (int)round(MAX(0, [date timeIntervalSinceDate:[NSDate new]])), + [dateFormatter stringFromDate:self.timerScheduleDate], + (int)round(MAX(0, [self.timerScheduleDate timeIntervalSinceDate:[NSDate new]]))); + return; + } + + // Update Schedule + NSDateFormatter *dateFormatter = [NSDateFormatter new]; + dateFormatter.dateStyle = NSDateFormatterShortStyle; + dateFormatter.timeStyle = kCFDateFormatterMediumStyle; + DDLogVerbose(@"%@ Scheduled run at %@ (%d sec.)", + self.tag, + [dateFormatter stringFromDate:timerScheduleDate], + (int)round(MAX(0, [timerScheduleDate timeIntervalSinceDate:[NSDate new]]))); + self.timerScheduleDate = timerScheduleDate; + [self.timer invalidate]; + self.timer = [NSTimer weakScheduledTimerWithTimeInterval:delaySeconds + target:self + selector:@selector(timerDidFire) + userInfo:nil + repeats:NO]; + }); +} + +- (void)timerDidFire +{ + [self.timer invalidate]; + self.timer = nil; + self.timerScheduleDate = nil; + + dispatch_async(OWSDisappearingMessagesJob.serialQueue, ^{ + [self runLoop]; + }); +} + #pragma mark - Logging + (NSString *)tag diff --git a/src/Messages/OWSIncomingMessageReadObserver.m b/src/Messages/OWSIncomingMessageReadObserver.m index 75eec63d4..13c16ff6f 100644 --- a/src/Messages/OWSIncomingMessageReadObserver.m +++ b/src/Messages/OWSIncomingMessageReadObserver.m @@ -1,5 +1,6 @@ -// Created by Michael Kirk on 9/24/16. -// Copyright © 2016 Open Whisper Systems. All rights reserved. +// +// Copyright (c) 2017 Open Whisper Systems. All rights reserved. +// #import "OWSIncomingMessageReadObserver.h" #import "NSDate+millisecondTimeStamp.h" @@ -13,7 +14,6 @@ NS_ASSUME_NONNULL_BEGIN @interface OWSIncomingMessageReadObserver () @property BOOL isObserving; -@property (nonatomic, readonly) OWSDisappearingMessagesJob *disappearingMessagesJob; @property (nonatomic, readonly) OWSSendReadReceiptsJob *sendReadReceiptsJob; @end @@ -34,7 +34,6 @@ NS_ASSUME_NONNULL_BEGIN } _isObserving = NO; - _disappearingMessagesJob = [[OWSDisappearingMessagesJob alloc] initWithStorageManager:storageManager]; _sendReadReceiptsJob = [[OWSSendReadReceiptsJob alloc] initWithMessageSender:messageSender]; return self; @@ -61,7 +60,7 @@ NS_ASSUME_NONNULL_BEGIN } TSIncomingMessage *message = (TSIncomingMessage *)notification.object; - [self.disappearingMessagesJob setExpirationForMessage:message]; + [OWSDisappearingMessagesJob setExpirationForMessage:message]; [self.sendReadReceiptsJob runWith:message]; } diff --git a/src/Messages/OWSMessageSender.m b/src/Messages/OWSMessageSender.m index 86b419f7a..dad0364d0 100644 --- a/src/Messages/OWSMessageSender.m +++ b/src/Messages/OWSMessageSender.m @@ -341,7 +341,6 @@ NSString *const OWSMessageSenderRateLimitedException = @"RateLimitedException"; @property (nonatomic, readonly) YapDatabaseConnection *dbConnection; @property (nonatomic, readonly) id contactsManager; @property (nonatomic, readonly) ContactsUpdater *contactsUpdater; -@property (nonatomic, readonly) OWSDisappearingMessagesJob *disappearingMessagesJob; @property (atomic, readonly) NSMutableDictionary *sendingQueueMap; @end @@ -366,7 +365,6 @@ NSString *const OWSMessageSenderRateLimitedException = @"RateLimitedException"; _uploadingService = [[OWSUploadingService alloc] initWithNetworkManager:networkManager]; _dbConnection = storageManager.newDatabaseConnection; - _disappearingMessagesJob = [[OWSDisappearingMessagesJob alloc] initWithStorageManager:storageManager]; OWSSingletonAssert(); @@ -1060,20 +1058,20 @@ NSString *const OWSMessageSenderRateLimitedException = @"RateLimitedException"; [self sendSyncTranscriptForMessage:message]; } - [self.disappearingMessagesJob setExpirationForMessage:message]; + [OWSDisappearingMessagesJob setExpirationForMessage:message]; } - (void)handleMessageSentRemotely:(TSOutgoingMessage *)message sentAt:(uint64_t)sentAt { [message updateWithWasSentAndDelivered]; [self becomeConsistentWithDisappearingConfigurationForMessage:message]; - [self.disappearingMessagesJob setExpirationForMessage:message expirationStartedAt:sentAt]; + [OWSDisappearingMessagesJob setExpirationForMessage:message expirationStartedAt:sentAt]; } - (void)becomeConsistentWithDisappearingConfigurationForMessage:(TSOutgoingMessage *)outgoingMessage { - [self.disappearingMessagesJob becomeConsistentWithConfigurationForMessage:outgoingMessage - contactsManager:self.contactsManager]; + [OWSDisappearingMessagesJob becomeConsistentWithConfigurationForMessage:outgoingMessage + contactsManager:self.contactsManager]; } - (void)handleSendToMyself:(TSOutgoingMessage *)outgoingMessage diff --git a/src/Messages/TSMessagesManager.h b/src/Messages/TSMessagesManager.h index a558cddf6..0a0951841 100644 --- a/src/Messages/TSMessagesManager.h +++ b/src/Messages/TSMessagesManager.h @@ -13,7 +13,6 @@ NS_ASSUME_NONNULL_BEGIN @class OWSSignalServiceProtosEnvelope; @class OWSSignalServiceProtosDataMessage; @class ContactsUpdater; -@class OWSDisappearingMessagesJob; @class OWSMessageSender; @protocol ContactsManagerProtocol; @protocol OWSCallMessageHandler; diff --git a/src/Messages/TSMessagesManager.m b/src/Messages/TSMessagesManager.m index bbc1f592a..3f0aea574 100644 --- a/src/Messages/TSMessagesManager.m +++ b/src/Messages/TSMessagesManager.m @@ -48,12 +48,13 @@ NS_ASSUME_NONNULL_BEGIN @property (nonatomic, readonly) id contactsManager; @property (nonatomic, readonly) TSStorageManager *storageManager; @property (nonatomic, readonly) OWSMessageSender *messageSender; -@property (nonatomic, readonly) OWSDisappearingMessagesJob *disappearingMessagesJob; @property (nonatomic, readonly) OWSIncomingMessageFinder *incomingMessageFinder; @property (nonatomic, readonly) OWSBlockingManager *blockingManager; @end +#pragma mark - + @implementation TSMessagesManager + (instancetype)sharedManager { @@ -103,7 +104,6 @@ NS_ASSUME_NONNULL_BEGIN _messageSender = messageSender; _dbConnection = storageManager.newDatabaseConnection; - _disappearingMessagesJob = [[OWSDisappearingMessagesJob alloc] initWithStorageManager:storageManager]; _incomingMessageFinder = [[OWSIncomingMessageFinder alloc] initWithDatabase:storageManager.database]; _blockingManager = [OWSBlockingManager sharedManager]; @@ -940,8 +940,8 @@ NS_ASSUME_NONNULL_BEGIN storageManager:self.storageManager]; [readReceiptsProcessor process]; - [self.disappearingMessagesJob becomeConsistentWithConfigurationForMessage:incomingMessage - contactsManager:self.contactsManager]; + [OWSDisappearingMessagesJob becomeConsistentWithConfigurationForMessage:incomingMessage + contactsManager:self.contactsManager]; // Update thread preview in inbox [thread touch]; diff --git a/src/Util/NSTimer+OWS.h b/src/Util/NSTimer+OWS.h new file mode 100644 index 000000000..e301b39b0 --- /dev/null +++ b/src/Util/NSTimer+OWS.h @@ -0,0 +1,19 @@ +// +// Copyright (c) 2017 Open Whisper Systems. All rights reserved. +// + +NS_ASSUME_NONNULL_BEGIN + +@interface NSTimer (OWS) + +// This method avoids the classic NSTimer retain cycle bug +// by using a weak reference to the target. ++ (NSTimer *)weakScheduledTimerWithTimeInterval:(NSTimeInterval)timeInterval + target:(id)target + selector:(SEL)selector + userInfo:(nullable id)userInfo + repeats:(BOOL)repeats; + +@end + +NS_ASSUME_NONNULL_END diff --git a/src/Util/NSTimer+OWS.m b/src/Util/NSTimer+OWS.m new file mode 100644 index 000000000..6abc56790 --- /dev/null +++ b/src/Util/NSTimer+OWS.m @@ -0,0 +1,69 @@ +// +// Copyright (c) 2017 Open Whisper Systems. All rights reserved. +// + +#import "NSTimer+OWS.h" +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface NSTimerProxy : NSObject + +@property (nonatomic, weak) id target; +@property (nonatomic) SEL selector; + +@end + +#pragma mark - + +@implementation NSTimerProxy + +- (void)timerFired:(NSDictionary *)userInfo +{ +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Warc-performSelector-leaks" + [self.target performSelector:self.selector withObject:userInfo]; +#pragma clang diagnostic pop +} + +@end + +#pragma mark - + +static void *kNSTimer_OWS_Proxy = &kNSTimer_OWS_Proxy; + +@implementation NSTimer (OWS) + +- (NSTimerProxy *)ows_proxy +{ + return objc_getAssociatedObject(self, kNSTimer_OWS_Proxy); +} + +- (void)ows_setProxy:(NSTimerProxy *)proxy +{ + OWSAssert(proxy); + + objc_setAssociatedObject(self, kNSTimer_OWS_Proxy, proxy, OBJC_ASSOCIATION_RETAIN); +} + ++ (NSTimer *)weakScheduledTimerWithTimeInterval:(NSTimeInterval)timeInterval + target:(id)target + selector:(SEL)selector + userInfo:(nullable id)userInfo + repeats:(BOOL)repeats +{ + NSTimerProxy *proxy = [NSTimerProxy new]; + proxy.target = target; + proxy.selector = selector; + NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:timeInterval + target:proxy + selector:@selector(timerFired:) + userInfo:userInfo + repeats:repeats]; + [timer ows_setProxy:proxy]; + return timer; +} + +@end + +NS_ASSUME_NONNULL_END