可以将文章内容翻译成中文,广告屏蔽插件可能会导致该功能失效(如失效,请关闭广告屏蔽插件后再试):
问题:
In my iOS app I have UICollectionView
that displays around 1200 small (35x35 points) images. The images are stored in application bundle.
I am correctly reusing UICollectionViewCell
s but still have performance problems that vary depending on how I address image loading:
My app is application extension and those have limited memory (40 MB in this case). Putting all 1200 images to Assets catalog and loading them using UIImage(named: "imageName")
resulted in memory crashes - system cached images which filled up the memory. At some point the app needs to allocate bigger portions of memory but these were not available because of cached images. Instead of triggering memory warning and cleaning the cache, operating system just killed the app.
I changed the approach to avoid images caching. I put images to my project (not to asssets catalog) as png files and I am loading them using NSBundle.mainBundle().pathForResource("imageName", ofType: "png")
now. The app no longer crashes due to memory error but loading of single image takes much longer and fast scrolling is lagging even on the newest iPhones.
I have full controll over the images and can transform them for example to .jpeg or optimize them (I already tried ImageOptim and some other options without success).
How can I resolve both these performance problems at once?
EDIT 1:
I also tried loading images in background thread. This is code from my subclass of UICollectionViewCell
:
private func loadImageNamed(name: String) {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), { [weak self] () in
let image = bundle.pathForResource(name, ofType: "png")?.CGImage
if name == self?.displayedImageName {
dispatch_async(dispatch_get_main_queue(), {
if name == self?.displayedImageName {
self?.contentView.layer.contents = image
}
})
}
})
}
This makes scrolling smooth without consuming additional memory for caching but when scrolling to some location programatically (for example when UICollectionView
scrolls to top) it causes another problem: During scrolling animation the images do not update (scroll is too fast for them to load) and after scrolling is finished it takes wrong images are displayed for fraction of second - and one after another replaced with correct ones. This is very disturbing visually.
EDIT 2:
I cannot group small images into bigger composed images and display those as suggested by this answer.
Reasons:
- Consider different screen sizes and orientations. There would have to be precomposed images for each of them which would make the app download huge.
- The small images can by displayed in different order, some of them might be hidden in some situation. I surrely cannot have precomposed images for each possible combinations and orders.
回答1:
I can propose alternative way that probably can solve your problem:
Consider to render blocks of images to single composed images. Such large image should cover size of app window. For user it will be looked like collection of small images, but technically it will be table of large images.
Your's current layout:
| | | |
| cell | cell | cell | -> cells outside of screen
| | | |
************************
*| | | |*
*| cell | cell | cell |* -> cells displayed on screen
*| | | |*
*----------------------*
*| | | |*
*| cell | cell | cell |* -> cells displayed on screen
*| | | |*
*----------------------*
*| | | |*
*| cell | cell | cell |* -> cells displayed on screen
*| | | |*
************************
| | | |
| cell | cell | cell | -> cells outside of screen
| | | |
Proposed layout:
| |
| cell with |
| composed image | -> cell outside of screen
| |
************************
*| |*
*| |*
*| |*
*| |*
*| cell with |*
*| composed image |* -> cell displayed on screen
*| |*
*| |*
*| |*
*| |*
*| |*
************************
| |
| cell with |
| composed image | -> cell outside of screen
| |
Ideally if you pre-render such composed images and put them to project at build time, but you can also render them in runtime. For sure first variant will work much more faster. But in any case single large image costs less memory then separate pieces of that image.
If you have possibility to pre-render them then use JPEG format. In this case probably your first solution (load images with [UIImage imageNamed:]
on main thread) will work good because less memory used, layout is much more simpler.
If you have to render them in runtime then you will need use your current solution (do work in background), and you will still see that image misplacements when quick animation happens, but in this case it will be single misplacement (one image covers window frame), so it should look better.
If you need to know what image (original small image 35x35) user clicked, you can use UITapGestureRecognizer
attached to cell. When gesture is recognized you can use locationInView:
method to calculate correct index of small image.
I can't say that it 100% resolves your issue, but it makes sense to try.
回答2:
Change from PNG to JPEG will not help saving the memory, because when you load an image from file to memory, it's extracted from the compressed data to uncompressed bytes.
And for the performance issue, I would recommend that you load the image asynchronously and update the view by using delegate/block. And keep some images in memory(but not all of them, let's say 100)
Hope this helps!
回答3:
- There is no need to fetch image from document directory every time the cell appears.
- Once you fetch the image you can save same in NSCache, next time you just have to get image from NSCache instead of fetching it again from document directory.
- Create an object for NSCache objCache;
In your cellForItemAtIndexPath, just write down
UIImage *cachedImage = [objCache objectForKey:arr_PathFromDocumentDirectory[indexPath.row]];
if (cachedImage) {
imgView_Cell.image = cachedImage;
} else {
dispatch_queue_t q = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0ul);
dispatch_async(q, ^{
/* Fetch the image from the Document directory... */
[self downloadImageWithURL:arr_PathFromDocument[indexPath.row] completionBlock:^(BOOL succeeded, CGImageRef data, NSString* path) {
if (succeeded) {
dispatch_async(dispatch_get_main_queue(), ^{
UIImage *img =[UIImage imageWithCGImage:data];
imgView_Cell.image = img;
[objCache setObject:img forKey::arr_PathFromDocument[indexPath.row]];
});
}
}];
});
}
- Once you fetch the image, set it in NSCache with path. Next time it is going to check if already downloaded then set from cache only.
If you need any assistance, please let me know.
Thanks!
回答4:
You should create an queue to load image asynchronously. The best choice is a last in first out queue. You can have look at this LIFOOperationQueue.
One important thing is prevent showing wrong image that need to be handle separately. To do that, when you create an operation to load the image, give it the current indexPath as identifier. And then in callback function, check if the given indexPath is visible to update the view
if (self.tableView.visibleIndexPath().containsObject(indexPath) {
cell.imageView.image = img;
}
You should also need to custom the LIFOOperationQueue to have maximum number of task in queue so that it can remove unnecessary task. It is good to set the maximum number of task is 1.5 * numberOfVisibleCell
.
One last thing, you should create load image operation in willDisplayCell
instead of cellForRowAtIndexPath
回答5:
Couple of things you can do for this,
- You should load only those images in which are reacquired and unload them when not reacquired this will make your app use less
memory.
Other thing when you load image in background decode it in background thread before setting it to image view... by default
image will decode when you set and that will happen usually on main
thread, this will make scrolling smooth, you can decode an image by code below.
static UIImage *decodedImageFromData:(NSData *data, BOOL isJPEG)
{
// Create dataProider object from provided data
CGDataProviderRef dataProvider = CGDataProviderCreateWithCFData((__bridge CFDataRef)data);
// Create CGImageRef from dataProviderObject
CGImageRef newImage = (isJPEG) ? CGImageCreateWithJPEGDataProvider(dataProvider, NULL, NO, kCGRenderingIntentDefault) : CGImageCreateWithPNGDataProvider(dataProvider, NULL, NO, kCGRenderingIntentDefault);
// Get width and height info from image
const size_t width = CGImageGetWidth(newImage);
const size_t height = CGImageGetHeight(newImage);
// Create colorspace
const CGColorSpaceRef colorspace = CGColorSpaceCreateDeviceRGB();
// Create context object, provide data ref if wean to store other wise NULL
// Set width and height of image
// Number of bits per comoponent
// Number of bytes per row
// set color space
// And tell what CGBitMapInfo
const CGContextRef context = CGBitmapContextCreate( NULL,width, height,8, width * 4, colorspace, (CGBitmapInfo)kCGImageAlphaPremultipliedLast);
//Now we can draw image
CGContextDrawImage(context, CGRectMake(0, 0, width, height), newImage);
// Get CGImage from drwan context
CGImageRef drawnImage = CGBitmapContextCreateImage(context);
CGContextRelease(context);
CGColorSpaceRelease(colorspace);
// Create UIImage from CGImage
UIImage *image = [UIImage imageWithCGImage:drawnImage];
CGDataProviderRelease(dataProvider);
CGImageRelease(newImage);
CGImageRelease(drawnImage);
return image;
}
回答6:
Check this tutorial:
http://www.raywenderlich.com/86365/asyncdisplaykit-tutorial-achieving-60-fps-scrolling
It uses AsyncDisplayKit to load images on a background thread.
回答7:
To resolve issues that are described in EDIT 1
you should override prepareForReuse
method in the UICollectionViewCell
subclass and reset your layer's content to nil:
- (void) prepareForReuse
{
self.contentView.layer.contents = nil;
}
To load your image in the background you can use the next:
- (void)collectionView:(UICollectionView *)collectionView willDisplayCell:(UICollectionViewCell *)cell forItemAtIndexPath:(NSIndexPath *)indexPath
{
NSString* imagePath = #image_path#;
weakself;
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
//load image
UIImage *image = [UIImage imageWithContentsOfFile:imagePath];
//redraw image using device context
UIGraphicsBeginImageContextWithOptions(image.size, YES, 0);
[image drawAtPoint:CGPointZero];
image = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
dispatch_async(dispatch_get_main_queue(), ^{
strongself;
if ([[self.collectionView indexPathsForVisibleItems] indexOfObject:indexPath] != NSNotFound) {
cell.contentView.layer.contents = (__bridge id)(image.CGImage);
} else {
// cache image via NSCache with countLimit or custom approach
}
});
});
}
How to improve:
- Use
NSCache
or custom caching algorithm in order not to load
images while scrolling all the time.
- Use the proper file format. The JPEG decompression works faster for images like photographs. Use PNG for images with flat areas of
color, sharp lines, etc.