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.
Related
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 an app that has 2 views (table view and details view). I am retrieving data from MySql via PHP file using, the data load in the table fast. I do not know how to update the table! Lets say one of the users update his name in details view, as soon as he goes back to the table view it should changed.
Her is the code:
-(void)retrieveVideos {
NSString *myUrl = [NSString stringWithFormat:#"http://MyWebSite.com/phpfiles/data.php"];
NSURL *blogURL = [NSURL URLWithString:myUrl];
NSData *jsonData = [NSData dataWithContentsOfURL:blogURL];
NSError *error = nil;
NSDictionary *dataDictionary = [NSJSONSerialization
JSONObjectWithData:jsonData options:0 error:&error];
for (NSDictionary *bpDictionary in dataDictionary) {
ListOfObjects *list = [[ListOfObjects alloc]initWithVTheIndex:[bpDictionary objectForKey:#"TheIndex"] timeLineVideoUserName:[bpDictionary objectForKey:#"timeLineVideoUserName"] timeLineVideoDetails:[bpDictionary objectForKey:#"timeLineVideoDetails"] timeLineVideoDate:[bpDictionary objectForKey:#"timeLineVideoDate"] timeLineVideoTime:[bpDictionary objectForKey:#"timeLineVideoTime"] timeLineVideoLink:[bpDictionary objectForKey:#"timeLineVideoLink"] timeLineVideoLikes:[bpDictionary objectForKey:#"timeLineVideoLikes"] videoImage:[bpDictionary objectForKey:#"videoImage"] timeDeviceToken:[bpDictionary objectForKey:#"deviceToken"]];
[self.objectHolderArray addObject:list];
dispatch_async(dispatch_get_main_queue(), ^{
[spinner stopAnimating];
[UIApplication sharedApplication].networkActivityIndicatorVisible = FALSE;
});
}
if(jsonData != nil)
{
NSError *error = nil;
id result = [NSJSONSerialization JSONObjectWithData:jsonData options:NSJSONReadingMutableContainers error:&error];
if (error == nil)
NSLog(#"%#", result);
}
}
My question is how could I update the table with the new update?
Reload the data of tableview inside dispatch :
dispatch_async(dispatch_get_main_queue(), ^{
[spinner stopAnimating];
[UIApplication sharedApplication].networkActivityIndicatorVisible = FALSE;
[self.tableView reloadData];
});
you should notify about the update ,with Delegate or KVO (notification)
after notify about update you should update your data source ( self.objectHolderArray ) with new data, finally you must call :
[self.tableView reloadData];
or
[self.tableView reloadRowsAtIndexPaths:indexArray withRowAnimation:UITableViewRowAnimationFade];
Make sure that dataSource and delegate methods is set for tableview.
self.tableView.dataSource = self;
self.tableView.delegate = self;
To update UItable you can call tableview's reloadData method.
ex: [self.tableView reloadData];
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?
If I use the following way to get data, it works fine. The UICollectionView shows the items properly.
NSURL *articleUrl = [NSURL URLWithString:url];
NSData *articleHTMLData = [NSData dataWithContentsOfURL:articleUrl];
<Process data here>
....
_objects = newArticles;
_objects will be feed to UICollectionView.
However, if I use the following async way to get data, the UICollectionView does not show anything.
dispatch_queue_t myQueue = dispatch_queue_create("My Queue",NULL);
dispatch_async(myQueue, ^{
// Perform long running process
NSURL *articleUrl = [NSURL URLWithString:url];
_articleHTMLData = [NSData dataWithContentsOfURL:articleUrl];
dispatch_async(dispatch_get_main_queue(), ^{
<Process data here>
....
_objects = newArticles;
});
});
Did I miss something?
Thanks.
You need to manually refresh UICollectionView after receiving new data.
- (void) reloadData {
[self.collectionView performBatchUpdates:^{
[self.collectionView reloadSections:[NSIndexSet indexSetWithIndex:0]];
} completion:nil];
}
Call this method inside
dispatch_async(dispatch_get_main_queue(), ^{
<Process data here>
_objects = newArticles;
[self reloadData];
});
Where those lines of code are located? Inside viewDidLoad?
Any way, after you get your data you'll need to reload your UICollectionView
add:
[_yourCollectionView reloadData];
Right after:
_objects = newArticles;
Try to do Process in myQueue and only UI update in dispatch_get_main_queue
dispatch_queue_t myQueue = dispatch_queue_create("My Queue",NULL);
dispatch_async(myQueue, ^{
// Perform long running process
NSURL *articleUrl = [NSURL URLWithString:url];
_articleHTMLData = [NSData dataWithContentsOfURL:articleUrl];
<Process data here>
....
dispatch_async(dispatch_get_main_queue(), ^{
_objects = newArticles;
});
});
And i think you have to reload your view,
[CollectionView reloadData];
If i am wrong correct me .
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.