├── LICENSE ├── MBAnimationView.h ├── MBAnimationView.m └── README.md /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Michael Behan 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /MBAnimationView.h: -------------------------------------------------------------------------------- 1 | // 2 | // MBAnimationView.h 3 | // AnimationTest 4 | // 5 | // Created by Michael Behan on 02/03/2014. 6 | // Copyright (c) 2014 Michael Behan. All rights reserved. 7 | // 8 | 9 | #define kMBAnimationImageViewOptionRepeatForever INT_MAX 10 | 11 | #import 12 | 13 | @interface MBAnimationView : UIView 14 | { 15 | UIImageView *imageView; 16 | NSArray *animationData; 17 | NSInteger animationNumFrames; 18 | } 19 | 20 | @property (nonatomic, readonly)NSInteger currentFrameNumber; 21 | @property (nonatomic, readonly) UIImage *currentFrameImage; 22 | 23 | -(void)playAnimation:(NSString *)animationName withRange:(NSRange)range numberPadding:(int)padding ofType:(NSString *)ext fps:(NSInteger)fps repeat:(int)repeat completion:(void (^)())completionBlock; 24 | 25 | -(void)setImage:(UIImage *)image; 26 | 27 | - (void) stopAnimating; 28 | - (BOOL) isAnimating; 29 | 30 | @end 31 | -------------------------------------------------------------------------------- /MBAnimationView.m: -------------------------------------------------------------------------------- 1 | // 2 | // MBAnimationView.m 3 | // AnimationTest 4 | // 5 | // Created by Michael Behan on 02/03/2014. 6 | // Copyright (c) 2014 Michael Behan. All rights reserved. 7 | // 8 | 9 | #import "MBAnimationView.h" 10 | #import 11 | 12 | @interface MBAnimationView() 13 | { 14 | NSInteger currentFrame; 15 | NSTimeInterval timeSinceLastAnimationFrame; 16 | 17 | CADisplayLink *displayLink; 18 | 19 | NSTimeInterval animationFrameDuration; 20 | 21 | NSInteger animationRepeatCount; 22 | void (^complete)(); 23 | 24 | BOOL retina; 25 | } 26 | 27 | @end 28 | 29 | @implementation MBAnimationView 30 | 31 | - (id)initWithFrame:(CGRect)frame 32 | { 33 | self = [super initWithFrame:frame]; 34 | if (self) { 35 | // Initialization code 36 | [self setup]; 37 | } 38 | return self; 39 | } 40 | 41 | 42 | - (id)initWithCoder:(NSCoder *)aDecoder 43 | { 44 | self = [super initWithCoder:aDecoder]; 45 | if (self) { 46 | // Initialization code 47 | [self setup]; 48 | } 49 | 50 | return self; 51 | } 52 | 53 | -(UIImage *)currentFrameImage 54 | { 55 | return imageView.image; 56 | } 57 | 58 | -(NSInteger)currentFrameNumber 59 | { 60 | return currentFrame; 61 | } 62 | 63 | -(void)setup 64 | { 65 | complete = nil; 66 | self.backgroundColor = [UIColor clearColor]; 67 | 68 | imageView = [[UIImageView alloc] initWithFrame:self.bounds]; 69 | imageView.backgroundColor = [UIColor clearColor]; 70 | [self addSubview:imageView]; 71 | 72 | retina = ([[UIScreen mainScreen] respondsToSelector:@selector(scale)] && [[UIScreen mainScreen] scale] == 2.0); 73 | } 74 | 75 | -(void)setImage:(UIImage *)image 76 | { 77 | imageView.image = image; 78 | } 79 | 80 | -(void)playAnimation:(NSString *)animationName withRange:(NSRange)range numberPadding:(int)padding ofType:(NSString *)ext fps:(NSInteger)fps repeat:(int)repeat completion:(void (^)())completionBlock 81 | { 82 | //set options 83 | animationRepeatCount = repeat; 84 | animationFrameDuration = 1.0 / (fps * 1.0); 85 | animationNumFrames = range.length - range.location; 86 | complete = completionBlock; 87 | 88 | //create array of urls for frames 89 | NSMutableArray *URLs = [[NSMutableArray alloc] initWithCapacity:range.length - range.location]; 90 | NSBundle* bundle = [NSBundle mainBundle]; 91 | 92 | for (int i = range.location; i < range.length; i++) 93 | { 94 | NSString *paddingFormat = [NSString stringWithFormat:@"%%0%dd", padding]; 95 | NSString *suffix = [NSString stringWithFormat:paddingFormat, i]; 96 | NSString *filename = [NSString stringWithFormat:@"%@%@", animationName, suffix]; 97 | NSString *path = [bundle pathForResource:filename ofType:ext]; 98 | 99 | NSString *retinaFilename = [NSString stringWithFormat:@"%@@2x.%@",filename,ext]; 100 | NSString *retinaPath = nil; 101 | 102 | //see if we have a retina version if we're on a retina device 103 | if(retina && [[NSFileManager defaultManager] fileExistsAtPath:[[bundle resourcePath] stringByAppendingPathComponent:retinaFilename]]) 104 | { 105 | retinaPath = [bundle pathForResource:retinaFilename ofType:nil]; 106 | } 107 | 108 | if(retina && retinaPath) 109 | { 110 | [URLs addObject:[NSURL fileURLWithPath:retinaPath]]; 111 | } 112 | else 113 | { 114 | [URLs addObject:[NSURL fileURLWithPath:path]]; 115 | } 116 | } 117 | 118 | //create data array 119 | NSMutableArray *mutableDataArray = [NSMutableArray arrayWithCapacity:[URLs count]]; 120 | for (NSURL *url in URLs) 121 | { 122 | NSData *frameData = [NSData dataWithContentsOfURL:url]; 123 | [mutableDataArray addObject:frameData]; 124 | } 125 | 126 | animationData = [NSArray arrayWithArray:mutableDataArray]; 127 | 128 | //show first frame 129 | currentFrame = 0; 130 | [self animationShowFrame: currentFrame]; 131 | currentFrame = currentFrame + 1; 132 | 133 | [self startAnimating]; 134 | } 135 | 136 | - (void) startAnimating 137 | { 138 | displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(update:)]; 139 | displayLink.frameInterval = 1; 140 | [displayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode]; 141 | currentFrame = 0; 142 | } 143 | 144 | -(void)update:(CADisplayLink *)dl 145 | { 146 | timeSinceLastAnimationFrame += displayLink.duration; 147 | 148 | if(timeSinceLastAnimationFrame >= animationFrameDuration) 149 | { 150 | timeSinceLastAnimationFrame = 0; 151 | NSUInteger frameNow; 152 | 153 | currentFrame += 1; 154 | frameNow = currentFrame; 155 | 156 | 157 | // don't go too far 158 | if (frameNow >= animationNumFrames) 159 | { 160 | frameNow = animationNumFrames - 1; 161 | } 162 | 163 | [self animationShowFrame: frameNow]; 164 | 165 | if (currentFrame >= animationNumFrames) 166 | { 167 | [self stopAnimating]; 168 | 169 | // continue to loop animation until loop counter reaches 0 170 | if (animationRepeatCount > 0) 171 | { 172 | animationRepeatCount = animationRepeatCount - 1; 173 | [self startAnimating]; 174 | } 175 | } 176 | } 177 | } 178 | 179 | - (void) stopAnimating 180 | { 181 | if (![self isAnimating]) 182 | return; 183 | 184 | if(complete != nil) 185 | { 186 | complete(); 187 | } 188 | 189 | [displayLink invalidate]; 190 | displayLink = nil; 191 | 192 | if (animationRepeatCount == 1) { 193 | animationData = nil; 194 | } 195 | 196 | //rest on final frame 197 | currentFrame = animationNumFrames - 1; 198 | [self animationShowFrame: currentFrame]; 199 | } 200 | 201 | - (BOOL) isAnimating 202 | { 203 | return (displayLink != nil); 204 | } 205 | 206 | - (void) animationShowFrame: (NSInteger) frame 207 | { 208 | if ((frame >= animationNumFrames) || (frame < 0)) 209 | return; 210 | 211 | NSData *imageData = animationData[frame]; 212 | imageView.image = [UIImage imageWithData:imageData]; 213 | } 214 | 215 | @end 216 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | MBAnimationView 2 | =============== 3 | 4 | Animation with `UIImageView` is super simple and for basic animations it is just what you need. Just throw an array of images at your image view and tell it to go, and it will go. For animations of more than a few frames though its simplicity is also its failing–an array of `UIImage`s is handy to put together but if you want large images or a reasonable number of frames then that array could take up a serious chunk of memory. If you've tried any large animations with `UIImageView` you'll know things get crashy very quickly. 5 | 6 | There are also a few features, like being able to know what frame is currently being displayed and setting a completion block that you regularly find yourself wanting when dealing with animations, so I've created `MBAnimationView` to provide those, and to overcome the crash inducing memory problems. 7 | 8 | My work was informed by the excellent [Mo DeJong](http://www.modejong.com) and you should check out his [PNGAnimatorDemo](http://www.modejong.com/iOS/#ex2) which I've borrowed from for my class. 9 | 10 | ## How It Works 11 | 12 | The premise for the memory improvements is the fact that image data is compressed, and loading it into a `UIImage` decompresses it. So, instead of having an array of `UIImage` objects (the decompressed image data), we're going to work with an array of `NSData` objects (the compressed image data). Of course, in order to ever see the image, it will have to be decompressed at some point, but what we're going to do is create a `UIImage` on demand for the frame we want to display next, and let it go away when we're done displaying it. 13 | 14 | So the `MBAniamtionView` has a `UIImageView`, it creates an array of `NSData` objects and then on a timer creates the frame images from the data, and sets the image view's image to it. 15 | 16 | ## Comparison 17 | 18 | As expected crashes using the animationImages approach disappeared with `MBAnimationView`, but to understand why, I tested the following 2 pieces of code, for different numbers of frames recording memory usage, CPU utilisation and load time. 19 | 20 | ```objective-c 21 | MBAnimationView *av = [[MBAnimationView alloc] initWithFrame:CGRectMake(0, 0, 350, 285)]; 22 | 23 | [av playAnimation: @"animationFrame" 24 | withRange : NSMakeRange(0, 80) 25 | numberPadding : 2 26 | ofType : @"png" 27 | fps : 25 28 | repeat : kMBAnimationViewOptionRepeatForever 29 | completion : nil]; 30 | 31 | [self.view addSubview:av]; 32 | ``` 33 | 34 | ```objective-c 35 | UIImageView *iv = [[UIImageView alloc] initWithFrame:CGRectMake(0, 0, 350, 285)]; 36 | iv.animationImages = @[[UIImage imageNamed:@"animationFrame00"], 37 | [UIImage imageNamed:@"animationFrame01"], 38 | 39 | ... 40 | 41 | [UIImage imageNamed:@"animationFrame79"]]; 42 | 43 | [self.view addSubview:iv]; 44 | [iv startAnimating]; 45 | ``` 46 | 47 | ## Results 48 | 49 | Starting off with small numbers of frames it's not looking too good for our new class, `UIImageView` is using less memory and significantly less CPU. 50 | 51 |
10 FramesMemory Average / PeakCPU Average / Peak
UIImageView4.1MB / 4.1MB 0% / 1%
MBAnimationView4.6MB / 4.6MB11% / 11%
20 FramesMemory Average / PeakCPU Average / Peak
UIImageView4.4MB / 4.4MB 0% / 1%
MBAnimationView4.9MB / 4.9MB11% / 11%
52 | 53 | 54 | But things start looking up for us as more frames are added. _MBAnimationView_ continues to use the same amount of CPU–memory usage is creeping up, but there are no spikes. `UIImageView` however is seeing some very large spikes during setup. 55 | 56 |
40 FramesMemory Average / PeakCPU Average / Peak
UIImageView4.1MB / 65MB 0% / 8%
MBAnimationView5.7MB / 5.7MB11% / 11%
80 FramesMemory Average / PeakCPU Average / Peak
UIImageView4.5MB / 119MB 0% / 72%
MBAnimationView8.4MB / 8.4MB11% / 11%
57 | 58 | Those `UIImageView` memory numbers are big enough to start crashing in a lot of situations, and remember this is for a single animation. 59 | 60 | ## The Trade Off 61 | 62 | There has to be one of course, but it turns out not to be a deal breaker. Decompressing the image data takes time, we're doing it during the animation rather than up front but it's not preventing us playing animations up to 30 fps and even higher. On the lower end devices I've tested on (iPad 2, iPhone 4) there doesn't seem to be any negative impact, in light of that I'm surprised the default animation mechanism provided by `UIImageView` doesn't take the same approach as `MBAnimationView`. 63 | --------------------------------------------------------------------------------