From 9fb0b81b9e4d361141314946208965dc607b861a Mon Sep 17 00:00:00 2001 From: xGeorge Date: Sat, 18 Oct 2025 23:50:48 -0600 Subject: [PATCH 1/2] Test MemoryLeaks related to NSOperations --- Sources/BranchSDK/BNCNetworkService.m | 32 ++++++++++++++++++----- Sources/BranchSDK/BNCPreferenceHelper.m | 34 ++++++++++++++++--------- 2 files changed, 48 insertions(+), 18 deletions(-) diff --git a/Sources/BranchSDK/BNCNetworkService.m b/Sources/BranchSDK/BNCNetworkService.m index 01c5178..1d2aa62 100644 --- a/Sources/BranchSDK/BNCNetworkService.m +++ b/Sources/BranchSDK/BNCNetworkService.m @@ -86,6 +86,19 @@ - (instancetype) init { return self; } +- (void)dealloc { + @synchronized (self) { + if (_session) { + [_session invalidateAndCancel]; + _session = nil; + } + } + if (_sessionQueue) { + [_sessionQueue cancelAllOperations]; + _sessionQueue = nil; + } +} + #pragma mark - Getters & Setters - (void) setDefaultTimeoutInterval:(NSTimeInterval)defaultTimeoutInterval { @@ -183,15 +196,22 @@ - (void)startOperation:(BNCNetworkOperation *)operation { } else { [[BranchLogger shared] logError:[NSString stringWithFormat:@"Expected NSMutableURLRequest, got %@", [operation.request class]] error:nil]; } - - operation.sessionTask = [self.session dataTaskWithRequest:operation.request completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) { + + operation.sessionTask = [self.session dataTaskWithRequest:operation.request completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) { operation.responseData = data; - operation.response = (NSHTTPURLResponse*) response; + operation.response = (NSHTTPURLResponse*)response; operation.error = error; - - if (operation.completionBlock) { - operation.completionBlock(operation); + + // Capture, clear, then invoke to avoid unexpected long-lived captures + void (^completion)(BNCNetworkOperation *) = operation.completionBlock; + operation.completionBlock = nil; + + if (completion) { + completion(operation); } + + // Break back-reference to the service + operation.networkService = nil; }]; [operation.sessionTask resume]; diff --git a/Sources/BranchSDK/BNCPreferenceHelper.m b/Sources/BranchSDK/BNCPreferenceHelper.m index 05c4d84..2680a21 100644 --- a/Sources/BranchSDK/BNCPreferenceHelper.m +++ b/Sources/BranchSDK/BNCPreferenceHelper.m @@ -1038,23 +1038,33 @@ - (void)writeObjectToDefaults:(NSString *)key value:(NSObject *)value { } - (void)persistPrefsToDisk { - if (self.useStorage) { - @synchronized (self) { - if (!self.persistenceDict) return; - - NSData *data = [self serializePrefDict:self.persistenceDict]; - if (!data) return; - - NSURL *prefsURL = [self.class.URLForPrefsFile copy]; - NSBlockOperation *newPersistOp = [NSBlockOperation blockOperationWithBlock:^ { + if (!self.useStorage) return; + + @synchronized (self) { + if (!self.persistenceDict) return; + + NSData *data = [self serializePrefDict:self.persistenceDict]; + if (!data) return; + + NSURL *prefsURL = [self.class.URLForPrefsFile copy]; + + // Coalesce: drop older pending writes to avoid retaining many large NSData snapshots + [_persistPrefsQueue cancelAllOperations]; + + NSBlockOperation *op = [NSBlockOperation blockOperationWithBlock:^{ + @autoreleasepool { NSError *error = nil; [data writeToURL:prefsURL options:NSDataWritingAtomic error:&error]; if (error) { [[BranchLogger shared] logWarning:@"Failed to persist preferences" error:error]; } - }]; - [_persistPrefsQueue addOperation:newPersistOp]; - } + } + }]; + + // Optional: lower priority; it’s background I/O + op.queuePriority = NSOperationQueuePriorityLow; + + [_persistPrefsQueue addOperation:op]; } } From 89b3ed41edfaa8ee35b3135d4b834c89d833e04d Mon Sep 17 00:00:00 2001 From: xGeorge Date: Sun, 19 Oct 2025 01:08:25 -0600 Subject: [PATCH 2/2] 2 iteration --- Sources/BranchSDK/BNCNetworkService.m | 36 +++++++++++++++++---------- Sources/BranchSDK/BNCURLFilter.m | 25 ++++++++++++++++--- 2 files changed, 44 insertions(+), 17 deletions(-) diff --git a/Sources/BranchSDK/BNCNetworkService.m b/Sources/BranchSDK/BNCNetworkService.m index 1d2aa62..9f6b4dd 100644 --- a/Sources/BranchSDK/BNCNetworkService.m +++ b/Sources/BranchSDK/BNCNetworkService.m @@ -86,16 +86,33 @@ - (instancetype) init { return self; } +// Avoid locking in dealloc; just tear down with ivars - (void)dealloc { + NSURLSession *session = _session; + if (session) { + [session invalidateAndCancel]; + _session = nil; + } + NSOperationQueue *queue = _sessionQueue; + if (queue) { + [queue cancelAllOperations]; + _sessionQueue = nil; + } +} + +// IMPORTANT: do not touch properties here; use ivars so you don’t create a session while tearing down +- (void)cancelAllOperations { @synchronized (self) { - if (_session) { - [_session invalidateAndCancel]; + NSURLSession *session = _session; + if (session) { + [session invalidateAndCancel]; _session = nil; } - } - if (_sessionQueue) { - [_sessionQueue cancelAllOperations]; - _sessionQueue = nil; + NSOperationQueue *queue = _sessionQueue; + if (queue) { + [queue cancelAllOperations]; + _sessionQueue = nil; + } } } @@ -217,11 +234,4 @@ - (void)startOperation:(BNCNetworkOperation *)operation { [operation.sessionTask resume]; } -- (void) cancelAllOperations { - @synchronized (self) { - [self.session invalidateAndCancel]; - _session = nil; - } -} - @end diff --git a/Sources/BranchSDK/BNCURLFilter.m b/Sources/BranchSDK/BNCURLFilter.m index 387070f..db5fb1d 100644 --- a/Sources/BranchSDK/BNCURLFilter.m +++ b/Sources/BranchSDK/BNCURLFilter.m @@ -106,19 +106,36 @@ - (void)updatePatternListFromServerWithCompletion:(void (^_Nullable) (void))comp return; } - NSString *urlString = [NSString stringWithFormat:@"%@/sdk/uriskiplist_v%ld.json", [BNCPreferenceHelper sharedInstance].patternListURL, (long) self.listVersion+1]; - NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:urlString] cachePolicy:NSURLRequestReloadIgnoringLocalCacheData timeoutInterval:30.0]; + NSString *urlString = [NSString stringWithFormat:@"%@/sdk/uriskiplist_v%ld.json", + [BNCPreferenceHelper sharedInstance].patternListURL, + (long)self.listVersion + 1]; + NSMutableURLRequest *request = + [NSMutableURLRequest requestWithURL:[NSURL URLWithString:urlString] + cachePolicy:NSURLRequestReloadIgnoringLocalCacheData + timeoutInterval:30.0]; __block id networkService = [[Branch networkServiceClass] new]; - id operation = [networkService networkOperationWithURLRequest:request completion: ^(id operation) { - [self processServerOperation:operation]; + + __weak typeof(self) weakSelf = self; + id operation = + [networkService networkOperationWithURLRequest:request + completion:^(id op) { + // Process result + [weakSelf processServerOperation:op]; + if (completion) { completion(); } + + // IMPORTANT: break the NSURLSession delegate retain cycle + [networkService cancelAllOperations]; // invalidates and cancels the session + networkService = nil; // drop our last reference ASAP }]; + [operation start]; } + - (BOOL)foundUpdatedURLList:(id)operation { NSInteger statusCode = operation.response.statusCode; NSError *error = operation.error;