Calling makeViewWithIdentifier:owner: causes ARC t

2019-07-08 14:13发布

问题:

I'm writing a sandboxed ARC app with a view-based NSTableView that accepts dragged-and-dropped files (NSURLs). I ran into some significant strangeness in the following NSTableViewDelegate method:

- (NSView *)tableView:(NSTableView *)tv
   viewForTableColumn:(NSTableColumn *)tc
                  row:(NSInteger)row
{
    // `files' is an NSMutableArray* ivar containing NSURLs
    // that have been dropped into this table
    NSURL *url = [files objectAtIndex:row];
    NSString *fileName = [url lastPathComponent];
    NSImage *icon = [self iconForURL:url];

    NSTableCellView *view = [tv makeViewWithIdentifier:[tc identifier] owner:self];
    [[view textField] setStringValue:fileName];
    [[view imageView] setImage:icon];

    return view;
}

I can drag one file into the table view, and it displays correctly. When I drag a second file, I get this error:

*** Canceling drag because exception 'NSRangeException' (reason '*** -[__NSArrayM insertObject:atIndex:]: index 1 beyond bounds for empty array') was raised during a dragging session

Stepping through the debugger, I discovered that files "becomes empty" -- actually becomes a new object instance -- after the call to makeViewWithIdentifier:owner:. I assume this is some aspect of ARC that I don't understand, but it seems to me that the object has a strong reference to its own ivar (by default); how could it get released and re-created out from under me?

I have come up with two hacks to work around this:

  1. pass the ivar as the owner of the table cell view (hoping that in future releases it will continue to hold a strong reference); or
  2. create a local variable to point to the ivar's object and re-assign the ivar to the old object (this is obviously wasteful as it creates a replacement array in the meantime).

What am I missing here? These workarounds should not be necessary.

回答1:

Calling -makeViewWithIdentifier:owner: will cause an -awakeFromNib message to be sent to owner. This is documented but only in the header file (EDIT: the main documentation has been updated to refer to this).

I imagine that your files array is simply becoming reinitialised in -awakeFromNib.

The solution in the given case (which is loading a view prototype rather than a nib) is simply to pass nil as the owner. Other implementations which load registered nibs (see -registerNib:forIdentifier:) may likely require an owner, which may be the delegate (or not). So multiple calls to -awakeFromNib may have to be detected and trapped. It is trivial to set a property to flag nib loading and only perform required initialisation once.

Note that the Apple docs for this method have been updated to reflect this:

Note that awakeFromNib is called each time this method is called, which means that awakeFromNib is also called on owner, even though the owner is already awake.