Scroll view and table view performance when loading images from disk - iphone

I'm trying to improve the performance of my image-intensive iPhone app by using a disk-based image cache instead of going over the network. I've modeled my image cache after SDImageCache (http://github.com/rs/SDWebImage/blob/master/SDImageCache.m), and is pretty much the same but without asynchronous cache in/out operations.
I have some scroll views and table views that load these images asynchronously. If the image is on the disk, it's loaded from the image cache, otherwise a network request is made and the subsequent result is stored in the cache.
The problem I'm running into is that as I scroll through the scroll views or table views, there's a noticeable lag as the image is loaded from disk. In particular, the animation of going from one page to another on a scroll view has a small freeze in the middle of the transition.
I've tried to fix this by:
Using an NSOperationQueue and NSInvocationOperation objects to make the disk access requests (in the same manner as SDImageCache), but it doesn't help with the lag at all.
Tweaking the scroll view controller code so that it only loads images when the scroll view is no longer scrolling. This means the disk access only fires when the scroll view stops scrolling, but if I immediately try to scroll to the next page I can notice the lag as the image loads from disk.
Is there a way to make my disk accesses perform better or have less of an effect on the UI?
Note that I'm already caching the images in memory as well. So once everything is loaded into memory, the UI is nice and responsive. But when the app starts up, or if low memory warnings are dispatched, I'll experience many of these UI lags as images are loaded from disk.
The relevant code snippets are below. I don't think I'm doing anything fancy or crazy. The lag doesn't seem to be noticeable on an iPhone 3G, but it's pretty apparent on an 2nd-gen iPod Touch.
Image caching code:
Here's a relevant snippet of my image caching code. Pretty straightforward.
- (BOOL)hasImageDataForURL:(NSString *)url {
return [[NSFileManager defaultManager] fileExistsAtPath:[self cacheFilePathForURL:url]];
}
- (NSData *)imageDataForURL:(NSString *)url {
NSString *filePath = [self cacheFilePathForURL:url];
// Set file last modification date to help enforce LRU caching policy
NSMutableDictionary *attributes = [NSMutableDictionary dictionary];
[attributes setObject:[NSDate date] forKey:NSFileModificationDate];
[[NSFileManager defaultManager] setAttributes:attributes ofItemAtPath:filePath error:NULL];
return [NSData dataWithContentsOfFile:filePath];
}
- (void)storeImageData:(NSData *)data forURL:(NSString *)url {
[[NSFileManager defaultManager] createFileAtPath:[self cacheFilePathForURL:url] contents:data attributes:nil];
}
Scroll view controller code
Here's a relevant snippet of the code that I use for displaying images in my scroll view controllers.
- (void)scrollViewDidScroll:(UIScrollView *)theScrollView {
CGFloat pageWidth = theScrollView.frame.size.width;
NSUInteger index = floor((theScrollView.contentOffset.x - pageWidth / 2) / pageWidth) + 1;
[self loadImageFor:[NSNumber numberWithInt:index]];
[self loadImageFor:[NSNumber numberWithInt:index + 1]];
[self loadImageFor:[NSNumber numberWithInt:index - 1]];
}
- (void)loadImageFor:(NSNumber *)index {
if ([index intValue] < 0 || [index intValue] >= [self.photoData count]) {
return;
}
// ... initialize an image loader object that accesses the disk image cache or makes a network request
UIView *iew = [self.views objectForKey:index];
UIImageView *imageView = (UIImageView *) [view viewWithTag:kImageViewTag];
if (imageView.image == nil) {
NSDictionary *photo = [self.photoData objectAtIndex:[index intValue]];
[loader loadImage:[photo objectForKey:#"url"]];
}
}
The image loader object is just a lightweight object that checks the disk cache and decides whether or not to fetch an image from disk or network. Once it's done, it calls a method on the scroll view controller to display the image:
- (void)imageLoadedFor:(NSNumber *)index image:(UIImage *)image {
// Cache image in memory
// ...
UIView *view = [self.views objectForKey:index];
UIImageView *imageView = (UIImageView *) [view viewWithTag:kImageViewTag];
imageView.contentMode = UIViewContentModeScaleAspectFill;
imageView.image = image;
}
UPDATE
I was experimenting with the app, and I disabled the image cache and reverted to always making network requests. It looks like simply using network requests to fetch images is also causing the same lag when scrolling through scroll views and table views! That is, when a network request finishes and the image is shown in the scroll view page or table cell, the UI slightly lags a bit and has a few split seconds of lag as I try to drag it.
The lag seems to be just more noticeable when using the disk cache, since the lag always occurs right at the page transition. Perhaps I'm doing something wrong when assigning the loaded image to the appropriate UIImageView?
Also - I've tried using small images (50x50 thumbnails) and the lag seems to improve. So it seems that the performance hit is due to either loading a large image from disk or loading a large image into an UIImage object. I guess one improvement would be to reduce the size of the images being loaded into the scroll view and table views, which was what I was planning to do nonetheless. However, I just don't understand how other photo-intensive apps are able to present what looks like pretty high-res photos in scrollable views without performance problems from going to disk or over the network.

You should check out the LazyTableImages sample app.

If you've narrowed it down to network activity I would try encapsulating your request to ensure it is 100% off of the main thread. While you can use NSURLConnection asynchronously and respond to it's delegate methods, I find it easier to wrap a synchronous request in a background operation. You can use NSOperation or grand central dispatch if your needs are more complex. An (relatively) simple example in an imageLoader implementation could be:
// imageLoader.m
// assumes that that imageCache uses kvp to look for images
- (UIImage *)imageForKey:(NSString *)key
{
// check if we already have the image in memory
UImage *image = [_images objectForKey:key];
// if we don't have an image:
// 1) start a background task to load an image from a file or URL
// 2) return a default image to display while loading
if (!image) {
[self performSelectorInBackground:#selector(loadImageForKey) withObject:key];
image = [self defaultImage];
}
return image;
}
- (void)loadImageForKey:(NSString *)key
{
NSAutoReleasePool *pool = [[NSAutoReleasePool alloc] init];
// attempt to load the image from the file cache
UIImage *image = [self imageFromFileForKey:key];
// if no image, load the image from the URL
if (!image) {
image = [self imageFromURLForKey:key];
}
// if no image, return default or imageNotFound image
if (!image) {
image = [self notFoundImage];
}
if ([_delegate respondsTo:#selector(imageLoader:didLoadImage:ForKey:)]) {
[_delegate imageLoader:self didLoadImage:image forKey:key];
}
[pool release];
}
- (UIImage *)imageFromURLForKey:(NSString *)key
{
NSError *error = nil;
NSData *imageData = [NSData dataWithContentsOfURL:[self imageURLForKey:key]
options:0
error:&error];
UIImage *image;
// handle error if necessary
if (error) {
image = [self errorImage];
}
// create image from data
else {
image = [UIImage imageWithData:imageData];
}
return image;
}

The image from the disk is actually read while drawing the image on the imageview. Even if we cache the image reading from the disk it does not affect since it just keeps reference to the file. You might have to use tiling of larger images for this purpose.
Regards,
Deepa

I've had this problem - you are hitting the limit of how fast the UI can load an image while scrolling - so i'd just work around the problem and improve the user experience.
Only load images when the scroll is at a new 'page' (static rectangle) and
Put an activity indicator behind a transparent scroll view to handle the case where the user is scrolling faster than the app can load content

It's typically the image decoding which takes time, and that causes the UI to freeze (since it's all happening on the main thread). Every time you call [UIImage imageWithData:] on a large image, you'll notice a hiccup. Decoding a smaller image is far quicker.
Two options:
You can load a thumbnail version of each image first, then 'sharpen up' on didFinishScrolling. The thumbnails should decode quickly enough such that you don't skip any frames.
You can load a thumbnail version of each image or show a loading indicator first, then decode the full-resolution image on another thread, and sub it in when it's ready. (This is the technique that is employed in the native Photos app).
I prefer the second approach; it's easy enough with GDC nowadays.

Related

iOS UIImageView why Instruments show less live bytes for ImageWithContentsOfFile instead of direct image assignment from UIImagePickerController?

I've stumbled upon a peculiar live bytes issue when profiling my app with instruments. I'm interested if this is any real "optimization", or an instruments glitch. Is there any benefit to having less "live bytes" if overall real memory footprint stays the same? The real memory usage stays the same for both cases.
I'm using UIImagePicker in a popover controller, it picks an large (up to 7mb) image and assigns it to an image view like this:
- (void)imagePickerController:(UIImagePickerController *)picker didFinishPickingImage:(UIImage *)img editingInfo:(NSDictionary *)editInfo {
activeImageView.image = img;
}
Here's the allocations profiling for this operation. After 2 image assignments, the live bytes has grown for each image (by about 5.4 mb). You can see the malloc block for the images below the graph (5.4mb and 768kb) Once I replace one of the images with a new one(5. mb to 768kb), the live bytes does a "step down", as displayed in the graph at about 00:55:
The peculiarity that I'm talking about is that if take this image, save it to disk and then I use imageWithContentsOfFile: as in code below, my live bytes looks very different:
//this is what I use to save image to disk
-(NSString* )saveImage:(UIImage*)image withID:(int)imageID
{
NSString* filepath = [[self imagesFolderPath] stringByAppendingPathComponent:[NSString stringWithFormat:#"%i.png",imageID]];
BOOL success = [UIImagePNGRepresentation(image) writeToFile:filepath atomically:YES];
if(!success)
{
DLog(#"failed to write to file: %#",filepath );
}
success = [[NSFileManager defaultManager]fileExistsAtPath:filepath];
DLog(#"file created: %#",success?#"YES":#"NO");
return filepath;
}
//this is the popover delegate telling me when the user has dismissed the image picker in popover
-(void)popoverControllerDidDismissPopover:(UIPopoverController *)popoverController
{
popoverController = nil;
//take the image from the image view, write it to disk and assign back to the image view
NSString* imageFilePath = [self saveImage:activeImageView.image withID:self.selectedID];
if(imageFilePath)
{
//this will work only for an existing icon file, otherwise
imageView.image =[UIImage imageWithContentsOfFile:imageFilePath];
}
}
Upon introducing this code, my live bytes allocations looks like the image below. I repeat the same operations (assigning two images to UIImageView from the image picker, but here I add a method to write them to disk, then read them back from disk. (the triangular ramp comes from when I'm creating NSData from the image for writing to disk):

What condition is set in uitableview lazy loading to load images

I fetched images from json and put all image url's in array, rather than calling json again. Now i don't know how to set the condition that it load images as table scrolled. Here is the method i called for this.
+ (NSMutableArray *) createImg: (NSArray*)sampleData
{
NSMutableArray *arrImg = [[NSMutableArray alloc]init];
for(int i=1; i<=[sampleData count]; i++)
{
NSString *strOfUrl = [sampleData objectAtIndex:i];
UIImage *img = [UIImage imageWithData:[NSData dataWithContentsOfURL:[NSURL URLWithString:strOfUrl]]];
if(img == NULL)
{
NSLog(#"null no image");
}
else{
[arrImg addObject:img];
}
}
return arrImg;
}
Please guide for the above and feel free to ask anything if not clear in this.
Thanks in advance.
I know this might not be exactly what you are looking for. However, I'd suggest you to use this AsyncImageView. It'll do all the logic you need for lazy loading. Also, it'll cache the images. To call this API:
ASyncImage *img_EventImag = alloc with frame;
NSURL *url = yourPhotoPath;
[img_EventImage loadImageFromURL:photoPath];
[self.view addSubView:img_EventImage]; // In your case you'll add in your TableViewCell.
It's same as using UIImageView. Easy and it does most of the things for you. AsyncImageView includes both a simple category on UIImageView for loading and displaying images asynchronously on iOS so that they do not lock up the UI, and a UIImageView subclass for more advanced features. AsyncImageView works with URLs so it can be used with either local or remote files.
Loaded/downloaded images are cached in memory and are automatically cleaned up in the event of a memory warning. The AsyncImageView operates independently of the UIImage cache, but by default any images located in the root of the application bundle will be stored in the UIImage cache instead, avoiding any duplication of cached images.
The library can also be used to load and cache images independently of a UIImageView as it provides direct access to the underlying loading and caching classes.
use this. It is perfect way to show images while scrolling:
https://github.com/enormego/EGOImageLoading

Table View Scrolling Async

I am loading an image to a table view cell, each cell has an image. I've adapter a couple tutorials to the code below, but I am still having slow down.
I am loading these images from the documents directory. Any tips or ideas on how to speed this process up?
Edit Revised Code:
Beer *beer = (Beer *) [self.fetchedResultsController objectAtIndexPath:indexPath];
cell.displayBeerName.text = beer.name;
// did we already cache a copy of the image?
if (beer.image != nil) {
// good. use it. this will run quick and this will run most of the time
cell.beerImage.image = beer.image;
} else {
// it must be the first time we've scrolled by this beer. do the expensive
// image init off the main thread
cell.beerImage.image = nil; // set a default value here. nil is good enough for now
[self loadImageForBeer:beer atIndexPath:indexPath];
}
- (void)loadImageForBeer:(Beer *)beer atIndexPath:(NSIndexPath *)indexPath {
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0ul);
dispatch_async(queue, ^{
UIImage *image = [UIImage imageWithContentsOfFile:beer.imagePath];
beer.image = image;
dispatch_sync(dispatch_get_main_queue(), ^{
UITableViewCell *cell = [self.tableView cellForRowAtIndexPath:indexPath];
cell.beerImage.image = image;
});
});
}
Your algorithm looks pretty good. You've avoided many of the typical pitfalls. If you're still having UI performance problems, I'd suggest a couple of things:
You should try caching your images in memory. You could use NSMutableArray or NSMutableDictionary, but at Best way to cache images on ios app? Caleb discusses the merits of the NSCache class, which simplifies the process. If you do cache images, make sure you respond to memory pressure and purge the cache if necessary. You can respond to didReceiveMemoryWarning or add yourself as an observer to the notification center's UIApplicationDidReceiveMemoryWarningNotification.
Make sure your cached images are thumbnail sized or else you'll always have a little stuttering in your UI (if you need a resizing algorithm, let us know) and it will use up memory unnecessarily;
When you dispatch your image update back to the main queue, you should do so asynchronously (why have that background queue hang around and tie up resources as it waits for the the block to be sent back to the main queue to finish ... this is especially an issue once you have a couple of images backed up during a fast scroll); and
When you dispatch back to the main queue, you should check to make sure cell you get from cellForRowAtIndexPath is not nil (because if cell loading logic gets too backed up (esp on slower devices), you could theoretically have the cell in question having scrolled off the screen and your algorithm could crash).
I use an algorithm very much like yours, with almost the same GCD structure (with the above caveats) and it's pretty smooth scrolling, even on older devices. If you want me to post code, I'm happy to.
If you're still having troubles, the CPU profiler is pretty great for identifying the bottlenecks and letting you know where you should focus your attention. There are some great WWDC sessions available online which focus on how to use Instruments to identify performance bottlenecks, and I found them to be very helpful to gain proficiency with Instruments.
Here is my code. In viewDidLoad, I initialize my image cache:
- (void)initializeCache
{
self.imageCache = [[NSCache alloc] init];
self.imageCache.name = #"Custom Image Cache";
self.imageCache.countLimit = 50;
}
And then I use this in my tableView:cellForRowAtIndexPath:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *CellIdentifier = #"ilvcCell";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
// set the various cell properties
// now update the cell image
NSString *imagename = [self imageFilename:indexPath]; // the name of the image being retrieved
UIImage *image = [self.imageCache objectForKey:imagename];
if (image)
{
// if we have an cachedImage sitting in memory already, then use it
cell.imageView.image = image;
}
else
{
cell.imageView.image = [UIView imageNamed:#"blank_image.png"];
// the get the image in the background
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
// get the UIImage
UIImage *image = [self getImage:imagename];
// if we found it, then update UI
if (image)
{
dispatch_async(dispatch_get_main_queue(), ^{
// if the cell is visible, then set the image
UITableViewCell *cell = [self.tableView cellForRowAtIndexPath:indexPath];
if (cell)
cell.imageView.image = image;
[self.imageCache setObject:image forKey:imagename];
});
}
});
}
return cell;
}
and
- (void)didReceiveMemoryWarning
{
[super didReceiveMemoryWarning];
[self.imageCache removeAllObjects];
}
As an aside, one further optimization that you might contemplate would be to preload your cached images in a separate queue, rather than loading images in a separate thread just-in-time. I don't think it's necessary, as this seems to be more than fast enough for me, but it's one more option to speed up the UI.
Not much you can do here for the initial load, you're about as fast as it gets.
If it's still too slow, try loading smaller images if you can.
A couple of things:
First, be careful with -imageWithContentsOfFile, it won't cache anything. You're taking the full hit every time you load the image, as opposed to -imageNamed that'll keep the image warm in some cache.
You can of course cache that in your domain object, but I'd personally strongly advice against that.
Your memory footprint is going to go through the roof, forcing you to implement your own cache expiration mechanism, while Apple has a very good image cache through -imageNamed.
I'd be surprised if you can do a better job than apple on all 3 family of devices :)
Then, you're breaking the flyweight pattern of the UITableView here:
dispatch_sync(dispatch_get_main_queue(), ^{
cell.beerImage.image = image;
beer.image = image;
[cell setNeedsLayout];
});
Ask the table view to give your the cell at a given index rather than capture the cell in the block: by the time the image is loaded, that cell instance might actually have been reused for another index path, and you'll be displaying the image in the wrong cell.
And no need for -setNeedsLayout here, just changing the image is enough.
Edit: whoops! I missed the obvious thing with images in table view. What size are your images, what size is the image view, and what is the content mode on the image?
If your images are of a very different size than the image view and you're asking the imageview to resize, this will happen on the main thread and you'll take a massive performance hit there.
Resize the image to the image view off thread, after loading (a quick google search will give you the core graphics code to do that).
The missing step is to update the model with the fetched image. As it is, you're doing a new load for every cell every time. The model is the right place to cache the result of the relatively expensive load. Can you add a Beer.image property?
Then, your config code would look like this:
Beer *beer = (Beer *) [self.fetchedResultsController objectAtIndexPath:indexPath];
cell.displayBeerName.text = beer.name;
// did we already cache a copy of the image?
if (beer.image != nil) {
// good. use it. this will run quick and this will run most of the time
cell.beerImage.image = beer.image;
} else {
// it must be the first time we've scrolled by this beer. do the expensive
// image init off the main thread
cell.beerImage.image = nil; // set a default value here. nil is good enough for now
[self loadImageForBeer:beer atIndexPath:indexPath];
}
Moved the loader logic here for clarity ...
- (void)loadImageForBeer:(Beer *)beer atIndexPath:(NSIndexPath *)indexPath {
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0ul);
dispatch_async(queue, ^{
UIImage *image = [UIImage imageWithContentsOfFile:beer.imagePath];
beer.image = image;
dispatch_sync(dispatch_get_main_queue(), ^{
UITableViewCell *cell = [self.tableView cellForRowAtIndexPath:indexPath];
cell.beerImage.image = image;
});
});
}
You can have a look on this question ,previously answered at stack overflow.
UIImage in uitableViewcell slowdowns scrolling table
or else try this code
- (void)configureCell:(BeerCell *)cell
atIndexPath:(NSIndexPath *)indexPath
{
Beer *beer = (Beer *) [self.fetchedResultsController objectAtIndexPath:indexPath];
cell.displayBeerName.text = beer.name;
UIImage *image = [UIImage imageWithContentsOfFile:beer.imagePath];
cell.beerImage.image = image;
[cell setNeedsLayout];
}

UIScrollView with UIImageView, UIImage

I've read a lot of UIScrollView with UIImageView threads here or other googled pages. But I still cannot get the problem I'm confronting. I'm having a cold right now. Hope I can still make it clear, lol. Here is the problem:
I'm building one app which mainly uses UIScrollView to display a few images. Here the amount counts, not the size, which is averagely 100KB(I even converted PNG to jpg, not sure whether it helps or not). With no more than 10 images, my app crashes with memory warning. This is the first time I encounter memory issue, which surprised me as the compiled app is less than 10MB.
At the very beginning, I load all the images on launch, looping all names of image files and do
UIImageView *imageView = [[UIImageView alloc] initWithImage:[UIImage imageNamed:imgName]];
[scrollview addSubview:imageView];
[imageView release];
If I'm right, I think after launch, all the images are in memory, right? But the funny thing here is, the app could launch without any problem(at most a level 1 memory warning). After I scroll a few images, it crashed. I checked leaks for sure and also allocations. No leak and allocation almost had no change during scrolling.
So, is there something special done by imageNamed rather than cache?
And then, yes, I turned to lazy load.
For fear of checking page and loading images on demand might jerk the scrolling(which was proved true), I used a thread which runs a loop to check offset of the scroll view and load/unload images.
I subclassed UIImageView by remembering the image name. It also contains loadImage and unloadImage which will be executed on that thread.
- (void)loadImage {
/if ([self.subviews count] == 0) {
UIImageView iv = [[UIImageView alloc] initWithImage:[UIImage imageNamed:self.imageName]];
[self performSelectorOnMainThread:#selector(renderImage:) withObject:iv waitUntilDone:NO];
//[self addSubview:iv];
[iv release];
}*/
if (self.image == nil) {
//UIImage *img = [UIImage imageNamed:self.imageName];
UIImage *img = [[UIImage alloc] initWithContentsOfFile:[[NSBundle mainBundle] pathForResource:[self.imageName stringByDeletingPathExtension] ofType:[self.imageName pathExtension]]];
// image must be set on main thread as UI rendering is main thread's responsibility
[self performSelectorOnMainThread:#selector(renderImage:) withObject:img waitUntilDone:NO];
[img release];
}
}
// render image on main thread
- (void)renderImage:(UIImage*)iv {
//[self addSubview:iv];
self.image = iv;
}
(void)unloadImage {
self.image = nil;
//[(UIView*)[self.subviews lastObject] removeFromSuperview];
}
You can see the commented code that I've played with.
In unloadImage, if I write [self.image release], then I get EXC_BAD_ACCESS, which is unexpected, as I think alloc and release are matched here.
The app still crashes with no leak. The initWithContentsOfFile version even crashed earlier than imageNamed version, and made the scrolling not that smooth.
I run the app on device. By checking allocations, I found imageNamed version used much less memory than initWithContentsOfFile version, though they both crash. Instruments also showed that the allocated images were 2,3 or 4, which indicated the lazy load did do his job.
I checked PhotoScroller of WWDC2010, but I don't think it solvs my problem. There is no zooming or huge picture involved.
Anybody helps! Thank you in advance.
The crash log says nothing. The app crashes mostly after memory warning level = 2. And if run on simulator, there will be no problem.
It doesn't matter which format do you use for your images. They're converted to bitmaps when you display them.
I'd suggest to use the technique similar to that one which is used by UITableView (hide the image and free the memory it uses when it disappears from the screen and instantiate the image only when you need to show it).
As an alternate way – if you need to show these images in a grid – you might take a look to a CATiledLayer.
Anyhow, loading all the images to the memory is not the best idea :)
You can load all the images to an array. And you can design a view having one image view and try the below code:
array name: examplearray and view name :exampleview
-(void)updateImagesInScrollView
{
int cnt = [examplearray count];
for(int j=0; j< cnt; ++j)
{
NSArray *nibContents = [[NSBundle mainBundle] loadNibNamed:#"exampleview"
owner:self
options:nil];
UIView *myView = [nibContents objectAtIndex:0];
exampleview * rview= (exampleview *)myView;
/* you can get your iamge from the array and set the image*/
rview.imageview.image = yourimage;
/*adding the view to your scrollview*/
[self.ScrollView addSubview:rview];
}
/* you need to set the content size of the scroll view */
self.ScrollView.contentSize = CGSizeMake(X, self.mHorizontalScrollView.contentSize.height);
}

UIImageView animation starts a few seconds after startAnimating is issued

Ok, I have the following very simple animation composed of 25 frames in PNG format. Each frame is 320 × 360 and about 170Kb in size. Here is the code I use
.h:
IBOutlet UIImageView *Animation_Normal_View;
In Interface Builder I have a UIImageView with a referencing outlet pointing to this. All my images are named normal_000_crop.png, normal_001_crop.png, normal_002_crop.png,...
.m:
Animation_Normal = [[NSMutableArray alloc] initWithCapacity:25];
for (int i = 0; i < 25; i++)
{
[Animation_Normal addObject:[UIImage imageWithContentsOfFile:[[NSBundle mainBundle] pathForResource:[NSString stringWithFormat:#"normal_%03d_crop.png", i] ofType:nil]]];
}
Animation_Normal_View.animationImages = Animation_Normal;
Animation_Normal_View.animationDuration = 1; // seconds
Animation_Normal_View.animationRepeatCount = 0; // 0 = loops forever
[Animation_Normal release];
[self.view addSubview:Animation_Normal_View];
[Animation_Normal_View startAnimating];
On the simulator everything loogs good visual animation start as soos as the startAnimating is issued.
But on the iPhone 3G running iOS 4.0.2, the visual animation starts a good 2 to 3 seconds after the startAnimating is issued.
I have tried about every technique on I could find in blogs or forum that should solve this to no avail.
Any hints appreciated even if it's a completly different way to to a PNG based animation.
Thanks.
This is a good question and I will address it here with a few thoughts.
First, you are loading a series of graphics that are around 4MB in total size. This may take a moment, especially on slower (older) devices.
In the #interface block of your .h file you may want to declare two properties such as:
IBOutlet UIImageView *animationViewNormal;
NSMutableArray *animationViewNormalImages;
The first is the UIImageView that you already have (just renamed for best practices) and the second is a mutable array to hold the stack of images for the image view. Let me state that if having "normal" implies state. For clarification, are you loading additional sets of images for different states?
In your .m file in the #interface create the following method:
- (void)loadAnimationImages;
This will provide the function to lead the image stack to the mutable array defined in the header.
In the same .m file in the #implementation you'll want the following:
- (void)loadAnimationImages {
for (NSUInteger i = 0; i < 23; i++) {
NSString *imageName = [NSString stringWithFormat:#"normalCrop%03u", i];
UIImage *image = [UIImage imageWithContentsOfFile:[[NSBundle mainBundle] pathForResource:imageName ofType:#"png"]];
if (image) {
[animationViewNormalImages addObject:image];
}
}
}
As you can see I renamed the PNG files from normal_%03u_crop to normalCrop%03u as it is best practice to put the index label at the end of the file name (also most apps will output the content this way). The loop loads an image, checks to see that it is an image and then adds the image to the "image stack" in the mutable array.
In the init() you'll need the following:
- (id)init {
...
animationViewNormalImages = [[NSMutableArray alloc] init];
...
}
This allocates the (animationViewNormalImages) mutable array to hold your stack of images for the image view.
We'll now move on to the code for the viewDidLoad():
- (void)viewDidLoad {
[super viewDidLoad];
...
[self loadAnimationImages];
[animationViewNormal setAnimationImages:animationViewNormalImages];
[animationViewNormal setAnimationDuration:1.1f];
[animationViewNormal setAnimationRepeatCount:0]; // 0=infinite loop
...
}
We load the stack of images into the mutable array then set the properties of our imageView with the image stack, duration and repeat count.
Next in the viewDidAppear() we start the image view animating:
- (void)viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated];
...
[animationViewNormal startAnimating];
...
}
Once the imageView is animating as an infinite loop we need to handle when leaving the view in the viewWillDisappear():
- (void)viewWillDisappear:(BOOL)animated {
[super viewWillDisappear:animated];
...
[animationViewNormal stopAnimating];
...
}
Last (which should be the second thing we add the .m file) we cleanup in the mutable array in the dealloc():
- (void)dealloc {
...
[animationViewNormalImages release];
[super dealloc];
}
This is how we handle it and works for us, but then again, we're normally not loading 4MB of images into memory to animate.
The .PNG files are compressed when building the app and I am not sure if they are decompressed on the fly when loading the images our of the resource bundle. This is a boolean value int he Build Properties Build Settings (COMPRESS_PNG_FILES).
For performance you may want to consider the following:
Mark opaque views as such:
Compositing a view whose contents are
opaque requires much less effort than
compositing one that is partially
transparent. To make a view opaque,
the contents of the view must not
contain any transparency and the
opaque property of the view must be
set to YES.
Remove alpha channels from opaque PNG
files: If every pixel of a PNG image
is opaque, removing the alpha
channel avoids the need to blend the
layers containing that image. This
simplifies compositing of the image
considerably and improves drawing
performance.
Furthermore, you may find it's better the make one large image with all 24 frames (offset by the width of the individual frame) and then load it once. Then using Core Graphics with CGContextClipToRect then just offset the image context. This means more code but may be faster than using the standard stack method.
Lastly, you may want to consider is converting the .PNG files into .PVR (PVRTC) files. More information can be found here: Apple Tech QA, Apple Docs, and Sample Code.
I hope this helps and please vote it up if it does.
Best,
Kevin
Able Pear Software
imageWithContentsOfFile: tends to take a long time to process, especially if there are lots of files (25 is kind of a lot) and/or they're big.
One thing you can try is to switch it out for imageNamed:, i.e.
[UIImage imageNamed:[NSString stringWithFormat:#"normal_%03d_crop.png", i]]
imageNamed: is generally much faster, but tends to cache images more or less indefinitely.
If loading the images into memory and keeping them around throughout the whole app is unacceptable, you may need to do some tweaky things to load them in at an appropriate time and to unload them after they've been used. That stuff is always tricky, and requires multithreading to not block the main UI while loading. But doable. And there are examples.
Load those images before using the start animating method . I advice an easier way " Just call start animating method in the applicationDidEnterForeground . When calling this one don't forget your UIImageView's alpha property . If you set uiimageiew.alpha=0.01; like this , your start animating method will be called and the user can't see this animation " so there will be no lag anymore.