NSOperationQueue memory leak

I have a project that's built with Objective-C and Swift.

In Objective-C, I have a class called CoreDataLayer that has an NSOperationQueue and another class called InternalService that can be initialized with CoreDataLayer and has its own NSOperationQueue.

CoreDataLayer class

// .h
@interface CoreDataLayer : NSObject

@property (atomic, strong) NSOperationQueue *afterInitializationCoreDataQueue;

@end

// .m
@implementation CoreDataLayer

@end

InternalService class

// .h
@interface InternalService : NSObject

- (instancetype)initWithCoreDataLayer:(CoreDataLayer *)coreDataLayer;

@end

// .m
@interface InternalService ()

@property (nonatomic, strong) NSOperationQueue *operationQueue;

@end

@implementation InternalService

- (instancetype)initWithCoreDataLayer:(CoreDataLayer *)coreDataLayer {
    self = [self init];
    if (self) {
        _operationQueue = [[NSOperationQueue alloc] init];
    }
    return self;
}

@end

In Swift, I have a class Service that inherits from InternalService and has a property of type OperationQueue. This class is intended to be a safer interface for the InternalService class.

fileprivate extension OperationQueue {
    static var serialSuspendedQueue: OperationQueue {
        let queue = OperationQueue()
        queue.maxConcurrentOperationCount = 1
        queue.isSuspended = true
        return queue
    }
}

class Service: InternalService {
    private let operationQueue: OperationQueue
    
    override init() {
        operationQueue = OperationQueue.serialSuspendedQueue
        super.init()
    }
    
    override init(coreDataLayer: CoreDataLayer) {
        operationQueue = OperationQueue.serialSuspendedQueue
        super.init(coreDataLayer: coreDataLayer)
        coreDataLayer.afterInitializationCoreDataQueue.addOperation {
            self.operationQueue.isSuspended = false
        }
    }
}

Project Link on GitHub

When I profile this app with Instruments, I find a leak in NSOperationQueue. I'm not sure why this is happening.

This isn't really a swift language issue it is more of an issue with using certain APIs in swift; however there are a few tips I can give to shed some light on what is going on here. First off the memory graph debugger is amazing for helping find where these things are being referenced. Secondly, operation queues that are suspended won't execute any operations. That in conjunction with the property that while operations are enqueued and not yet executed they hold a strong reference to the queue they participate in (to avoid the queue itself from being destroyed while executing work). My guess is there is an enqueued operation that is never run holding a strong reference to your suspended queue that is causing the whole works to get clogged. Tip: NSOperations can have cross queue dependencies. e.g. if you have an operation that is enqueued on one NSOperationQueue, that operation can depend on another enqueued in a totally different queue. I would guess that with a bit of restructuring you could likely adjust it to fit that pattern and express it as dependencies instead of suspensions. Which likely might also catch where you are blocked and have a retain cycle. Plus, dependencies are much more sensible to reason about.

I tried using the Memory Graph Debugger but I couldn't find any helpful information there. I attached an image. I also replaced the NSOperationQueue with an array to see if the issue still persist. I found out that the leak is gone when I removed the NSOperationQueue. I also decided to remove the suspicion all together from the NSOperationQueue and found out that leak is still there. Therefore, I think that the leak is actually related to the NSOperationQueue itself.

The weird thing is that I decided to change this line OperationQueueLeak/InternalService.m at main · HassanElDesouky/OperationQueueLeak · GitHub from self init to super init and the memory leak in the memory debugger went away.

I also tried re writing the Service class to Objective-C and in that case the memory leak went away also.

So, the question is, is this an issue in the NSOperationQueue itself or what?

I don't think so. You can simplify your code quite a bit to distill it down to the essentials and still have Instruments trigger a memory leak warning.

@implementation InternalService // NSObject

// Init 4
- (instancetype)init {
  self = [super init];
  return self;
}

// Init 2
- (instancetype)initWithCoreDataLayer:(CoreDataLayer *)coreDataLayer {
  self = [self init];
  return self;
}

@end
class Service: InternalService {
  private let emptyObject: EmptyObject
    
  // Init 3
  override init() {
    emptyObject = EmptyObject()
    super.init()
  }
  
  // Init 1
  override init(coreDataLayer: CoreDataLayer) {
    emptyObject = EmptyObject()
    super.init(coreDataLayer: coreDataLayer)
  }
}

// Two are created, but only one is deallocated.
class EmptyObject {
  init() {
    print("EmptyObject.init")
  }
  
  deinit {
    print("EmptyObject.deinit")
  }
}

I don't know the root cause of it, but I suspect the rather unorthodox initialization pattern and the bridging through Objective-C is contributing to the problem. As you noted, calling [super init] solved the problem.

If you trace through the initializations, you'll see that both Service.init() and Service.init(coreDataLayer:) are being called. Both are creating an EmptyObject and (surprisingly) assigning it to a let property.

If possible, I would try and simplify your object graph so that you're not bouncing between so many initializers. The memory leak itself has nothing to do with OperationQueue because you can trigger the leak in Instruments using any object, as shown above.