I have a serial queue which contains two methods which load and image and then, once completed, add the image to the subview. The images are in a NSMutableArray so I am iterating over a For loop to load them in as follows :
dispatch_queue_t queue = dispatch_queue_create("com.example.MyQueue", NULL);
for (int i =0; i<=[pictureThumbnailArray count]-1; i++) {
dispatch_async(queue, ^{
NSLog(#"Thumbnail count is %d", [pictureThumbnailArray count]);
finishedImage = [self setImage:[pictureThumbnailArray objectAtIndex:i]:i];
if (finishedImage !=nil) {
dispatch_async(dispatch_get_main_queue(), ^ {
[self.view addSubview:finishedImage];
});
}
});
}
The problem is that the images seem to be loaded randomly. What I want to achieve is that each iteration of the For loop runs and completes before the next iteration starts - that way the images should load in the same way each time.
Can anyone suggest the best way to achieve this - I am thinking I may need to synchronise the setImage method (first method in queue) ?
Changed To :
for (int i =0; i<=[pictureThumbnailArray count]-1; i++) {
NSLog(#"Thumbnail count is %d", [pictureThumbnailArray count]);
finishedImage = [self setImage:[pictureThumbnailArray objectAtIndex:i]:i];
if (finishedImage !=nil) {
dispatch_async(dispatch_get_main_queue(), ^ {
[self.view addSubview:finishedImage];
});
}
}
});
You have some other problem - perhaps your image array is not in the order you think it is. Both queue and mainQueue are serial queues. To verify this I just did a quick test and got the log messages in the expected order. I suggest you try adding log messages or so to figure out why the order is not as you expect:
- (void)viewDidLoad
{
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
static dispatch_queue_t queue;
queue = dispatch_queue_create("com.example.MyQueue", NULL);
for (int i =0; i<=20; i++) {
dispatch_async(queue, ^{
dispatch_async(dispatch_get_main_queue(), ^ {
NSLog(#"Image %d", i);
});
} );
}
}
What if we do the things simpler without GCD? I suggest get rid of it and use NSURLConnectionDelegate methods.
This method downloads next image:
-(void)startDownload
{
if (index < URLs.count)
{
NSURL *URL = [NSURL URLWithString:URLs[index]];
_connection = [[NSURLConnection alloc] initWithRequest:[NSURLRequest requestWithURL:URL] delegate:self];
}
}
The connectionDidFinishLoading: delegate method places the image to the view and starts next download.
- (void)connectionDidFinishLoading:(NSURLConnection *)connection
{
UIImage *image = [UIImage imageWithData:_data];
_data = nil;
_connection = nil;
UIImageView *imageView = (UIImageView *)[self.view viewWithTag:100+index];
imageView.image = image;
index++;
[self startDownload];
}
Here is the complete example: https://github.com/obrizan/TestImageDownload The images rather big so give some time to load them.
Related
I am using the following code to conduct what I want to be a background sync but the main thread is slowing down or even coming to a halt when the json received is larger than 20 or so records. Is there anything wrong with this code for a background operation? What could be blocking the main thread. Thank you for any suggestions.
Note there is a commented out line below performSelectorOnMainThread where the app processes the JSON received that I changed to yet another background thread but the change does not seem to help.
#define kBgQueue dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0) //1
#define kProductsURL [NSURL URLWithString: #"http://~/getproducts.php"]
//in viewDidLoad
if(hasInternet==YES && [loggedIntoServer isEqual:#1]) {
dispatch_async(kBgQueue, ^{
NSData* data = [NSData dataWithContentsOfURL: kProductsURL];
//previous line grabed data from api.
if (data) {
// [self performSelectorOnMainThread:#selector(fetchData:) withObject:data waitUntilDone:YES];//no longer doing this on main thread
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[self fetchData:data];
});
}
});
;
} //close hasInternet, logged into server.
- (void)fetchData:(NSData *)jsonFeed {
NSError* error;
NSDictionary* json = [NSJSONSerialization JSONObjectWithData:jsonFeed
options:kNilOptions
error:&error];
NSMutableArray* latestProducts = [[NSMutableArray alloc] init];
//this is specific to format of JSON
if (![[json objectForKey:#“products"] isKindOfClass:[NSNull class]]) {
latestProducts = [[json objectForKey:#“products"]mutableCopy];
getProducts = latestProducts;
int size = [latestProducts count];
[self.tableView reloadData];
getProducts = [self convertFeedtoObject:latestProducts];
[self importAndSaveProducts:getProducts];//this imports and saves
self.recentlySynced=YES;
}
}
You just did something redundant. You dispatched the fetching of data in a background thread. But then you also did the [self.tableView reloadData]; in the background thread. That's why your UI will be affected.
Try this:
if(hasInternet==YES && [loggedIntoServer isEqual:#1])
{
dispatch_async(kBgQueue, ^
{
NSData* data = [NSData dataWithContentsOfURL: kProductsURL];
if (data)
{
dispatch_async(dispatch_get_main_queue(), ^
{
[self fetchData:data];
});
}
});
}
What I did is I changed this part of your code:
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[self fetchData:data];
});
Because you should only do any changes to UI in the main thread. and this part of my code is doing the job in main thread.
dispatch_async(dispatch_get_main_queue(), ^
{
[self fetchData:data];
});
You do not need to have a nested call to the same queue. Also you should do any UI work on the main thread. For more information look at Apple's Concurrency Programming Guide
In your fetchData method load your table like this.
dispatch_async(dispatch_get_main_queue(), {
// Your UI work
[self.tableView reloadData];
})
// Remove second dispatch_async call
//in viewDidLoad
if(hasInternet==YES && [loggedIntoServer isEqual:#1]) {
dispatch_async(kBgQueue, ^{
NSData* data = [NSData dataWithContentsOfURL: kProductsURL];
//previous line grabed data from api.
if (data) {
[self fetchData:data];
}
});
;
} //close hasInternet, logged into server.
There are several errors in your original code, change to the following:
#define kBgQueue dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0) //CHANGE TO BACKGROUND
#define kProductsURL [NSURL URLWithString: #"http://~/getproducts.php"]
//in viewDidLoad
if(hasInternet==YES && [loggedIntoServer isEqual:#1]) {
dispatch_async(kBgQueue, ^{
NSData* data = [NSData dataWithContentsOfURL: kProductsURL];
if (data) {
[self fetchData:data];
}
});
} //close hasInternet, logged into server.
Change the fetch data to the following:
- (void)fetchData:(NSData *)jsonFeed {
NSError* error;
NSDictionary* json = [NSJSONSerialization JSONObjectWithData:jsonFeed
options:kNilOptions
error:&error];
NSMutableArray* latestProducts = [[NSMutableArray alloc] init];
//this is specific to format of JSON
if (![[json objectForKey:#"products"] isKindOfClass:[NSNull class]]) {
latestProducts = [[json objectForKey:#"products"]mutableCopy];
getProducts = latestProducts;
int size = [latestProducts count];
//Do this on the main thread:
dispatch_async(dispatch_get_main_queue(), ^{
[self.tableView reloadData];
});
getProducts = [self convertFeedtoObject:latestProducts];
[self importAndSaveProducts:getProducts];//this imports and saves
self.recentlySynced=YES;
}
}
Depending on how your table view works and what the data source is like, you may want to move the reload table view line (with the main queue dispatch) to underneath self.recentSynced = YES.
I'm trying to run some intensive processes serially, with multiple serial queues. The code is working, however my UI update doesn't occur, even though the method is called.
Here is the code that runs several processes serially.
- (void)importProcess {
dispatch_queue_t serialQueue = dispatch_queue_create("com.cyt.importprocessqueue", DISPATCH_QUEUE_SERIAL);
NSLog(#"checking image sizes");
__block NSMutableArray *assets;
dispatch_sync(serialQueue, ^() {
assets = [self checkImageSizes];
});
dispatch_sync(serialQueue, ^() {
[self appendLogToTextView:[NSString stringWithFormat:#"%i screenshot(s) ignored due to invalid size.",(int)(self.assets.count-assets.count)]];
if(assets.count==0) {
[self showNoRunesFoundAlert];
}
else {
[self appendLogToTextView:#"Preparing to process screenshots..."];
self.assets = [NSArray arrayWithArray:assets];
}
});
NSLog(#"processing uploads");
dispatch_sync(serialQueue, ^() {
[self processUploads];
});
NSLog(#"recognizing images");
dispatch_sync(serialQueue, ^() {
[self recognizeImages];
});
NSLog(#"recognizing images");
dispatch_sync(serialQueue, ^() {
[self processRuneText];
});
//dispatch_sync(dispatch_get_main_queue(), ^ {
//});
}
Within checkImageSizes, I have another serial queue:
- (NSMutableArray *)checkImageSizes {
dispatch_queue_t serialQueue = dispatch_queue_create("com.cyt.checkimagesizequeue", DISPATCH_QUEUE_SERIAL);
NSMutableArray *assets = [NSMutableArray new];
for(int i=0;i<self.assets.count;i++) {
dispatch_sync(serialQueue, ^{
PHAsset *asset = (PHAsset *)self.assets[i];
if(asset.pixelWidth==self.screenSize.width && asset.pixelHeight==self.screenSize.height) {
[assets addObject:asset];
NSString *logText = [NSString stringWithFormat:#"Screenshot %i/%i size is OK.",i+1,(int)self.assets.count];
[self performSelectorOnMainThread:#selector(appendLogToTextView:) withObject:logText waitUntilDone:YES];
}
else {
[self appendLogToTextView:[NSString stringWithFormat:#"ERROR: Screenshot %i/%i has invalid size. Skipping...",i+1,(int)self.assets.count]];
}
});
}
return assets;
}
The appendLogToTextView method is supposed to update the UI. Here is that code:
- (void)appendLogToTextView:(NSString *)newText {
dispatch_block_t block = ^ {
self.logText = [NSString stringWithFormat:#"%#\n%#", self.logText, newText];
NSString *textViewText = [self.logText substringFromIndex:1];
[UIView setAnimationsEnabled:NO];
if(IOS9) {
[self.textView scrollRangeToVisible:NSMakeRange(0,[self.textView.text length])];
self.textView.scrollEnabled = NO;
self.textView.text = textViewText;
self.textView.scrollEnabled = YES;
}
else {
self.textView.text = textViewText;
NSRange range = NSMakeRange(self.textView.text.length - 1, 1);
[self.textView scrollRangeToVisible:range];
}
[UIView setAnimationsEnabled:YES];
};
if ([NSThread isMainThread]) {
block();
}
else {
dispatch_sync(dispatch_get_main_queue(), block);
}
}
As you can see, I have tried calling appendLogToTextView both directly and using performSelectorOnMainThread. However, I'm not getting any updates to my text view, even though I confirm that the method is being called properly.
Interestingly, the UI updating works properly when I only use a single serial queue and use iteration counts to call the next method, as shown in the code below (_serialQueue is defined in viewDidLoad). However, I do not believe that implementation is good practice, as I'm wrapping serial queues within serial queues.
- (void)checkImageSizes {
NSMutableArray *assets = [NSMutableArray new];
for(int i=0;i<self.assets.count;i++) {
dispatch_async(_serialQueue, ^{
PHAsset *asset = (PHAsset *)self.assets[i];
if(asset.pixelWidth==self.screenSize.width && asset.pixelHeight==self.screenSize.height) {
[assets addObject:asset];
[self appendLogToTextView:[NSString stringWithFormat:#"Screenshot %i/%i size is OK.",i+1,(int)self.assets.count]];
}
else {
[self appendLogToTextView:[NSString stringWithFormat:#"ERROR: Screenshot %i/%i has invalid size. Skipping...",i+1,(int)self.assets.count]];
}
//request images
if(i==self.assets.count-1) {
[self appendLogToTextView:[NSString stringWithFormat:#"%i screenshot(s) ignored due to invalid size.",(int)(self.assets.count-assets.count)]];
if(assets.count==0) {
[self showNoRunesFoundAlert];
}
else {
[self appendLogToTextView:#"Preparing to process screenshots..."];
self.assets = [NSArray arrayWithArray:assets];
[self processUploads];
}
}
});
}
}
What am I not understanding about serial queues that is causing the UI updates in this version of the code to work, but my attempt at a "cleaner" implementation to fail?
In the end, I just ended up using dispatch groups and completion blocks in order to solve this problem.
I have downloaded images and saved it to a GCD, updated the count of images and performed a post notification in queue. What is happening now is that it does not register that I have downloaded the images. Sometimes I am missing some images, and I think it is because I have missed something in my GCD logic.
Here is my code:
for (NSString *i in items)
{
[[RequestAPI sharedInstance]downloadImage:i completion:^(AFHTTPRequestOperation *operation, UIImage *image, NSError *error) {
//1. here main thread I receive images and go to BG
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^{
//2. here I save image on disk and get path
NSString *path = [ImageManager saveImageToDisk:image toEntity:entity withparams:#{#"save" : #"lala"}];
__block NSMutableDictionary *attachments = [NSMutableDictionary dictionary];
__block NSMutableArray *photoPaths = [NSMutableArray array];
dispatch_async(dispatch_get_main_queue(), ^{
//3. here I load entity and dictionary from it with NSKeyedUnarchiver from CD and set to it image path
if (entity.attachments)
{
attachments = [NSKeyedUnarchiver unarchiveObjectWithData:entity.attachments];
if (attachments[type])
{
photoPaths = attachments[type];
}
}
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^{
//4. here I check all images equality ti themselves in entity
BOOL haveDublicate = NO;
NSData *i = [ImageManager imageDataFromPath:path];
NSArray *photoImages = [ImageManager imageDatasFromPaths:photoPaths];
for (NSData *saved in photoImages)
{
if ([saved isEqualToData: i])
{
haveDublicate = YES;
}
}
if (!photoPaths)
{
photoPaths = [NSMutableArray array];
}
dispatch_async(dispatch_get_main_queue(), ^{
//5. and finally if all ok I save image path, change load counter and post notification
if (path.length
&& ![photoPaths containsObject:path]
&& !haveDublicate
)
{
[photoPaths addObject:path];
[savedLinks setObject:photoPaths forKey:type];
entity.attachments = [NSKeyedArchiver archivedDataWithRootObject:savedLinks];
[self saveContext];
}
[RequestAPI sharedInstance].downloadsCount -= 1;
[[NSNotificationCenter defaultCenter]postNotificationName:kReloadFeedData object:nil];
});
});
});
});
}];
I think here I need process 1-2-3-4-5 to get desired result. Am I right or how can I do this queuing?
I am playing around with NSOperationQueue in order to run some code in the background and have it update a UILabel. Here's the viewDidLoad.
- (void)viewDidLoad
{
[super viewDidLoad];
queue = [[NSOperationQueue alloc] init];
NSInvocationOperation *operation = [[NSInvocationOperation alloc] initWithTarget:self selector:#selector(counterTask) object:nil];
[queue addOperation:operation];
}
And here's the method called as the invocation operation:
- (void)counterTask {
for (int i=0; i<5000000; i++) {
if (i % 100 == 0) {
[self.firstLabel performSelectorOnMainThread:#selector(setText:)
withObject:[NSString stringWithFormat:#"%d", i]
waitUntilDone:YES];
}
}
[self.firstLabel performSelectorOnMainThread:#selector(setText:) withObject:#"finished." waitUntilDone:NO];
}
As the loop counts up, and more and more #"%d" NSStrings are created, the memory usage naturally goes up. Once the loop finishes however, the memory doesn't seem to deallocate. I expected the memory to fall as the setText: message uses new instances of NSString and releases the old ones.
If I change the loop condition to i<5000000*2, the memory usage is roughly double by the end – so it's definitely something happening on each iteration causing the leak.
Why is memory leaking here?
EDIT: Forgot to mention that I'm using ARC.
ARC doesn't remove retain / release / autorelease, it just controls the calling of these methods. You can add your own autorelease pool into your loop to force cleanup as it goes:
for (int i=0; i<5000000; i++) {
if (i % 100 == 0) {
#autoreleasepool {
[self.firstLabel performSelectorOnMainThread:#selector(setText:)
withObject:[NSString stringWithFormat:#"%d", i]
waitUntilDone:YES];
}
}
}
Let's try:
- (void)counterTask {
#autoreleasepool {
for (int i=0; i<5000000; i++) {
if (i % 100 == 0) {
[self.firstLabel performSelectorOnMainThread:#selector(setText:)
withObject:[NSString stringWithFormat:#"%d", i]
waitUntilDone:YES];
}
}
}
[self.firstLabel performSelectorOnMainThread:#selector(setText:) withObject:#"finished." waitUntilDone:NO];
}
What happening in your loop that you are creating NSString and ARC add it to auto release pool.
not releases the memory(NSString*) immediately , will release later.
One more thing is that, actually performSelectorOnMainThread retains both target and objects.
So best way is that you create nsstring instance after sending it to selector set it to nil so ARC will release it.
NSString* strText=[[NSString alloc]initWithFormat:#"%d",i ];
[self.firstLabel performSelectorOnMainThread:#selector(setText:)
withObject:strText
waitUntilDone:YES];
strText=nil;
IMO, the suggested approach of #Wain should fix the issue.
But you may also use this:
- (void)counterTask {
assert([NSThread currentThread] != [NSThread mainThread]);
for (int i=0; i<5000000; i++) {
if (i % 100 == 0) {
dispatch_sync(dispatch_get_main_queue(), ^{
#autoreleasepool {
self.firstLabel.text = [NSString stringWithFormat:#"%d", i];
}
});
}
}
dispatch_async(dispatch_get_main_queue, ^{
self.firstLabel.text = #"finished";
});
}
Try this
(void)counterTask {
__weak NSString *str;
for (int i=0; i<50000; i++) {
if (i % 100 == 0) {
str = [NSString stringWithFormat:#"%d", i];
[self.logInTxtField performSelectorOnMainThread:#selector(setText:)
withObject:str
waitUntilDone:YES];
}
}
[self.logInTxtField performSelectorOnMainThread:#selector(setText:) withObject:#"finished." waitUntilDone:NO];
}
I have a singleton that loads up an a bunch of ALAssets when my app launches. This is causing the main thread to freeze for more then 10 seconds while it loads each image into memory. Obviously a big no no.
I tried to put it on a background thread, but it only partially executes.
+ (CCPhotos*) sharedPhotos
{
static CCPhotos* shared = nil;
if (!shared)
{
shared = [[CCPhotos alloc] init];
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_async(queue, ^{
[shared loadPhotosArray];
});
}
return shared;
}
- (void) loadPhotosArray
{
NSLog(#"Loading photos");
_photos = [[NSMutableArray alloc] init];
NSData* data = [[NSUserDefaults standardUserDefaults] objectForKey: #"savedImages"];
if (data)
{
NSArray* storedUrls = [[NSArray alloc] initWithArray: [NSKeyedUnarchiver unarchiveObjectWithData: data]];
// reverse array
NSArray* urls = [[storedUrls reverseObjectEnumerator] allObjects];
for (NSURL* assetUrl in urls)
{
NSLog(#"Looking up %#", assetUrl);
// Block to handle image handling success
// This initializes, but doesn't get called
////-->>
ALAssetsLibraryAssetForURLResultBlock resultblock = ^(ALAsset *myasset)
{
ALAssetRepresentation *rep = [myasset defaultRepresentation];
CGImageRef iref = [rep fullScreenImage];
if (iref) {
UIImage* tempImage = [UIImage imageWithCGImage:iref];
UIImage* image = [[UIImage alloc] initWithCGImage: tempImage.CGImage scale: 1.0 orientation: UIImageOrientationUp];
// Set image in imageView
[_photos addObject: image];
NSLog(#"Added photo with url: %#", [rep url]);
[[NSNotificationCenter defaultCenter] postNotificationName: #"PhotosChanged" object: self];
}
};
// Handles failure of getting image
ALAssetsLibraryAccessFailureBlock failureblock = ^(NSError *myerror)
{
NSLog(#"Can't get image - %#",[myerror localizedDescription]);
};
// Load image then call appropriate block
ALAssetsLibrary* assetslibrary = [[ALAssetsLibrary alloc] init];
[assetslibrary assetForURL: assetUrl
resultBlock: resultblock
failureBlock: failureblock];
}
}
else
{
NSLog(#"Photo storage is empty");
}
}
I've narrowed down the problem to ALAssetsLibraryAssetForURLResultBlock resultblock which doesn't get called. Multiple threads spawn at the beginning, and each one gets to this line, initializes the result block, but doesn't call it. I think it has to do with the thread safety on the block. Any thoughts?
Try doing this when you post the notification:
dispatch_async(dispatch_get_main_queue(), ^{
[[NSNotificationCenter defaultCenter] postNotificationName: #"PhotosChanged" object: self];
});
I believe the reason it is not working for you is because this is not being called on the main thread. All UI changes/updates (which I am assuming this leads to) must be executed on the main thread. This will force the notification to happen on the necessary thread and should work properly!
This is a pretty common thing to do. I actually created a code snippet because I found myself typing this out so often:
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
<#on back thread#>
dispatch_async(dispatch_get_main_queue(), ^{
<#on main thread#>
});
});
I then set the completion short cut to dispatch_async
From what I'm understanding, you are spinning up multiple instances of your singleton. In that case you will probably want to put a #synchronized block in there. For example:
+ (CCPhotos*) sharedPhotos
{
static CCPhotos* shared = nil;
#synchronized ([CCPhotos class])
{
if (!shared)
{
shared = [[CCPhotos alloc] init];
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_async(queue, ^{
[shared loadPhotosArray];
});
}
}
return shared;
}
Hope that helps.