Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

ARC: sending nil to an object doesn't call its dealloc immediately

I'm transitioning from manual memory management to ARC and have an issue. Most of the time, I'm performing data load asynchronously by calling performSelectorInBackground in my model classes. The thing is I need to stop any model code execution when model receives nil (release). In non-arc, everything was straightforward - as soon as a user closes the window, its controller starts to deallocate itself and deallocates its model [_myModel release], and so model stops its code execution (data loading) and gets called its dealloc method.

This seems to be different in ARC. Model still executes the code even after receiving nil message from controller. Its dealloc method gets called after its code execution (data load) only. This is an issue because the code execution should stop ASAP when a user closes the window (controller). It's some sort of a lack of control over the code - controller tells to model - "go away, I don't need your work anymore" but the model still "is working to finish its job" :).

Imagine a model performs some very heavy data processing with duration of 10 seconds. A model starts to do its processing when a user opens the window (controller). But image a user changes his mind and closes the window, just after the opening it. The model still perform wasteful processing. Any ideas how to solve or workaround that? I don't like an idea to have a special BOOL "shouldDealloc" property in my model and set to YES in controller dealloc method, and use in my model class conditions. Is there more elegant solution?

I have made some demo project to show the problem. For testing just create single view application and paste the code. Create to buttons- "Start calculate" and "Stop calculate" in ViewController.xib file, and connect their IBActions with startCalculationPressed and stopCalculationPressed:

ViewController.h

#import "MyModel.h"

@interface ViewController : UIViewController <MyModelDelegate>

- (IBAction)startCalculationPressed:(id)sender;
- (IBAction)stopCalculationPressed:(id)sender;

@end

ViewController.m

@interface ViewController (){

  __strong MyModel *_myModel;
}
@end

@implementation ViewController

- (void)viewDidLoad
{
  [super viewDidLoad];
  // Do any additional setup after loading the view, typically from a nib.
}

- (void)didReceiveMemoryWarning
{
  [super didReceiveMemoryWarning];
  // Dispose of any resources that can be recreated.
}

- (void)didCalculated
{
  NSLog(@"Did calculated...");
}

- (IBAction)startCalculationPressed:(id)sender
{
  NSLog(@"Starting to calculate...");

  _myModel = nil;
  _myModel = [[MyModel alloc] init];
  _myModel.delegate = self;

  [_myModel calculate];
}

- (IBAction)stopCalculationPressed:(id)sender
{
  NSLog(@"Stopping calculation...");
  _myModel.delegate = nil;
  _myModel = nil;
}
@end

Add new MyModel class to the project:

MyModel.h

@protocol MyModelDelegate <NSObject>

  - (void)didCalculated;

@end

@interface MyModel : NSObject

  @property (nonatomic, weak) id<MyModelDelegate> delegate;

  - (void)calculate;

@end

MyModel.m

@implementation MyModel

- (void)dealloc
{
  NSLog(@"MyModel dealloc...");
}

- (void)calculate
{
  [self performSelectorInBackground:@selector(performCalculateAsync) withObject:nil];
}

- (void)performCalculateAsync
{
  // Performing some longer running task
  int i;
  int limit = 1000000;
  NSMutableArray *myList = [[NSMutableArray alloc] initWithCapacity:limit];

  for (i = 0; i < limit; i++) {

    [myList addObject:[NSString stringWithFormat:@"Object%d", i]];
  }

  [self performSelectorOnMainThread:@selector(calculateCallback) withObject:nil waitUntilDone:NO];

}

- (void)calculateCallback
{
  [self.delegate didCalculated];
}

@end

UPDATE Martin is right, performSelectorOnMainThread always retains self, so there's no way how to stop code execution on other thread (both in ARC and non-ARC) so dealloc is not called immediately when releasing model. So, that should be done explicitly using appropriate property (for example delegate) with conditional checking.

like image 343
Centurion Avatar asked Apr 13 '13 07:04

Centurion


1 Answers

An object is deallocated if its release count goes down to zero, or in ARC language, if the last strong reference to that object is gone.

[self performSelectorInBackground:@selector(performCalculateAsync) withObject:nil];

adds a strong reference to self, which explains why the object is not deallocated before the background thread has finished.

There is no way (that I know of) to make a background thread stop "automatically". The same holds true for blocks started with dispatch_async() or for NSOperation. Once started, the thread/block/operation must monitor some property at points where it is save to stop.

In your example you could monitor self.delegate. If that becomes nil, nobody is interested in the result anymore, so the background thread can return. In that case, it would make sense to declare the delegate property as atomic.

Note that self.delegate is also automatically set to nil if the view controller is deallocated (because it is a weak property) even if stopCalculationPressedhas not been called.

like image 141
Martin R Avatar answered Nov 15 '22 05:11

Martin R