I am curious if it is possible to easily assign and retain __block objects inside asynchronous blocks in MRC and ARC? I obtained the following code while rewriting a piece of code with minimal changes. I got stuck when trying to return the image from an asynchronous block. A concern is that the code would one day be converted to ARC. I do not want to have hidden memory crash after the conversion. My options were
Using a GCD object holder if such a thing exists
Using a homebrew object holder, an array or others
Directly adding the image to the array (which I am using)
Rewriting the code to another structure
Basically the code load multiple images in a background thread. Searching // image deallocated :( will give you the location.
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
ALAssetsLibrary *assetsLibrary = nil;
for (int i = 0; i < sources.count; ++i) {
__block UIImage *image = nil;
id source = sources[i];
if ([source isKindOfClass:[NSString class]]) {
image = something
} else if ([source isKindOfClass:[NSURL class]]) {
NSURL *url = source;
if ([url.scheme isEqualToString:#"assets-library"]) {
dispatch_group_t group = dispatch_group_create();
if (!assetsLibrary)
assetsLibrary = [[[ALAssetsLibrary alloc] init] autorelease];
dispatch_group_enter(group);
[assetsLibrary assetForURL:url resultBlock:^(ALAsset *asset) {
image = something
dispatch_group_leave(group);
} failureBlock:^(NSError *error) {
dispatch_group_leave(group);
}];
dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
// image deallocated :(
} else if (url.isFileURL) {
image = something
}
}
// add image to an array
}
dispatch_async(dispatch_get_main_queue(), ^{
// notify
});
});
I think moving the image variable declaration would be the first right step to think about this. You can't expect to have one image variable for multiple concurrent operations.
so to get started, let's do this-
ALAssetsLibrary *assetsLibrary = nil;
for (int i = 0; i < sources.count; ++i) {
__block UIImage *image = nil;
After that, you are currently keeping UIImage instances in an array? This is not recommended. Please find out a way to write these images to documents directory and later read from there when needed. Keeping UIImage instances in memory will lead to severe memory overuse issues.
You need to come up with a naming convention to write your images into documents directory. Whenever an image load completes, just save it to your local folder and later read from there.
You could also call the notify completion part as soon as one image is loaded and keep notifying about others as you get them. Not sure how your design is working.
Also, I noticed you are creating multiple groups. One new group with each iteration inside for loop here-
dispatch_group_t group = dispatch_group_create();
Moving it outside for loop would be a good call.
Hope this helps.
Related
I am using following code to fetch images and AVAsset from PHAsset. Here are two arrays in code :
galleryArr : to store images for collection view.
mutableDataArr : store images (for image asset) and videos (for AVAsset) to upload on server
Its very slow to fetch all images from PHAssets array.
I googled about this, most of people says remove this line [options setSynchronous:YES]; but if I remove this line then completion is called twice and array duplicates the objects (as objects are appended in array within completion).
for (int i = 0; i < assets.count; i++) {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
PHImageRequestOptions *options = [[PHImageRequestOptions alloc] init];
options.deliveryMode = PHImageRequestOptionsDeliveryModeOpportunistic;
options.resizeMode = PHImageRequestOptionsResizeModeExact;
[options setNetworkAccessAllowed:YES];
[options setSynchronous:YES];
PHImageManager *manager = PHImageManager.defaultManager;
PHVideoRequestOptions *videoOptions = [[PHVideoRequestOptions alloc] init];
videoOptions.networkAccessAllowed = YES;
__weak typeof(self) weakSelf = self;
if (assets[i].mediaType == PHAssetMediaTypeVideo) {
[manager requestAVAssetForVideo:[assets objectAtIndex:i] options:videoOptions resultHandler:^(AVAsset * _Nullable asset, AVAudioMix * _Nullable audioMix, NSDictionary * _Nullable info) {
if ([asset isKindOfClass:[AVURLAsset class]])
{
[weakSelf.mutableDataArr addObject:asset];
}
}];
}
[manager requestImageForAsset:[assets objectAtIndex:i]
targetSize: CGSizeMake(1024, 1024) //PHImageManagerMaximumSize
contentMode:PHImageContentModeAspectFit
options:options
resultHandler:^(UIImage *image, NSDictionary *info) {
if (image) {
dispatch_async(dispatch_get_main_queue(), ^{
if (assets[i].mediaType != PHAssetMediaTypeVideo) {
[weakSelf.mutableDataArr addObject:image];
}
[galleryArr addObject:image];
if (i+1 == assets.count) {
[SVProgressHUD dismiss];
[weakSelf.galleryCollectionView reloadData];
}
});
}
}];
});
}
Any suggestion please?
Just one thought, it looks like you are loading all the images from the array before removing your progress HUD and displaying the gallery. As the number of images could be very large and presuming you are using a collection view or similar, that's quite an overhead before anything is displayed.
I did something like this a while ago and instead of looping through the array and loading everything up front, I let the cells request images as they needed them. This makes it very fast and efficient as cells can display immediately with a loading icon, then flip to the image when it was available. Efficiency comes from only loading images the user is actually going to see.
To make things performant, and by performant I mean I could scroll as fast as I liked without the display freezing, each cell would first check an in memory cache for the image, then trigger a request for an image on a background thread.
When the image was returned, the cell would add it to the in memory cache and then if the cell had not being reused for a different image (due to fast scrolling) it would display the image.
Further, I also used a NSCache for the in memory cache so that if the app started to use a lot of memory, images would be automatically dropped and reloaded the next time a cell wanted one.
The summary is to use a memory aware cache, and only load what you actually need.
That is how I have my data:
(
{
image = "https://www.myurl/abc- a/uploads/1161570652.jpg";
}
{
image = "https://www.myurl/abc- a/uploads/1161570652.jpg";
}
{
image = "https://www.myurl/abc- a/uploads/1161570652.jpg";
}
{
image = "https://www.myurl/abc- a/uploads/11615720652.jpg";
}
{
image = "https://www.myurl/abc- a/uploads/11615730652.jpg";
}
)
I could retrieve it successfully. But the problem is it is very slow.
Here is my code:
seeview=[[UIImageView alloc]init];
for (int k=0; k<[dictofimage count]; k++)
{
img = [UIImage imageWithData:[NSData dataWithContentsOfURL:[NSURL URLWithString:[image_array objectAtIndex:k]]]];
NSLog(#"image name is 0 %#",img);
[myimageview setImage:img];
}
It takes time to pick every element and is slow in progress. Is there any faster way to do it?
You can't get away from looping through all of the contents of image_array so you're stuck with O(n) but you can use a faster construct. The for:in loop is much faster than a typical C style loop, so something like:
for (imageString in image_array) {
img = [UIImage imageWithData:[NSData dataWithContentsOfURL:[NSURL URLWithString:imageString]]];
NSLog(#"image name is 0 %#",img);
[myimageview setImage:img];
}
For a breakdown of the benchmarks check out: http://iosdevelopertips.com/objective-c/high-performance-collection-looping-objective-c.html
EDIT: I should also note that dataWithContentsOfURL is a synchronous call that you probably shouldn't be making in this loop. You'd be better off creating an NSOperation that downloads the image, and when it finishes you can dispatch back to the main queue to set the actual image in the imageView.
You are better off handling the array of objects concurrently, in the background. This is precisely what dispatch_apply is for.
dispatch_apply(
image_array.count,
dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0),
^(size_t index) {
NSURL *url = [NSURL URLWithString:[image_array objectAtIndex:index]];
UIImage *image = [UIImage imageWithData:[NSData dataWithContentsOfURL:url]];
dispatch_async(dispatch_get_main_queue(), ^{
// Handle the image on the main thread for UI elements
});
});
You only need the dispatch_async if you need to handle the image on the main thread as part of a UI change.
Note, that dispatch_apply is synchronous, in that it kicks off one background task for each iteration. Those tasks run in the background, but dispatch_apply will not return until they are all done. Thus, you may also want to wrap that in an async block.
Or, you can do it yourself, and get notified when all the blocks are complete...
dispatch_group_t group = dispatch_group_create();
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0);
for (size_t i = 0; i < image_array.count; ++i) {
dispatch_group_async(group, queue, ^{
NSURL *url = [NSURL URLWithString:[image_array objectAtIndex:index]];
// The rest is the same...
});
}
dispatch_group_notify(group, queue, ^{
// This code will execute on `queue` when all the above blocks have finished
});
Note, however, that dispatch_apply has a nice algorithm to not overload the system. The above approach is not so kind.
However, you can get dispatch_apply nice-ness using an async call.
dispatch_async(queue, ^{
dispatch_apply(image_array.count, queue, ^(size_t index) {
NSURL *url = [NSURL URLWithString:[image_array objectAtIndex:index]];
// The rest is the same...
});
// This code will execute on `queue` when all the above blocks have finished
});
I have a cache of files in which I need to write/read images.
All the work with the file system I need to perform in background.
For this purposes for saving files I use:
dispatch_barrier_async([AxPanoramaDataManager sharedInstance].dataManagerQueue, ^{
[data writeToFile:tileFilePathName atomically:YES];
});
And for reading:
__block UIImage *tileImage = nil;
dispatch_sync([AxPanoramaDataManager sharedInstance].dataManagerQueue, ^{
tileImage = [[UIImage imageWithContentsOfFile:tileFilePathName] retain];
dispatch_async(dispatch_get_main_queue(), ^{
completion (tileCoordValue, side, tileImage, error);
[tileImage release];
});
});
Everything works well, there is a great amount of files in cash folder, but I sometimes I need to clean the cash. I need to cancel all the queue blocks and perform a block with cleaning the folder.
The first my realization of the method looks like this:
+ (void) cleanCash
{
NSString *folderPath = [[NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) objectAtIndex:0] stringByAppendingString:#"cash"];
dispatch_sync([AxPanoramaDataManager sharedInstance].dataManagerQueue, ^{
NSError *error = nil;
[[NSFileManager defaultManager] removeItemAtPath:folderPath error:&error];
NSParameterAssert(error == nil);
}
});
}
But I have a numerous problems with it because of it is not cancel the all the waiting operations in queue. I try to look solutions in SO but can't implement them unfortunately. Can anyone help me with this?
You can use NSOperationQueue with maxConcurrentOperationCount = 1 (it will make a serial queue) for run NSOperation that will load your images;
NSOperation is a cancelable object. Also you can make dependencies between operations.
P.S.
You can view SDWebImage, it contains SDImageCache class. It class is very good for manage cache of images.
I have a music app that I want to preload album arts for. Right now, when you hit next song it loads the album art from a URL but it would be nice if it was already preloaded and in memory. I've tried a few things but nothing seems to really work. Does anyone know of a technique that where I can preload images in iOS and then look for that image later and if its not there, then download it?
Edit with some more detail...
When I go to play a 'station' on my app, I make a request to a server that returns JSON for the playlist. Inside of this JSON is each song as well as data including the album art URL. When a user goes to play a 'station' I would like to preload images for either the next album art URL in the stack or all album art URLs in the entire playlist.
Anyways, I've tried loading the next album art URL in a UIImage in the background as like a temporaryAlbumArt and then when the next song is played simply do albumArt = temporaryAlbumArt and then repeat the process setting the next songs album art to temp and so on..
I've tried something like this:
if (temporaryAlbumArt) {
albumArt = temporaryAlbumArt
} else {
albumArt = [UIImage imageWithData:[NSData dataWithContentsOfURL:[NSURL URLWithString:firstAlbumArtURL]]];
}
temporaryAlbumArt = [UIImage imageWithData:[NSData dataWithContentsOfURL:[NSURL URLWithString:nextAlbumArtURL]]];
I guess my question is, what is the best method of:
loading an image or multiple images from a URL(s) into memory
using those images later on without any lag time
You can use this "must have" tool APSmartStorage. It supports everything you need.
This library does what you need: JMImageCache
Loop through your URLs, download them through JMImageCache and when you need to show an image the library will fetch the stored image if it's finished downloading.
//You can do this when you get the array of urls to download the images in the background
for(NSURL *url in urls) {
[[JMImageCache sharedCache] imageForURL:url];
}
//and then when you need an image:
[imageView setImageWithURL:url placeholder:[UIImage imageNamed:#"placeholder.png"]];
I'm not for sure what you are asking but if you know what the next song is going to be couldn't you just down load it in a background thread then have it ready for display when the next song starts?
If you want the app to store all the downloaded images locally you may eventually run into space issues but its do able. You could use the downloaded image then store it in the caches directory (not document dir or Apple will get mad!). When a song comes just do a quick check for the image and if its not local download it. Songs take a few minutes so there should be time to do all this in the background.
#define GET_QUEUE dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)
// while song is playing...
dispatch_async(GET_QUEUE, ^{
// On a separate thread look for the image locally in the cache and then
// download it if necessary
});
I'm not sure if this answers your question or not...
Ok so here is a bit more code - basically it lets me ask hopefully helpful questions in context.
// Inside some loop/method that plays songs...
if (temporaryAlbumArt)
albumArt = temporaryAlbumArt
else
albumArt = [UIImage imageWithData:[NSData dataWithContentsOfURL:[NSURL URLWithString:firstAlbumArtURL]]];
// If this keeps executing then perhaps temporaryAlbumArt is getting released somehow?
// Are you persisting it correctly through each iteration/method call?
// show art...
// start music
// Call art update
dispatch_async(GET_QUEUE, ^{
temporaryAlbumArt = nil; // In case the data hasn't been obtained yet on the next iteration???
temporaryAlbumArt = [UIImage imageWithData:[NSData dataWithContentsOfURL:[NSURL URLWithString:nextAlbumArtURL]]];
// This works ok then? Can you make sure this is completing correctly before the next iteration/method call?
});
Sorry I can't be of more help.
I have written this code to download data from server and save that data into document directory so that next time if anybody hit the same url,it is fetched from document directory without any server hit.
-(void)downloadImageForProductsFromArray:(NSArray *)inventoryArr {
BOOL isAllDataDownloaded = YES;
MBProgressHUD *hud = [MBProgressHUD showHUDAddedTo:self.view animated:YES];
hud.labelText = #"Data Sync...";
[[NSOperationQueue mainQueue] addObserver:self forKeyPath:#"operations" options:0 context:nil];
for (int i = 0; i < inventoryArr.count; i++) {
NSString *productUrlStr = [inventoryArr objectAtIndex:i];
NSData *dirData = [CommonUtils fetchImageDataFromDirectoryWithImageName: productUrlStr];/*****Check whether Image data is already downloaded and saved in document directory*****/
if (!dirData) {
isAllDataDownloaded = NO;
NSBlockOperation *downloadImgOpr = [[NSBlockOperation alloc] init];
downloadImgOpr.queuePriority = NSOperationQueuePriorityVeryHigh;
downloadImgOpr.qualityOfService = NSQualityOfServiceUserInitiated;
[downloadImgOpr addExecutionBlock:^{
NSData *imageData = [NSData dataWithContentsOfURL:[NSURL URLWithString: productUrlStr]];
if (imageData) {
/*****Save Image Data in Document Directory*****/
}
}];
}
}
if (isAllDataDownloaded) {
[MBProgressHUD hideAllHUDsForView:self.view animated:YES];
}}
- (void) observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object
change:(NSDictionary *)change context:(void *)context{
if (object == [NSOperationQueue mainQueue] && [keyPath isEqualToString:#"operations"]) {
if ([[NSOperationQueue mainQueue].operations count] == 0) {
// Do something here when your queue has completed
[MBProgressHUD hideAllHUDsForView:self.view animated:YES];
NSLog(#"queue has completed");
}
}
else {
[super observeValueForKeyPath:keyPath ofObject:object
change:change context:context];
}
}
In This code i have all the url strings in the inventory array.In for loop i have made several operations and added those operations into the queue(I have used main queue for holding UI and syncing data.Choose your queue appropriately).Also I have added the KVO to the queue for keypath value "operations" so that if count of operations is 0,we can do the useful operation.
I am using ChuteSDK to import multiple images from photo library something like this:
-(void)doneSelected{
NSMutableArray *returnArray = [NSMutableArray array];
[self showHUD];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^(void){
for(id object in [self selectedAssets]){
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
if([object isKindOfClass:[GCAsset class]]){
ALAsset *asset = [object alAsset];
NSMutableDictionary* temp = [NSMutableDictionary dictionary];
[temp setObject:[[asset defaultRepresentation] UTI] forKey:UIImagePickerControllerMediaType];
[temp setObject:[UIImage imageWithCGImage:[[asset defaultRepresentation] fullScreenImage] scale:1 orientation:(UIImageOrientation)[[asset defaultRepresentation] orientation]] forKey:UIImagePickerControllerOriginalImage];
[temp setObject:[[asset defaultRepresentation] url] forKey:UIImagePickerControllerReferenceURL];
[returnArray addObject:temp];
}
[pool release];
}
dispatch_async(dispatch_get_main_queue(), ^(void) {
if(delegate && [delegate respondsToSelector:#selector(PhotoPickerPlusController:didFinishPickingArrayOfMediaWithInfo:)])
[delegate PhotoPickerPlusController:[self P3] didFinishPickingArrayOfMediaWithInfo:returnArray];
[self hideHUD];
});
});
}
But fullScreenImage is giving me a scaled down version of the original image and if I use fullResolutionImage it is causing low memory warning issue due to which the app is crashing.
How can I get the image with original resolution without causing memory problems.
P.S: I'm not using ARC in my project.
'returnArray' variable you declared is outside the autorelease pool block.
Now you are adding your image to a dictionary 'temp' which is inside auto release pool but ultimately you are adding this 'temp' to 'returnArray' and hence its retain count increased which is causing a leak actually.
Even do keep in mind one more thing while working with images. When you are using an image it doesn't take the memory what it shows as file size as many would be expecting (ie., some thing less than 3MB for a 2048 x 1536 ) . Instead it is actually loaded in raw format taking memory based on a calculation as follows:
width x height x n bytes where n is number of bits being taken to represent colors per pixel mostly it is 4.
so for same 2048 x 1536 image it is going to take 12MB.
So now check what is the original resolution of the image you are talking about and calculate how much MB its gonna take, and change your code as per that.
You are decompressing all the images at once. Wait until you absolutely need them.