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