5

My iOS app stalls on setDelegate for about 15 seconds after about 100,000 setDelegate calls. Changing the delegate property from weak to assign fixes the problem. Any idea why the weak property has so much overhead and stalls the app?

I suspect the weak references are maintained in an array so the runtime can loop thru them and set the pointers to nil when the delegate object gets deallocated. Does the array have a max size? The stall gets much longer as n nears 100,000.

Sample code is below:

Header file:

#import <Foundation/Foundation.h>

@protocol GraphDataPointDelegate <NSObject>
- (BOOL)drawGraphByDistance;
@end

@interface GraphDataPoint : NSObject
- (id)initWithYValue:(NSNumber *)yValue withTime:(NSNumber *)time withDistance:(NSNumber *)distance withDelegate:(id <GraphDataPointDelegate> )delegate;
@end

@interface Graph : NSObject <GraphDataPointDelegate>
@end

M File

#import "Graph.h"

@interface GraphDataPoint ()

@property (nonatomic, weak, readwrite) id <GraphDataPointDelegate> delegate;
@property (nonatomic, strong, readwrite) NSNumber *yValue;
@property (nonatomic, strong, readwrite) NSNumber *time;
@property (nonatomic, strong, readwrite) NSNumber *distance;

@end

@implementation GraphDataPoint

- (id)initWithYValue:(NSNumber *)yValue withTime:(NSNumber *)time withDistance:(NSNumber *)distance withDelegate:(id<GraphDataPointDelegate>)delegate {
    self = [super init];
    if (self) {
        self.yValue = yValue;
        self.time = time;
        self.distance = distance;
        self.delegate = delegate;
    }
    return self;
}

- (id)graphXValue {
    if ([_delegate drawGraphByDistance]) {
        return _distance;
    } else {
        return _time;
    }
}

@end

@implementation Graph

- (id)init  {
    self = [super init];
    if (self) {

        NSMutableArray *array = [NSMutableArray array];
        NSLog(@"before");
        for (int i = 0; i < 100000; i++) {
            GraphDataPoint *graphData = [[GraphDataPoint alloc] initWithYValue:@1 withTime:@1 withDistance:@1 withDelegate:self];
            [array addObject:graphData];
        }
        NSLog(@"after");
    }
    return self;
}

- (BOOL)drawGraphByDistance {
    return YES;
}

@end
cbartel
  • 1,298
  • 1
  • 11
  • 18
  • 1
    Do you know what's special about `weak` properties? – rmaddy May 15 '14 at 21:40
  • 1
    Weak specifies a reference that does not keep the referenced object alive. A weak reference is set to nil when there are no strong references to the object. – cbartel May 15 '14 at 21:47
  • 3
    Exactly. And that's the overhead. `assign` has no overhead but logs of danger. `weak` has overhead but none of the danger. – rmaddy May 15 '14 at 22:23
  • 1
    Does every data point in your graph really need its own delegate? I'm not clear on what you're trying to do, but it seems likely that the only object that should know about graph data points is the graph that contains them. If the data point needs some information (like the value from `drawGraphByDistance`), the graph could simply pass that value as a parameter to `graphXValue`. – Caleb May 16 '14 at 14:46
  • GraphByDistance (or Time) is a dynamic setting that is changed with a switch in the UI. I plan on updating the code to pull from NSUserDefaults instead of using the delegate pattern. Just trying to learn something about weak references. thanks. – cbartel May 16 '14 at 15:34
  • I don't understand why this question doesn't have an accepted answer. My answer answers the question and Mecki's explains it in a lot more depth (and is the one that deserves to be marked accepted). If there is something the current answers don't cover that you'd like explained, please make it clear. If not, you should probably mark Mecki's as accepted. – nhgrif May 16 '14 at 21:55

2 Answers2

7

The system needs to keep track of every memory address where a weak pointer to an object is stored. Why? Because if the object is to be dealloc'ed (its memory will be freed), all those pointers must be set to nil first. That's what makes weak pointers special: They do not retain objects (not keep them alive), but they are also never dangling pointers (never pointing to memory addresses of former, now dead objects); if the object dies, they become nil. So whenever a weak reference is changed in value, the system must first tell a global weak pointer tracking manager to delete the information recorded for this memory address in the past (if any) and then record the new information after the object was changed. Needless to say that the whole thing must be thread-safe and thus involves (slightly expensive) locking.

__weak id x;
// ...
x = anObject;
// ...
x = anotherObject;
// ....
x = nil;

is in fact (not really, just to get the concept across):

__weak id x;
// ...
[WeakPointerTrackingManager lock];
x = anObject;
[WeakPointerTrackingManager var:&x pointsTo:anObject];
[WeakPointerTrackingManager unlock];
// ...
[WeakPointerTrackingManager lock];
x = anotherObject;
[WeakPointerTrackingManager var:&x pointsTo:anotherObject];
[WeakPointerTrackingManager unlock];
// ...
[WeakPointerTrackingManager lock];
x = nil;
[WeakPointerTrackingManager deleteInfoForVar:&x];
[WeakPointerTrackingManager unlock];

assign does nothing like that. Is just stores a reference to the object without increasing the object retain counter. However, if the object dies, the assign variable still points to the memory address where the object used to live. If you now send a message to this non-existing object, your app may crash or other undefined things may happen.

But seriously, every app that performs 100,000 setDelegate calls is broken by design IMHO. I can't think of any serious use case where this would be meaningful. There is probably a much better way to do whatever you intend to do here.

Just for the records, accessing a weak variable is expensive, too.

__weak id x;
// ...
[x sendMessage];
// ...
__strong id y; // Strong is optional, 
               // vars are strong by default
y = x;

is in fact (not really):

__weak id x;
// ...
__strong id tmp;
[WeakPointerTrackingManager lock];
tmp = [x retain];
[WeakPointerTrackingManager unlock];
[tmp sendMessage];
[tmp release];
// ...
__strong id y;
[WeakPointerTrackingManager lock];
y = [x retain];
[WeakPointerTrackingManager unlock];
Mecki
  • 125,244
  • 33
  • 244
  • 253
  • "So whenever a weak reference is changed in value..." In this case the weak reference is not getting changed. The problem is the number of weak references is large. See the sample code above. thanks. – cbartel May 16 '14 at 15:37
  • @cbartel: When you call `setDelegate:` for the very first time on an object the weak variable **is changed**. It is changed form `nil` (its initial value after calling `alloc`) to whatever parameter you passed to `setDelegate:` (`self.distance =` is the same, just a different syntax) and that causes its memory address to be recorded (and the initital recording of a weak variable is even more expensive that any later change). I wouldn't use objects at all for what you are doing (and especially not NSNumber). Why do you think `CGRect/CGSize/CGPoint` are `structs` and use simple `float` values? – Mecki May 16 '14 at 17:17
1

weak has some overhead that assign does not.

They're both different from strong in that they don't increase the retain count for ARC.

But as for the difference between weak and assign, when an object pointed to by a weak reference is deallocated, the weak pointer automatically zeros out to nil. This doesn't happen with assign. This means if you try to call a method on a deallocated delegate that is an assign property, you'll get a crash, but assign also doesn't have that extra overhead.

nhgrif
  • 61,578
  • 25
  • 134
  • 173
  • 5
    once upon a time, when ARC and weak references were introduced, I stopped using "unsafe_unretained" variables. From that moment on, my apps stopped crashing ;) If I have a performance problem with weak variables, I would try to produce less garbage objects. I wouldn't change them to unsafe_unretained variables. Creating 100000 objects is the mistake, IMHO. – Michael May 15 '14 at 21:52
  • 5
    @Michael Agreed. `weak` (and ARC in general) is one of the greatest features ever added to Objective-C with blocks being a close 2nd. :) – rmaddy May 15 '14 at 22:25