Search code examples
ioscocoa-touchanimationcalayer

Optimising CALayer setPosition


I would like to have around 400 CALayers in an application and reposition (and scale them) all during an animated gesture. They don't need to redraw their content during this operation, just move and re-scale. I'd like to maintain animation at 60 frames a second.

I'm targeting at least anything that can run iOS 7. It's be nice to support the iOS 6 devices too.

To check for feasibility, I've written a small test application based on Apple's Single View template project. The code is included below.

Profiling with instruments, the vast majority of the run time (94%) is in the display link callback. Within this 54% is spent in [CALayer setPosition] and 33% in CA::Transaction::commit(). I've included a profile showing this.

Much of the position setting profile trace seems to be about doing lots of things that I don't need the framework to do. So, does anyone know if there is some way to turn all this stuff off and have layers be a bit more stupid :-) Ie – basically enable some kind of fast path if I just want to animate everything myself?

As a small note, I've included a technique mentioned here that provides some speed up by setting a null action for changes to the position key.

If you have other ideas for speed ups (like using UIViews instead of CALayers), I'd love to hear them.

Thanks!

The relevant parts, I hope, of the profile:

    Running Time    Self        Symbol Name
    147914.0ms  100.0%  1754.0      -[STViewController displayLinkCallback:]
    85265.0ms   57.6%   323.0        -[CALayer setPosition:]
    83842.0ms   56.6%   8301.0        CA::Layer::set_position(CA::Vec2<double> const&, bool)
    27247.0ms   18.4%   811.0          CA::Layer::begin_change(CA::Transaction*, unsigned int, objc_object*&)
    21103.0ms   14.2%   316.0           actionForKey(CALayer*, CA::Transaction*, NSString*)
    4819.0ms    3.2%    2296.0          CAAtomGetString
    188.0ms    0.1% 188.0           -[CALayer actionForKey:]
    171.0ms    0.1% 171.0           DYLD-STUB$$x_strtod
    88.0ms    0.0%  88.0            DYLD-STUB$$-[NSString(CAMLWriter) CAMLType]
    34.0ms    0.0%  34.0            CA::Transaction::get_value(unsigned int, _CAValueType, void*)
    33.0ms    0.0%  33.0            DYLD-STUB$$-[NSNumber(CAMLWriter) CAMLType]
    23316.0ms   15.7%   730.0          CA::Layer::end_change(CA::Transaction*, unsigned int, objc_object*)
    12759.0ms    8.6%   1266.0         CA::Layer::writable_state(CA::Transaction*)
    7523.0ms    5.0%    557.0          -[NSObject(NSKeyValueObserverNotification) willChangeValueForKey:]
    1145.0ms    0.7%    1145.0         OSSpinLockLock
    594.0ms    0.4% 594.0          _Unwind_SjLj_Unregister
    327.0ms    0.2% 327.0          OSSpinLockLock$shim
    310.0ms    0.2% 310.0          CAAtomGetString
    306.0ms    0.2% 306.0          __inline_isnand
    298.0ms    0.2% 246.0          CA::Transaction::unlock()
    215.0ms    0.1% 215.0          objc_msgSend
    203.0ms    0.1% 203.0          _Unwind_SjLj_Register
    168.0ms    0.1% 168.0          -[CATextLayer didChangeValueForKey:]
    149.0ms    0.1% 149.0          DYLD-STUB$$-[NSNumber(CAMLWriter) CAMLType]
    141.0ms    0.0% 141.0          CA::Layer::property_did_change(CA::Transaction*, unsigned int)
    129.0ms    0.0% 129.0          CFRetain
    123.0ms    0.0% 123.0          CA::Transaction::lock()
    112.0ms    0.0% 112.0          CA::Transaction::ensure_compat()
    100.0ms    0.0% 100.0          _NSKeyValueRetainedObservationInfoForObject
    89.0ms    0.0%  89.0           objc_msgSend$shim
    78.0ms    0.0%  78.0           DYLD-STUB$$-[NSURL(CAMLWriter) encodeWithCAMLWriter:]
    57.0ms    0.0%  57.0           DYLD-STUB$$CAMLWriterFreeAttributeList(_CAMLWriterAttribute*)
    42.0ms    0.0%  42.0           actionForKey(CALayer*, CA::Transaction*, NSString*)
    30.0ms    0.0%  30.0           DYLD-STUB$$-[CAShapeLayer setFillColor:]
    25.0ms    0.0%  25.0           DYLD-STUB$$x_strtod
    24.0ms    0.0%  24.0           DYLD-STUB$$CFNumberFormatterSetProperty$shim
    23.0ms    0.0%  23.0           _CFRetain
    8.0ms    0.0%   8.0        magazine_alloc_
    410.0ms    0.2% 410.0         DYLD-STUB$$x_strtod
    135.0ms    0.0% 135.0         CA::Layer::begin_change(CA::Transaction*, unsigned int, objc_object*&)
    133.0ms    0.0% 133.0         -[NSObject(NSKeyValueObserverNotification) willChangeValueForKey:]
    107.0ms    0.0% 107.0         CA::Transaction::unlock()
    74.0ms    0.0%  74.0          DYLD-STUB$$-[NSURL(CAMLWriter) encodeWithCAMLWriter:]
    60.0ms    0.0%  60.0          DYLD-STUB$$CAMLWriterFreeAttributeList(_CAMLWriterAttribute*)
    53.0ms    0.0%  53.0          CA::Layer::writable_state(CA::Transaction*)
    53.0ms    0.0%  53.0          DYLD-STUB$$-[NSString(CAMLWriter) CAMLType]
    27.0ms    0.0%  27.0          CA::Layer::end_change(CA::Transaction*, unsigned int, objc_object*)
    27.0ms    0.0%  27.0          CA::Transaction::ensure_compat()
    21.0ms    0.0%  21.0          DYLD-STUB$$CA::Display::DisplayLinkItem::set_user_info(void const*)
    52572.0ms   35.5%   46.0         CA::Transaction::commit()
    38450.0ms   25.9%   185.0         CA::Context::commit_transaction(CA::Transaction*)
    33772.0ms   22.8%   75.0           CA::Layer::commit_if_needed(CA::Transaction*, void (*)(CA::Layer*, unsigned int, unsigned int, void*), void*)
    33649.0ms   22.7%   36.0            CA::Layer::commit_if_needed(CA::Transaction*, void (*)(CA::Layer*, unsigned int, unsigned int, void*), void*)
    33522.0ms   22.6%   458.0            CA::Layer::commit_if_needed(CA::Transaction*, void (*)(CA::Layer*, unsigned int, unsigned int, void*), void*)
    32936.0ms   22.2%   3694.0            CA::Layer::commit_if_needed(CA::Transaction*, void (*)(CA::Layer*, unsigned int, unsigned int, void*), void*)
    28899.0ms   19.5%   298.0              CA::Context::commit_layer(CA::Layer*, unsigned int, unsigned int, void*)
    84.0ms    0.0%  84.0               CA::Render::Object::unref() const
    83.0ms    0.0%  83.0               CA::Render::encode_set_object(CA::Render::Encoder*, unsigned long, unsigned int, CA::Render::Object*, unsigned int)
    54.0ms    0.0%  54.0               DYLD-STUB$$x_strtod
    47.0ms    0.0%  47.0               CA::Render::Encoder::encode_int32(unsigned int)
    36.0ms    0.0%  36.0               CA::Render::Layer::~Layer()
    22.0ms    0.0%  21.0               CA::Layer::thread_flags_(CA::Transaction*)
    17.0ms    0.0%  17.0               CA::Layer::copy_render_layer(CA::Transaction*, unsigned int, unsigned int*)
    128.0ms    0.0% 48.0              CA::Context::commit_layer(CA::Layer*, unsigned int, unsigned int, void*)
    91.0ms    0.0%  0.0          CA::Context::commit_layer(CA::Layer*, unsigned int, unsigned int, void*)
    46.0ms    0.0%  0.0         CA::Context::commit_layer(CA::Layer*, unsigned int, unsigned int, void*)
    1.0ms    0.0%   1.0         CA::Render::Object::unref() const
    1.0ms    0.0%   1.0         CA::Render::Encoder::encode_int32(unsigned int)
    1765.0ms    1.1%    61.0           CA::Render::Encoder::send_message(unsigned int, unsigned int)
    904.0ms    0.6% 15.0           _dispatch_queue_wakeup_global_slow
    711.0ms    0.4% 3.0        CA::Render::Encoder::~Encoder()
    100.0ms    0.0% 1.0        CA::Layer::layout_and_display_if_needed(CA::Transaction*)
    97.0ms    0.0%  13.0           pthread_mutex_unlock
    87.0ms    0.0%  14.0           _dispatch_async_f_slow
    85.0ms    0.0%  41.0           _pthread_mutex_lock
    74.0ms    0.0%  2.0        CA::Transaction::foreach_root(void (*)(CA::Layer*, void*), void*)
    70.0ms    0.0%  29.0           CA::Layer::prepare_commit(CA::Transaction*)
    66.0ms    0.0%  18.0           CA::Transaction::get_value(unsigned int, _CAValueType, void*)
    56.0ms    0.0%  31.0           CA::Render::Encoder::ObjectCache::encode_invalidations(CA::Render::Encoder*)
    51.0ms    0.0%  0.0        CA::Layer::collect_animations(CA::Transaction*, double, double*)
    38.0ms    0.0%  38.0           dispatch_async_f
    38.0ms    0.0%  38.0           _dispatch_queue_push_slow
    34.0ms    0.0%  32.0           CA::Context::retain_all_contexts(bool, CA::Context**, unsigned long&)
    33.0ms    0.0%  12.0           CA::Render::Encoder::Encoder(x_heap_struct*, unsigned int, void*, unsigned int, double)
    31.0ms    0.0%  16.0           CA::Context::unref(bool)
    29.0ms    0.0%  29.0           CA::Layer::set_next_animation_time(CA::Transaction*, double, double)
    26.0ms    0.0%  20.0           CA::Transaction::unlock()
    20.0ms    0.0%  20.0           x_heap_malloc_small_
    19.0ms    0.0%  19.0           CA::DispatchGroup::enqueue(void (*)(void*), void*)
    17.0ms    0.0%  17.0           CARecordTransaction
    17.0ms    0.0%  17.0           CABackingStoreCollectAsync
    15.0ms    0.0%  15.0           CAMarkStatistic
    15.0ms    0.0%  15.0           CALayerGetLayer
    13.0ms    0.0%  13.0           x_mem_dealloc_chain
    10.0ms    0.0%  10.0           CA::Transaction::lock()
    8.0ms    0.0%   8.0        x_heap_new_with_ptr
    7.0ms    0.0%   7.0        x_heap_free
    6.0ms    0.0%   6.0        DYLD-STUB$$-[NSString(CAMLWriter) CAMLType]
    5.0ms    0.0%   5.0        CA::Transaction::foreach_command(unsigned int, void (*)(int, unsigned long, void const*, void*), void*)
    5.0ms    0.0%   4.0        CATimeWithHostTime
    4.0ms    0.0%   4.0        DYLD-STUB$$-[OS_dispatch_data finalize]
    4.0ms    0.0%   4.0        DYLD-STUB$$-[NSNumber(CAMLWriter) CAMLType]
    3.0ms    0.0%   3.0        OSSpinLockUnlock$shim
    3.0ms    0.0%   3.0        OSSpinLockLock$shim
    3.0ms    0.0%   3.0        pthread_mutex_lock
    3.0ms    0.0%   3.0        DYLD-STUB$$dlsym
    2.0ms    0.0%   2.0        CA::CG::Queue::collect(double)
    2.0ms    0.0%   0.0        CACurrentMediaTime
    2.0ms    0.0%   2.0        CA::Render::Encoder::set_object_cache(CA::Render::Encoder::ObjectCache*)
    2.0ms    0.0%   2.0        x_heap_malloc
    2.0ms    0.0%   2.0        __mtx_droplock
    1.0ms    0.0%   1.0        OSSpinLockLock
    1.0ms    0.0%   1.0        CA::Render::Encoder::encode_int64(unsigned long long)
    1.0ms    0.0%   1.0        CA::Layer::collect_layers_(CA::Layer::CollectLayersData*)
    1.0ms    0.0%   1.0        CA::Render::Coder::Coder(x_heap_struct*)
    1.0ms    0.0%   1.0        DYLD-STUB$$CAMLWriterFreeAttributeList(_CAMLWriterAttribute*)
    1.0ms    0.0%   1.0        x_hash_table_foreach
    1.0ms    0.0%   1.0        CA::Render::Coder::~Coder()
    1.0ms    0.0%   1.0        pthread_threadid_np
    13744.0ms    9.2%   2112.0        CA::Layer::free_transaction(CA::Transaction*)
    70.0ms    0.0%  26.0          x_hash_table_free
    67.0ms    0.0%  0.0       CA::Transaction::Level::free_levels(CA::Transaction::Level*)
    34.0ms    0.0%  12.0          x_hash_table_foreach
    30.0ms    0.0%  30.0          CALayerRelease
    29.0ms    0.0%  29.0          CA::Layer::State::unref(CA::Transaction*) const
    25.0ms    0.0%  20.0          x_hash_table_remove_if
    16.0ms    0.0%  16.0          x_hash_table_size
    11.0ms    0.0%  11.0          DYLD-STUB$$__39-[CAWindowServer displayWithDisplayId:]_block_invoke
    8.0ms    0.0%   8.0       DYLD-STUB$$-[NSString(CAMLWriter) CAMLType]
    7.0ms    0.0%   7.0       x_mem_dealloc_size
    4.0ms    0.0%   4.0       OSSpinLockLock
    4.0ms    0.0%   4.0       CA::Render::Encoder::ObjectCache::encode_invalidations(CA::Render::Encoder*)
    3.0ms    0.0%   3.0       DYLD-STUB$$usleep$shim
    3.0ms    0.0%   0.0       magazine_dealloc_
    2.0ms    0.0%   2.0       DYLD-STUB$$-[NSNumber(CAMLWriter) CAMLType]
    2.0ms    0.0%   2.0       x_heap_malloc
    2.0ms    0.0%   2.0       x_heap_free
    2.0ms    0.0%   2.0       CA::Render::Encoder::~Encoder()
    2.0ms    0.0%   2.0       DYLD-STUB$$mig_put_reply_port$shim
    2.0ms    0.0%   2.0       CA::release_root_if_unused(CA::Layer*, CA::Layer*, void*)
    2.0ms    0.0%   2.0       CA::Render::Encoder::send_message(unsigned int, unsigned int)
    1.0ms    0.0%   1.0       CAMarkStatistic
    1.0ms    0.0%   1.0       CA::Layer::collect_animations(CA::Transaction*, double, double*)
    1.0ms    0.0%   1.0       pthread_mutex_unlock
    1.0ms    0.0%   1.0       CA::release_root(CA::Layer*, CA::Layer*, void*)
    1.0ms    0.0%   1.0       x_heap_malloc_small_
    1.0ms    0.0%   1.0       _pthread_mutex_lock
    1.0ms    0.0%   1.0       x_mem_dealloc_chain
    4061.0ms    2.7%    4061.0       cos
    2317.0ms    1.5%    2317.0       objc_msgSend
    1155.0ms    0.7%    1155.0       sin
    144.0ms    0.0% 0.0      +[CATransaction setAnimationDuration:]
    104.0ms    0.0% 24.0         objc_object::sidetable_release(bool)
    92.0ms    0.0%  78.0         CA::Transaction::push()
    86.0ms    0.0%  17.0         objc_object::sidetable_retain()
    76.0ms    0.0%  76.0         objc_retain
    59.0ms    0.0%  59.0         -[__NSArrayM countByEnumeratingWithState:objects:count:]
    56.0ms    0.0%  56.0         CA::Layer::set_position(CA::Vec2<double> const&, bool)
    42.0ms    0.0%  42.0         objc_release
    41.0ms    0.0%  41.0         DYLD-STUB$$objc::DenseMapBase<objc::DenseMap<objc_object*, unsigned long, true, objc::DenseMapInfo<objc_object*> >, objc_object*, unsigned long, objc::DenseMapInfo<objc_object*>, true>::erase(objc_object* const&)
    19.0ms    0.0%  19.0         CA::Transaction::pop()
    17.0ms    0.0%  17.0         +[CATransaction begin]
    17.0ms    0.0%  13.0         CATimeWithHostTime
    15.0ms    0.0%  15.0         -[CADisplayLink timestamp]
    10.0ms    0.0%  10.0         +[CATransaction commit]
    4.0ms    0.0%   4.0      os_lock_trylock
    2.0ms    0.0%   2.0      CA::Transaction::ensure_compat()
    1.0ms    0.0%   1.0      x_hash_table_remove_if
    1.0ms    0.0%   1.0      DYLD-STUB$$-[NSString(CAMLWriter) CAMLType]
    1.0ms    0.0%   1.0      DYLD-STUB$$-[NSNumber(CAMLWriter) CAMLType]
    1.0ms    0.0%   1.0      x_heap_free
    1.0ms    0.0%   1.0      x_hash_table_free
    1.0ms    0.0%   1.0      objc::DenseMapBase<objc::DenseMap<objc_object*, unsigned long, true, objc::DenseMapInfo<objc_object*> >, objc_object*, unsigned long, objc::DenseMapInfo<objc_object*>, true>::find(objc_object* const&)

And this is the code I added on top of the template Single View Application project:

//  STViewController.m
//  LayerSpeedTest

#import "STViewController.h"

@implementation CALayer (Extensions)

- (void)setNullAsActionForKeys:(NSArray *)keys
{
  NSMutableDictionary *dict = [self.actions mutableCopy];

  if(dict == nil)
  {
    dict = [NSMutableDictionary dictionaryWithCapacity:[keys count]];
  }

  for(NSString *key in keys)
  {
    [dict setObject:[NSNull null] forKey:key];
  }

  self.actions = dict;
}

@end

@interface STViewController ()
@property (nonatomic, retain) NSArray *layers;
@property (nonatomic, retain) CADisplayLink *displayLink;
-(void) displayLinkCallback:(CADisplayLink*) displayLink;
@end

@implementation STViewController

- (void)viewDidLoad
{
  [super viewDidLoad];

  UIView *const view = [self view];
  [view setBackgroundColor: [UIColor blackColor]];

  CALayer *const layer = [view layer];
  NSMutableArray *layers = [NSMutableArray array];

  for(NSUInteger i=0; i<24*3*4; ++i)
  {
    CATextLayer *const textLayer = [CATextLayer layer];
    [textLayer setContentsScale: [[UIScreen mainScreen] scale]];
    [textLayer setForegroundColor: [[UIColor whiteColor] CGColor]];
    [textLayer setBackgroundColor: [[UIColor clearColor] CGColor]];
    [textLayer setBounds: CGRectMake(0,0,100,100)];
    [textLayer setString: @"100"];
    [textLayer setNeedsDisplay];
    [textLayer setPosition: CGPointMake(160,200)];
    [textLayer setNullAsActionForKeys: @[@"position"]];
    [layer addSublayer: textLayer];
    [layers addObject: textLayer];
  }

  _layers = layers;

  _displayLink = [CADisplayLink displayLinkWithTarget: self selector: @selector(displayLinkCallback:)];
  [_displayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSDefaultRunLoopMode];
}

-(void) displayLinkCallback:(CADisplayLink *)displayLink
{
  const NSTimeInterval timestamp = [displayLink timestamp]*0.3;
  [CATransaction begin];
  [CATransaction setAnimationDuration:0];
  {
    NSUInteger i=0;
    for(CALayer *const layer in _layers)
    {
      const CGFloat angle = i*0.1 + timestamp;
      [layer setPosition: CGPointMake( 160 + 100 * sin(angle), 250 + 200 * cos(angle*0.728734))];
      ++i;
    }
  }
  [CATransaction commit];
}

@end

Solution

  • I really think you should use Sprite Kit, OpenGLES or any other thing to animate 400 items and keep up 60 fps. BTW, 60fps is too much - even Xcode asks me to reduce from 60 to 30 in my tests.

    Assuming you can't change from CoreAnimation to a more efficient tool, you are really angry with iOS to use "manual" animations. Instead of manually setting position, use a KeyPath:

    https://developer.apple.com/library/ios/documentation/Cocoa/Conceptual/CoreAnimation_guide/CreatingBasicAnimations/CreatingBasicAnimations.html

    If you think you have to manually set positions, you probably need to transform this "random" motion into an equation. CA transactions are "smarter" to manage themselves, instead of making 60 transactions per second.

    And, as a last remind, you need to make your model way simpler. Using CATextLayer is an evil way to waste CPU cycles - specially if you plan to use scaling. You probably can squeeze some FPS by pre-rendering to a CGImage.

    But seriously, consider not using CoreAnimation for that: 60 FPS means you have only 16ms of CPU time to calculate the next frame, build the scene and all the mumbo-jumbo needed to render that stuff. All using only one thread (CA is not thread-safe). Using GL or SK can leverage the real power of iOS devices, by doing a lot of that work in GPU.