In Part 1 of this blog post, we have created a perfectly working Flickr App. Now, let’s tinker with it!
The first thing we will tinker with is performance. We will actually use Part 1 of this project in a further lesson having to do with performance. First of all let’s just take note that loading pictures from the web is a very “expensive” operation. Expensive meaning that it consumes a lot of resources.
If you recall from Part 1, our app builds a tableview and when it wants to fulfill the image requirement, it calls this line:
NSData *imageData = [NSData dataWithContentsOfURL:[photoURLs objectAtIndex:indexPath.row]];
A web call had already been made in our loadFlickrPhotos method. But that is a very simple web call. Let’s review that method next. We created a string to fetch initial data from its URL like so:
NSString *urlString = [NSString stringWithFormat:@"http://api.flickr.com/services/rest/?method=flickr.photos.search&api_key=%@&tags=%@&per_page=10&format=json&nojsoncallback=1", FlickrAPIKey, @"mayan2012"];
As you can see, we go to the resource api.flick.com and send our api_key and get 10 results per page in json format while sending the keyword mayan2012. Then we put the results into an NSDictionary and finally build our photoNames and photoURLs arrays.
Notice we are not specifying what size of picture to get, so we just fetch the default size which is thumbnail. Notice it takes very little time to load the images and even when you scroll the tableview the UI is not SO slow. But let’s look at the next web fetch in that method:
NSString *photoURLString = [NSString stringWithFormat:@"http://farm%@.static.flickr.com/%@/%@_%@_s.jpg", [photo objectForKey:@"farm"], [photo objectForKey:@"server"], [photo objectForKey:@"id"], [photo objectForKey:@"secret"]];
Here we are building the photoURLs array and we build it using the known info so far. Let’s make it fetch a medium sized image instead by changing it to this:
NSString *sizeit = @"m";
NSString *photoURLString = [NSString stringWithFormat:@"http://farm%@.static.flickr.com/%@/%@_%@_%@.jpg", [photo objectForKey:@"farm"], [photo objectForKey:@"server"], [photo objectForKey:@"id"], [photo objectForKey:@"secret"], sizeit];
Now we are telling it we want the m-sized image. Run the app once again and watch how much slower it is at fetching the images. Scroll the tableview and weep!
Enter GCD
GCD, known as Grand Central Dispatch, is a very good tool for tuning your app’s performance. Basically when your app launches, it fetches most of its files from the sandboxed content included in the app. Of course apps would not be as fun nor would they be as popular if they didn’t fetch data from the almighty cloud. This is a resource-heavy operation.
Other expensive transactions include editing media files or preloading large media files, complex database searches etc. However, most apps you see in the appstore manage to do precisely these tasks and still seem quite fast and responsive. They do this by creating multiple threads.
The main thread is where the apps’ UI runs in. That thread is used for drawing cells, local images, text, scrolling, gesturing etc. This is the thread the user interacts with which makes it the most important thread. You never want to slow this thread down, much less stop it or freeze it. That’s for Android and Windows phones.
GCD lets you, quite simply, take expensive transactions and send them out to another thread. So let’s take a look at how this works.
We want to take that expensive line that fetches NSData and “wrap it” around a GCD call, which will send it off to the side and return the results whenever they are ready WITHOUT slowing us down. So go to your cFRAIP method and change it to this:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{
//P1
UITableViewCell *cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:@"Cell Identifier"] autorelease];
cell.textLabel.text = [photoNames objectAtIndex:indexPath.row];
//P2
dispatch_async(kfetchQueue, ^{
//P1
NSData *imageData = [NSData dataWithContentsOfURL:[photoURLs objectAtIndex:indexPath.row]];
//P2
dispatch_async(dispatch_get_main_queue(), ^{
//P1
cell.imageView.image = [UIImage imageWithData:imageData];
//P2
[self setImage:cell.imageView.image forKey:cell.textLabel.text];
[cell setNeedsLayout];
});
});
return cell;
}
The lines in bold are the ones we added. Notice how we simply wrapped our original call around a dispatch_async to kfetchQueue which sends a shout out to a special thread to perform that dataWithContentsOfURL call and then it returns to the main queue or main thread via the dispatch_async dispatch_get_main_queue(). Finally once we get back to the main queue we call cell setNeedsLayout to redraw the tableview cell with the new image data.
To make this work we have to add this import and this define to our .m file:
#import <dispatch/dispatch.h>
#define kfetchQueue dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)
Fetching data on a separate thread is better, but fetching only the images you need to fetch is better still!
Enter Caches
A cache is just a fancy name for a repository for already used stuff. Anything that you use at one point and want to discard when not in current use but might need later, you want to store in a cache. We will create a very simple cache here. To keep things neat we will create a Cache Class. Let’s call it ImageCache and make your .h file look like this:
#import <Foundation/Foundation.h>
@interface ImageCache : NSObject
@property (nonatomic, retain) NSCache *imgCache;
+ (ImageCache*)sharedImageCache;
- (void)addImage:(NSString *)imageURL: (UIImage *)image;
- (UIImage*)getImage:(NSString *)imageURL;
- (BOOL)doesExist:(NSString *)imageURL;
@end
Don’t be intimidated by the Class method. Such a method is typical of a class which will take data temporarily and do something to it, without ever really storing any data of its own. These are sometimes called Helper Classes.
The methods declared are sharedImageCache which as you will see basically creates one instance of itself and no more. The addImage method takes a URL and an image and stores it in the cache. The getImage returns an image based on the passed URL and finally a method that returns YES or NO based on if the passed URL returns an existing image.
Let’s look at the implementation of this simple class:
#import "ImageCache.h"
@implementation ImageCache
@synthesize imgCache;
static ImageCache* sharedImageCache = nil;
+(ImageCache*)sharedImageCache{
@synchronized([ImageCache class]) {
//If there is no instance, create one and return it
if (!sharedImageCache)
sharedImageCache= [[self alloc] init];
return sharedImageCache;
}
//Otherwise return nil (Only a SINGLE instance can exist)
return nil;
}
+(id)alloc{
@synchronized([ImageCache class]) {
NSAssert(sharedImageCache == nil, @"Attempted to allocate a second instance of a singleton.");
sharedImageCache = [super alloc];
return sharedImageCache;
}
return nil;
}
-(id)init{
self = [super init];
if (self != nil) {
imgCache = [[NSCache alloc] init];
}
return self;
}
- (void)addImage:(NSString *)imageURL: (UIImage *)image{
//Take a URL and an image and store it in a cache
[imgCache setObject:image forKey:imageURL];
}
- (UIImage*)getImage:(NSString *)imageURL{
//Take a passed URL and return an image
return [imgCache objectForKey:imageURL];
}
- (BOOL)doesExist:(NSString *)imageURL{
//If image doesn't exist, return false
if ([imgCache objectForKey:imageURL] == nil){
return false;
}
//Otherwise, return true
return true;
}
@end
Note the comments in the code. The first three methods take care of creating what is called a Singleton Class. This is a class which can only have one instance of itself, otherwise it returns nil and a log. The last few methods are pretty simple to understand. They handle the storing and fetching of images in a cache. Now let’s put it to the test. Modify your cellForRowAtIndexPath to look like this:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{
//P1
UITableViewCell *cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:@"Cell Identifier"] autorelease];
cell.textLabel.text = [photoNames objectAtIndex:indexPath.row];
//If image exists then get it from the cache
if ([[ImageCache sharedImageCache] DoesExist:[photoNames objectAtIndex:indexPath.row]] == true) {
cell.imageView.image = [[ImageCache sharedImageCache] GetImage:[photoNames objectAtIndex:indexPath.row]];
} else {
//Otherwise fetch the imageData
dispatch_async(kfetchQueue, ^{
//P1
NSData *imageData = [NSData dataWithContentsOfURL:[photoURLs objectAtIndex:indexPath.row]];
dispatch_async(dispatch_get_main_queue(), ^{
cell.imageView.image = [UIImage imageWithData:imageData];
// Add the image to the cache
[[ImageCache sharedImageCache] AddImage:[photoURLs objectAtIndex:indexPath.row] :cell.imageView.image];
[cell setNeedsLayout];
});
});
}
return cell;
}
First we check to see if the image already exists in the cache. If it exists then we use it, otherwise we fetch it. Whenever we fetch it, we set the image and immediately afterwards we fill the cache containing the image data object with its key for each image we set. Thus our cache is built. This way we save valuable bandwidth and battery power along with it.
We have now taken our Flickr app and really tuned it up. Once again, drop us a line if you have any questions.