Load large UIImages asynchronously - ios

I have an NSManagedObject which has pictures that are stored somewhere like /var/mobile/Applications/.../.../uniqueIDforNSMO/Pictures/
I have no problem getting these pictures off the disk by finding them based on the NSMO's uniqueID, I have a DataController that will pull them for me, and that is very performant. But I run into issues when I try to add these pictures to a view, the UI becomes blocked. I suspect the problem is that these images are very high resolution (they are images from the iPad camera roll).
Displaying 5 images takes about 3 seconds, leaving the UI blocked. Displaying even just 2 images blocks the damn UI. Here's how I add them, on a background thread:
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^{
UIImage *image = [self.photos objectAtIndex:index];
dispatch_async(dispatch_get_main_queue(), ^{
cell.imageView.image = image;
});
});
If I set the imageView.image without dispatching it takes even longer.
Could the problem be that the images are so large, while the cell's imageViews are only 150x150? Or is it because I only have a reference to the image at first, so the realization of the image is the slow part?
Can anyone suggest anything here? I have tried resizing the images before they are returned in the array and that did not help.

The problem is that the images are being lazily loaded - UIImage only loads the image into memory when it needs to be drawn. What you can do is eagerly load the image on a background thread:
Create a CGBitmapContextRef using UIGraphicsBeginImageContext
Draw the image into the context
Get a new image from the context using UIGraphicsGetImageFromCurrentImageContext
Pass the image back to the main thread

As they are high-resolution images, I would suggest that you should make persistent thumbnails cache for that. Generating thumbnail real-time every time would waste too much CPU time unnecessarily.

Related

Loading large images faster in UIImageView

Is there an accepted way, or updated way, to more quickly load images into a UIImageView?
My scenario: A collection view, with a large UIImageView. Only 1 cell is displayed at a time. I have implemented NSCache and Prefetching on the collection view already. Performance on scrolling has a pause partway through. The images I am using are "relatively" large, in order to accomodate both an iPad and iPhone layout. For example, images are 1600x1600px, RGB, PNG. (from 2-5MB compressed, ~10MB uncompressed, stored locally in the app)
Once the images are loaded, scrolling back and forth is usually OK then, ~60fps visually. But on first load they are ALWAYS jittery. BUT if I make the images physically smaller, such as 800x800 then they load quickly and I can not see a jitter on scroll. So I am dealing with an image size vs drawing speed issue. Same issue seen on a 5s as on an iPhone X.
The same performance hit happens with [UIImage imageNamed:imageName] or [UIImage imageWithContentsOfFile:imagePath]
I am reading how UIImages are decompressed before actually being drawn, and if the system has to draw a subsampled image, it can significantly affect main thread performance. I've done a little Instruments testing and confirmed that it appear that none of my code is actually slow, the image drawing of a PNG is slow.
Is there a newer way to do something similar to the content in links below, IE draw an image in a CGContext and hope it stays cached?
https://www.cocoanetics.com/2011/10/avoiding-image-decompression-sickness/
- (void)decompressImage:(UIImage *)image
{
UIGraphicsBeginImageContext(CGSizeMake(1, 1));
[image drawAtPoint:CGPointZero];
UIGraphicsEndImageContext();
}
https://gist.github.com/steipete/1144242
this seems like real overkill for me:
https://github.com/path/FastImageCache
I have implemented NSCache and Prefetching on the collection view already.
Good, but
(1) you should not NSCache images
(2) you should downsize the image to the max size needed for display
(3) you should not be doing anything time-consuming in itemForRowAt: (you didn't show yours) — you have only a couple of milliseconds to produce the cell and get out
(4) if you can't provide the image in time, provide a placeholder and get out of itemForRowAt:; you can always reload later when you have the real image
(5) do all time-consuming work off the main thread (includes converting to UIImage and drawing UIImage to downsize it)
(6) measure measure measure! this is why we have Instruments; do not guess where the problem is

Quick way to load images

Right now I'm loading images view a file url and its taking a long time. I've even put it on the main thread as a high priority but its still slow. This is actually in a loop for like 6 images. My question is:
Is there a faster way to load images to a view than this? Like an alternative to a file url?
//check the filetype
if ([fileType isEqual: #"image"])
{
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
//get image
NSURL *imageFileUrl = [[NSURL alloc] initWithString:file.url];
NSData *imageData = [NSData dataWithContentsOfURL:imageFileUrl];
dispatch_async(dispatch_get_main_queue(), ^{
imageView.image = [UIImage imageWithData:imageData];
});
});
}
My images are at a quality of this preset and the size is the size of the iphone screen whether that be a 5,6,or 6 plus
self.camera = [[LLSimpleCamera alloc] initWithQuality:AVCaptureSessionPreset1280x720
position:CameraPositionBack
videoEnabled:YES];
thanks,
While there isn't any shortcut to loading the images there are things you can do to help.
Initially only load the image that are visible, after that load any that may be visible soon.
If the size of the image is larger than the view create a smaller image, perhaps a couple for different screen resolution devices.
Create a low resolution image versions, load them first to get something up for the user and then load the full resolution images replacing the low resolution version..
Please, Add the sizes of the images and the sizes of the UIImageView and screen resolution to the question.
What can I suggest you that,
Save the duplicate copy of each image in lower resolution/Size on
server.
Load the duplicate image on first load.
When user particularly views an image, load original image then.

CollectionView Cell taking more time to load after image resized

As I have received memory warning because I have large images from server(Around 10mb each). I resized all images and then filled it into collection view. My issue is when I scroll collection view it got stuck and taking more time to load. I am using following code.Item.b_imageUrl is path of server. I am using NSDictionary.
UIImage * image = [UIImage imageWithData:[NSData dataWithContentsOfURL:[NSURL URLWithString:item.b_imageUrl]]];
CGSize sacleSize = CGSizeMake(100, 100);
UIGraphicsBeginImageContextWithOptions(sacleSize, NO, 0.0);
[image drawInRect:CGRectMake(0, 0, sacleSize.width, sacleSize.height)];
UIImage * resizedImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
[cell.imgV_b_image setImage:resizedImage];
Also when I checked it in instruments allocation tool my overall bytes going crazy(700mb-its very big)
How can I scroll smoothly with no memory warning...? Thanks for any help.
Ideally you want your server to give you resized images but if that's not possible here are my ideas:
1. Refactor
Your image resizing code does not belong in a view controller. Ideally you should be downloading these images from a network service and you could either add custom image resizing requests to the network (similar to how AFNetworking 1.0 had AFImageRequestOperation). This way all your controller needs to do is ask for an image of a particular size and the network service will queue the image operation and return a resized image.
2. Image resizing
Another concern of mine is where you are resizing your images. It looks to me like this is happening in the main thread. Don't. When the image is downloaded you should resize the image to the background thread and let the controller dispatch the flow to the main thread so that the image can be binded to the image view.
3. Image persistence
You should also be caching these images somewhere on the device. It's a poor user experience to have to always re-download what you've already seen. Perhaps somewhere inside the NSCachesDirectory()
4. Background thread to collection view
Don't forget that while the images are downloading the user can be anywhere in the collection view. You should take it upon yourself to either cancel image requests that have been scrolled off screen or at least use a LIFO queue so that the most recently viewed image requests get priority. So when your image finishes you can query -[UICollectionView cellForItemAtIndexPath:] to see if the cell is still on screen

Preloading images ios app

I have an app with 150 local images (about 500kb each). I have loaded them all into an array like this:
allPics = [[NSMutableArray alloc] init];
//NSString *imagePath;
NSArray *result = [database performQuery:#"SELECT image_path FROM validWords order by valid_word"];
for (NSArray *row in result) {
NSString *temp = [row objectAtIndex:0];
NSLog(#"%#", temp);
//imagePath = temp;
UIImage *newImage = [UIImage imageNamed:temp];
[allPics addObject:newImage];
}
When I set my UIImageView later to one of these pics, it hangs my interface up for a second, due to lazy loading from what I have read. I tried to prerender them, but that spiked my memory usage to over 3gb before it got a third of the way through my images. Should I be looking to use a background thread to render the image when I need it? When I reduced the image total to 4, once all 4 were rendered once, the transitions between them was seamless.
I appreciate any and all tips and solutions!
Yes, I would suggest a background thread and paging. If the user is looking at image 7, you should load images, say, 5,6,8 and 9. If the user then moves onto image 8, you can discard image 5 and lazy load image 10. This way the user sjhould be able to move through your images without a significant memory or performance overhead.
You can then also add heuristics such as 'if the user is paging through the images very quickly, don't load any images until they slow down',
Another tip is to store a very low resolution version of the image (say, a 50kb version) and store that at a different path. Then you can show the thumbnail images to the user and only lazy load in the high res image if the user stops on that image for a period of time.
Finally, be careful when you talk about image sizes. Is the 500KB compressed or uncompressed? If it is a 500KB compressed JPeg, the actual image on the device could be vastly bigger. A jpg with fairly uniform colour and a reasonably high level of compression can be very small on disk, but decompressed it could be a massive image. This could be another source of the lag you experience.

How to find UIImage Bottleneck

I have an app that uses UIImage objects. Up to this point, I've been using image objects initialized using something like this:
UIImage *image = [UIImage imageNamed:imageName];
using an image in my app bundle. I've been adding functionality to allow users to use imagery from the camera or their library using UIImagePickerController. These images, obviously, can't be in my app bundle, so I initialize the UIImage object a different way:
UIImage *image = [UIImage imageWithContentsOfFile:pathToFile];
This is done after first resizing the image to a size similar to the other files in my app bundle, in both pixel dimensions and total bytes, both using Jpeg format (interestingly, PNG was much slower, even for the same file size). In other words, the file pointed to by pathToFile is a file of similar size as an image in the bundle (pixel dimensions match, and compression was chosen so byte count was similar).
The app goes through a loop making small pieces from the original image, among other things that are not relevant to this post. My issue is that going through the loop using an image created the second way takes much longer than using an image created the first way.
I realize the first method caches the image, but I don't think that's relevant, unless I'm not understanding how the caching works. If it is the relevant factor, how can I add caching to the second method?
The relevant portion of code that is causing the bottleneck is this:
[image drawInRect:self.imageSquare];
Here, self is a subclass of UIImageView. Its property imageSquare is simply a CGRect defining what gets drawn. This portion is the same for both methods. So why is the second method so much slower with similar sized UIImage object?
Is there something I could be doing differently to optimize this process?
EDIT: I change access to the image in the bundle to imageWithContentsOfFile and the time to perform the loop changed from about 4 seconds to just over a minute. So it's looking like I need to find some way to do caching like imageNamed does, but with non-bundled files.
UIImage imageNamed doesn't simply cache the image. It caches an uncompressed image. The extra time spent was not caused by reading from local storage to RAM but by decompressing the image.
The solution was to create a new uncompressed UIImage object and use it for the time sensitive portion of the code. The uncompressed object is discarded when that section of code is complete. For completeness, here is a copy of the class method to return an uncompressed UIImage object from a compressed one, thanks to another thread. Note that this assumes data is in CGImage. That is not always true for UIImage objects.
+(UIImage *)decompressedImage:(UIImage *)compressedImage
{
CGImageRef originalImage = compressedImage.CGImage;
CFDataRef imageData = CGDataProviderCopyData(
CGImageGetDataProvider(originalImage));
CGDataProviderRef imageDataProvider = CGDataProviderCreateWithCFData(imageData);
CFRelease(imageData);
CGImageRef image = CGImageCreate(
CGImageGetWidth(originalImage),
CGImageGetHeight(originalImage),
CGImageGetBitsPerComponent(originalImage),
CGImageGetBitsPerPixel(originalImage),
CGImageGetBytesPerRow(originalImage),
CGImageGetColorSpace(originalImage),
CGImageGetBitmapInfo(originalImage),
imageDataProvider,
CGImageGetDecode(originalImage),
CGImageGetShouldInterpolate(originalImage),
CGImageGetRenderingIntent(originalImage));
CGDataProviderRelease(imageDataProvider);
UIImage *decompressedImage = [UIImage imageWithCGImage:image];
CGImageRelease(image);
return decompressedImage;
}

Resources