realm / realm-swift

Realm is a mobile database: a replacement for Core Data & SQLite
https://realm.io
Apache License 2.0
16.33k stars 2.15k forks source link

EXC_BAD_ACCESS crash when using NSOutlineView #1734

Closed enricenrich closed 8 years ago

enricenrich commented 9 years ago

I'm creating an NSOutlineView populated by objects saved on Realm. The object is called Person. And this is how it looks:

@interface Person : RLMObject

@property NSString *name;
@property NSInteger indent;
@property NSInteger order;

// Ignored properties
@property NSArray *contacts;

The contacts array returns a query. Those contacts are child of the person, but they also have the same object class, Person.

And this is the view controller:

#import "MainWindowController.h"
#import "Person.h"

@interface MainWindowController () <NSOutlineViewDataSource, NSOutlineViewDelegate>

@property (strong) IBOutlet NSOutlineView *outlineView;

@property (nonatomic, strong) RLMResults *array;
@property (nonatomic, strong) RLMNotificationToken *notification;

@end

@implementation MainWindowController

- (id)init
{
    self = [super initWithWindowNibName:@"MainWindow"];

    if (self) {
        __weak typeof(self) weakSelf = self;

        self.notification = [RLMRealm.defaultRealm addNotificationBlock:^(NSString *note, RLMRealm *realm) {
            [weakSelf.outlineView reloadData];
        }];

        self.array = [[Person objectsWhere:@"indent == 1"] sortedResultsUsingProperty:@"order" ascending:YES];
    }

    return self;
}

- (void)windowDidLoad
{
    [super windowDidLoad];

    [self.outlineView setRowHeight:50];
}

#pragma mark - NSOutlineView

- (id)outlineView:(NSOutlineView *)outlineView child:(NSInteger)index ofItem:(id)item
{
    if (item == nil) {
        return self.array[index];
    }
    else if ([item isKindOfClass:[Person class]]) {
        return [item contacts][index];
    }

    return nil;
}

- (BOOL)outlineView:(NSOutlineView *)outlineView isItemExpandable:(id)item
{
    if (item == nil) {
        return NO;
    }
    else if ([item isKindOfClass:[Person class]]) {
        return [item contacts].count > 0;
    }

    return NO;
}

- (NSInteger)outlineView:(NSOutlineView *)outlineView numberOfChildrenOfItem:(id)item
{
    if (item == nil) {
        return self.array.count;
    }
    else if ([item isKindOfClass:[Person class]]) {
        return [item contacts].count;
    }

    return 0;
}

- (NSView *)outlineView:(NSOutlineView *)outlineView viewForTableColumn:(NSTableColumn *)tableColumn item:(id)item
{
    NSView *cellView = [[NSView alloc] initWithFrame:NSMakeRect(0, 0, 250, 0)];

    NSTextField *titleTextField = [[NSTextField alloc] initWithFrame:NSMakeRect(0, 15, 250, 18)];
    [titleTextField setEditable:NO];
    [titleTextField setBordered:NO];
    titleTextField.backgroundColor = [NSColor clearColor];
    titleTextField.textColor = [NSColor blackColor];
    titleTextField.focusRingType = NSFocusRingTypeNone;
    titleTextField.stringValue = [item name];
    [cellView addSubview:titleTextField];

    return cellView;
}

The "root" cells are displayed correctly, but then, when I click the arrow to expand one of those root cells, the app crashes with an EXC_BAD_ACCESS error. I used NSZombie to know more about this crash, and that's what it tells me:

-[RLMAccessor_v0_Person retain]: message sent to deallocated instance 0x60800016ba00

I debugged all the data source methods of the NSOutlineView when tapping the arrow to expand the row, but no method is called.

Any idea of what's happening?

segiddins commented 9 years ago

Hi @enricenrich, it's hard to tell if this is actually a Realm issue or not from the code snippet you shared. I'm not super familiar with AppKit, but I'd recommend double-checking that you're not storing RLMObjects in an unsafe_unretained variable?

segiddins commented 9 years ago

@enricenrich just checking in to see if you ever made any progress of figuring out what was going on here?

jpsim commented 9 years ago

Closing as this is likely not a Realm issue.

lm2s commented 8 years ago

For future reference of anyone having this problem, like I did.

Instead of using the RLMResults array directly in - (id)outlineView:(NSOutlineView *)outlineView child:(NSInteger)index ofItem:(id)item.

Copy the objects into a NSArray to make sure that they are not released and then use the NSArray.

Using the above code as an example:

gf3 commented 8 years ago

i ran into the same issue and ended up solving it in a similar way as @lm2s—however this doesn't seem like a very performant solution.

are there any examples of using realm properly with NSOutlineView?

jpsim commented 8 years ago

@gf3 I we have yet to receive confirmation that this is a realm issue, and if so, how we can reproduce this to identify the cause. If you care to provide enough information for us to do this, we'll be happy to take a closer look.

gf3 commented 8 years ago

i've created a demo project with instructions on how to reproduce the issue as well as the solution both @lm2s and i have implemented. maybe you guys can recommend a better approach?

jpsim commented 8 years ago

Thanks for the sample project, we'll take a look!

bdash commented 8 years ago

There are two factors at play here:

  1. -[RLMResults objectAtIndex:] returns a new instance of your RLMObject subclass each time it is called. It does not retain the existing instances.
  2. NSOutlineView does not retain the objects returned by the -outlineView:child:ofItem: data source method.

The result is that by the time NSOutlineView calls other data source or delegate methods (in @gf3's test case this is often -outlineView:isGroupItem:), the object returned by -outlineView:child:ofItem: has been deallocated.

bdash commented 8 years ago

Due to the two factors mentioned in my previous comment, you do need to explicitly store the objects returned by RLMResults somewhere to ensure they live long enough for NSOutlineView. Eagerly storing the objects into an array as you describe is certainly the simplest way to achieve this. There are alternatives with different tradeoffs (e.g., you could reduce the up front cost at the expense of more complex code by only storing the objects as they're first retrieved by NSOutlineView).

gf3 commented 8 years ago

@bdash (if you have time) could you provide an example of better solution to this problem. i'm hoping to use Realm to build a chat application where users could receive thousands of messages per room and it seems like the current array-wrap solution would be very expensive

harryworld commented 8 years ago

I am having EXC_BAD_ACCESS when expandItem is called (similar to expand group in UI)

When I try to use debugger to po item in viewForTableColumn delegate method, it shows as [Deleted Object], but it is the Realm Object if I log it using dump(item)

My solution to this, is copying the array, and call expandItem as late as possible (in my case viewDidAppear)

Hope this provides an idea about the issue.

jpsim commented 8 years ago

To hold a strong reference to Realm objects when getting them from a Results (which does not hold strong references to all its returned objects, unlike NSArray), you can place them in any collection that strongly hold their contents such as NSArray.

So you could have a wrapper to -[RLMResults objectAtIndex:] which places the objects in an array before returning them:

@@ -7,6 +7,7 @@

 @property (nonatomic, strong) RLMResults *array;
 @property (nonatomic, strong) RLMNotificationToken *notification;
+@property (nonatomic, strong) NSMutableArray *stronglyHeldRealmObjects;

 @end

@@ -24,6 +25,7 @@
         }];

         self.array = [[Person objectsWhere:@"indent == 1"] sortedResultsUsingProperty:@"order" ascending:YES];
+        self.stronglyHeldRealmObjects = [NSMutableArray array];
     }

     return self;
@@ -40,14 +42,19 @@

 - (id)outlineView:(NSOutlineView *)outlineView child:(NSInteger)index ofItem:(id)item
 {
+    id returnItem = nil;
     if (item == nil) {
-        return self.array[index];
+        returnItem = self.array[index];
     }
     else if ([item isKindOfClass:[Person class]]) {
-        return [item contacts][index];
+        returnItem = [item contacts][index];
     }

-    return nil;
+    if (returnItem != nil) {
+        [self.stronglyHeldRealmObjects addObject:returnItem];
+    }
+
+    return returnItem;
 }

Ideally, you'd also be removing objects from the array when they're no longer used, but that would be a bit trickier to do. One way to do this would be to use associated objects with a RETAIN association, to make the outline view strongly reference the Realm object:

@@ -1,5 +1,8 @@
 #import "MainWindowController.h"
 #import "Person.h"
+#import <objc/runtime.h>
+
+static char kRealmAssociatedObjectKey;

 @interface MainWindowController () <NSOutlineViewDataSource, NSOutlineViewDelegate>

@@ -40,14 +43,19 @@

 - (id)outlineView:(NSOutlineView *)outlineView child:(NSInteger)index ofItem:(id)item
 {
+    id returnItem = nil;
     if (item == nil) {
-        return self.array[index];
+        returnItem = self.array[index];
     }
     else if ([item isKindOfClass:[Person class]]) {
-        return [item contacts][index];
+        returnItem = [item contacts][index];
+    }
+
+    if (returnItem != nil) {
+        objc_setAssociatedObject(outlineView, kRealmAssociatedObjectKey, returnItem, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
     }

-    return nil;
+    return returnItem;
 }

I haven't fully tested this code, so if you run into issues with it written exactly as-is, please let me know and I'll spend more time to fully work out the kinks.

jhoughjr commented 8 years ago

I just thought I would chime in on this as I've ran into a similar issue in RealmSwift regarding this I think. I've been searching for a solution for two days now :( I'll keep looking a bit more. The error in swift is fairly useless, but it does seem to be related to NSOutlineView. When I changed one to NSTableView, the issue resolved. But when i refactored the outline view I actually needed I ran into the same issue. I've tried copying the Results I'm using to drive my outline into an Array but that doesn't seem to work.

jpsim commented 8 years ago

@jhoughjr have you tried applying the strategies suggested in this thread? You mention you've tried copying the Realm objects into an Array and that not working. Can you please elaborate? How about the associated object approach?

jhoughjr commented 8 years ago

I'm going to take a closer look at the project that was shared here now that I'm fresh. I don't know if setAssociatedObject is available in swift or not.

jpsim commented 8 years ago

I don't know if setAssociatedObject is available in swift or not.

It sure is! See http://nshipster.com/swift-objc-runtime/#associated-objects

jhoughjr commented 8 years ago

Thanks for the info, I'm getting ready to take a stab at it again with a fresh mind. Been busy with other things the last couple of days. I hope it works as I really need NSOutlineView for this project.

jhoughjr commented 8 years ago

I've gotten things working by maintaining an Array I update with the Results I need to display. It's somewhat optimized in that I dump it before I reload. I use a second Array to hold the objects for an expanded cell's children since it is a different type. I am a bit curious as to why this isn't an issue for NSTableView, since NSOutlineView is a subclass of it.

gf3 commented 8 years ago

@jhoughjr it's because the NSTableViewDelegate protocol doesn't pass around objects, you get a row index as an Int and optionally an NSTableColumn

jpsim commented 8 years ago

I think we've sufficiently addressed the questions raised here, and provided a handful of potential solutions, so I'm closing this issue. If anyone has any further questions or issues related to this, please file a new GitHub issue referencing this one.

cliftonlabrum commented 6 years ago

@harryworld I know this post is old, but I ran into the issue of getting EXC_BAD_ACCESS when using expandItem. It happened because inside isItemExpandable I was allowing it to return true for the lowest-level items that didn't have any children. It turned out to not be related to Realm.

duncangroenewald commented 5 years ago

I have just run into the same issue when trying to delete items from the outline view. It seems this is not possible because outlineView seems to try and access properties of the objects during the delete.

      ```

realm.beginWrite() realm.delete(item)

        do {
            try realm.commitWrite()

            self.items?.remove(at: selectedRow). // Note this is an Array of items

            self.outlineView?.beginUpdates()
            self.outlineView?.removeItems(at: IndexSet(integer: selectedRow), inParent: nil, withAnimation: NSTableView.AnimationOptions.slideUp)
            self.outlineView?.endUpdates()

        } catch {...


Has anyone been able to use outlineView with Realm objects at all?