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
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:
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.