Poor UICollectionView Scrolling Performance With UIImage - objective-c

I have a UICollectionView in my app, and each cell is a UIImageView and some text labels. The problem is that when I have the UIImageViews displaying their images, the scrolling performance is terrible. It's nowhere near as smooth as the scrolling experience of a UITableView or even the same UICollectionView without the UIImageView.
I found this question from a few months ago, and it seems like an answer was found, but it's written in RubyMotion, and I don't understand that. I tried to see how to convert it to Xcode, but since I have never used NSCache either, it's a little hard to. The poster there also pointed to here about implementing something in addition to their solution, but I'm not sure where to put that code either. Possibly because I don't understand the code from the first question.
Would someone be able to help translate this into Xcode?
def viewDidLoad
...
#images_cache = NSCache.alloc.init
#image_loading_queue = NSOperationQueue.alloc.init
#image_loading_queue.maxConcurrentOperationCount = 3
...
end
def collectionView(collection_view, cellForItemAtIndexPath: index_path)
cell = collection_view.dequeueReusableCellWithReuseIdentifier(CELL_IDENTIFIER, forIndexPath: index_path)
image_path = #image_paths[index_path.row]
if cached_image = #images_cache.objectForKey(image_path)
cell.image = cached_image
else
#operation = NSBlockOperation.blockOperationWithBlock lambda {
#image = UIImage.imageWithContentsOfFile(image_path)
Dispatch::Queue.main.async do
return unless collectionView.indexPathsForVisibleItems.containsObject(index_path)
#images_cache.setObject(#image, forKey: image_path)
cell = collectionView.cellForItemAtIndexPath(index_path)
cell.image = #image
end
}
#image_loading_queue.addOperation(#operation)
end
end
Here is the code from the second question that the asker of the first question said solved the problem:
UIImage *productImage = [[UIImage alloc] initWithContentsOfFile:path];
CGSize imageSize = productImage.size;
UIGraphicsBeginImageContext(imageSize);
[productImage drawInRect:CGRectMake(0, 0, imageSize.width, imageSize.height)];
productImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
Again, I'm not sure how/where to implement that.
Many thanks.

Here's the pattern I follow. Always load asynch and cache the result. Make no assumption about the state of the view when the asynch load finishes. I have a class that simplifies the loads as follows:
//
// ImageRequest.h
// This class keeps track of in-flight instances, creating only one NSURLConnection for
// multiple matching requests (requests with matching URLs). It also uses NSCache to cache
// retrieved images. Set the cache count limit with the macro in this file.
#define kIMAGE_REQUEST_CACHE_LIMIT 100
typedef void (^CompletionBlock) (UIImage *, NSError *);
#interface ImageRequest : NSMutableURLRequest
- (UIImage *)cachedResult;
- (void)startWithCompletion:(CompletionBlock)completion;
#end
//
// ImageRequest.m
#import "ImageRequest.h"
NSMutableDictionary *_inflight;
NSCache *_imageCache;
#implementation ImageRequest
- (NSMutableDictionary *)inflight {
if (!_inflight) {
_inflight = [NSMutableDictionary dictionary];
}
return _inflight;
}
- (NSCache *)imageCache {
if (!_imageCache) {
_imageCache = [[NSCache alloc] init];
_imageCache.countLimit = kIMAGE_REQUEST_CACHE_LIMIT;
}
return _imageCache;
}
- (UIImage *)cachedResult {
return [self.imageCache objectForKey:self];
}
- (void)startWithCompletion:(CompletionBlock)completion {
UIImage *image = [self cachedResult];
if (image) return completion(image, nil);
NSMutableArray *inflightCompletionBlocks = [self.inflight objectForKey:self];
if (inflightCompletionBlocks) {
// a matching request is in flight, keep the completion block to run when we're finished
[inflightCompletionBlocks addObject:completion];
} else {
[self.inflight setObject:[NSMutableArray arrayWithObject:completion] forKey:self];
[NSURLConnection sendAsynchronousRequest:self queue:[NSOperationQueue mainQueue] completionHandler:^(NSURLResponse *response, NSData *data, NSError *error) {
if (!error) {
// build an image, cache the result and run completion blocks for this request
UIImage *image = [UIImage imageWithData:data];
[self.imageCache setObject:image forKey:self];
id value = [self.inflight objectForKey:self];
[self.inflight removeObjectForKey:self];
for (CompletionBlock block in (NSMutableArray *)value) {
block(image, nil);
}
} else {
[self.inflight removeObjectForKey:self];
completion(nil, error);
}
}];
}
}
#end
Now the cell (collection or table) update is fairly simple:
-(UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath {
UICollectionViewCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:#"Cell" forIndexPath:indexPath];
NSURL *url = [NSURL URLWithString:#"http:// some url from your model"];
// note that this can be a web url or file url
ImageRequest *request = [[ImageRequest alloc] initWithURL:url];
UIImage *image = [request cachedResult];
if (image) {
UIImageView *imageView = (UIImageView *)[cell viewWithTag:127];
imageView.image = image;
} else {
[request startWithCompletion:^(UIImage *image, NSError *error) {
if (image && [[collectionView indexPathsForVisibleItems] containsObject:indexPath]) {
[collectionView reloadItemsAtIndexPaths:#[indexPath]];
}
}];
}
return cell;
}

In general bad scrolling behaviour for UICollectionViews or UITableViews happens because the cells are dequeued and constructed in the main thread by iOS. There is little freedom to precache cells or construct them in a background thread, instead they are dequeued and constructed as you scroll blocking the UI. (Personally I find this bad design by Apple all though it does simplify matters because you don't have to be aware about potential threading issues. I think they should have given a hook though to provide a custom implementation for a UICollectionViewCell/UITableViewCell pool which can handle dequeuing/reusing of cells.)
The most important causes for performance decrease are indeed related to image data and (in decreasing order of magnitude) are in my experience:
Synchronous calls to download image data: always do this asynchronously and call [UIImageView setImage:] with the constructed image when ready in the main thread
Synchronous calls to construct images from data on the local file system, or from other serialized data: do this asynchronously as well. (e.g. [UIImage imageWithContentsOfFile:], [UIImage imageWithData:], etc).
Calls to [UIImage imageNamed:]: the first time this image is loaded it is served from the file system. You may want to precache images (just by loading [UIImage imageNamed:] before the cell is actually constructed such that they can be served from memory immediately instead.
Calling [UIImageView setImage:] is not the fastest method either, but can often not be avoided unless you use static images. For static images it is sometimes faster to used different image views which you set to hidden or not depending on whether they should be displayed instead of changing the image on the same image view.
First time a cell is dequeued it is either loaded from a Nib or constructed with alloc-init and some initial layout or properties are set (probably also images if you used them). This causes bad scrolling behaviour the first time a cell is used.
Because I am very picky about smooth scrolling (even if it's only the first time a cell is used) I constructed a whole framework to precache cells by subclassing UINib (this is basically the only hook you get into the dequeuing process used by iOS). But that may be beyond your needs.

I had issues about UICollectionView scrolling.
What worked (almost) like a charm for me: I populated the cells with png thumbnails 90x90. I say almost because the first complete scroll is not so smooth, but never crashed anymore.
In my case, the cell size is 90x90.
I had many original png sizes before, and it was very choppy when png original size was greater than ~1000x1000 (many crashes on first scroll).
So, I select 90x90 (or the like) on the UICollectionView and display the original png's (no matter the size). hope it may help others.

Related

PHImageManager requestImageForAsset returns a non-nil but incorrect result

I have a page view controller which prepares multiple view controllers (and shows one at a time of course). Each view controller loads corresponding asset based on asset's localIdentifier. It works fine most of the time. But, if it tries to load an asset that has been deleted from Camera Roll or Synced albums from Mac, the view controller dismisses automatically. The reason the asset ends up in the array of local identifiers in the first place is the asset for some reason remains in the moments tab in Photos app (but again, it doesn't exist in Camera Roll or the synced albums).
// Get the image
PHCachingImageManager *imageManager = [[PHCachingImageManager alloc] init];
CGSize targetSize = CGSizeZero;
if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPhone) {
// iPhone
targetSize = CGSizeMake(actualSizedImageRect.size.width,actualSizedImageRect.size.height);
} else {
// iPad
targetSize = CGSizeMake(2000,2000);
}
PHImageRequestOptions *options = [[PHImageRequestOptions alloc]init];
options.deliveryMode = PHImageRequestOptionsDeliveryModeOpportunistic;
options.resizeMode = PHImageRequestOptionsResizeModeNone;
options.version = PHImageRequestOptionsVersionCurrent;
options.progressHandler = ^(double progress, NSError *error, BOOL *stop, NSDictionary *dictionary) {
dispatch_async(dispatch_get_main_queue(), ^{
});
};
[imageManager requestImageForAsset:self.asset
targetSize:targetSize
contentMode:PHImageContentModeAspectFit
options:options
resultHandler:^(UIImage *result, NSDictionary *info) {
dispatch_async(dispatch_get_main_queue(), ^{
[self.assetImageView setImage:result]; //iwashere memo when a photo that no longer exists in albums (but in moments) is about to be presented, above result receives an incorrect image. That's probably causing issues with storyboard constraint so the view controller dismisses automatically.
if (#available(iOS 11.0, *)){
if (self.asset.playbackStyle == PHAssetPlaybackStyleLivePhoto){
[self.assetImageView setHidden:YES];
}
}
});
}];
I have found so far that the result handler above is called several times, and eventually it returns the result containing a different image, whose different dimensions cause a problem with the storyboard. So, the view controller simply dismisses.
If it's returning a nil, I can simply decide not to load it to self.assetImageView, but even when the wrong image is returned, the result is not nil. I want to know if there's a way to figure out if the returned result really belongs to the asset.

Lazy Loading UICollectionView Images

There's a UIImageView for every cell of the UICollectionView and I need to load them so that as you scroll through the collection they show up properly and every image loads on its cell at its index path, once.
Every image is downloaded from Kinvey.
Code to download every image:
UICollectionViewCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:#"Cell" forIndexPath:indexPath];
UIImageView *imageView = [[UIImageView alloc]initWithFrame:CGRectMake(0, 0, cell.bounds.size.width, cell.bounds.size.width)];
[cell addSubview:imageView];
[KCSFileStore downloadData:#"id for item at index path"] completionBlock:^(NSArray *downloadedResources, NSError *error) {
if (error == nil) {
KCSFile* file = downloadedResources[0];
dispatch_async(dispatch_get_main_queue(), ^(void){
imageView.image = [UIImage imageWithData:file.data];
});
NSLog(#"Downloaded.");
} else {
NSLog(#"Error: %#", error.localizedDescription);
}
} progressBlock:nil];
That "dispatch_async(dispatch_get_main_queue(), ^(void)..." is something I tried to solve my problem downloading images asynchronously, but that didn't work.
Thanks in advance.
why you try to download the images.
If you want to perform lazy loading for images in collectionView using third party library. then SDWebimages
is the best third party library for that.this library not only perform the lazy loading but provide multiple other option also. just take a look.
Hope this will Help You and solve your problem.

ALAssetRepresentation fullResolutionImage never returns

I am writing a multithreaded application which needs to upload photos from the ALAssetsLibrary en masse in the background. So I have an NSOperation subclass which finds the appropriate ALAsset via the asset's URL and adds the image to an upload queue.
In the upload queue for the current ALAsset, I need to get the metadata from the image, but I've encountered a problem: both the -metadata and the -fullResolutionImage methods never return when they are called on the ALAssetRepresentation of the ALAsset. They simply hang there indefinitely. I tried printing the value of each of these methods in LLDB, but it hung the debugger up, and I ended up killing Xcode, signal 9 style. These methods are being called on a background queue.
I am testing these on an iPad 2. This is the method in which the ALAsset is added to the upload queue when it is found in the success block of -assetForURL:resultBlock:failureBlock:
- (void)addMediaToUploadQueue:(ALAsset *)media {
#autoreleasepool {
ALAssetRepresentation *defaultRepresentation = [media defaultRepresentation];
CGImageRef fullResolutionImage = [defaultRepresentation fullResolutionImage];
// Return if the user is trying to upload an image which has already been uploaded
CGFloat scale = [defaultRepresentation scale];
UIImageOrientation orientation = [defaultRepresentation orientation];
UIImage *i = [UIImage imageWithCGImage:fullResolutionImage scale:scale orientation:orientation];
if (![self isImageUnique:i]) return;
NSDictionary *imageDictionary = [self dictionaryForAsset:media withImage:i];
dispatch_async(self.background_queue, ^{
NSManagedObjectContext *ctx = [APPDELEGATE createManagedObjectContextForThread];
[ctx setUndoManager:nil];
[ctx performBlock:^{
ImageEntity *newImage = [NSEntityDescription insertNewObjectForEntityForName:#"ImageEntity"
inManagedObjectContext:ctx];
[newImage updateWithDictionary:imageDictionary
inManagedObjectContext:ctx];
[ctx save:nil];
[APPDELEGATE saveContext];
dispatch_async(dispatch_get_main_queue(), ^{
[self.fetchedResultsController performFetch:nil];
});
if (!currentlyUploading) {
currentlyUploading = YES;
[self uploadImage:newImage];
}
}];
});
}
}
I had a similar problem and I was tearing my hair out trying to figure it out.
Turns out while I had thought I setup a singleton for ALAssetsLibrary, my code was not calling it properly and some ALAssets were returning an empty 'fullResolutionImage'
In all of my NSLogs I must have missed the most important message from Xcode:
"invalid attempt to access ALAssetPrivate past the lifetime of its owning ALAssetsLibrary"
Follow this link
http://www.daveoncode.com/2011/10/15/solve-xcode-error-invalid-attempt-to-access-alassetprivate-past-the-lifetime-of-its-owning-alassetslibrary/
I hope that helps

NSImageView is not deallocated using ARC

Im pretty new to Cocoa development, and I probably do not clearly understand how ARC works.
My problem is that when I'm using NSImageView it is not getting deallocated as I want so the program is leaking memory.
__block CMTime lastTime = CMTimeMake(-1, 1);
__block int count = 0;
[_imageGenerator generateCGImagesAsynchronouslyForTimes:stops
completionHandler:^(CMTime requestedTime, CGImageRef image, CMTime actualTime,
AVAssetImageGeneratorResult result, NSError *error)
{
if (result == AVAssetImageGeneratorSucceeded)
{
if (CMTimeCompare(actualTime, lastTime) != 0)
{
NSLog(#"new frame found");
lastTime = actualTime;
}
else
{
NSLog(#"skipping");
return;
}
// place the image onto the view
NSRect rect = CGRectMake((count+0.5) * 110, 100, 100, 100);
// the problem is here!!! ImageView object gets allocated, but never released by the program even though I'm using ARC
NSImageView *imgV = [[NSImageView alloc] initWithFrame:rect];
[imgV setImageScaling:NSScaleToFit];
NSImage *myImage = [[NSImage alloc] initWithCGImage:image size:(NSSize){50.0,50.0}];
[imgV setImage:myImage];
[self.window.contentView addSubview: imgV];
}
if (result == AVAssetImageGeneratorFailed)
{
NSLog(#"Failed with error: %#", [error localizedDescription]);
}
if (result == AVAssetImageGeneratorCancelled)
{
NSLog(#"Canceled");
}
count++;
}];
Therefore, when I'm returning to this block again t generate new images and display them, everything works perfect except that my program memory use increases by the number of views got created.
If anyone can help me with this I would really appreciate it! Thank you!
Your problem is that you don't remove your subviews when you are generating new ones - make sure you remove your subviews before with something along those lines:
NSArray *viewsToRemove = [self.contentView subviews];
for (NSView *v in viewsToRemove) {
[v removeFromSuperview];
}
So your problem is not related to the usage of ARC actually. Each time you create a NSImageView and add it to contentView it is your responsability to remove them before adding a series of new ones. Note that adding those views to contentView will increment the ref count by one and removing them from the contentView will decrement the ref count by one leading to the memory usage for those views being freed by the system (because nothing else is retaining your views in btw).
Offending piece of code:
[self.window.contentView addSubview: imgV];
You've allocated an NSImageView. and keep adding it to the view. You never remove it, meaning the view is creating many references to different instances of the same object, all allocating their own piece of memory.
Solution: You'll need to keep track of the view, to make sure you can remove it later. Typically, I use class extensions.
For example:
#interface ClassName() {
NSImageView* m_imgV;
}
#end
....
// place the image onto the view
NSRect rect = CGRectMake((count+0.5) * 110, 100, 100, 100);
if (m_imgV) {
[m_imgV removeFromSuperView];
}
m_imgV = [[NSImageView alloc] initWithFrame:rect];
[m_imgV setImageScaling:NSScaleToFit];
NSImage *myImage = [[NSImage alloc] initWithCGImage:image size:(NSSize){50.0,50.0}];
[m_imgV setImage:myImage];
[self.window.contentView addSubview:m_imgV];
I was fighting with this problem for the whole day and finally found the way. For some reason the program wanted me to add a whole function which looks like:
// remove all the view from the superview
// and clean up a garbage array
-(void) killAllViews
{
for (NSImageView *iv in _viewsToRemove)
{
[iv removeFromSuperview];
}
[_viewsToRemove removeAllObjects]; // clean out the array
}
where _viewsToRemove is an array of NSImageViews which I'm filling every time my block is generating new images and adds them to the view.
Still don't understand why just adding the pure code from inside my killAllViews method somewhere into program couldn't solve the problem. Right now I'm basically doing the same, but just calling this method.

How does NSImage's data retaining behaviour work?

I have a method that constantly loads new image data from the web. Whenever all data for an image has arrived, it is forwarded to a delegate like this:
NSImage *img = [[NSImage alloc] initWithData:dataDownloadedFromWeb];
if([[self delegate] respondsToSelector:#selector(imageArrived:)]) {
[[self delegate] imageArrived:img];
}
[img release];
In imageArrived: the image data is assigned to an NSImageView:
- (void)imageArrived:(NSImage *)img
{
[imageView1 setImage:img];
}
This works nicely, the image is being displayed and updated with every new download cycle. I have profiled my application with Instruments to ensure that there is no leaking - it doesn't show any leaking. However, if I check with Instruments' Activity Monitor, I can see my application grabbing more and more memory with time, roughly increasing by the size of the downloaded images. If I omit [imageView1 setImage:img], memory usage stays constant. My question is: how is that happening? Is my code leaking? How does the NSImage instance within NSImageView determine when to release its data? Thanks for your answers!
When you do initWithData, the retain count on the data is incremented by one. Releasing the image does not change the retain count on the data. That's where your memory is going. The image structures are coming and going the way you want, but the data chunks are accumulating.
save the data handle separately and clean that out after disposing of the nsimage, and all should be well:
(id) olddata = 0; // global or something with persistence
your_routine
{
(id) newdata = dataDownloadedFromWeb;
NSImage *img = [[NSImage alloc] initWithData: newdata];
if([[self delegate] respondsToSelector:#selector(imageArrived:)])
{
[[self delegate] imageArrived:img];
}
[img release];
if (olddata) [olddata release];
olddata = newdata;
}
cleanup
{
if (olddata) [olddata release];
olddata = 0;
}