Reference Counting Issue?

I seem to be running into an issue with NSArray and NSMutableArray where they are not being deallocated if I use the [NSMutableArray arrayWithObject:xxx] or @[xxx] syntax in a lambda. I've attached a complete listing of a viewcontroller from a sample app that I threw together to show the problem. It does look like the alloc/init pattern works for both array types in this scenario though. Thoughts?


This code is meant to fully replace the ViewController source of the Single View example project entirely with this code and it will run. You will need to rename to .mm though.


//
//  ViewController.m
//  MutableArrayTest
//


#import "ViewController.h"
#include <functional>
#include <iostream>
#include <thread>
#include <mutex>
#include <vector>


// TempObject: Simple object to just test lifecycle counting
@interface TempObject : NSObject {
}
@end


// This is the "data" class for the job execution.  There aren't any member methods/etc since it doesn't materially effect the test
class TestCPPClass {
public:
};


// Create a definition of the lambda for loading data
typedef std::function<void(std::shared_ptr<TestCPPClass> classInstance)> LoadBlock;


// Create a container that will load the data:  This contains the "loading" business logic
class TestCPPClassExecutor {
public:
    LoadBlock _loadBlock;
    ~TestCPPClassExecutor() {
        _loadBlock = nullptr;
    }
    void setLoadBlock(LoadBlock aBlock) { _loadBlock = aBlock; }
    void doWork() {
        std::shared_ptr<TestCPPClass> temp = std::make_shared<TestCPPClass>();
        _loadBlock(temp);
    }
};


//
typedef std::function<void(std::shared_ptr<TestCPPClass> classInstance)> ThreadFunction;


class TestCPPThreadJob {
public:
    ThreadFunction _threadFunction;
    std::shared_ptr<TestCPPClass> _threadData;
   
    TestCPPThreadJob(ThreadFunction aFunction, std::shared_ptr<TestCPPClass> aData) { _threadFunction = aFunction; _threadData = aData; }
    ~TestCPPThreadJob() {
        _threadFunction = nullptr;
        _threadData = nullptr;
    }
    void executeThread() {
        if (_threadFunction) {
            _threadFunction(_threadData);
        }
    }
};




@interface ViewController () {
    LoadBlock _loadBlock;
    std::shared_ptr<TestCPPClassExecutor> _executor;
    std::thread _thread;
    std::mutex _threadMutex;
    std::vector<TestCPPThreadJob> _threadJobs;
    std::condition_variable threadCV;






}
@end












static int tempObjectCount = 0;




@implementation ViewController


- (void)viewDidLoad {
    [super viewDidLoad];


    // Create a lambda for "working" with our TestCPPClass
    _loadBlock = [=](std::shared_ptr<TestCPPClass> classInstance) {
        /
        // ----- RUN1 --------------------
        // This will just create the temp object and that's it..  The log will show that the temp object is being deallocated
       
        // Create an OBJC Object within this lambda
        TempObject *tempObject = [[TempObject alloc] init];
       
        // ----- END RUN1 -----------------
        */
       
       
        /
        // ----- RUN2 --------------------
        // This will create the temp object and add it to an array.  This will cause both the arraay to stay alive as well as the object.
        // Even removing all objects from the array won't delete the temp object
       
        // Create an OBJC Object within this lambda
        TempObject *tempObject = [[TempObject alloc] init];
       
        // Add to array
        NSMutableArray *testArray = [NSMutableArray arrayWithObject:tempObject];
       
        // TEST: Try removing all the objects and the TempObject is released, but the array still isn't
//        [testArray removeAllObjects];
        // ----- END RUN2 -----------------
         */
       
       
        /
        // ----- RUN3 --------------------
        // Even just creating a local array with nothing in it will cause the array to no be deallocated (Instances accumulate in Instruments)
        NSMutableArray *testArray = [NSMutableArray array];
        // ----- END RUN3 --------------------
        */


        /
        // ----- RUN4 --------------------
        // This however, unline RUN3 does seeem to de-allocate the array (they do not accumulate in Instruments)
        NSMutableArray *testArray = [[NSMutableArray alloc] init];
        // ----- END RUN4 --------------------
        */


       
        /
         // ----- RUN5 --------------------
         // This will create the temp object and add it to an array using the array's alloc/init pattern and this seems to release the object array and the array
        
         // Create an OBJC Object within this lambda
         TempObject *tempObject = [[TempObject alloc] init];
        
         // Add to array
        NSMutableArray *testArray = [[NSMutableArray alloc] init];
        [testArray addObject:tempObject];
        
         // TEST: Try removing all the objects and the TempObject is released, but the array still isn't
         //[testArray removeAllObjects];
         // ----- END RUN5 -----------------
         */
       


        /
         // ----- RUN6 --------------------
         // This will create the temp object and add it to an array using the array's shortcut @[] and it retains the object and the array doesn't release
        
         // Create an OBJC Object within this lambda
         TempObject *tempObject = [[TempObject alloc] init];
        
         // Add to array: This accumlates NSArrayI instances and doesn't release the object
         NSArray *testArray = @[tempObject];
       
         // ----- END RUN6 -----------------
        */
       
       
        // ----- RUN7 --------------------
        // This will create the temp object and add it to an array using the array's alloc/init pattern and this seems to release the object array and the array
       
        // Create an OBJC Object within this lambda
        TempObject *tempObject = [[TempObject alloc] init];
       
        // Add to array: This accumlates NSArrayI instances and doesn't release the object
        NSArray *testArray = [[NSArray alloc] initWithObjects:tempObject, nil];
       
        // ----- END RUN7 -----------------


       
    };
   
    // Create a common "load" executor
    _executor = std::make_shared<TestCPPClassExecutor>();
    _executor->setLoadBlock(_loadBlock);


    // Create the thread
    _thread = std::thread([=](){
        [self threadExecute];
    });
   
    // Kick off a timer to add jobs to the queue
    NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:0.1
                                                      target:self
                                                    selector:@selector(doWork:)
                                                    userInfo:nil
                                                     repeats:YES];
   
   
}


-(void)threadExecute {
    for (;;) {
        // If there's a job, pop it and execute it
        if (_threadJobs.size() > 0) {
            TestCPPThreadJob &job = _threadJobs[0];
            job.executeThread();
            _threadJobs.erase(_threadJobs.begin());
        }


        // Check to see if there are waiting jobs and if so, don't wait
        if (_threadJobs.size() == 0) {
            std::unique_lock<std::mutex> lk(_threadMutex);
            threadCV.wait(lk);
        }
       
    }


}


-(void)doWork:(id)sender {


    // Create new Job w/Test Class
    std::shared_ptr<TestCPPClass> testClass = std::make_shared<TestCPPClass>();
    TestCPPThreadJob testJob([=](std::shared_ptr<TestCPPClass> classInstance) {
        _executor->doWork();


    }, testClass);
   
    // Add the test class to the job queue
    _threadJobs.push_back(testJob);
   
    // Notify thread that something has been added
    threadCV.notify_one();
   
}


@end




/ This temp object is just here to log reference counting to console */
@implementation TempObject


-(id)init {
    if (self = [super init]) {
        @synchronized([TempObject class]) {
            tempObjectCount++;
            NSLog(@"TempObjectCount: %i", tempObjectCount);
        }
    }
    return self;
}


-(void)dealloc {
    @synchronized([TempObject class]) {
        tempObjectCount--;
        NSLog(@"Destroy TempObjectCount: %i", tempObjectCount);
    }
   
}


@end

Created RADAR:

22632320

Accepted Answer

You've created a thread. Whenever you create a thread, you are responsible for setting up an autorelease pool in its main entry point (or the outermost context where you might invoke Cocoa APIs). You don't do that, so anything that is autoreleased just leaks. There should be console messages to that effect.

So.. That worked, the autorelease pool does release the convience styles, however, the alloc/init ones always worked (without autorelease pool), so there still seems to be an issue. However, for the short term, I will add the autorelease pool wrapper.


Thank you!

It is always possible for any method you call to autorelease some objects. So, it's somewhat down to happenstance that it didn't happen with alloc/init.


Because of the memory management rules, though, it is more common for objects to end up in the autorelease pool with convenience constructors and the Objective-C literals than with alloc/init. An object returned by +alloc is owned by the caller. The call to -init nominally consumes that reference and returns another owned reference. So, the object resulting from an alloc/init pair is, again, owned by the caller. Therefore, there's no need for anything to be put in the autorelease pools.


In contrast, an object returned from a convenience constructor is not owned by the caller. One of the most common ways for the implementation of such a convenience constructor to achieve this without leaking is to allocate and initialize an object and then autorelease it before returning it to the caller. So, it is very common (but not mandatory) for objects returned from convenience constructors to be in the autorelease pool.


Objective-C literals actually compile down to a call to a convenience constructor, more or less. They have the same memory management semantics (the caller doesn't own the resulting object) and may use the same technique to achieve that.

Ah... this was new to me "Because of the memory management rules, though, it is more common for objects to end up in the autorelease pool with convenience constructors and the Objective-C literals than with alloc/init." I had always lumped them into the same memory management scheme. I appreciate the exposition. Really helpful. I did update my original codebase (the example was obviously that.. just a whittled down example to post here) with @autoreleasepool and it worked as expected. I was able to go back to my convience methods of @[]. I will be using @autoreleaspools in the threaded lambdas from now on!


Thanks again!

Reference Counting Issue?
 
 
Q