Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

premature dealloc in ARC based app

I have a problem that seems to be a premature release of an in-use object in an ARC based app. I'm trying to create a folder on an FTP server. The relevant parts of code are below; i'll describe the problem first.

problem with the code is, that the debug output in the

- (void)stream:(NSStream *)aStream handleEvent:(NSStreamEvent)eventCode

method is never called.

Instead, i just get an _EXC_BAD_ACCESS_ error. While debugging i found out two things:

  1. the error only appears if the following line of code (createDir method) is executed:

    [ftpStream open];
    

if that message isn't sent, the rest of the code doesn't really make sense - but it doesn't crash either...

  1. I tracked the EXC_BAD_ACCESS down with NSZombieEnabled: With zombie objects enabled, the GDB produces the following debugger info:

     *** -[FTPUploads respondsToSelector:]: message sent to deallocated instance 0x9166590
    

The referred address 0x9166590 is the address of my FTPUploads object. It looks like the streams delegate is deallocated before it can handle messages.

Why does the system deallocate an in-use object? How can i prevent it from being deallocated prematurely?

code:

FTPUploads.h excerpt:

#import <Foundation/Foundation.h>

enum UploadMode {

    UploadModeCreateDir, 
    UploadModeUploadeData
};

@class UploadDatasetVC;

@interface FTPUploads : NSObject<NSStreamDelegate> {

    @private
    NSString *uploadDir;
    NSString *ftpUser;
    NSString *ftpPass;

    NSString *datasetDir;
    NSArray *files;

    /* FTP Upload fields */
    NSInputStream *fileStream;
    NSOutputStream *ftpStream;
    // some more fields...
    enum UploadMode uploadMode;
    UploadDatasetVC *callback;
}

- (id) initWithTimeseriesID: (int) aTimeseriesID 
             fromDatasetDir: (NSString *) aDir
                  withFiles: (NSArray *) filesArg 
          andCallbackObject: (UploadDatasetVC *) aCallback;

- (void) createDir;

@end

FTPUploads.m excerpt

#import "FTPUploads.h"
#import "UploadDatasetVC"

@implementation FTPUploads

- (id) initWithTimeseriesID: (int) aTimeseriesID 
             fromDatasetDir: (NSString *) aDir
                  withFiles: (NSArray *) filesArg 
          andCallbackObject: (UploadDatasetVC *) aCallback {

    self = [super init];

    if (self) {

        uploadDir = [NSString stringWithFormat: @"ftp://aServer.org/%i/", aTimeseriesID];
        ftpUser = @"aUser";
        ftpPass = @"aPass";

            datasetDir = aDir;
            files = filesArg;

        bufferOffset = 0;
        bufferLimit = 0;

        index = 0;

        callback = aCallback;
    }

    return self;
}

- (void) createDir {

    uploadMode = UploadModeCreateDir;
    NSURL *destinationDirURL = [NSURL URLWithString: uploadDir];

    CFWriteStreamRef writeStreamRef = CFWriteStreamCreateWithFTPURL(NULL, (__bridge CFURLRef) destinationDirURL);
    assert(writeStreamRef != NULL);

    ftpStream = (__bridge_transfer NSOutputStream *) writeStreamRef;
    [ftpStream setProperty: ftpUser forKey: (id)kCFStreamPropertyFTPUserName];
    [ftpStream setProperty: ftpPass forKey: (id)kCFStreamPropertyFTPPassword];

    ftpStream.delegate = self;
    [ftpStream scheduleInRunLoop: [NSRunLoop currentRunLoop] forMode: NSDefaultRunLoopMode];
    // open stream
    [ftpStream open];

    CFRelease(writeStreamRef);
}

- (void)stream:(NSStream *)aStream handleEvent:(NSStreamEvent)eventCode {

    NSLog(@"aStream has an event: %i", eventCode);

    switch (eventCode) {
        // all cases handled properly
        default:
            // no event
            NSLog(@"default mode; no event");
            break;
    }
}

EDIT: added creation code that is used in the class UploadDatasetVC:

FTPUploads *uploads = [[FTPUploads alloc] initWithTimeseriesID: timeseries_id 
                                                fromDatasetDir: datasetDir 
                                                     withFiles: files 
                                             andCallbackObject: self];
[uploads createDir];
like image 367
m Jae Avatar asked Feb 05 '12 14:02

m Jae


2 Answers

It looks to me like the only reference to your FTPUploads object is the delegate property on the stream. This won't retain your object, so if nothing else has a reference to the object, the object will be dealloced. A.R.C. doesn't try to prevent this scenario.

What you need to do is have the code that allocates the FTPUploads object keep a reference to the object until it completes.

It also wouldn't be a bad idea to set the ftpStream.delegate property to nil in your FTPUploads dealloc method, as this will prevent a crash if the object is dealloced prematurely.

like image 97
grahamparks Avatar answered Oct 11 '22 11:10

grahamparks


The problem is that your ftpStream object is being deallocated. You create it with CFWriteStreamCreateWithFTPURL(), then release it with CFRelease(). You used a __bridge cast, which basically means "don't do any memory management on this assignment". So ARC didn't retain it when you assigned it to ftpStream. Since your intention was to transfer ownership from CF to ARC, that was the wrong cast to use.

You actually wanted either __bridge_retained or __bridge_transfer. I can never remember which is which, though. Luckily, there's another option—the CFBridgingRetain() and CFBridgingRelease() macros. They resolve down to those same bridging casts, but are named far more clearly.

In this case, you want CF to release it, but bridge it over to ARC. So you want CFBridgingRelease(). That will tell ARC to take ownership of the object, and then do a CFRelease. In short, replace this:

ftpStream = (__bridge NSOutputStream *) writeStreamRef;

with this:

ftpStream = CFBridgingRelease(writeStreamRef);

And then remove the call to CFRelease() a few lines later.

like image 43
BJ Homer Avatar answered Oct 11 '22 10:10

BJ Homer