Core Animation Is a Bit Garbage Collection-y

| Comments

Phew, long time no post, eh? Sorry ‘bout that. Recently, I’ve really been delving into some of the depths of Core Animation. I gotta tell ya’, Apple really hit it out of the park with Core Animation. I have no clue what Android or some of the other mobile platforms have out there, but if it’s not close to Core Animation, then it’s a fail. Anyway, I ran into an interesting “issue” involing Core Animation’s automatic removal of animations added to layers.

CAAnimation’s removedOnCompletion property, which defaults to YES, has this summary description:

Determines if the animation is removed from the target layer’s animations upon completion.

Unfortunately, Apple doesn’t really elaborate on the reality of what this actually means.

Consider the following delegate method which is repeatedly—as in, several times a second—invoked under certain events.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#pragma mark - SCScanControllerDelegate Methods

- (void)scanController:(SCScanController *)scanController didScanImageRef:(CGImageRef)imageRef withScanResult:(NSString *)scanResult
{
    dispatch_async(dispatch_get_main_queue(), ^
        {
            if ([self.someLayer animationForKey:SCSomeLayerAnimationKey] == nil)
            {
                CAKeyframeAnimation *keyframeAnimation = [CAKeyframeAnimation animationWithKeyPath:@"opacity"];
                keyframeAnimation.duration = 0.5;
                keyframeAnimation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
                keyframeAnimation.values = [NSArray arrayWithObjects:[NSNumber numberWithFloat:1.0f], [NSNumber numberWithFloat:0.0f], nil];

                [self.someLayer addAnimation:keyframeAnimation forKey:SCSomeLayerAnimationKey];
            }
        }
    );
}

The goal is to perform an animation over and over, but the animation should only be repeated when the previous animation is finished. Seemingly, that code should do the trick. removedOnCompletion being YES will remove the animation for key SCSomeLayerAnimationKey after the animation completes, so the if statement should be true on the very next cycle.

Unfortunately, this doesn’t work! I observed that it was several seconds before the animation would fire again. Logging [self.someLayer animationKeys] in fact reveals that the animation for key SCSomeLayerAnimationKey is still present. That’s lame.

After chatting with David Duncan about this, he mentioned that Core Animation has some “garbage collection-y” attributes to it, as was readily observed in my test above. Under normal circumstances, Core Animation will remove completed animations from the target layer practically right away. We didn’t get into too many of the details, as this behavior isn’t exactly documented anywhere, so I’m not actually sure why the case above yields significantly different results. Maybe it’s worth filing a Radar? My brain is still booting up this morning, but I have a suspicion that this has something to do with the fact that I’m constantly dispatching that block to the main queue, so something may be getting gummed up for a lot longer than it normally would. Somehow, Core Animation’s internals aren’t able to remove the completed animation.

The solution is easy enough: just remove the animation explicitly.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#pragma mark - SCScanControllerDelegate Methods

- (void)scanController:(SCScanController *)scanController didScanImageRef:(CGImageRef)imageRef withScanResult:(NSString *)scanResult
{
    dispatch_async(dispatch_get_main_queue(), ^
        {
            if ([self.someLayer animationForKey:SCSomeLayerAnimationKey] == nil)
            {
                CAKeyframeAnimation *keyframeAnimation = [CAKeyframeAnimation animationWithKeyPath:@"opacity"];
                keyframeAnimation.duration = 0.5;
                keyframeAnimation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
                keyframeAnimation.values = [NSArray arrayWithObjects:[NSNumber numberWithFloat:1.0f], [NSNumber numberWithFloat:0.0f], nil];

                [self.someLayer addAnimation:keyframeAnimation forKey:SCSomeLayerAnimationKey
                    withStopBlock:^(BOOL finished)
                    {
                        [self.someLayer removeAnimationForKey:SCSomeLayerAnimationKey];
                    }
                ];
            }
        }
    );
}

(Interestingly, if I include an empty stop block, this also seems to work. Not sure why, but again, I imagine it has something to do with freeing up Core Animation just long enough to do whatever internal garbage collection it does in this case.)

Side Note: What’s that stop block thingy?! It’s something that Apple should have blockified themselves, but I couldn’t wait. It’s part of LTKit’s CALayer category.

Comments