UIImageView+AFNetworking Managing Caching & Allocations - uitableview

I am using the UIImageView+AFNetworkingin order to download and display an image from a URL in a UITableView. Everything is working great and my question is around the caching elements & allocations that are implemented with this call.
In using instruments to track memory I see that as I scroll my largest memory allocation quickly becomes this VM: Foundation which appears as if it is caching the images I am downloading in some way.
Which is great for user when the view the same image but I never see it release the memory. (I have not been able to get it to a memory warning yet). I just want to make sure then when needed this allocation will get released when needed. Below is the stack track of those VM : Foundation
Is there any need for me to monitor this and release the memory for the cache when needed to ensure smooth scrolling? Or is there anything else I should be watching to ensure this is handled properly?
Here is how I am calling for the images in cellForRowAtIndexPath I am calling the setImageWithURLRequest. Any advice or improvements would be appreciated.
NSString *imageURL = [NSString stringWithFormat:#"https://IMAGE-URL/%#",imageURL];
__weak MainTableViewCell *weakCell = cell;
NSURLRequest *urlRequest = [[NSURLRequest alloc]initWithURL:[NSURL URLWithString:imageURL]];
[cell.postImage setImageWithURLRequest:urlRequest placeholderImage:nil success:^(NSURLRequest *request, NSHTTPURLResponse *response, UIImage *image) {
MainTableViewCell *strongCell = weakCell;
strongCell.postImage.image = image;
[UIView animateWithDuration:.15 animations:^{
strongCell.postImage.alpha = 1;
}];
} failure:^(NSURLRequest *request, NSHTTPURLResponse *response, NSError *error) {
}];

You really shouldn't worry about clearing image cache as AFNetworking handle this for you automatically.
It stores images internally in NSCache that as the docs say :
incorporates various auto-removal policies, which ensure that it does not use too much of the system’s memory. The system automatically carries out these policies if memory is needed by other applications. When invoked, these policies remove some items from the cache, minimizing its memory footprint.
However, during AFNetworking development there were some troubles with this automatic removal behaviour, so finally manual cache clearing was added when receiving UIApplicationDidReceiveMemoryWarningNotification. But this all happens internally and you shouldn't do it yourself.

Related

Uploading UIImage data to the server getting memory peaks?

I am trying to upload images data to the server, it uploaded successfully, but getting memory peaks on per image upload. And also uploading more than 20 images getting App shut's down and receiving memory warnings.
How to resolve this issue?
Edit:
I am using NSURLConnection.
image = [params valueForKey:key];
partData = UIImageJPEGRepresentation([ImageUtility getQualityFilterImage:[params valueForKey:key]], 0.8f);
if([partData isKindOfClass:[NSString class]])
partData=[((NSString*)partData) dataUsingEncoding:NSUTF8StringEncoding];
[body appendData:partData];
[body appendData:[[NSString stringWithFormat:#"\r\n--%#--\r\n", boundary] dataUsingEncoding:NSUTF8StringEncoding]];
[urlRequest setHTTPBody:body];
A couple of thoughts:
If uploading multiple images, you might want to constrain how many you perform concurrently.
For example, this is my memory when I issued 20 uploads of roughly 5mb per image, memory spiked up to 113 mb:
If, however, I constrained this to only doing no more than 4 at a time (using operation queues), the memory usage was improved, maxing out at 55mb (i.e. roughly 38mb over baseline):
Others have suggested that you might consider not doing more than one upload at a time, and that further reduces the peak memory usage further (in my example, peak memory usage was 27mb, only 10mb over baseline), but recognize that you pay a serious performance penalty for that.
If using NSURLSession, if you use at upload task with the fromFile option rather than loading the asset into a NSData, even when doing 4 concurrently it used dramatically less memory, less than 1 mb over baseline:
I notice that you're using the Xcode gauges to analyze memory usage. I'd suggest using Instruments' Allocations tool (as shown above). The reason I suggest this is not only do I believe it to be more accurate, but more importantly, you seem to have some pattern of memory not falling completely back down to your baseline after performing some actions. This suggests that you might have some leaks. And only Instruments will help you identifying these leaks.
I'd suggest watching WWDC 2013 video Fixing Memory Issues or WWDC 2012 video iOS App Performance: Memory which illustrate (amongst other things) how to use Instruments to identify memory problems.
I notice that you're extracting the NSData from the UIImage objects. I don't know how you're getting the images, but if they're from your ALAssetsLibrary (or if you downloaded them from another resource), you might want to grab the original asset rather than loading it into a UIImage and then creating a NSData from that. If you use UIImageJPEGRepresentation, you invariably either (a) make the NSData larger than the original asset (making the memory issue worse); or (b) reduce the quality unnecessarily in an effort to make the NSData smaller. Plus you strip out much of the meta data.
If you have to use UIImageJPEGRepresentation (e.g. it's a programmatically created UIImage), then so be it, do what you have to do. But if you have access to the original digital asset (e.g. see https://stackoverflow.com/a/27709329/1271826 for ALAssetsLibrary example), then do that. Quality may be maximized without making the asset larger than it needs to be.
Bottom line, you first want to make sure that you don't have any memory leaks, and then you want to minimize how much you hold in memory at any given time (not using any NSData if you can).
Memory peaks are harmless. You are going to get them when you are performing heavy uploads and downloads. But it should go back to its normal level later on, which is the case in the image you have posted. So i feel it is harmless and obvious in your case.
You should only worry when your application uses memory and doesnt release it later...
If you are performing heavy upload.. have a look at answer in this thread.
Can you show your code ? Have you tried with NSURLSession ? I don't have problems with the memory if I upload photos with NSURLSession.
NSURL *url = [NSURL URLWithString:#"SERVER"];
NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration defaultSessionConfiguration];
NSURLSession *session = [NSURLSession sessionWithConfiguration:configuration];
for (UIImage *image in self.arrayImages) {
UIImage *imageToUPload = image;
NSData *data = UIImagePNGRepresentation(imageToUPload);
NSURLSessionUploadTask *task = [session uploadTaskWithRequest:[NSURLRequest requestWithURL:url] fromData:data completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
}];
[task resume];
}

Queue of AFHTTPRequestOperations creating Memory Buildup

I just updated to AFNetworking 2.0 and I am re-writing my code to download data & insert it into Core Data.
I download JSON data files (anywhere from 10-200mb files), write them to disk, then pass them off to background threads to process the data. Below is the code that downloads the JSON & write it to disk. If I just let this run (without even processing the data), the app uses up memory until it is killed.
I assume as the data is coming in, it is being stored in memory, but once I save to disk why would it stay in memory? Shouldn't the autorelease pool take care of this? I also set the responseData, and downloadData to nil. Is there something blatantly obvious that I am doing wrong here?
#autoreleasepool
{
for(int i = 1; i <= totalPages; i++)
{
NSString *path = ....
NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:path]];
AFHTTPRequestOperation *op = [[AFHTTPRequestOperation alloc] initWithRequest:request];
op.responseSerializer =[AFJSONResponseSerializer serializer];
[op setCompletionBlockWithSuccess:^(AFHTTPRequestOperation *operation, id responseObject)
{
//convert dictionary to data
NSData *downloadData = [NSKeyedArchiver archivedDataWithRootObject:responseObject];
//save to disk
NSError *saveError = nil;
if (![fileManager fileExistsAtPath:targetPath isDirectory:false])
{
[downloadData writeToFile:targetPath options:NSDataWritingAtomic error:&saveError];
if (saveError != nil)
{
NSLog(#"Download save failed! Error: %#", [saveError description]);
}
}
responseObject = nil;
downloadData = nil;
} failure:^(AFHTTPRequestOperation *operation, NSError *error) {
DLog(#"Error: %#", error);
}];
}
[mutableOperations addObject:op];
}
NSArray *operations = [AFURLConnectionOperation batchOfRequestOperations:mutableOperations progressBlock:^(NSUInteger numberOfFinishedOperations, NSUInteger totalNumberOfOperations) {
DLog(#"%lu of %lu complete", (unsigned long)numberOfFinishedOperations, (unsigned long)totalNumberOfOperations);
} completionBlock:^(NSArray *operations) {
DLog(#"All operations in batch complete");
}];
mutableOperations = nil;
[manager.operationQueue addOperations:operations waitUntilFinished:NO];
Thanks!
EDIT #1
Adding an #autoreleasepool within my complete block seemed to slow the memory usage a bit, but it still builds up and eventually crashes the app.
If your JSON files are really 10-200mb each, this would definitely cause memory problems, because this sort of request is going to load the responses in memory (rather than streaming them to persistent storage). Worse, because your using JSON, I think the problem is twice as bad, because you're going to be loading this into a dictionary/array, which also takes up memory. So, if you have four 100mb downloads going on, your peak memory usage could be of the order of magnitude of 800mb (100mb for the NSData plus ~100mb for the array/dictionary (possibly much larger), times four for the four concurrent requests). You could quickly run out of memory.
So, a couple of reactions:
When dealing with this volume of data, you'd want to pursue a streaming interface (a NSURLConnection or NSURLSessionDataTask where you write the data as it comes in, rather than holding it in memory; or use NSURLSessionDownloadTask which does this for you), one that writes the data directly to persistent storage (rather than trying to hold it in a NSData in RAM as it's being downloaded).
If you use NSURLSessionDownloadTask, this is really simple. If you need to support iOS versions prior to 7.0, I'm not sure if AFNetworking supports streaming of the responses directly to persistent storage. I'd wager you could write your own response serializer that does that, but I haven't tried it. I've always written my own NSURLConnectionDataDelegate methods that download directly to persistent storage (e.g. something like this).
You might not want to use JSON for this (because NSJSONSerialization will load the whole resource into memory, and then parse it to a NSArray/NSDictionary, also in memory), but rather use a format that lends itself to streamed parsing of the response (e.g. XML) and write a parser that stores the data to your data store (Core Data or SQLite) as it's being parsed, rather than trying to load the whole thing in RAM.
Note, even NSXMLParser is surprisingly memory inefficient (see this question). In the XMLPerformance sample, Apple demonstrates how you can use the more cumbersome LibXML2 to minimize the memory footprint of your XML parser.
By the way, I don't know if your JSON includes any binary data that you have encoded (e.g. base 64 or the like), but if so, you might want to consider a binary transfer format that doesn't have to do this conversion. Using base-64 or uuencode or whatever can increase your bandwidth and memory requirements. (If you're not dealing with binary data that has been encoded, then ignore this point.)
As an aside, you might want to use Reachability to confirm the user's connection type (Wifi vs cellular), because it is considered bad form to download that much data over cellular (at least not without the user's permission), not only because of speed issues, but also the risk of using up an excessive portion of their carrier's monthly data plan. I've even heard that Apple historically rejected apps that tried to download too much data over cellular.

Caching on AFNetworking 2.0

So here's the deal. I recently started using AFNetworking to download a few files on start using the following code:
NSMutableURLRequest* rq = [api requestWithMethod:#"GET" path:#"YOUR/URL/TO/FILE" parameters:nil];
AFHTTPRequestOperation *operation = [[[AFHTTPRequestOperation alloc] initWithRequest:rq] autorelease];
NSString* path=[#"/PATH/TO/APP" stringByAppendingPathComponent: imageNameToDisk];
operation.outputStream = [NSOutputStream outputStreamToFileAtPath:path append:NO];
[operation setCompletionBlockWithSuccess:^(AFHTTPRequestOperation *operation, id responseObject) {
NSLog(#"SUCCCESSFULL IMG RETRIEVE to %#!",path)
} failure:^(AFHTTPRequestOperation *operation, NSError *error) {
// Deal with failure
}];
With my path actually plugged into the path variable (Sorry, not on the right computer right now to actually copy pasta the text, but it's exactly the same as the above with different path stuffs)
And everything is working great! I'm getting the file successfully downloaded and everything. My current issue is that I'm trying to get caching to work, but I'm having a lot of difficulties. Basically, I'm not sure what I actually have to do client side as of AFNetworking 2.0. Do I still need to set up the NSURlCache? Do I need to set the caching type header on the request operation differently? I thought that maybe it was just entirely built in, but I'm receiving a status of 200 every time the code runs, even with no changes in the file. If I do have to use the NSUrlCache, do I have to manually save the e-tag on the success blocks requestoperation myself and then feed that back in? Any help on how to progress would be much appreciated. Thanks guys!
AFNetworking uses NSURLCache for caching by default. From the FAQ:
AFNetworking takes advantage of the caching functionality already provided by NSURLCache and any of its subclasses. So long as your NSURLRequest objects have the correct cache policy, and your server response contains a valid Cache-Control header, responses will be automatically cached for subsequent requests.
Note that this mechanism caches NSData, so every time you retrieve from this cache you need to perform a somewhat expensive NSData-to-UIImage operation. This is not performant enough for rapid display, for example if you're showing images in a UITableView or UICollectionView.
If this is the case, look at UIImageView+AFNetworking, which adds downloads and caching of UIImage objects to UIImageView. For some applications you can just use the out-of-the-box implementation, but it is very basic. You may want to look at the source code for this class (it's not very long) and use it as a starting point for your own caching mechanism.

How to download many images with AFNetworking?

I am making an app..
I need to download many pictures from a server, but I don't know how to do :(
I try to use this code:
UIImageView *image=(UIImageView *)[cell viewWithTag:100];
dispatch_async(dispatch_get_global_queue(0,0), ^{
NSData * data = [[NSData alloc] initWithContentsOfURL: [NSURL URLWithString:#"FileName"]];
if ( data == nil )
return;
dispatch_async(dispatch_get_main_queue(), ^{
image.image=[UIImage imageWithData:data];
});
data=nil;
});
This code is slow... I need to do it more quickly... I think AFNetworking is the best option, isn't it ?
You have several options, one of the AFNetworking. However, if you go with AFNetworking, you should use the UIImageView+AFNetworking class, inside the UIKit+AFNetworking folder.
Here's the documentation for it: http://cocoadocs.org/docsets/AFNetworking/2.0.1/Categories/UIImageView+AFNetworking.html
Another great option is SDWebImage which gives you more advanced control over caching and image handling, such as processing images before they're displayed, handling your own caching, etc.
AFNetworking would be perfect for your needs. It has its own UIImageView category that handles asynchronous image loading very smoothly. Give it a try.
Like previous answers have suggested, AFNetworking is a great way to go.
I too, though, want to suggest using SDWebImage. There are a lot of nice features in there, such as cool cache handling and image decompression.
For starters, try doing something like
[self.image setImageWithURL:[NSURL URLWithString:imagePath]
placeholderImage:[UIImage imageNamed:#"placeholder"] completed:^(UIImage *image, NSError *error, SDImageCacheType cacheType) {
[image setImage:image];
}];
When it comes to speeding up the code, I am not sure why your code is slow in the first place since you are already doing it asynchronously.
AFNetworking is a fantastic framework and will make easy for you to do async image request.
However using AFNetworking will not speed up your code.
Probably will make your code slow, but the difference should be irrelevant, only if your images are too larger and the time to decode the image are high (I don't think this is the problem).

Cannot download images from remote server using SDWebImageManager on iOS

Want to achieve image gallery through UICollectionView. I have a set of image url's in an array, say around 20-30 url's. Using SDWebImageManager to download images and cache it and display on the collection view.
See my code below:
for(int i=0;i<[imagePath count];i++) {
[manager downloadWithURL:[NSURL URLWithString:[imagePath objectAtIndex:i]] options:0 progress:^(NSUInteger receivedSize, long long expectedSize) {
} completed:^(UIImage *image, NSError *error, SDImageCacheType cacheType, BOOL finished) {
if(image){
NSLog(#"SUCCESS");
NSString *localKey = [NSString stringWithFormat:#"Item-%d", i];
NSLog(#"%#",localKey);
[[SDImageCache sharedImageCache] storeImage:image forKey:localKey];
}
}];
}
All it does is, display the first image that has the url link indexed 0 in my imagePath array and the rest of the cell's remain blank. When tried to print it just displays SUCCESS once and Item-0. I guess its not going any further. It downloads just one image(first url's image in the array). Please help me with this. I am breaking my head on this from a long time. Not sure if i am on right track. Or please do suggest me other alternatives of achieving image gallery through multiple url's stored in an array.
I have to wonder what your cellForItemAtIndexPath is doing. If it's trying to retrieve the images from the cache directly, you might see precisely the behavior you describe (where the above code is running asynchronously and thus the images are not yet downloaded by the time cellForItemAtIndexPath tries to retrieve them from the cache).
Personally, I'd suggest that you do not even bother trying to populate your cache in advance. Just have cellForItemAtIndexPath just use the UIImageView+WebCache category method setImageWithURL and remove the for loop in your question altogether. It will automatically fill the cache for you and the problem of missing images will probably go away.
If you're having troubles retrieving your images, you should use one of the SDWebImage methods that passes a NSError object back to the block. For example:
[cell.imageView setImageWithURL:url completed:^(UIImage *image, NSError *error, SDImageCacheType cacheType) {
if (error)
NSLog(#"%s: setImageWithURL error: %#", __FUNCTION__, error);
}];
If the URL address is not valid, you may see a 404 error.
If you're determined to use your for loop, a couple of other thoughts:
BTW, you are logging only if image is non-nil. You might want an else statement for your if statement fails (e.g. perhaps the error object can tell you something interesting). If an API provides an error object, you should avail yourself of it.
It's a little curious that are taking a downloaded image and adding it to the cache with another key; now you have two copies of the image in your cache, which is a wasteful use of space. Once you've downloaded an image, it's already in the cache, and if you try to retrieve the image again using the same URL, it will pull it from the cache. There's no point in creating another entry in the cache with your own key.
If you're not going to use the progress block, you can just pass nil for that parameter.

Resources