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
Related
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.
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.
I'm using SDWebImagePrefetcher to prefetch a list of images off my plist (about 100, max size 600kb) in order to save them (on disk possibily) for later use.
Point is that on simulator this is working fine, while on device, it's loading and caching only some of them and I honestly don't get why.
I know this because after prefetching them, i turn off WiFi connection and see if the images have been loaded correctly in the imageView (on detailView), and surprisingly there are only 1\5 of the images prefetched, even if the consoloe said it prefetched them all.
I tried to put in SDWebImagePrefetcher.m in method startPrefetchingAtIndex: this line of code
- (void)startPrefetchingAtIndex:(NSUInteger)index withManager:(SDWebImageManager *)imageManager
{
if (index >= [self.prefetchURLs count]) return;
_requestedCount++;
[imageManager downloadWithURL:[self.prefetchURLs objectAtIndex:index] delegate:self options:self.options];
[cache storeImage:[UIImage imageWithData:[NSData dataWithContentsOfURL:[NSURL URLWithString:[self.prefetchURLs objectAtIndex:index]]]] forKey:[self.prefetchURLs objectAtIndex:index] toDisk:YES];
}
and then in my imageView class:
SDImageCache* cache = [SDImageCache sharedImageCache];
UIImage* cachedImage = [cache imageFromKey:aString fromDisk:YES];
if (cachedImage) {
[imageView setImage:cachedImage];
}
else{
[imageView setImageWithURL:[NSURL URLWithString:anUrl]
placeholderImage:nil options:SDWebImageProgressiveDownload
success:^(UIImage *image) { [activityIndicator stopAnimating];[activityIndicator removeFromSuperview]; }
failure:^(NSError *error) { [activityIndicator stopAnimating];[activityIndicator removeFromSuperview]; }];
[imageView addSubview:activityIndicator];
}
With no concrete results. Always working on simulator, not on device. Am I missing something here?
Should I put some caching method in SDWebImagePrefetcher to store them on disk or what?
I've being trying to make it work but failing every time.
Seems like i found the issue.
I shouldn't set the option to cache ( [prefetcher setOptions:SDWebImageMemoryCacheOnly]; ) ,but I had to leave it in low priority, so now everything works perfectly!
Ok so i have a uitableview and when an item is selected it segues to a new view controller to show an image (inside of a uiscrollview). The image begins downloading in a dispatch_queue from prepareforsegue. To be clear I am doing all UI updates in the main queue. The problem is that if i hit the back button quick enough then my program crashes with an exc_bad_address. I think the problem has to deal with zoomToRect animated:YES because when i set animated to NO i cant get it to crash. Plus the call stack below deals with animation. What is the best way to go about fixing this and is there a better way to get what i need done?
Also when debugging the problem print 'Block completed' and then crashes shortly after.
stack trace
Here is the method called. It is called in a setter of the destination controller in prepareForSegue.
-(void) updateDisplay {
dispatch_queue_t queue = dispatch_queue_create("Load Flickr Photo", NULL);
UIActivityIndicatorView *spinner = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleGray];
self.navigationItem.rightBarButtonItem = [[UIBarButtonItem alloc] initWithCustomView:spinner];
[spinner startAnimating];
dispatch_async(queue, ^{
UIImage *image = [FlickrFetcher imageForPhoto:self.currentPhoto format:FlickrPhotoFormatLarge];
dispatch_async(dispatch_get_main_queue(), ^{
self.navigationItem.rightBarButtonItem = nil;
self.imageView.image = image;
self.title = [FlickrFetcher titleForPhoto:self.currentPhoto];
CGAffineTransform transform = CGAffineTransformMakeScale(1.0, 1.0);
self.imageView.transform = transform;
self.imageView.frame = CGRectMake(0, 0, self.imageView.image.size.width, self.imageView.image.size.height);
self.scrollView.maximumZoomScale = 4.0;
self.scrollView.minimumZoomScale = .2;
self.scrollView.zoomScale = 1;
self.scrollView.contentSize = self.imageView.bounds.size;
//i think problem is here
[self.scrollView zoomToRect:self.imageView.frame animated:YES];
NSLog(#"Block completed");
});
});
}
I think a potential problem could be that while your block retains your self the view controller while its active in queue...
If you send -zoomToRect:animated:NO, the block will still be active and blocking on the call which ensures that all the objects are still valid in memory.
If you send -zoomToRect:animated:YES, the block will exit potentially generating a race condition where your view controller would be released since storyboards will also release your view controller when you go back in a segue leaving it with an effective retain count of zero.
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;
}