Recently when I was playing with Stuart Hall's tutorial for UIKit Dynamics (http://stuartkhall.com/posts/flipcase-bounce-in-uikit-dynamics), I found there was a performance issue.
After I added about 50 items (bouncing balls) to the animator, the app became very slow -- almost frozen. A profiling shows [UIDynamicAnimator _animatorStep] takes 96% of CPU.
Does anybody know how to improve performance for an UIKit Dynamics app with large amount of UIDynamicItems?
You can download my code and see the performance issue yourself:
https://www.dropbox.com/s/zy7ajj6molxm9up/Flipper-UIKit-Dynamics-Poor-Performance.zip
Following is the code where everything happens:
#import "ViewController.h"
#import <CoreMotion/CoreMotion.h>
@interface ViewController () {
CMMotionManager *_motionManager;
NSOperationQueue *_queue;
int _count;
}
@property(nonatomic, strong) UIDynamicAnimator *animator;
@property(nonatomic, strong) UIGravityBehavior *gravityBehavior;
@property(nonatomic, strong) UICollisionBehavior *collisionBehavior;
@property(nonatomic, strong) UIDynamicItemBehavior *bounceBehaviour;
@end
@implementation ViewController
static NSInteger const kBallSize = 25;
- (void)startMotion {
if (_motionManager == nil) {
_queue = [[NSOperationQueue alloc] init];
_motionManager = [[CMMotionManager alloc] init];
_motionManager.deviceMotionUpdateInterval = 0.1;
}
[_motionManager startDeviceMotionUpdatesToQueue:_queue withHandler:^(CMDeviceMotion *motion, NSError *error) {
// angleLabel.text = [NSString stringWithFormat:@"%g", motion.attitude.pitch];
self.gravityBehavior.angle = atan2(motion.gravity.x + motion.userAcceleration.x, motion.gravity.y + motion.userAcceleration.y); // motion.attitude.pitch + M_PI / 2.0;
}];
}
- (void)stopMotion {
if (_motionManager) {
[_motionManager stopDeviceMotionUpdates];
}
_motionManager = nil;
}
- (void)viewDidLoad {
[super viewDidLoad];
[self startMotion];
// Simple tap gesture
UITapGestureRecognizer *tapGesture = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(onTap:)];
[self.view addGestureRecognizer:tapGesture];
// Create our animator, we retain this ourselves
self.animator = [[UIDynamicAnimator alloc] initWithReferenceView:self.view];
// Gravity
self.gravityBehavior = [[UIGravityBehavior alloc] initWithItems:@[]];
self.gravityBehavior.magnitude = 10;
[self.animator addBehavior:self.gravityBehavior];
// Collision - make a fake platform
self.collisionBehavior = [[UICollisionBehavior alloc] initWithItems:@[]];
UIBezierPath *aPath = [UIBezierPath bezierPathWithArcCenter:CGPointMake(284, 160)
radius:160
startAngle:(CGFloat) (M_PI * 0.0)
endAngle:(CGFloat) (M_PI * 2)
clockwise:YES];
[self.collisionBehavior addBoundaryWithIdentifier:@"bottom" forPath:aPath];
self.collisionBehavior.translatesReferenceBoundsIntoBoundary = YES;
[self.animator addBehavior:self.collisionBehavior];
CAShapeLayer *layer = [CAShapeLayer layer];
layer.path = aPath.CGPath;
layer.fillColor = [UIColor lightGrayColor].CGColor;
[self.view.layer addSublayer:layer];
// Bounce!
self.bounceBehaviour = [[UIDynamicItemBehavior alloc] initWithItems:@[]];
self.bounceBehaviour.elasticity = 0.75;
self.bounceBehaviour.resistance = 0.1;
self.bounceBehaviour.friction = 0.01;
[self.animator addBehavior:self.bounceBehaviour];
}
- (void)didReceiveMemoryWarning {
[super didReceiveMemoryWarning];
}
- (BOOL)prefersStatusBarHidden {
return YES;
}
#pragma mark - Gesture Recognizer
- (void)onTap:(UITapGestureRecognizer *)gesture {
if (gesture.state == UIGestureRecognizerStateEnded) {
// Grab the x of the touch for the center of our ball
// Ignore the y, we'll drop from the top
CGPoint pt = [gesture locationInView:self.view];
[self dropBallAtX:pt];
}
}
#pragma mark - Helpers
- (void)dropBallAtX:(CGPoint)p {
_count++;
self.label.text = [NSString stringWithFormat:@"Balls: %d", _count];
// Create a ball and add it to our view
UIView *ball = [[UIView alloc] initWithFrame:CGRectMake(p.x - (kBallSize / 2), p.y - (kBallSize / 2), kBallSize, kBallSize)];
CGFloat hue = (arc4random() % 256 / 256.0); // 0.0 to 1.0
CGFloat saturation = (arc4random() % 64 / 256.0) + 0.75; // 0.5 to 1.0, away from white
CGFloat brightness = (arc4random() % 64 / 256.0) + 0.75;
ball.backgroundColor = [UIColor colorWithHue:hue saturation:saturation brightness:brightness alpha:1.0];
ball.layer.cornerRadius = kBallSize / 2;
ball.layer.masksToBounds = YES;
[self.view addSubview:ball];
// Add some gravity
[self.gravityBehavior addItem:ball];
// Add the collision
[self.collisionBehavior addItem:ball];
// Add the bounce
[self.bounceBehaviour addItem:ball];
}
@end
This strikes me as a pretty hairy problem, where each ball will impact its neighbors, which will in turn impact their neighbors recursively, etc. I'm not surprised this non-linear-complexity problem suffers from pretty serious performance degradation as you add more balls. I found that fps started to fall observably as you approached 30-40 balls (on iPhone 5), and like you said, as you approached 50 balls, it quickly approaches 1-2 fps. If you let it to reach quiescence (which is unlikely if you keep CMMotionManager
in the mix), after a few seconds fps was restored, but as soon as you dropped another ball or CMMotionManager
changes the gravity again, the complexity of the problem quickly brought the fps to its knees again.
Simple fixes (like increasing friction, reducing bounce, introducing angular resistance, moving to rectangular shapes, fixing items in one spot once they stop moving by adding attachment behavior (which doesn't make sense if using CMMotionManager
, anyway), etc.) seemed to have modest impact. It strikes me that the only way to address this would be to abandon UIKit Dynamics for some other approach that could make some simplifying assumptions on the basis of your specific problem (e.g. since you're dealing with circular objects that are angularly symmetric you could eliminate rotational complexity, you could employ far simpler collision logic, adjust some of the aforementioned variables in order to dampen the behavior more quickly; etc.). I'm not familiar with any frameworks that could do that for you, and thus this might entail a non-trivial amount of code. And even if you did that, you're still dealing with a problem with non-linear complexity and performance is likely to still degrade at some point.