Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why does CoreAnimation appear to be caching the presentation layer copy?

So we are running into some interesting issues with what appears to be the caching of the presentation layer, and we are wondering if other people are seeing this, and what workarounds we can do.

We have a couple of our own properties that we are implicitly animating. We are doing this by declaring them as properties and synthesizing them dynamically:

@interface GOOLayer : CALayer
@property float gooValue;
@end

NSString *const kGOOLayerValueKey = @"gooValue";

@implementation GOOLayer

@dynamic gooValue;

- (id)initWithLayer:(id)layer {
  if ((self = [super initWithLayer:layer])) {
    id value = [layer valueForKey:kGOOLayerValueKey];
    [self setValue:value forKey:kGOOLayerValueKey];
  }
  return self;
}

- (id<CAAction>)actionForKey:(NSString *)event {
  id <CAAction> action = [super actionForKey:event];
  if (action == nil && [event isEqual:kGOOLayerValueKey]) {
    CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:event];
    animation.fromValue = [self.presentationLayer valueForKey:event];
    animation.duration =  [CATransaction animationDuration];
    return animation;
  }
  return action;
}

- (void)display {
  GOOLayer *presoLayer = [self presentationLayer];
  NSLog(@"Display GooValue: %f", presoLayer.gooValue);
}

+ (BOOL)needsDisplayForKey:(NSString *)key {
  if ([key isEqual:kGOOLayerValueKey]) {
    return YES;
  }
  return [super needsDisplayForKey:key];
}

In most cases this works fine, and we see the appropriate values being logged. In the first case, we get proper implicit animation:

- (IBAction)doSomeGoo:(id)sender {
  sublayer_.gooValue = random();
}

Output:

2012-12-19 10:10:41.440 CoreAnimation[94149:c07] Display GooValue: 1804289408.000000
2012-12-19 10:10:41.502 CoreAnimation[94149:c07] Display GooValue: 1804289408.000000
...
2012-12-19 10:10:41.739 CoreAnimation[94149:c07] Display GooValue: 898766464.000000
2012-12-19 10:10:41.757 CoreAnimation[94149:c07] Display GooValue: 846930880.000000

In the second case, we disable the actions so we get a single value update which is fine:

- (IBAction)doSomeGoo2:(id)sender {
  long newValue = random();
  NSLog(@"newValue = %f", (float)newValue);
  [CATransaction begin];
  [CATransaction setDisableActions:YES];
  sublayer_.gooValue = newValue;
  [CATransaction commit];
}

First time through:

2012-12-19 10:11:16.208 CoreAnimation[94149:c07] newValue = 1714636928.000000
2012-12-19 10:11:16.209 CoreAnimation[94149:c07] Display GooValue: 1714636928.000000

Second time through:

2012-12-19 10:11:26.258 CoreAnimation[94149:c07] newValue = 1957747840.000000
2012-12-19 10:11:26.259 CoreAnimation[94149:c07] Display GooValue: 1957747840.000000

It's the third case in which we just call [CALayer presentationLayer] before we do the transaction that is interesting:

- (IBAction)doSomeGoo3:(id)sender {
  GOOLayer *presoLayer = sublayer_.presentationLayer;
  long newValue = random();
  NSLog(@"presoLayer.gooValue = %f newValue = %f", presoLayer.gooValue, (float)newValue);
  [CATransaction begin];
  [CATransaction setDisableActions:YES];
  sublayer_.gooValue = newValue;
  [CATransaction commit];
}

First time through:

2012-12-19 10:12:12.505 CoreAnimation[94149:c07] presoLayer.gooValue = 1957747840.000000 newValue = 424238336.000000
2012-12-19 10:12:12.505 CoreAnimation[94149:c07] Display GooValue: 1957747840.000000

Second time through:

2012-12-19 10:12:13.905 CoreAnimation[94149:c07] presoLayer.gooValue = 424238336.000000 newValue = 719885376.000000
2012-12-19 10:12:13.906 CoreAnimation[94149:c07] Display GooValue: 424238336.000000

Note how the addition of the presentationLayer call makes it so that display doesn't get the current value.

Any suggestions as to how to deal with this, or if I am completely misunderstanding things, and this is expected behavior?

like image 265
dmaclach Avatar asked Jan 10 '13 04:01

dmaclach


People also ask

Does Core Animation use cached content?

Although Core Animation uses cached content as much as possible, your app must still provide the initial content and update it from time to time. There are several ways for your app to provide a layer object with content, which are described in detail in Providing a Layer’s Contents.

What is a layer in Core Animation?

A layer captures your content into a bitmap that can be manipulated easily by the graphics hardware. In most apps, layers are used as a way to manage the content of views but you can also create standalone layers depending on your needs. Most of the animations you create using Core Animation involve the modification of the layer’s properties.

What are the properties of Core Animation?

Most of the animations you create using Core Animation involve the modification of the layer’s properties. Like views, layer objects have a bounds rectangle, a position onscreen, an opacity, a transform, and many other visually-oriented properties that can be modified.

What is an anchor point in Core Animation?

The anchor point is one of several properties that you specify using the unit coordinate system. Core Animation uses unit coordinates to represent properties whose values might change when the layer’s size changes. You can think of the unit coordinates as specifying a percentage of the total possible value.


1 Answers

I didn't access the presentationLayer, but still got to the same problem today.

Whenever a UIScrollView's scroll bars appeared, then some animations did not work as intended. It took me hours to figure out that it is a problem related to CATransaction and accessing the presentationLayer.

Accessing the presentationLayer causes CATransaction to ignore (or defer) any commits of explicit transactions until the current runloop cycle ends. Looking at the log of messages, +[CATransaction commit] for explicit transactions becomes a no-op as soon as the presentationLayer was accessed for ANY layer.

I found two way to make these commits work again, but with negative side-effects.

  1. Flushing all layer changes using +[CATransaction flush] updates the presentationLayer of all layers and makes +[CATransaction begin…commit] work as expected.
  2. Committing the implicit transaction using +[CATransaction commit] yields the same result.

But as soon as I did that, other random problems occurred throughout the app. Some views were simply invisible even through they were layouted correctly and even drawRect: was called. Going deeper I noticed that UIView's and their backing CALayer became out of sync very often.

I came to the conclusion what I was wrong to rely on the presentationLayer for what I wanted to do.

Apple's documentation article Core Animation Rendering Architecture makes clear that presentationLayer has all values of the layer as they are currently visible to the user. Any changes made with or without animation won't be reflected by the presentationLayer until the runloop cycle ends (or the outer usually implicit CATransaction was flushed/committed) and all layer changes are presented to the user.

As my animations rely on changes made to the layer within the same runloop cycle, these changes are not yet visible to the user and thus not available through the presentationLayer.

-[CALayer display] should only access the layer's values and not those of its presentationLayer, as the examples in Providing Layer Content show. Animations which should start from what the user currently sees can use the presentationLayer's value as fromValue.

Any other problems which still arise are most likely due to using the rendering architecture in a wrong way. I will rethink my animations so that they use the layer's current state instead of the presentationLayer's.

like image 131
fluidsonic Avatar answered Oct 16 '22 15:10

fluidsonic