Is there a best practice or library that helps cache processed images (i.e. images that have been created while the app is running) in iOS? I use SDWebImage for images that I download, but in various places in the app I blur or in other ways process some of these images. I would like to store the processed images in a cache so that I can access them easily rather than reprocess each time a user opens that image. What's the best way to do this?
Thanks!
The answer it seems is using NSCache. It's quite straightforward to do. I ended up with a subclass of NSCache to make sure memory warnings are handled.
Implementation of NATAutoPurgeCache (heavily based on other posts on StackOverflow)
#implementation NATAutoPurgeCache
+ (NATAutoPurgeCache *)sharedCache
{
static NATAutoPurgeCache *_sharedCache = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
_sharedCache = [[self alloc] init];
});
return _sharedCache;
}
- (id)init
{
self = [super init];
if (self) {
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(removeAllObjects) name:UIApplicationDidReceiveMemoryWarningNotification object:nil];
}
return self;
}
- (void)dealloc
{
[[NSNotificationCenter defaultCenter] removeObserver:self name:UIApplicationDidReceiveMemoryWarningNotification object:nil];
}
#end
And using it when needed for an image: (in this case to store a blurred image)
UIImage* blurImage = [myCache objectForKey:#"blurred placeholder image"];
if (!blurImage)
{
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
UIImage* blurImage = self.activityPic.image;
blurImage= [blurImage applyLightEffect];
dispatch_async(dispatch_get_main_queue(), ^{
self.activityPic.image = blurImage;
});
[myCache setObject:blurImage forKey:#"blurred placeholder image"];
});
}
else {
self.activityPic.image = blurImage;
}
Related
I have a UITableView that loads thumbnails into cells aynchronously as follows:
NSBlockOperation *operation = [NSBlockOperation blockOperationWithBlock:
^{
ThumbnailButtonView *thumbnailButtonView = [tableViewCell.contentView.subviews objectAtIndex:i];
UIImage *image = [self imageAtIndex:startingThumbnailIndex + i];
[self.thumbnailsCache setObject: image forKey:[NSNumber numberWithInt:startingThumbnailIndex + i]];
[[NSOperationQueue mainQueue] addOperationWithBlock:
^{
UITableViewCell *tableViewCell = [self cellForRowAtIndexPath:indexPath];
if (tableViewCell)
{
[activityIndicatorView stopAnimating];
[self setThumbnailButtonView:thumbnailButtonView withImage:image];
}
}];
}];
[self.operationQueue addOperation:operation];
[self.operationQueues setObject:operation forKey:[NSNumber numberWithInt:startingThumbnailIndex + i]];
As per a technique I learned in a WWDC presentation, I store all of my operation queues in a NSCache called operationQueues so that later on I can cancel them if the cell scrolls off the screen (there are 3 thumbnails in a cell):
- (void) tableView:(UITableView *)tableView didEndDisplayingCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath
{
NSInteger startingThumbnailIndex = [indexPath row] * self.thumbnailsPerCell;
for (int i = 0; i < 3; i++)
{
NSNumber *key = [[NSNumber alloc] initWithInt:i + startingThumbnailIndex];
NSOperation *operation = [self.operationQueues objectForKey:key];
if (operation)
{
[operation cancel];
[self.operationQueues removeObjectForKey:key];
}
}
}
However, I notice if I repeatedly launch, load, then close my UITableView, I start recieving memory warnings, and then eventually the app crashes. When I remove this line:
[self.operationQueues setObject:operation forKey:[NSNumber numberWithInt:startingThumbnailIndex + i]];
The memory issues go away. Does anyone have any clue on why storing the operation queues in a cache or an array causes the app to crash?
Note: I learnt about NSCache and NSOperationQueue a couple of days ago so I might be wrong.
I don't think that this is a problem with NSOperationQueue, you are adding images to your thumbnailsCache but when the view scrolls off-screen they are still in memory. I am guessing that when the cells scroll back in, you re-create your images. This is probably clogs your memory.
Also, shouldn't you be caching your images instead of your operations?
EDIT
I did some detailed testing with NSCache by adding images and strings until my app crashed. It doesn't seem to be evicting any items so I wrote my custom cache, which seems to work:
#implementation MemoryManagedCache : NSCache
- (id)init
{
self = [super init];
if (self) {
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(reduceMemoryFootprint) name:UIApplicationDidReceiveMemoryWarningNotification object:nil];
}
return self;
}
- (void)reduceMemoryFootprint
{
[self setCountLimit:self.countLimit/2];
}
#end
I use ARC for my project
I have 1 class like this:
#implementation MyObject
+ (instancetype)shareInstance {
static id _shareInstance = nil;
static dispatch_once_t oncePredicate;
dispatch_once(&oncePredicate, ^{
_shareInstance = [[self alloc] init];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(freeInstance)
name:kLC_Notification_FreeAllInstance object:nil];
});
return _shareInstance;
}
+ (void)freeInstance {
/*I want to release object "_shareInstance" but how??? */
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
#end
But I can not release my instance object, so I have to change:
(move code line static id _shareInstance = nil; out of +shareInstance
#implementation MyObject
static id _shareInstance = nil;
+ (instancetype)shareInstance {
static dispatch_once_t oncePredicate;
dispatch_once(&oncePredicate, ^{
_shareInstance = [[self alloc] init];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(freeInstance)
name:kLC_Notification_FreeAllInstance object:nil];
});
return _shareInstance;
}
+ (void)freeInstance {
_shareInstance = nil;
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
#end
When I push notifcation with name:kLC_Notification_FreeAllInstance, all instance objects are release (all dealloc methods are both called). It's OK
BUT when I call it again....
all instances are not initialized on next call. And all instance objects will be nil after that
I made many breakpoint in block of dispatch_once and no breakpoint is called.
So my questions are:
Writing static id object;in a method and writing out of method, are they different?
How can I free all instance objects so I can still call them again?(I want to use ARC, I can do it without ARC)
I think you should set oncePredicate to 0 when releasing _shareInstance
#implementation MyObject
static id _shareInstance = nil;
static dispatch_once_t oncePredicate;
+ (instancetype)shareInstance {
dispatch_once(&oncePredicate, ^{
_shareInstance = [[self alloc] init];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(freeInstance)
name:kLC_Notification_FreeAllInstance object:nil];
});
return _shareInstance;
}
+ (void)freeInstance {
_shareInstance = nil;
oncePredicate = 0;
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
#end
It works for me!
To answer your question in order...
The difference between writing the static variable inside and outside the method is scope, i.e. when writing the static inside the method it is only accessible from within that method.
This is a bit more tricky, as suggested by the name dispatch_once only runs the code in the block once, but I believe that it relies on the token/predicate to synchronize this, so moving this outside the shareInstance and setting to 0 should mean the the dispatch_once runs the block (once) the next time around
XCode 4.5.2; I'm downloading an image from a remote server like this :
- (void)viewDidLoad
{
[super viewDidLoad];
NSOperationQueue *queue = [NSOperationQueue new];
NSInvocationOperation *operation = [[NSInvocationOperation alloc]
initWithTarget:self
selector:#selector(loadImage)
object:nil];
[queue addOperation:operation];
}
- (void)loadImage{
self.theobject = [RemoteQuery loadObjectWithImage:self.imageKey];
[self performSelectorOnMainThread:#selector(displayImage) withObject:nil waitUntilDone:YES];
}
-(void)displayImage{
UIImage *image = [UIImage imageWithData: self.theobject.imageData];
[self.imageView setImage:image];
}
This works fine on IOS simulator, but doesn't work on a device; it seems like displayImage is called before the data is loaded from [RemoteQuery loadImage]. What would be the best way to ensure that the image has loaded properly before showing it ?
Create a delegate protocol which will call back to the original object when the image download finishes. This NSOperation tutorial has more details on how to do this
Alternatively, use the NSOperation's completionBlock to perform the image display.
I've just setup a UITableview with Core Data and Grand Central Dispatch to update my app and display information through my fetchedResultsController. I have the application updating my database; however, the UITableView only gets populated once I redeploy the application to my phone through Xcode. For instance I run the update and everything works fine except I have an empty UITableView. Then I can close the app and click "Run" again through Xcode and when the app comes up the information is in the UITableView. I'm including the code below in hopes someone can help me discover why this is the case. If I need to include more code please just let me know. Thanks!
TeamTableViewController.m
- (NSFetchedResultsController \*)fetchedResultsController {
...
NSFetchedResultsController *aFetchedResultsController = [[NSFetchedResultsController alloc] initWithFetchREquest:fetchRequest managedObjectContext:self.managedObjectContext sectionNameKeyPath:#"stateCode" cacheName:nil];
self.fetchedResultsController = aFetchedResultsController;
...
}
-(IBAction) refreshList:(id)sender {
dispatch_queue_t queue = dispatch_queue_create("updateQueue", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(queue,^ { [self updateFromXMLFile:#"https://path/to/file.xml"];});
dispatch_async(queue,^ { [self updateFromXMLFile:#"https://path/to/file2.xml"];});
dispatch_async(queue,^ { [self updateFromXMLFile:#"https://path/to/file3.xml"];});
}
- (BOOL)updateFromXMLFile:(NSString *)pathToFile {
AppDelegate *appDelegate = (AppDelegate *)[[UIApplication sharedApplication] delegate];
XMLParser *parser1 = [[XMLParser alloc] initXMLParser];
XMLParser *parser2 = [[XMLParser alloc] initXMLParser];
XMLParser *parser3 = [[XMLParser alloc] initXMLParser];
BOOL success = FALSE;
NSURL *url = [[NSURL alloc] initWithString:pathToFile];
NSXMLParser *xmlParser = [[NSXMLParser alloc] initWithContentsOfURL:url];
if ([pathToFile rangeOfString:#"people"].location != NSNotFound) {
NSManagedObjectContext * peopleMOC = [[NSManagedObjectContext alloc] init];
[peopleMOC setPersistentStoreCoordinator:[appDelegate persistentStoreCoordinator]];
NSNotificationCenter * notify = [NSNotificationCenter defaultCenter];
[notify addObserver:self selector:#selector(mergeChanges:) name:NSManagedObjectContextDidSaveNotification object: peopleMOC];
parser1.managedObjectContext = peopleMOC;
[xmlParser setDelegate: parser1];
success = [xmlParser parse];
if (success) {
NSError * error = nil;
#try {
[parser1.managedObjectContext save:&error];
} catch (NSException * exception) {
// NSLog logs the exception...
}
[NSNotificationCenter defaultCenter] removeObserver:self];
Return TRUE;
} else {
// NSLog logs errors
return FALSE;
}
} elseif ... { // other 3 use practically same code here }
[appDelegate saveContext];
}
-(void) mergeChanges:(NSNotification *) notification {
AppDelegate *theDelegate = [[UIApplication sharedApplication] delegate];
[[theDelegate managedObjectContext] performSelectorOnMainThread:#selector(mergeChangesFromContextDidSaveNotification:) withObject: notification waitUntilDone:YES];
}
UPDATE
I'm able to kind of get it to work by adding [theDelegate saveContext]; to the end of my -(void)mergeChanges method... This just doesn't seem like the proper way of doing it to me. Thoughts?
UPDATE 2
The above method worked one time but I've been unable to get it to replicate.
You should use the NSFetchedResultsControllerDelegate protocol to inform your view controller of any changes in the data.
Use controllerDidChangeContent: rather than the other methods. Like this, the results will be reflected once all the downloads have finished. Shorter incremental updates might get computationally expensive.
There is a very succinct "Typical Use" example in the delegate documentation.
I'm seeking a tutorial on how to cache images loaded from a url into cells of a uitableview.
I found an example here
http://www.ericd.net/2009/05/iphone-caching-images-in-memory.html#top
But the code is incomplete. I'm an objective c novice so I found it very difficult to fill in the missing pieces.
Here is a simple ImageCache implementation using NSCache. ImageCache is a singelton.
ImageCache.h
#import <Foundation/Foundation.h>
#interface ImageCache : NSObject
#property (nonatomic, retain) NSCache *imgCache;
#pragma mark - Methods
+ (ImageCache*)sharedImageCache;
//- (void) AddImage:(NSString *)imageURL: (UIImage *)image;
- (void) AddImage:(NSString *)imageURL withImage:(UIImage *)image;
- (UIImage*) GetImage:(NSString *)imageURL;
- (BOOL) DoesExist:(NSString *)imageURL;
#end
ImageCache.m
#import "ImageCache.h"
#implementation ImageCache
#synthesize imgCache;
#pragma mark - Methods
static ImageCache* sharedImageCache = nil;
+(ImageCache*)sharedImageCache
{
#synchronized([ImageCache class])
{
if (!sharedImageCache)
sharedImageCache= [[self alloc] init];
return sharedImageCache;
}
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
- (void) AddImage:(NSString *)imageURL withImage:(UIImage *)image
{
[imgCache setObject:image forKey:imageURL];
}
- (NSString*) GetImage:(NSString *)imageURL
{
return [imgCache objectForKey:imageURL];
}
- (BOOL) DoesExist:(NSString *)imageURL
{
if ([imgCache objectForKey:imageURL] == nil)
{
return false;
}
return true;
}
#end
Example
UIImage *image;
// 1. Check the image cache to see if the image already exists. If so, then use it. If not, then download it.
if ([[ImageCache sharedImageCache] DoesExist:imgUrl] == true)
{
image = [[ImageCache sharedImageCache] GetImage:imgUrl];
}
else
{
NSData *imageData = [[NSData alloc] initWithContentsOfURL: [NSURL URLWithString: imgUrl]];
image = [[UIImage alloc] initWithData:imageData];
// Add the image to the cache
//[[ImageCache sharedImageCache] AddImage:imgUrl :image];
[[ImageCache sharedImageCache] AddImage:imgUrl withImage:image];
}
A nice working example was found here
http://ezekiel.vancouver.wsu.edu/~wayne/yellowjacket/YellowJacket.zip
You could also try using the awesome EgoImage library written by the sharp fellows at enormego to accomplish this. It is very simple to use, makes efficient use of cache behind the scenes and is ideally suited to meet your requirements.
Here's the github path for the library which includes a demo app.
I wrote this (with concepts and some code taken from Lane Roathe's excellent UIImageView+Cache category) for an app I've been working on. It uses the ASIHTTPRequest classes as well, which are great. This could definitely be improved.. for example, by allowing requests to be canceled if no longer needed, or by utilizing the notification userInfo to allow for more precise UI updating.. but it's working well for my purposes.
#implementation ImageFetcher
#define MAX_CACHED_IMAGES 20
static NSMutableDictionary* cache = nil;
+ (void)asyncImageFetch:(UIImage**)anImagePtr withURL:(NSURL*)aUrl {
if(!cache) {
cache = [[NSMutableDictionary dictionaryWithCapacity:MAX_CACHED_IMAGES] retain];
}
UIImage* newImage = [cache objectForKey:aUrl.description];
if(!newImage) { // cache miss - doh!
ASIHTTPRequest *imageRequest = [ASIHTTPRequest requestWithURL:aUrl];
imageRequest.userInfo = [NSDictionary dictionaryWithObject:[NSValue valueWithPointer:anImagePtr] forKey:#"imagePtr"];
imageRequest.delegate = self;
[imageRequest setDidFinishSelector:#selector(didReceiveImage:)];
[imageRequest setDidFailSelector:#selector(didNotReceiveImage:)];
[imageRequest startAsynchronous];
}
else { // cache hit - good!
*anImagePtr = [newImage retain];
}
}
+ (void)didReceiveImage:(ASIHTTPRequest *)request {
NSLog(#"Image data received.");
UIImage **anImagePtr = [(NSValue*)[request.userInfo objectForKey:#"imagePtr"] pointerValue];
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
UIImage *newImage = [[UIImage imageWithData:[request responseData]] retain];
if(!newImage) {
NSLog(#"UIImageView: LoadImage Failed");
}
else {
*anImagePtr = newImage;
// check to see if we should flush existing cached items before adding this new item
if( [cache count] >= MAX_CACHED_IMAGES)
[cache removeAllObjects];
[cache setValue:newImage forKey:[request url].description];
NSNotificationCenter *nc = [NSNotificationCenter defaultCenter];
[nc postNotificationName: #"ImageDidLoad" object: self userInfo:request.userInfo];
}
[pool drain];
}
You call this code as follows:
[ImageFetcher asyncImageFetch:&icon withURL:url];
I'm also using notifications, for better or worse, to let any owners of the corresponding UIImage know when they should redisplay- in this case, it's in a tableView context:
- (void)viewDidLoad {
[super viewDidLoad];
NSNotificationCenter *nc = [NSNotificationCenter defaultCenter];
[nc addObserver:self selector:#selector(imageDidLoad:) name:#"ImageDidLoad" object:nil];
}
- (void)imageDidLoad:(NSNotification*)notif {
NSLog(#"Received icon load notification.");
// reload table view so that new image appears.. would be better if I could
// only reload the particular UIImageView that holds this image, oh well...
[self.tableView reloadData];
}
- (void)dealloc {
NSNotificationCenter *nc = [NSNotificationCenter defaultCenter];
[nc removeObserver:self];
// ...
}
You also might wanna check HJCache. It comes with a UIImageView compatible view class that does all the caching transparently and is suitable to be used in UITableViewCells where scrolling performance is important.