2

I'm trying to write a category based on node.js EventEmitter, which can take a number of blocks, store them weakly in an array, and execute them later if the instance creating the block isn't deallocated (in which case they would be removed from the array). This is in order not to keep filling the array with old, unused blocks.

The problem is that the blocks seem to be copied by the class, and thusly never released, even though the instance creating the block is deallocated.

So the implementation looks something like this;

Usage

[object on:@"change" do:^(id slf, NSArray *args) {
    NSLog(@"something changed");
}];

Implementation (WeakReference class found here, courtesy of noa)

- (void)on:(NSString *)eventType do:(Callback)callback
{
    NSMutableArray *callbacks = self.emitterEvents[eventType];
    __weak Callback wcb = callback;
    // Wrap the callback in NSValue subclass in order to reference it weakly
    WeakReference *cbr = [WeakReference weakReferenceWithObject:wcb];
    callbacks[callbacks.count] = cbr;
}

- (void)emit:(NSString *)eventType withArgs:(NSArray *)objArgs
{
    NSInteger idx = 0;
    NSMutableIndexSet *indices = [NSMutableIndexSet indexSet];
    callbacks = (NSMutableArray *)callbacks;
    for (WeakReference *cbv in callbacks) {
        __weak id cb = [cbv nonretainedObjectValue];
        if (cb) {
            Callback callback = (Callback)cb;
            __weak id slf = self;
            callback(slf, objArgs);
        } else {
            [indices addIndex:idx];
        }
        idx++;
    }
    [callbacks removeObjectsAtIndexes:indices];
}

I read something about blocks being copied when used past their scope, but frankly, reading about all these block semantics is kind of making my head spin right now.

Is this way of approaching the problem even possible?

Community
  • 1
  • 1
Goos
  • 121
  • 6

3 Answers3

0

copy a block which is already copied is same as retain it, so if the caller of the method copy the block first then pass it to the method, it should works as you expected. but this means you cannot simply use the method as you described in your usage section.

you have use it like this

typeofblock block = ^(id slf, NSArray *args) {
    NSLog(@"something changed");
};
self.block = [block copy]
[object on:@"change" do:self.block];

to actual solve the problem, you have to figure out owns the block. the caller of on:do:, or the object been called?

sounds to me you want to remove the block when the caller is deallocated, which means the owner of the block is the caller. but your on:do: method does not aware the owner of the block, and cannot remove the block when the caller is deallocated.

one way is to pass the owner of the block into the method and remove the block when it deallocated. this can be done use associate object.

- (void)on:(NSString *)eventType do:(Callback)callback sender:(id)sender
{
    // add the block to dict
    // somehow listen to dealloc of the sender and remove the block when it is called
}

another way is to add new method to remove the block, and call the method in dealloc or other place to remove the block manually.

your approach is similar to KVO, which require the observer to unregister the observation, and I think is a good practice that you should follow.

Bryan Chen
  • 45,816
  • 18
  • 112
  • 143
0

In Objective-C, blocks are objects, but unlike other objects, they are created on the stack. If you want to use the block outside of the scope it was created you must copy it.

[object on:@"change" do:^(id slf, NSArray *args) {
    NSLog(@"something changed");
}];

Here, you are passing a pointer to a block on the stack. Once your current stack frame is out of scope, your block is gone. You could either pass a copy to the block, making the caller the owner of the block, or you could copy the block in the receiver.

If you want the caller to own the block, then you have to keep a strong reference to the block in the caller (e.g. as a property). Once the caller gets deallocated, you lose your strong reference and your weak reference is set to nil.

Sebastian
  • 7,670
  • 5
  • 38
  • 50
  • the caller shouldn't need to copy it in this case. it's the called method's job to decide if it needs to be copied. – newacct Apr 12 '13 at 09:11
0

Thanks for the answers, I realize I was a little bit off on how blocks are managed. I solved it with a different approach, inspired by Mike Ash's implementation of KVO with blocks & automatic dereferencing, and with xlc's advice on doing it in dealloc.

The approach is along the lines of this (in case you don't want to read the whole gist):

  • Caller object assigns listener to another object with on:event do:block with:caller
  • Emitter object creates a Listener instance, with a copy of the block, reference to emitter & the event-type
  • Emitter adds the copied block to an array inside a table (grouped by event-types), creates an associated object on the caller and attaches the listener
  • Emitter method-swizzles the caller, and adds a block to its dealloc, which removes itself from the emitter
  • The caller can then choose to handle the listener-instance, which is returned from the emit-method, if it wants to manually stop the listener before becoming deallocated itself

Source here

I don't know if it is safe for use, I've only tested it on a single thread in a dummy-application so far.

Community
  • 1
  • 1
Goos
  • 121
  • 6