9

I have a CADisplayLink that triggers a draw method in a Director object. I want to invalidate the CADisplayLink and then deallocate some singleton Cache objects that are used by the Director object. The singleton Cache objects are not retained by the draw method.

In a method called stopAnimation in the Director (this method is unrelated to the draw method), I do:

[displayLink invalidate];

and then I start releasing the singleton Cache objects, but then the CADisplayLink fires and the draw method gets called one last time. The draw methods tries to access the deallocated singleton objects and everything crashes.

This only happens sometimes: there are times in which the app doesn't crash because the Cache objects are released after the displayLink is actually invalidated and the draw method has already finished running.

How can I check, after invalidating the displayLink, that the draw method has finished running and that it won't fire again, in order so safely invalidate the Cache objects? I don't want to modify the draw method if possible.

I tried a number of combinations, including performing displayLink invalidate on the main thread using

[self performSelectorOnMainThread:@selector(stopAnimation) withObject:self waitUntilDone:YES]

or trying to perform it in the currentRunLoop by using

[[NSRunLoop currentRunLoop] performSelector:@selector(stopAnimation) target:self argument:nil order:10 modes:[NSArray arrayWithObject:NSDefaultRunLoopMode]];

but the results is always the same, sometimes it releases the shared Caches too early.

I also don't want to use the performSelector:withObject:afterDelay: method with an arbitrary delay. I want to make sure the displayLink is invalidated, that the draw method ended, and that it won't be run again.

Ricardo Sanchez-Saez
  • 9,466
  • 8
  • 53
  • 92

2 Answers2

8

This might be a bit late but since there has been no answers...

I do not think, your selector is called once more, but rather the display link's thread is in the middle of your draw frame method. In any case, the problem is quite the same.. This is multithreading and by trying to dealloc some objects in one thread while using them in another will usually result in a conflict.

Probably the easiest solution would be putting a flag and an "if statement" in your draw frame method as

if(schaduledForDestruction) {
[self destroy];
 return;
}

and then wherever you are invalidating your display link set "schaduledForDestruction" to YES.

If you really think the display link calls tis method again, you could use another if inside that one "destructionInProgress".

If you do not want to change the draw frame method, you could try forcing a new selector to the display link...

CADisplayLink *myDisplayLink;
BOOL resourcesLoaded;
SEL drawSelector;

- (void)destroy {    
    if(resourcesLoaded) {
        [myDisplayLink invalidate];
        //free resources
        resourcesLoaded = NO;
    }    
}
- (void)metaLevelDraw {
    [self performSelector:drawSelector];
}
- (void)drawFrame {
    //draw stuff
}
- (void)beginAnimationing {
    drawSelector = @selector(drawFrame);
    myDisplayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(metaLevelDraw)];
    [myDisplayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
}
- (void)endAnimationing {
    drawSelector = @selector(destroy);
}

or just consider something like this (but I can't say this is safe. If the newly created display link can run the selector on a different thread then the original, it solves nothing)..

CADisplayLink *myDisplayLink;
BOOL resourcesLoaded;

- (void)destroy {    
    if(resourcesLoaded) {
        [myDisplayLink invalidate];
        //free resources
        resourcesLoaded = NO;
    }    
}
- (void)drawFrame {
    //draw stuff
}
- (void)beginAnimationing {
    myDisplayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(drawFrame)];
    [myDisplayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
}
- (void)endAnimationing {
    [myDisplayLink invalidate];
    myDisplayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(destroy)];
    [myDisplayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
}
Matic Oblak
  • 16,318
  • 3
  • 24
  • 43
  • 1
    Never is to late for useful information! That was a great answer, the information is really helpful. I haven't done some testing yet, but probably you are right about the threading causing the problem (rather than the draw method being called again after the end of the CADisplayLink). I have not tried your solutions yet, but I think that the second one seems very elegant. Thanks a lot for taking the time to answer! – Ricardo Sanchez-Saez Dec 04 '11 at 03:15
  • Unfortunately, neither method works for me in iOS 11. Haven't tested in iOS 10. – slider Oct 16 '17 at 21:11
  • @chicobermuda I had no special issues with display link on iOS 11 so it might be your problem is a bit unique. Do you have any additional info or code to show? Maybe asked another question about it? – Matic Oblak Oct 17 '17 at 06:03
  • I tried both methods in Swift, and found that setting and manipulating selectors didn't prevent the layer's `draw` or `display` methods from firing even after releasing from the run loop mode. From Apple: "if a timer fires when the run loop is in the middle of executing a handler routine, the timer waits until the next time through the run loop to invoke its handler routine." Your first solution is safer, although it should be called from the layer's subclass since we know the superclass's selector doesn't have to be called for the layer to be redrawn. – slider Oct 17 '17 at 18:53
0

The problem is that CALayer's display() continues to be called even after the CADisplayLink is released from the run loop mode.

If a timer fires when the run loop is in the middle of executing a handler routine, the timer waits until the next time through the run loop to invoke its handler routine.

The most future-proof way to prevent the layer from updating after calling invalidate() is to subclass CALayer, add a flag, change the flag alongside invalidate(), and check the flag's value before calling super.display().

class Layer: CALayer {

    var shouldDisplay: Bool = true

    override func display() {
        if shouldDisplay {
            super.display()
        }
    }
}

So, in conjunction with invalidating your display link, set the layer's shouldDisplay to false. This will prevent the subclass from continuing to reload the contents of the layer, regardless of which thread calls invalidate().

slider
  • 2,736
  • 4
  • 33
  • 69