In my app I have a tableView filled with contents from a server. To download these contents I use NSURLConnection and I create a NSMutableArray (tableItems) to hold and to manage the addresses to the images I want to use.
In connectionDidFinishLoading, after populating tableItems, there is this for-loop:
for (int i = 0; i < [tableItems count]; i++) {
// HERE I CHECK IF THE IMAGE I'M LOOKING FOR IS ALREADY ON DISK
NSString *pathToCheckImage = [NSString stringWithFormat:#"%#/%#.png", pathToPreviews, [tableItems objectAtIndex:i]];
NSData *dataOfCheckImage = [NSData dataWithContentsOfFile:pathToCheckImage];
UIImage *checkImage = [UIImage imageWithData:dataOfCheckImage];
NSString *pathToImage;
UIImage *image;
if (checkImage != nil) {
// THE IMAGE IS ALREADY ON DISK SO I GET IT FROM TEMP FOLDER
image = checkImage;
} else {
// THE IMAGE IS NEW SO I HAVE TO GET THE IMAGE FROM THE SERVER
pathToImage = [NSString stringWithFormat:#"http://www.SERVER.com/SERVER_PATH/%#.png", [tableItems objectAtIndex:i]];
NSURL *url = [NSURL URLWithString:pathToImage];
NSData *data = [NSData dataWithContentsOfURL:url];
image = [UIImage imageWithData:data];
// AND SAVE IT ON DISK
NSString *path = [NSString stringWithFormat:#"%#/%#.png", pathToPreviews, [tableItems objectAtIndex:i]];
[self cacheImageOnDisk:image withPath:path];
}
if (image != nil) {
[arrayOfImages addObject:image];
}
}
This code is working, even if, depending on the number and size of the images I have to download from the server, it can take 1 or 2 minutes to perform its task.
The problem is that if the user quits (home button pushed) while this for-loop is running it keeps on performing its task until the end, even if it needs 1 minute to finish it.
During this period, launching the app again inevitably ends up crashing on startup.
I've tried to stop this for-loop on exit but applicationDidEnterBackground, applicationWillResignActive and applicationWillTerminate are not called until for-loop ends its task.
I've tried to set "application does not run in background", but nothing changed.
Any suggestion will be really appreciated.
You should not be downloading images on the main thread. The app won't be responsive. Grand Central Dispatch is an easy method to accomplish these tasks.
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSUInteger count = [tableItems count];
for (int i = 0; i < count; i++) {
// TODO: check if app didResignActive and break
UIImage *image = ... // get image from disk or network
// Add the image to the array on the main thread to avoid any threading issues
dispatch_sync(dispatch_get_main_queue(), ^{
[arrayOfImages addObject:image];
});
}
});
You need to either process your loop off the main thread (very easy to get wrong), or break your work into chunks (much simpler). Try extracting your loop into a separate method that runs it 10 times and then schedules another run with [... performSelector:#selector(myMethod) afterDelay:0]; That will give the runloop a chance to cycle and process events (like quit) every 10 iterations.
Threading (whether via older ways or the newer dispatch_async) will still get you better responsiveness though, so if you want to do that, my recommendation is this:
Share NO data between the background thread and the main thread while it's working.
Spawn your background thread, do your work entirely with local state not accessed anywhere else, then dispatch back to the main thread with your completed array. Any shared state is very likely to make your life very hard.
Related
Here is the thing:
I have a scroll view, it did the lazy load for full screen image of user's photo:
[self.assetsLibrary assetForURL:[NSURL URLWithString:[[self.assets objectAtIndex:index] objectForKey:#"asset_url"]]
resultBlock:^(ALAsset *asset) {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
CGImageRef cgImage = asset.defaultRepresentation.fullScreenImage;
UIImage *image = [UIImage imageWithCGImage:cgImage];
dispatch_async(dispatch_get_main_queue(), ^{
imageView.image = image;
});
});
}
failureBlock:^(NSError *error) {
NSLog(#"error");
}];
I know it is expensive to load full screen image, so I put it in to the background thread, but it is still lag when I do the scroll. And still lag even I change it like this:
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
CGImageRef cgImage = asset.defaultRepresentation.fullScreenImage;
UIImage *image = [UIImage imageWithCGImage:cgImage];
imageView.image = image;
dispatch_async(dispatch_get_main_queue(), ^{
});
});
Obviously, nothing to do in the main queue, but it still lag until I comment the line:
// CGImageRef cgImage = asset.defaultRepresentation.fullScreenImage;
So I am so confused, is there something wrong when I used GCD?
Somebody can help me to explain it? Any thing will be helpful.
Thank you, guys.
UPDATE
To #Fogmeister : The size of the photo is the full screen size, actuel imageView size is around half. Even I comment the line: "imageView.image = image;" it is still lag. Which means it is not from the resizing. I know where the time is being taken, here: "asset.defaultRepresentation.fullScreenImage;". When I comment it, everything fine, there is no more lag.
So, which I don't understand is, I've already put it in the background thread...
Ok, finally I solved problem:
Instead of getting image directly by
asset.defaultRepresentation.fullScreenImage
I use the method from Apple's Exemple PhotosByLocation (code below) to get the image in BG thread. That works great, no more lag when scroll. But I am still confused, I don't know exactly why. So I appreciate if someone can explain it to me.
- (UIImage *)fullSizeImageForAssetRepresentation:(ALAssetRepresentation *)assetRepresentation {
UIImage *result = nil;
NSData *data = nil;
uint8_t *buffer = (uint8_t *)malloc(sizeof(uint8_t)*[assetRepresentation size]);
if (buffer != NULL) {
NSError *error = nil;
NSUInteger bytesRead = [assetRepresentation getBytes:buffer fromOffset:0 length:[assetRepresentation size] error:&error];
data = [NSData dataWithBytes:buffer length:bytesRead];
free(buffer);
}
if ([data length]) {
CGImageSourceRef sourceRef = CGImageSourceCreateWithData((__bridge CFDataRef)data, nil);
NSMutableDictionary *options = [NSMutableDictionary dictionary];
[options setObject:(id)kCFBooleanTrue forKey:(id)kCGImageSourceShouldAllowFloat];
[options setObject:(id)kCFBooleanTrue forKey:(id)kCGImageSourceCreateThumbnailFromImageAlways];
[options setObject:(id)[NSNumber numberWithFloat:640.0f] forKey:(id)kCGImageSourceThumbnailMaxPixelSize];
//[options setObject:(id)kCFBooleanTrue forKey:(id)kCGImageSourceCreateThumbnailWithTransform];
CGImageRef imageRef = CGImageSourceCreateThumbnailAtIndex(sourceRef, 0, (__bridge CFDictionaryRef)options);
if (imageRef) {
result = [UIImage imageWithCGImage:imageRef scale:[assetRepresentation scale] orientation:(UIImageOrientation)[assetRepresentation orientation]];
CGImageRelease(imageRef);
}
if (sourceRef) CFRelease(sourceRef);
}
return result;
}
You're solution taken from Apple's PhotosByLocation is actually grabbing the biggest resolution image, not the fullscreen image. IOW, it's essentially the same as calling fullResolutionImage instead of fullScreenImage. How that fixes your problem, I'm not sure. I'm struggling with the same performance issue. If I use fullScreenImage, I get lags in my scrolling. But switching to fullResolutionImage gets rid of the lags. fullResolutionImage takes about twice as long as fullScreenImage, but since this is always in the background, it shouldn't really matter how much time it takes. I suspect that fullScreenImage is returning an image that needs some sort of additional processing once it gets rendered to the screen in the main thread - hence the lag.
Do you know the actual size of the photo? What is very expensive is scrolling images that are being resized to fit the screen.
Seeing as you're already loading in a BG thread it might be worth resizing the image to the size you are displaying it at before sticking it on the screen.
You can see where the time is being taken by using the CoreAnimation tool in Instruments by profiling the app from Xcode. It will even tell you which line of code is causing the slow down and missed animation frames.
From the apple documentation:
DISPATCH_QUEUE_PRIORITY_DEFAULT
Items dispatched to the queue run at the default priority; the queue is scheduled for execution after all high priority queues have been scheduled, but before any low priority queues have been scheduled.DISPATCH_QUEUE_PRIORITY_BACKGROUNDItems dispatched to the queue run at background priority; the queue is scheduled for execution after all high priority queues have been scheduled and the system runs items on a thread whose priority is set for background status. Such a thread has the lowest priority and any disk I/O is throttled to minimize the impact on the system.
You're running it in a separate thread, but that's not necessarily a thread "in the background." A background thread loading something in my experience will be completely blocked by doing a UI update such as scrolling a UIScrollView. Have you tried using DISPATCH_QUEUE_PRIORITY_BACKGROUND?
I have the following code that attempts to load a row of thumbnails in a tableview asynchronously:
for (int i = 0; i < totalThumbnails; i++)
{
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0),
^{
__block GraphicView *graphicView;
__block Graphic *graphic;
dispatch_async(dispatch_get_main_queue(),
^{
graphicView = [[tableViewCell.contentView.subviews objectAtIndex:i] retain];
graphic = [[self.thumbnailCache objectForKey: [NSNumber numberWithInt:startingThumbnailIndex + i]] retain];
if (!graphic)
{
graphic = [[self graphicWithType:startingThumbnailIndex + i] retain];
[self.thumbnailCache setObject: graphic forKey:[NSNumber numberWithInt:startingThumbnailIndex + i]];
}
[graphicView setGraphic:graphic maximumDimension:self.cellDimension];
});
[graphicView setNeedsDisplay];
dispatch_async(dispatch_get_main_queue(),
^{
CGRect graphicViewFrame = graphicView.frame;
graphicViewFrame.origin.x = ((self.cellDimension - graphicViewFrame.size.width) / 2) + (i * self.cellDimension);
graphicViewFrame.origin.y = (self.cellDimension - graphicViewFrame.size.height) / 2;
graphicView.frame = graphicViewFrame;
});
[graphicView release];
[graphic release];
});
}
However when I run the code I get a bad access at this line: [graphicView setNeedsDisplay]; It's worth mentioning that the code works fine when I have it set up like this:
for (int i = 0; i < totalThumbnails; i++)
{
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0),
^{
dispatch_async(dispatch_get_main_queue(),
^{
//put all the code here
});
}
It works fine and the UITableView loads asynchronously when it's first called, however the scrolling is still really choppy.
So I'd like to get the to get the first bit of code to work so I can get the drawing done in the global thread instead of the main thread (which I assume will fix the choppy scrolling?).
Since iOS4 drawing is able to be done asynchronously so I don't believe that is the problem. Possibly I'm misusing the __Block type?
Anyone know how I can get this to work?
You completely misunderstand how to use GCD. Looking at your code:
__block GraphicView *graphicView;
Your variable here is not initialised to nil. It is unsafe to send messages to.
__block Graphic *graphic;
dispatch_async(dispatch_get_main_queue(),
^{
//statements
});
Your dispatch statement here returns immediately. The system works for you to spin this task off on a different thread. Before, or perhaps at the same time as, the above statements are executed we move on to the next line of execution here...
[graphicView setNeedsDisplay];
At this point graphic view may or may not have been initialised by your dispatch statement above. Most likely not as there wont have been time. As it still hasn't been initialised it points to random memory and hence trying to send messages to it causes EXC_BAD_ACCESS.
If you want to draw cell contents asynchronously (or pre-render images or whatever.) I thouroughly reccommend watching WWDC 2012 session 211 "Building Concurrent User Interfaces on iOS". They do almost exactly what you seem to be attempting to do and explain all the pitfalls you can run into.
I think the issue is because you are trying to re-draw the UIView on a working thread. You should move this:
[graphicView setNeedsDisplay];
To the main queue.
I have an iPhone app that is using sqlite 3.6 (not with FMDB) to store and load data. I load the database when the app loads and uses the same database connection through the whole app.
In a background thread the app downloads some data from a webserver and writes to the database. At the same time the main thread also might need to write to the same database. This sometimes leads to EXC_BAD_ACCESS as both threads are trying to access the database.
What is the best and easiest way to be able to use the database from different threads?
This is an example that shows the problem:
sqlite3 *database;
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *documentsDirectory = [paths objectAtIndex:0];
NSString *path = [documentsDirectory stringByAppendingPathComponent:#"database.db"];
if (sqlite3_open([path UTF8String], &database) != SQLITE_OK) {
sqlite3_close(database);
return YES;
}
[NSThread detachNewThreadSelector:#selector(test) toTarget:self withObject:nil];
[self test];
return YES;
}
-(void)test {
for (int i = 0; i < 2000; i++) {
NSLog(#"%i",i);
sqlite3_exec([self getDb],"UPDATE mytable SET test=''", 0, 0, 0);
}
}
EDIT:
After willcodejavaforfood's answer below I've tried to change my code to use a separate database object (connection) for each separate thread and also added sqlite3_busy_timeout() so that sqlite will retry to write if the database is busy. Now I don't get EXC_BAD_ACCESS anymore but I've noticed that not all data get inserted. So this is not a stable solution either. It seems to be really hard to get sqlite working with threading..
My new solution with separate connections:
-(void)test {
sqlite3 *db = [self getNewDb];
for (int i = 0; i < 2000; i++) {
NSLog(#"%i",i);
sqlite3_exec(db,"UPDATE mytable SET test=''", 0, 0, 0);
}
}
- (sqlite3 *)getNewDb {
sqlite3 *newDb = nil;
if (sqlite3_open([[self getDbPath] UTF8String], &newDb) == SQLITE_OK) {
sqlite3_busy_timeout(newDb, 1000);
} else {
sqlite3_close(newDb);
}
return newDb;
}
I solved this problem by using one thread and an NSOperationQueue to insert the Data. I would give it some thought. I've never been able to get a stable System with mutliple threads, and most writes aren't that important that queuing really helps.
As per request, some more Infos:
I have a subclass of NSOperation that I instantiate with the model object I want to store.
These operations are than submitted to an extension of NSOperationsQueue that runs in a seperate thread. This custom Queue just adds a pointer to the database instance. When the operation is executed, it uses the [NSOperationsQueue currentQueue] property to access the queue and than the database. On purpose, i used non-concurrent operations (maxOperations was set to 1)
Hence, only one query (or update) is executed at a time consecutivly, completely in the background.
Obviously you need some kind of callback after you're finished.
It is possibly not the fast, but the most stable and cleanest solution i could find.
Docs:
http://developer.apple.com/library/ios/documentation/General/Conceptual/ConcurrencyProgrammingGuide/OperationObjects/OperationObjects.html
http://www.cimgf.com/2008/02/16/cocoa-tutorial-nsoperation-and-nsoperationqueue/
http://icodeblog.com/2010/03/04/iphone-coding-turbo-charging-your-apps-with-nsoperation/
As you've noticed only one thread can access an sqlite database at a time. Options to prevent simultaneous access:
Create a new database connection in each thread and rely on file locking (costly).
Turn on sqlite3_config(SQLITE_CONFIG_SERIALIZED).
Use NSLock's.
Use GCD (Grand Central Dispatch) queue's.
The first three options may cause busy waiting (one thread waiting on another to release the lock) which is wasteful.
I use option 4 because it simplifies the task of creating new queries to run in the background and has no busy waiting. It also makes sure all queries execute in the order they were added (which my code tends to assume).
dispatch_queue_t _queue = dispatch_queue_create("com.mycompany.myqueue", DISPATCH_QUEUE_SERIAL);
// Run a query in the background.
dispatch_async(_queue, ^{
...some query
// Perhaps call a completion block on the main thread when done?
dispatch_async(dispatch_get_main_queue(), ^{
//completion(results, error);
});
});
// Run a query and wait for the result.
// This will block until all previous queries have finished.
// Note that you shouldn't do this in production code but it may
// be useful to retrofit old (blocking) code.
__block NSArray *results;
dispatch_sync(_queue, ^{
results = ...
});
...use the results
dispatch_release(_queue);
In a perfect world sqlite would let you perform simultaneous reads but only one write at a time (eg. like using dispatch_barrier_async() for writes and dispatch_async() for reads).
This is all explained in the Core Data Programming Guide in the section for Concurrency.
The pattern recommended for concurrent
programming with Core Data is thread
confinement.
You should give each thread its own
entirely private managed object
context and keep their associated
object graphs separated on a
per-thread basis.
There are two possible ways to adopt
the pattern:
Create a separate managed object
context for each thread and share a
single persistent store coordinator.
This is the typically-recommended
approach.
Create a separate managed object
context and persistent store
coordinator for each thread. This
approach provides for greater
concurrency at the expense of greater
complexity (particularly if you need
to communicate changes between
different contexts) and increased
memory usage.
I've tried these two solutions and they worked perfectly. You can either use critical sections or NSOperationQueue and I prefer the first one, here is the code for both of them:
define some class "DatabaseController" and add this code to its implementation:
static NSString * DatabaseLock = nil;
+ (void)initialize {
[super initialize];
DatabaseLock = [[NSString alloc] initWithString:#"Database-Lock"];
}
+ (NSString *)databaseLock {
return DatabaseLock;
}
- (void)writeToDatabase1 {
#synchronized ([DatabaseController databaseLock]) {
// Code that writes to an sqlite3 database goes here...
}
}
- (void)writeToDatabase2 {
#synchronized ([DatabaseController databaseLock]) {
// Code that writes to an sqlite3 database goes here...
}
}
OR to use the NSOperationQueue you can use:
static NSOperationQueue * DatabaseQueue = nil;
+ (void)initialize {
[super initialize];
DatabaseQueue = [[NSOperationQueue alloc] init];
[DatabaseQueue setMaxConcurrentOperationCount:1];
}
+ (NSOperationQueue *)databaseQueue {
return DatabaseQueue;
}
- (void)writeToDatabase {
NSInvocationOperation * operation = [[NSInvocationOperation alloc] initWithTarget:self selector:#selector(FUNCTION_THAT_WRITES_TO_DATABASE) object:nil];
[operation setQueuePriority:NSOperationQueuePriorityHigh];
[[DatabaseController databaseQueue] addOperations:[NSArray arrayWithObject:operation] waitUntilFinished:YES];
[operation release];
}
these two solutions block the current thread until the writing to database is finished which you may consider in most of the cases.
I have an app that loads multiple thumbnail images into a UIScrollVIew. This is a lengthy operation, and so as not to block up the display of the rest of the UI, I am running it in a separate thread. This works fine the first time at application launch, but later a new set of images needs to be loaded into the UIScrollView. When I detach a thread a second time the app crashes (sometimes). Code follows:
// this call is in a separate method
[NSThread detachNewThreadSelector:#selector(addThumbnailsToScrollView) toTarget:self withObject:nil];
// this is the main entry point for the thread
- (void) addThumbnailsToScrollView {
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; // Top-level pool
// now place all the thumb views as subviews of the scroll view
float xPosition = THUMB_H_PADDING;
int pageIndex = 0;
for (Page *page in self.pages) {
// get the page's bitmap image and scale it to thumbnail size
NSString *name = [page valueForKey:#"pageBackground"];
NSString *basePath = [[NSBundle mainBundle] pathForResource:page.pageBackground ofType:#"jpg" inDirectory:nil];
UIImage *thumbImage = [UIImage imageWithContentsOfFile:basePath];
thumbImage = [thumbImage imageScaledToSize:CGSizeMake(80, 100)];
// create a ThumbImageView for each page and add it to the thumbnailScrollView
if (thumbImage) {
ThumbImageView *thumbView = [[ThumbImageView alloc] initWithImage:thumbImage];
[thumbView setDelegate:self];
[thumbView setImageName:name];
[thumbView setImageSize:CGSizeMake(80, 100)];
[thumbView setPageIndex:pageIndex];
pageIndex ++;
CGRect frame = [thumbView frame];
frame.origin.y = 0;
frame.origin.x = xPosition;
[thumbView setFrame:frame];
[thumbnailPagesScrollView addSubview:thumbView];
[thumbView release];
xPosition += (frame.size.width + THUMB_H_PADDING);
}
}
[self hightlightThumbnailPageAtIndex:0];
[(UIActivityIndicatorView *)[thumbnailPagesScrollView.superview viewWithTag:100] stopAnimating];
[pool release]; // Release the objects in the pool.
}
I thought that a detached thread exits as soon as the main entry routine was completed. Wouldn't the second call to detach a thread be a new thread? Why is the app crashing, but sometimes not?
Thanks
Jk
You cannot touch UIKit (meaning UIScrollVIew) in a secondary thread - you need to reorganize so that the fetch takes place in a secondary thread but you make a NSData object (containing the image binary) available to your primary thread for each thumbnail so that it can do everything related to actually displaying them.
Apple repeatedly warn in documentation that UIKit is not thread-safe.
I would suggest adding the thumbView to the thumbnailPagesScrollView on the main thread rather than a separate thread. There might be issues on the retain count of the object across threads. There is a convenience method performSelectorOnMainThread I think it is to do that. You could pass thumbView to that and then add it to the subview.
Alternatively you could do the whole if statement on the main thread as thats not the thing that will interrupt the user.
Also with your activity indicator this should be stopped on the main thread. Everything UI related should be done on the main thread.
I have a UIScrollView that has a set of images loaded side-by-side inside it. You can see an example of my app here: http://www.42restaurants.com. My problem comes in with memory usage. I want to lazy load the images as they are about to appear on the screen and unload images that aren't on screen. As you can see in the code I work out at a minimum which image I need to load and then assign the loading portion to an NSOperation and place it on an NSOperationQueue. Everything works great apart from a jerky scrolling experience.
I don't know if anyone has any ideas as to how I can make this even more optimized, so that the loading time of each image is minimized or so that the scrolling is less jerky.
- (void)scrollViewDidScroll:(UIScrollView *)scrollView{
[self manageThumbs];
}
- (void) manageThumbs{
int centerIndex = [self centerThumbIndex];
if(lastCenterIndex == centerIndex){
return;
}
if(centerIndex >= totalThumbs){
return;
}
NSRange unloadRange;
NSRange loadRange;
int totalChange = lastCenterIndex - centerIndex;
if(totalChange > 0){ //scrolling backwards
loadRange.length = fabsf(totalChange);
loadRange.location = centerIndex - 5;
unloadRange.length = fabsf(totalChange);
unloadRange.location = centerIndex + 6;
}else if(totalChange < 0){ //scrolling forwards
unloadRange.length = fabsf(totalChange);
unloadRange.location = centerIndex - 6;
loadRange.length = fabsf(totalChange);
loadRange.location = centerIndex + 5;
}
[self unloadImages:unloadRange];
[self loadImages:loadRange];
lastCenterIndex = centerIndex;
return;
}
- (void) unloadImages:(NSRange)range{
UIScrollView *scrollView = (UIScrollView *)[[self.view subviews] objectAtIndex:0];
for(int i = 0; i < range.length && range.location + i < [scrollView.subviews count]; i++){
UIView *subview = [scrollView.subviews objectAtIndex:(range.location + i)];
if(subview != nil && [subview isKindOfClass:[ThumbnailView class]]){
ThumbnailView *thumbView = (ThumbnailView *)subview;
if(thumbView.loaded){
UnloadImageOperation *unloadOperation = [[UnloadImageOperation alloc] initWithOperableImage:thumbView];
[queue addOperation:unloadOperation];
[unloadOperation release];
}
}
}
}
- (void) loadImages:(NSRange)range{
UIScrollView *scrollView = (UIScrollView *)[[self.view subviews] objectAtIndex:0];
for(int i = 0; i < range.length && range.location + i < [scrollView.subviews count]; i++){
UIView *subview = [scrollView.subviews objectAtIndex:(range.location + i)];
if(subview != nil && [subview isKindOfClass:[ThumbnailView class]]){
ThumbnailView *thumbView = (ThumbnailView *)subview;
if(!thumbView.loaded){
LoadImageOperation *loadOperation = [[LoadImageOperation alloc] initWithOperableImage:thumbView];
[queue addOperation:loadOperation];
[loadOperation release];
}
}
}
}
EDIT:
Thanks for the really great responses. Here is my NSOperation code and ThumbnailView code. I tried a couple of things over the weekend but I only managed to improve performance by suspending the operation queue during scrolling and resuming it when scrolling is finished.
Here are my code snippets:
//In the init method
queue = [[NSOperationQueue alloc] init];
[queue setMaxConcurrentOperationCount:4];
//In the thumbnail view the loadImage and unloadImage methods
- (void) loadImage{
if(!loaded){
NSString *filename = [NSString stringWithFormat:#"%03d-cover-front", recipe.identifier, recipe.identifier];
NSString *directory = [NSString stringWithFormat:#"RestaurantContent/%03d", recipe.identifier];
NSString *path = [[NSBundle mainBundle] pathForResource:filename ofType:#"png" inDirectory:directory];
UIImage *image = [UIImage imageWithContentsOfFile:path];
imageView = [[ImageView alloc] initWithImage:image andFrame:CGRectMake(0.0f, 0.0f, 176.0f, 262.0f)];
[self addSubview:imageView];
[self sendSubviewToBack:imageView];
[imageView release];
loaded = YES;
}
}
- (void) unloadImage{
if(loaded){
[imageView removeFromSuperview];
imageView = nil;
loaded = NO;
}
}
Then my load and unload operations:
- (id) initWithOperableImage:(id<OperableImage>) anOperableImage{
self = [super init];
if (self != nil) {
self.image = anOperableImage;
}
return self;
}
//This is the main method in the load image operation
- (void)main {
[image loadImage];
}
//This is the main method in the unload image operation
- (void)main {
[image unloadImage];
}
I'm a little puzzled by the "jerky" scrolling. Since NSOperationQueue runs operations on separate thread(s) I'd have expected at worst you might see empty UIImageViews showing up on the screen.
First and foremost I'd be looking for things that are impacting the processor significantly as NSOperation alone should not interfere with the main thread. Secondly I'd be looking for details surrounding the NSOperation setup and execution that might be causing locking and syncing issues which could interrupt the main thread and therefore impact scrolling.
A few items to consider:
Try loading your ThumbnailView's with a single image at the start and disabling the NSOperation queuing (just skip everything following the "if loaded" check. This will give you an immediate idea whether the NSOperation code is impacting performance.
Keep in mind that -scrollViewDidScroll: can occur many times during the course of a single scroll action. Depending on how for the scroll moves and how your -centerThumbIndex is implemented you might be attempting to queue the same actions multiple times. If you've accounted for this in your -initWithOperableImage or -loaded then its possible you code here is causing sync/lock issues (see 3 below). You should track whether an NSOperation has been initiated using an "atomic" property on the ThumbnailView instance. Prevent queuing another operation if that property is set and only unset that property (along with loaded) at the end of the NSOperation processes.
Since NSOperationQueue operates in its own thread(s) make sure that none of your code executing within the NSOperation is syncing or locking to the main thread. This would eliminate all of the advantages of using the NSOperationQueue.
Make sure your "unload" operation has a lower priority than your "load" operation, since the priority is the user experience first, memory conservation second.
Make sure you keep enough thumbnails for at least a page or two forward and back so that if NSOperationQueue falls behind, you have a high margin of error before blank thumbnails become visible.
Make sure your load operation is only loading a "pre-scaled" thumbnail and not loading a full size image and rescaling or processing. This would be a lot of extra overhead in the middle of a scrolling action. Go even further and make sure you've converted them to PNG16 without an alpha channel. This will give at least a (4:1) reduction in size with hopefully no detectable change in the visual image. Also consider using PVRTC format images which will take the size down even further (8:1 reduction). This will greatly reduced the time it takes to read the images from "disk".
I apologize if any of this doesn't make sense. I don't see any issues with the code you've posted and problems are more likely to be occurring in your NSOperation or ThumbnailView class implementations. Without reviewing that code, I may not be describing the conditions effectively.
I would recommend posting your NSOperation code for loading and unloading and at least enough of the ThumbnailView to understand how it interacts with the NSOperation instances.
Hope this helped to some degree,
Barney
One option, although less visually pleasing, is to only load images when the scrolling stops.
Set a flag to disable image loading in:
-scrollViewWillBeginDragging:
Re-enable loading images when the scrolling stops using the:
-scrollViewDidEndDragging:willDecelerate:
UIScrollViewDelegate method. When the willDecelerate: parameter is NO, the movement has stopped.
the problem is here:
UIImage *image = [UIImage imageWithContentsOfFile:path];
It seems that threaded or not when you load a file from disk (which maybe that happens on the main thread regardless, I'm not totally sure) everything stalls. You normally don't see this in other situations because you don't have such a large area moving if any at all.
While researching this problem, I found two more resources that may be of interest:
Check out the iPhone sample project "PageControl": http://developer.apple.com/iphone/library/samplecode/PageControl/index.html
It lazy loads view controllers in a UIScrollView.
and -
Check out the cocoa touch lib: http://github.com/facebook/three20 which has a 'TTPhotoViewController' class that lazy loads photos/thumbnails from web/disk.
Set shouldRasterize = YES for the sub content view adde to the scrollview. It is seen to remove the jerky behavior of custom created scroll view like a charm. :)
Also do some profiling using the instruments in the Xcode. Go through the tutorials created for profiling by Ray Wenderlich it helped me a lot.