downloading data from several pffile's at once asynchronosly - ios

If I have an array of Message objects, each with a PFile containing data, is it possible to download the data for every single message by queuing them up asynchronously like so:
for (int i = 0; i < _downloadedMessages.count; i++) {
PFObject *tempMessage = (PFObject *)[_downloadedMessages objectAtIndex:i];
[[tempMessage objectForKey:#"audio"] getDataInBackgroundWithBlock:^(NSData *data, NSError *error) {
[self persistNewMessageWithData:data];
}];
}
This seems to cause my app to hang, even though this should be done in the background...
Using the solution below:
NSMutableArray* Objects = ...
[self forEachPFFileInArray:Objects retrieveDataWithCompletion:^BOOL(NSData* data, NSError*error){
if (data) {
PFObject *tempObj = (PFObject *)Object[someIndex...];
[self persistNewMessageWithData:data andOtherInformationFromObject:tempObj];
return YES;
}
else {
NSLog(#"Error: %#", error);
return NO; // stop iteration, optionally continue anyway
}
} completion:^(id result){
NSLog(#"Loop finished with result: %#", result);
}];

What you are possibly experiencing is, that for a large numbers of asynchronous requests which run concurrently, the system can choke due to memory pressure and due to network stalls or other accesses of resources that get exhausted (including CPU).
You can verify the occurrence of memory pressure using Instruments with the "Allocations" tool.
Internally (that is, in the Parse library and the system) there might be a variable set which sets the maximum number of network requests which can run concurrently. Nonetheless, in your for loop you enqueue ALL requests.
Depending of what enqueuing a request means in your case, this procedure isn't free at all. It may cost a significant amount of memory. In the worst case, the network request will be enqueued by the system, but the underlying network stack executes only a maximum number of concurrent requests. The other enqueued but pending requests hang there and wait for execution, while their network timeout is already running. This may lead to cancellation of pending events, since their timeout expired.
The simplest Solution
Well, the most obvious approach solving the above issues would be one which simply serializes all tasks. That is, it only starts the next asynchronous task when the previous has been finished (including the code in your completion handler). One can accomplish this using an asynchronous pattern which I name "asynchronous loop":
The "asynchronous loop" is asynchronous, and thus has a completion handler, which gets called when all iterations are finished.
typedef void (^loop_completion_handler_t)(id result);
typedef BOOL (^task_completion_t)(PFObject* object, NSData* data, NSError* error);
- (void) forEachObjectInArray:(NSMutableArray*) array
retrieveDataWithCompletion:(task_completion_t)taskCompletionHandler
completion:(loop_completion_handler_t)completionHandler
{
// first, check termination condition:
if ([array count] == 0) {
if (completionHandler) {
completionHandler(#"Finished");
}
return;
}
// handle current item:
PFObject* object = array[0];
[array removeObjectAtIndex:0];
PFFile* file = [object objectForKey:#"audio"];
if (file==nil) {
if (taskCompletionHandler) {
NSDictionary* userInfo = #{NSLocalizedFailureReasonErrorKey: #"file object is nil"}
NSError* error = [[NSError alloc] initWithDomain:#"RetrieveObject"
code:-1
userInfo:userInfo];
if (taskCompletionHandler(object, nil, error)) {
// dispatch asynchronously, thus invoking itself is not a recursion
dispatch_async(dispatch_get_global(0,0), ^{
[self forEachObjectInArray:array
retrieveDataWithCompletion:taskCompletionHandler
completionHandler:completionHandler];
});
}
else {
if (completionHandler) {
completionHandler(#"Interuppted");
}
}
}
}
else {
[file getDataInBackgroundWithBlock:^(NSData *data, NSError *error) {
BOOL doContinue = YES;
if (taskCompletionHandler) {
doContinue = taskCompletionHandler(object, data, error);
}
if (doContinue) {
// invoke itself (note this is not a recursion")
[self forEachObjectInArray:array
retrieveDataWithCompletion:taskCompletionHandler
completionHandler:completionHandler];
}
else {
if (completionHandler) {
completionHandler(#"Interuppted");
}
}
}];
}
}
Usage:
// Create a mutable array
NSMutableArray* objects = [_downloadedMessages mutableCopy];
[self forEachObjectInArray:objects
retrieveDataWithCompletion:^BOOL(PFObject* object, NSData* data, NSError* error){
if (error == nil) {
[self persistNewMessageWithData:data andOtherInformationFromObject:object];
return YES;
}
else {
NSLog(#"Error %#\nfor PFObject %# with data: %#", error, object, data);
return NO; // stop iteration, optionally continue anyway
}
} completion:^(id result){
NSLog(#"Loop finished with result: %#", result);
}];

Related

How to call same service multiple time and store data in ios

I have a situation where I will be getting more than 25000 records from web service, it is sending using pagination technique.
so the problem is I just want to store the data so for that I am thinking to run it in a loop but in future records may vary (i.e 30000,50000 etc)
from backend I am getting on each page 10000 records,but i dont know how many times i have run the loop so how do I handle this problem?
-(void)vendorsListCalling:(NSInteger)pageIndex{
[[ServicesHandler new] callVendorDetailsServiceWithParams:#{#"pageno":#(pageIndex)} CompletionBLock:^(NSDictionary *response, NSError *error) {
if (error) {
NSLog(#"error log %#",error.localizedDescription);
}else{
NSDictionary *dict = response[#"params"][#"data"];
[vendorDictionay addEntriesFromDictionary:dict];
pageCount++;
[[NSUserDefaults standardUserDefaults] setObject:vendorDictionay forKey:#"vendorsDict"];
}
}];
}
above block is where i stuck .
Any suggestions would be more appreciated.
You can store data into sqlite database. And for recursive calling for service, you can modify the same method as,
-(void)vendorsListCalling:(NSInteger)pageIndex {
if (!loader) {
//Write code to Show your loader here
}
[[ServicesHandler new] callVendorDetailsServiceWithParams:#{#"pageno":#(pageIndex)} CompletionBLock:^(NSDictionary *response, NSError *error) {
if (error) {
NSLog(#"error log %#",error.localizedDescription);
//If it fails you need to call the service again with the same Index
[self vendorsListCalling:pageCount];
} else {
if (!response[#"params"][#"data"]) {
//Stop loader since you didn't received any data
} else {
NSDictionary *dict = response[#"params"][#"data"];
[vendorDictionay addEntriesFromDictionary:dict];
pageCount++;
// Store Data in database here //
//Call service with incremented Index
[self vendorsListCalling:pageCount];
}
}
}];
}

nested callback hell in objective c

I am now working on an app that works with BLE, Backend server and location. I am facing a problem which I am not sure how to get out of which is what people call "Callback hell". The entire CoreBluetooth framework in iOS is based on a delegate pattern, which until you can use the CBPeripheral has to go to at least 3 callbacks:
DidConnectToPeripheral
DidDiscoverServices
DidDiscoverCharacteristics
But in fact there could be many more, and every action you take with the device will come back as a callback to one of those functions. Now when I want to "Rent" this ble product, I must connect to it, after connecting send a requests to the server and get the user's current location, after that all happens I have to write a value in the bluetooth device and get confirmation. This would not be so difficult, but unfortunately each and every one of those stages is failable, so error handling needs to be added. Not to mention implementing timeout.
I am sure I am not the only one to approach such issues so I looked around and I found 2 things that might help:
the Advanced NSOperations talk in the wwdc 2015, but after trying for 4 days to make it work, it seems like the code is too buggy.
Promisekit but I couldn't find a way to wrap CoreBluetooth.
How are people with even more complicated apps deal with this? in swift or objc.
Some sample problematic code:
-(void)startRentalSessionWithLock:(DORLock *)lock timeOut:(NSTimeInterval)timeout forSuccess:(void (^)(DORRentalSession * session))successBlock failure:(failureBlock_t)failureBlock{
//we set the block to determine what happens
NSAssert(lock.peripheral, #"lock has to have peripheral to connect to");
if (!self.rentalSession) {
self.rentalSession = [[DORRentalSession alloc] initWithLock:nil andSessionDict:#{} active:NO];
}
self.rentalSession.lock = lock;
[self connectToLock:self.rentalSession.lock.peripheral timeOut:timeout completionBlock:^(CBPeripheral *peripheral, NSError *error) {
self.BTConnectionCompleted = nil;
if (!error) {
[[INTULocationManager sharedInstance] requestLocationWithDesiredAccuracy:INTULocationAccuracyHouse timeout:1 delayUntilAuthorized:YES block:^(CLLocation *currentLocation, INTULocationAccuracy achievedAccuracy, INTULocationStatus status) {
if (status == INTULocationStatusSuccess || status == INTULocationStatusTimedOut) {
[self startServerRentalForSessionLockWithUserLocation:currentLocation.coordinate forSuccess:^(DORRentalSession *session) {
if (self.rentalSession.lock.peripheral && self.rentalSession.lock.peripheral.state == CBPeripheralStateConnected) {
[self.rentalSession.lock.peripheral setNotifyValue:YES forCharacteristic:self.rentalSession.lock.charectaristics.sensorCharacteristic];
}else{
//shouldnt come here
}
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
if (self.rentalSession.lock.peripheral.state == CBPeripheralStateConnected) {
!self.rentalSession.lock.open ? [self sendUnlockBLECommandToSessionLock] : nil;
if (successBlock) {
successBlock(session);
}
}else{
[self endCurrentRentalSessionWithLocation:self.rentalSession.lock.latLng andPositionAcc:#(1) Success:^(DORRentalSession *session) {
if (failureBlock) {
failureBlock([[NSError alloc] initWithDomain:DonkeyErrorDomain code:46 userInfo:#{NSLocalizedDescriptionKey:#"Could't connect to lock"}],200);
}
} failure:^(NSError *error, NSInteger httpCode) {
if (failureBlock) {
failureBlock([[NSError alloc] initWithDomain:DonkeyErrorDomain code:45 userInfo:#{NSLocalizedDescriptionKey:#"fatal error"}],200);
}
}];
}
});
} failure:^(NSError *error, NSInteger httpCode) {
if (failureBlock) {
failureBlock(error,httpCode);
}
}];
}else{
NSError *gpsError = [self donkeyGPSErrorWithINTULocationStatus:status];
if (failureBlock) {
failureBlock(gpsError,200);
}
}
}];
}else{
if (failureBlock) {
failureBlock(error,200);
}
}
}];
}
To get rid of this nested calls you can use GCD group + serial execution queue:
dispatch_queue_t queue = ddispatch_queue_create("com.example.queue", NULL);
dispatch_group_t group = dispatch_group_create();
// Add a task to the group
dispatch_group_async(group, queue, ^{
// Some asynchronous work
});
// Make dispatch_group_async and dispatch_group_sync calls here
// Callback to be executed when all scheduled tasks are completed.
dispatch_group_notify(serviceGroup,dispatch_get_main_queue(),^{
// Do smth when everything has finished
});
// wait for all tasks to complete
dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
The other solution based on GCD groups is described here

AWS SNS Unsubscribe loop is not releasing memory - Causing Jetsam to kill app

Problem
When I unsubscribe to my endpoints subscriptions, instruments memory graph goes from 32MB to 300MB and app is killed Jetsam. It was my understanding that AWS BFTasks clean up after themselves… If I remove the do...while and allow it to process the first 100 subscriptions, all works fine.
Process
I have about 8000 subscriptions in my development app that uses AWS SNS to push alerts to iOS devices and email. After the user selects the subscriptions they want, I remove their existing subscriptions, then subscribe them to the new ones. The [_awsSnsClient listSubscriptions:request] returns a max of 100 subscriptions, I loop through these, performing [_awsSnsClient unsubscribe:unsubscribeRequest] as needed. Then get the next 100...
Code
- (void) awsUsubscribeAllSubscriptions {
DLog(#"Removing existing subscriptions");
AWSSNSListSubscriptionsInput *request = [AWSSNSListSubscriptionsInput new];
__block Boolean sentLastUnsubscribe = NO; // YES when the last unsubscribe request was sent
__block int pendingUnsubscribe = 0; // Number of outsianding unsubscribes (async process not yet complete)
do {
[[[_awsSnsClient listSubscriptions:request] continueWithSuccessBlock:^id(BFTask *task) {
AWSSNSListSubscriptionsResponse *response = (AWSSNSListSubscriptionsResponse *)task.result;
NSString *nextToken = response.nextToken;
NSArray *subscriptions = response.subscriptions;
for (AWSSNSSubscription *subscription in subscriptions) {
if ([subscription.endpoint isEqualToString:_awsPlatformEndpoint.endpointArn]) {
AWSSNSUnsubscribeInput *unsubscribeRequest = [AWSSNSUnsubscribeInput new];
unsubscribeRequest.subscriptionArn = subscription.subscriptionArn;
pendingUnsubscribe++;
[[[_awsSnsClient unsubscribe:unsubscribeRequest] continueWithSuccessBlock:^id(BFTask *task) {
DLog(#"Unsubscribed from:%#",subscription.subscriptionArn);
return nil;
}] continueWithBlock:^id(BFTask *task) {
pendingUnsubscribe--;
if (task.error) {
// failed with error
ALog(#"Error unsubscribing to: %#, %#, %#", subscription, [task.error localizedDescription], [task.error localizedFailureReason]);
}
// If we have processed the last unsubscribe, then create topics and subscribe
if (sentLastUnsubscribe && (pendingUnsubscribe <= 0)) {
[self awsCreateTopicsAndSubscriptions];
}
return nil;
}];
}
}
request.nextToken = nextToken;
if(!nextToken) sentLastUnsubscribe = YES; // Reached the end of the SNS subscriptions
return nil;
}] continueWithBlock:^id(BFTask *task) {
if (task.error) {
// failed with error
ALog(#"Error listing subscriptions: %#, %#", [task.error localizedDescription], [task.error localizedFailureReason]);
}
return nil;
}];
} while (!sentLastUnsubscribe);
}
Is there a better way to remove all SNS subscriptions from an endpoint? Is there a way for force release any retained data during this loop?
Solution
Using both recursion and - waitUntilFinished. Memory load is now much lower.
-(void) awsUnsubscribeFromAllSubscriptionsWithNextToken:(NSString *)nextToken AndSubscribe:(BOOL)subscribe {
AWSSNSListSubscriptionsInput *request = [AWSSNSListSubscriptionsInput new];
request.nextToken = nextToken;
[[[_awsSnsClient listSubscriptions:request] continueWithSuccessBlock:^id(BFTask *task) {
AWSSNSListSubscriptionsResponse *response = (AWSSNSListSubscriptionsResponse *)task.result;
NSArray *subscriptions = response.subscriptions;
for (AWSSNSSubscription *subscription in subscriptions) {
if ([subscription.endpoint isEqualToString:_awsPlatformEndpoint.endpointArn]) {
DLog(#"Unsubscribe from %#", subscription.topicArn);
AWSSNSUnsubscribeInput *unsubscribeRequest = [AWSSNSUnsubscribeInput new];
unsubscribeRequest.subscriptionArn = subscription.subscriptionArn;
[[_awsSnsClient unsubscribe:unsubscribeRequest] waitUntilFinished];
}
}
if(response.nextToken) {
[self awsUnsubscribeFromAllSubscriptionsWithNextToken:response.nextToken AndSubscribe:subscribe];
} else if (subscribe) {
[self awsCreateTopicsAndSubscriptions];
}
return nil;
}] continueWithBlock:^id(BFTask *task) {
if (task.error) {
// failed with error
ALog(#"Error listing subscriptions: %#, %#", [task.error localizedDescription], [task.error localizedFailureReason]);
}
return nil;
}];
}
Please note that - listSubscriptions: is an asynchronous method and returns immediately. You are calling an async method in a for loop. It means you are potentially calling - listSubscriptions: hundreds or even thousands of times in a short period.
The easiest thing you can do to fix it is to call - waitUntilFinished at the end of the async block. It makes the entire method synchronous
However, in general, you should avoid calling - waitUntilFinished as much as possible. AWSKinesisRecorderTests.m has a function to call - getRecords: recursively. You can adopt a similar pattern.

only continue loop if method has finished

I have a for/in loop like so:
for(NSString *paymentId in success){
[self getPaymentDetails:paymentId];
}
The method getPaymentDetails is asynchronous. How do I create a completion block to only continue the for/in loop if the method has finished?
details
the method getPaymentDetails looks like this:
-(void)getPaymentDetails:(NSString *)paymentId{
PFUser *currentUser = [PFUser currentUser];
[PFCloud callFunctionInBackground:#"getpaymentdetails"
withParameters:#{#"objectid": paymentId, #"userid": currentUser.objectId}
block:^(NSDictionary *success, NSError *error) {
if(success){
NSDictionary *payment = success;
NSString *amount = [payment objectForKey:#"amount"];
if (![amount isKindOfClass:[NSNull class]]) {
[self.amountArray addObject:amount];
}
else {
[self.amountArray addObject:#""];
}
NSString *currency = [payment objectForKey:#"currency"];
if (![currency isKindOfClass:[NSNull class]]) {
[self.currencyArray addObject:currency];
}
else {
[self.currencyArray addObject:#""];
}
[self.tableView reloadData];
} else{
NSLog(#"Error logged getpaymentdetails: %#", error);
}
}];
}
The definition of "finished" is hereby defined when the amount as well as the currency has been stored in the array. Or in other words: when the code block has reached the end of the method for that specific paymentId
You can use semaphores for this kind of synchronisation. Semaphores are a basic building block in concurrency and provide among other things non-busy waiting. GCD provides semaphores through dispatch_semaphore-create, dispatch_semaphore_signal and dispatch_semaphore_wait.
In very general outline first create a semaphore:
dispatch_semaphore_t sema = dispatch_semaphore_create(0);
then in your loop wait on this semaphore:
for(NSString *paymentId in success)
{
[self getPaymentDetails:paymentId];
dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER); // wait for call to signal completion
}
and then at the appropriate place in your background block signal completion with:
dispatch_semaphore_signal(sema);
For more details on the API see the manual (man command), for semaphores find a book (or the internet).
HTH

Chaining dependent signals in ReactiveCocoa

In ReactiveCocoa, if we chaining several dependent signals, we must use subscribeNext: for the next signal in the chain to receive the value previous signal produced (for example, a result of an asynchronous operation). So after a while, the code turns into something like this (unnecessary details are omitted):
RACSignal *buttonClickSignal = [self.logIn rac_signalForControlEvents:UIControlEventTouchUpInside];
[buttonClickSignal subscribeNext:^(UIButton *sender) { // signal from a button click
// prepare data
RACSignal *loginSignal = [self logInWithUsername:username password:password]; // signal from the async network operation
[loginSignal subscribeNext:^void (NSDictionary *json) {
// do stuff with data received from the first network interaction, prepare some new data
RACSignal *playlistFetchSignal = [self fetchPlaylistForToken:token]; // another signal from the async network operation
[playlistFetchSignal subscribeNext:^(NSDictionary *json) {
// do more stuff with the returned data
}];
// etc
}];
}];
This ever-increasing nesting does not look much better than the non-reactive example that is given in the documentation:
[client logInWithSuccess:^{
[client loadCachedMessagesWithSuccess:^(NSArray *messages) {
[client fetchMessagesAfterMessage:messages.lastObject success:^(NSArray *nextMessages) {
NSLog(#"Fetched all messages.");
} failure:^(NSError *error) {
[self presentError:error];
}];
} failure:^(NSError *error) {
[self presentError:error];
}];
} failure:^(NSError *error) {
[self presentError:error];
}];
Am I missing something? Is there a better pattern of chaining dependent work in ReactiveCocoa?
This is when the RACStream and RACSignal operators start really coming in handy. In your particular example, you can use -flattenMap: to incorporate results into new signals:
[[[buttonClickSignal
flattenMap:^(UIButton *sender) {
// prepare 'username' and 'password'
return [self logInWithUsername:username password:password];
}]
flattenMap:^(NSDictionary *json) {
// prepare 'token'
return [self fetchPlaylistForToken:token];
}]
subscribeNext:^(NSDictionary *json) {
// do stuff with the returned playlist data
}];
If you don't need the results from any step, you can use -sequenceMany: or -sequenceNext: instead for a similar effect (but for a clearer expression of intent).

Resources