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.
Related
I'm using WKWebView in my app to get data from The HTML with javascript, I'm using this code:
NSString *jsCall = [NSString stringWithFormat:#"xxxxxx('%#');",html];
__block NSString *searchJson = nil;
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
dispatch_sync(dispatch_get_main_queue(), ^{
[self.searchWebView evaluateJavaScript:jsCall completionHandler:^(id result, NSError * _Nullable error) {
if (error || result == nil || ![result isKindOfClass:[NSString class]]) {
searchJson = nil;
} else {
searchJson = result;
}
dispatch_semaphore_signal(semaphore);
}];
});
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
if ([NSString stringIsNilOrEmpty:searchJson]) {
return nil;
}
return [self getResultsWithJSonString:searchJson];
I need to wait for the response so i'm using the semaphore_wait method, It's working fine but sometimes the searchJson is released from memory and in the [NSString stringIsNilOrEmpty:searchJson] I'm getting this error:
EXC_BAD_ACCESS KERN_INVALID_ADDRESS 0x0000000443439e70
Any idea of how to fix this released problem? I'm using Non-ARC in my project.
If it is non-ARC, the you must retain result explicitly (because callback might be in different thread), like
} else {
searchJson = [result retain];
}
and then, to avoid leak, use
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
[searchJson autorelease]; // << call it here, once !!
if ([NSString stringIsNilOrEmpty:searchJson]) {
return nil;
}
return [self getResultsWithJSonString:searchJson];
I need to send a large amount of HTTP request in a for loop, and once I finish a task, I need to update progress on main thread.
float __block progress = 0.0f;
float __block tempValue = 0.0f;
dispatch_queue_t operationQueue = dispatch_queue_create("Operation Data", DISPATCH_QUEUE_CONCURRENT);
dispatch_semaphore_t semaphore = dispatch_semaphore_create(1);
for (int uploadCount = 0; uploadCount < mutableArray.count; uploadCount++) {
dispatch_barrier_async(operationQueue, ^{
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
DDLogInfo(#"add Task.");
[MPRestAPI copyMediaToAlbum:mutableArray[uploadCount] targetAlbum:albumName success:^(BOOL bSuccess){
if (bSuccess) {
dispatch_async(dispatch_get_main_queue(), ^{
progress = (float)(uploadCount+1)/(float)mutableArray.count;
if(uploadCount+1 == mutableArray.count){
progressHUD.progress = 1.0;
[progressHUD hide:YES];
[self dismissViewControllerAnimated:NO completion:nil];
}else if(progress > tempValue) {
progressHUD.progress = progress;
tempValue = progress + 0.01;
}
});
}
}failure:^(NSError* error){
if(uploadCount == mutableArray.count -1){
[progressHUD hide:YES];
[self dismissViewControllerAnimated:NO completion:nil];
}
}];
});
dispatch_semaphore_signal(semaphore);
}
Then there comes low memory, and it crashed. and mutableArray.count can be 5000,so I wonder what should I do to fix this problem?
Below is the method with the code, where I am getting a memory leak using manual memory management. The memory leak is detected by using Xcode instruments and specifically points to the line where I am using NSJSONSerialization. I am running the target app (on a device with iOS 6.1).
The first time that i tap on the refreshButton there is no leak. Any subsequent tap generates the leak(and more leaks on top of that if i continue tapping the button). Below is the code - This is basic stuff for consuming JSON web services(the web service link is bogus but the real one that I am using works). You will notice that I am using Grand Central Dispatch so that I can update the UI without waiting for the parsing of the JSON to finish.
The line detected by instruments is surrounded by the asterisks. I would like to get some help to anyone who might have an idea of what is going on here. The full stack trace(as mentioned in the below comments i will put here:)
+(NSJSONSerialization JSONObjectWithData:option:error:] -> -[_NSJSONReader parseData:options:] -> -[_NSJSONReader parseUTF8JSONData:skipBytes:options]->newJSONValue->newJSONString->[NSPlaceholderString
initWithBytes:length:encoding:]
-(void)parseDictionary:(NSDictionary *)dictonary{
self.transactions = [dictonary objectForKey:#"transactions"];
if(!self.transactions){
NSLog(#"Expected 'transactions' array");
return;
}
for (int arrayIndex = 0; arrayIndex < [self.transactions count]; arrayIndex++) {
TransactionResult *result = [[[TransactionResult alloc] init] autorelease];
result.transactionID = [[self.transactions objectAtIndex:arrayIndex] objectForKey:#"ID"];
result.transactionDescription = [[self.transactions objectAtIndex:arrayIndex] objectForKey:#"description"];
result.transactionPrice = [[self.transactions objectAtIndex:arrayIndex] objectForKey:#"price"];
self.totalPrice += [result.transactionPrice doubleValue];
NSLog(#"total price: %f", self.totalPrice);
[self.transactionResults addObject:result];
result = nil;
}
}
- (IBAction)refreshButtonPressed:(UIBarButtonItem *)sender {
__block id resultObject;
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0),^{
NSURL *url = [NSURL URLWithString:#"http://mywebservice.php"];
NSData *data = [NSData dataWithContentsOfURL:url];
NSError *error;
***resultObject = [NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingAllowFragments error:&error];***
if(!error){
if([resultObject isKindOfClass:[NSDictionary class]]){
NSDictionary *dictonary = resultObject;
[self parseDictionary:dictonary];
NSLog(#"Done parsing!");
dispatch_async(dispatch_get_main_queue(), ^{
self.isLoading = NO;
[self.transactionsTableView reloadData];
});
}
else{
NSLog(#"JSON Error: Expected Dictionary");
resultObject = nil;
return;
}
}
else{
NSLog(#"JSON Error: %#", [error localizedDescription]);
dispatch_async(dispatch_get_main_queue(), ^{
resultObject = nil;
[self.transactionsTableView reloadData];
[self showError];
});
return;
}
});
}
I used a ARC as soon as it came out with 4.3, and put an app in the store with it - point being you could switch to ARC. That said, I tried to reproduce your problem by creating a class/file that has the no-arc flag applied to it, but cannot reproduce the problem. This makes me believe your problem is elsewhere. In the code below, I create a Test object in another file, retain it, and send it the test message. No matter what I set "i" to, it always deallocs the object:
#import "Tester.h"
#interface Obj : NSObject <NSObject>
#end
#implementation Obj
- (id)retain
{
NSLog(#"retain");
id i = [super retain];
return i;
}
- (oneway void)release
{
NSLog(#"release");
[super release];
}
- (void)foo
{
}
- (void)dealloc
{
NSLog(#"Obj dealloced");
[super dealloc];
}
#end
#implementation Tester
- (void)test
{
int i = 2;
__block Obj *obj;
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0),^{
obj = [[Obj new] autorelease];
if(i == 0) {
Obj *o = obj;
dispatch_async(dispatch_get_main_queue(), ^
{
[o foo];
} );
} else if(i == 1) {
obj = nil;
} else if(i == 2) {
dispatch_async(dispatch_get_main_queue(), ^
{
obj = nil;
} );
}
} );
}
#end
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.
I'm trying to write this method that returns an NSArray. My NSMutableArray (friendUsers) adds the objects right, but outside the dispatch_async the array is empty.
I try to add the users in the main queue ( as ashowed) but the array is empty to. Any ideas ? Thanks for all your help.
- (NSArray *)checkUsersInGroup {
NSMutableArray *friendUsers = [[NSMutableArray alloc] init];
dispatch_queue_t checkUSers = dispatch_queue_create("CheckUsers", NULL);
dispatch_async(checkUSers, ^{
NSArray *totalUsers = [VVDataRead lecturaDades];
NSArray *usersToSearch = [_grup objectForKey:#"groupFriends"];
for (NSString *tempUserId in usersToSearch){
for (NSDictionary *user in totalUsers){
NSString *id = [user objectForKey:#"id"];
if ([tempUserId isEqualToString:id])
dispatch_async(dispatch_get_main_queue(), ^{
[friendUsers addObject:user];
});
}
}
});
NSLog(#"people:%#",friendUsers);
return [friendUsers copy];
}
you can use blocks, it can make your life easier in this case.
- (void)checkUsersInGroupWithCompleteBlock:(void(^)(NSMutableArray * resultArray))completeBlock {
NSMutableArray *friendUsers = [[NSMutableArray alloc] init];
dispatch_queue_t checkUSers = dispatch_queue_create("CheckUsers", NULL);
dispatch_async(checkUSers, ^{
NSArray *totalUsers = [VVDataRead lecturaDades];
NSArray *usersToSearch = [_grup objectForKey:#"groupFriends"];
for (NSString *tempUserId in usersToSearch){
for (NSDictionary *user in totalUsers){
NSString *id = [user objectForKey:#"id"];
if ([tempUserId isEqualToString:id])
dispatch_async(dispatch_get_main_queue(), ^{
[friendUsers addObject:user];
});
}
}
// call the complete block with the result when you finished
if (completeBlock) completeBlock(friendUsers);
});
}
...and here is how you can call the method:
- (void)anyMethod {
// ... do whetever you want here before
[self checkUsersInGroupWithCompleteBlock:^(NSMutableArray *resultArray) {
NSLog(#"%#", resultArray);
}];
// ... or after
}
EDITED:
NOTE: here is another possible solution, but in your case it just suspends the main thread (which is definitely bad), so you won't gain anything with this solution but pain on the main thread, but if you are on two background threads, this solution can give a very nice example of synchronisation between the threads.
- (NSArray *)checkUsersInGroup {
NSMutableArray *friendUsers = [[NSMutableArray alloc] init];
// our semaphore is here
dispatch_semaphore_t _semaphore = dispatch_semaphore_create(0);
dispatch_queue_t checkUSers = dispatch_queue_create("CheckUsers", NULL);
dispatch_async(checkUSers, ^{
NSArray *totalUsers = [VVDataRead lecturaDades];
NSArray *usersToSearch = [_grup objectForKey:#"groupFriends"];
for (NSString *tempUserId in usersToSearch){
for (NSDictionary *user in totalUsers){
NSString *id = [user objectForKey:#"id"];
if ([tempUserId isEqualToString:id])
dispatch_async(dispatch_get_main_queue(), ^{
[friendUsers addObject:user];
});
}
}
// the process finished
dispatch_semaphore_signal(_semaphore);
});
// ... we are wainitng for the semaphore's signal
dispatch_semaphore_wait(_semaphore, DISPATCH_TIME_FOREVER);
dispatch_release(_semaphore);
NSLog(#"people:%#",friendUsers);
return [friendUsers copy];
}
There are number of strategies to solve this, but as your operation happens on a background thread, returning the array isn't one of them. You could use NSNotificationCenter to signal that the task as finished and read the array. i.e.
- (void)checkUsersInGroup {
NSMutableArray *friendUsers = [[NSMutableArray alloc] init];
dispatch_queue_t checkUSers = dispatch_queue_create("CheckUsers", NULL);
dispatch_async(checkUSers, ^{
NSArray *totalUsers = [VVDataRead lecturaDades];
NSArray *usersToSearch = [_grup objectForKey:#"groupFriends"];
for (NSString *tempUserId in usersToSearch){
for (NSDictionary *user in totalUsers){
NSString *id = [user objectForKey:#"id"];
if ([tempUserId isEqualToString:id])
dispatch_async(dispatch_get_main_queue(), ^{
[friendUsers addObject:user];
});
}
}
// Signal background task is finished
// Make sure to add an observer to this notification
[[NSNotificationCenter defaultCenter] postNotificationName:#"friendsAddLiteral"
object:nil];
});
}
//this method will respond to the notification
- (void) onFriendsAdded:(NSNotification*)notif {
//do something on the main thread
}