ios - Program with block executed out of order? - ios

I'm trying to get an array of urls from my backend.
I use AFNetworking and I have a HTTPUtil class implemented as singleton to handle my requests.
HTTPUtil.m
#import "HTTPUtil.h"
#implementation HTTPUtil
+(instancetype)sharedInstance{
NSLog(#"sharedInstance"); //to check the order
static HTTPUtil* manager;
static dispatch_once_t once;
dispatch_once(&once, ^{
manager = [[HTTPUtil alloc] init];
});
manager.responseSerializer = [AFJSONResponseSerializer serializer];
manager.requestSerializer = [AFJSONRequestSerializer serializer];
return manager;
}
-(void)getImageArrayFromURL:(NSString *)url success:(void(^)(NSArray* array))success failure:(void(^)(NSError* error))failure{
NSLog(#"getting..."); //to check the order
[self GET:url parameters:nil progress:nil success:^(NSURLSessionDataTask* task, id response){
NSLog(#"Response: %#", response);
NSString* imgStr = [[response objectForKey:kResponseDataKey] objectForKey:#"img"];
//convert nsstring to nsarray
NSArray* array = [StringUtil arrayFromString:imgStr];
//construct urls
NSMutableArray* ret = [[NSMutableArray alloc] init];
NSMutableString* url;
for (NSString* rawStr in array) {
url = [NSMutableString stringWithString:kUrlBase];
[url appendString:[rawStr stringByReplacingOccurrencesOfString:#"/" withString:#"+"]];
[ret addObject:url];
}
success(ret);
}failure:^(NSURLSessionDataTask* task, NSError* error){
NSLog(#"Error: %#", error);
failure(error);
}];
}
In my view controller, I call the method to fetch the array.
_vCycleScrollView = [SDCycleScrollView cycleScrollViewWithFrame:CGRectMake(0, 0, 0, 0) delegate:self placeholderImage:[UIImage imageNamed:#"checked"]];
NSMutableString* url = [NSMutableString stringWithString:kUrlBase];
[url appendString:#"activityImgArray"];
//
__block NSArray* imgarr;
[[HTTPUtil sharedInstance] getImageArrayFromURL:url success:^(NSArray* array){
imgarr = [NSArray arrayWithArray:array];
}failure:^(NSError* error){
NSLog(#"%#", error);
}];
NSLog(#"adding...");
_vCycleScrollView.imageURLStringsGroup = imgarr;
[self.view addSubview:_vCycleScrollView];
[_vCycleScrollView mas_makeConstraints:^(MASConstraintMaker* make){
make.top.equalTo(self.view);
make.left.equalTo(self.view);
make.right.equalTo(self.view);
make.height.mas_equalTo(180);
make.width.equalTo(self.view.mas_width);
}];
In the console, I got
2016-05-20 14:41:19.411 SCUxCHG[10470:4909076] sharedInstance
2016-05-20 14:41:19.415 SCUxCHG[10470:4909076] getting...
2016-05-20 14:41:19.417 SCUxCHG[10470:4909076] adding...
2016-05-20 14:41:19.591 SCUxCHG[10470:4909076]
Response: {
data = {
img = "[activity/test1, acti/1]";
};
message = success;
result = 0;
}
I thought imgArr should be assigned in the success block and it shouldn't be nil when I assign it to _vCycleScrollView.imageURLStringsGroup.
However, I can tell from the output in the console that the HTTP request is sent after NSLog(#"adding..."); and that leads to the fact that imgArr is still nil when _vCycleScrollView.imageURLStringsGroup = imgarr; is executed.
Why is that?

Yes below code is in block so this will continue in background
[[HTTPUtil sharedInstance] getImageArrayFromURL:url success:^(NSArray* array){
imgarr = [NSArray arrayWithArray:array];
}failure:^(NSError* error){
NSLog(#"%#", error);
}];
solution - You should add _vCycleScrollView.imageURLStringsGroup = imgarr; inside of success block because you d0 not know when it will completed Or there is another way you should not call in block or should not create block.

Try bellow:
__block NSArray* imgarr;
[[HTTPUtil sharedInstance] getImageArrayFromURL:url success:^(NSArray* array){
imgarr = [NSArray arrayWithArray:array];
NSLog(#"adding...");
_vCycleScrollView.imageURLStringsGroup = imgarr;
}failure:^(NSError* error){
NSLog(#"%#", error);
}];
The completion block is executed once data is fetched.
In your case code continues to execute after the completion block is set but data hasn't been fetched yet, that's why imgarr is nil.

That's the whole idea: That blocks are executed out of order. The trick is that you don't wait for a block to finish. Instead, the block finishes and then it does what is needed. The code in your viewcontroller isn't going to work, can't work, and we don't want it to work. Instead, the callback block deposits the image somewhere, and then tells the tableview to reload the row.

Related

How to modify a non-local (global) variable from inside of a block?

I am quite new to Objective-C & have to dynamically change the value of #property (strong, nonatomic) NSMutableArray *allCategories from inside of AFHTTPRequestOperationManager in success block.
[self.allCategories addObject:tempObject]; doesn't change the value of allCategories while iterating in a loop.
The variable has been initialized as self.allCategories = [[NSMutableArray alloc]init]; in viewDidLoad.
I have also tried creating a temporary variable as __block NSMutableArray *tempCategories = [[NSMutableArray alloc]init]; before initiating AFHTTPRequestOperationManager object. tempCategories doesn't even retain its value.Can't figure out what's happening.EditSorry for inconvenienceviewDidLoad has the following code self.allCategories = [[NSMutableArray alloc]init];[self loadData];Here's the code
-(NSMutableArray *)loadData
{
__block NSMutableArray *tempCategories = [[NSMutableArray alloc]init];
manager = [AFHTTPRequestOperationManager manager];
[manager GET:kAPICategoryList
parameters:nil
success:^(AFHTTPRequestOperation *operation, id responseObject) {
// downcast id to NSMutableDictionary
NSMutableDictionary *json = (NSMutableDictionary *)responseObject;
// check if dictionary is non nil has at least 1 element
if (json != nil && [json count] >= 1) {
// NSLog(#"json:\t%#", json);
// check json is non nil & has success message
if ([json objectForKey:kAPIKeyCategoryRoot] != nil) {
NSArray *arrCategoriesRoot = [json objectForKey:kAPIKeyCategoryRoot];
// check categories has some data
if (arrCategoriesRoot.count >= 1) {
for (int i = 0; i < arrCategoriesRoot.count; i++) {
SomeModel *pCategory;
NSDictionary *dctCategorySingle = [arrCategoriesRoot objectAtIndex:i];
// check category has sub category
if ([dctCategorySingle objectForKey:kAPIKeyCategorySubCategory] != nil) {
// create category with sub category
pCategory = [[SomeModel alloc]initWithSubCategorisedCategoryID:[dctCategorySingle objectForKey:kAPIKeyCategoryID]
name:[dctCategorySingle objectForKey:kAPIKeyCategoryName]
image:kIMGCategoryDefault
subCategory:[dctCategorySingle objectForKey:kAPIKeyCategorySubCategory]];
} else{
// create just a category
pCategory = [[SomeModel alloc]initWithCategoryID:[dctCategorySingle objectForKey:kAPIKeyCategoryID]
name:[dctCategorySingle objectForKey:kAPIKeyCategoryName]
image:kIMGCategoryDefault];
} // else just
[tempCategories addObject:pCategory];
[_allCategories addObject:pCategory];
} // for
NSLog(#"categories count %lu", [self.allCategories count]);
} // if count >= 1
}
else if ([json objectForKey:kAPIRespMsgCategoryFetchErrKey] != nil) {
[Utility showAlertWithTitle:kAPIRespMsgCategoryFetchErrKey
message:[json objectForKey:kAPIRespMsgCategoryFetchErrVal]
button:kMsgButtonOkayTtl];
}
} else {
// error in login => enable login
NSLog(#"%#", kMsgNetworkEmptyJSON);
}
}
// network error
failure:^(AFHTTPRequestOperation *operation, NSError *error) {
NSLog(#"error %#", [error localizedDescription]);
}];
NSLog(#"tempCategories count %lu", [tempCategories count]);
return tempCategories;
}
Here's the output form NSLog:2015-03-19 18:27:17.845 MyProject[4011:121268] viewDidLoad
2015-03-19 18:27:18.133 MyProject[4011:121268] tempCategories count 0
2015-03-19 18:27:18.136 MyProject[4011:121268] numberOfRowsInSection count 0
2015-03-19 18:27:18.137 MyProject[4011:121268] numberOfRowsInSection count 0
2015-03-19 18:27:19.019 MyProject[4011:121268] categories count 20when loadData finishes allCategories has not data in it (nil).
As far as I know it should work that way.. are you sure your success block is being called before you check the content of allCategories?
A success block work asynchronously, which means it will be executed only when the RequestOperationis completed (which can take a long time if you're downloading something big)
If you are trying to get the value of allCategories before the success block is executed you won't get what you're expecting. I would recommend using breakpoints or NSLog on your success block to see if it's been executed when you think it's doing it.
e.g
...
successBlock:^(AFHTTPRequestOperation *operation, id responseObject)
{
NSLog(#"Success");
[self.allCategories addObject:tempObject]
}]; //End of request
[operation start]; //Begin executing the AFHTTPOperation
NSLog("%#",self.allCategories.description); //probably nil or empty
//since the success block hasn't been called yet
EDIT:
As I though, you are returning a value before is been set by the async operation, to return a value from an async operation I would suggest take a look to this answer and this one . Also you should read a bit of how async task work.
Basically what you want to do with async operations/tasks is make sure the value will be available when you want to use it. The main issue with that is that you don't know when the value will be set, but you can make sure what you want to do whenever it's set.
To do that you can create a simple method with a custom completion block
- (void)myCustomMethodWithCompletionBlock: (void (^)(NSArray *))completion {
//Do your request
//...
successBlock:^(AFHTTPRequestOperation *operation, id responseObject)
{
NSLog(#"Success");
completionBlock(allCategories);
}]; //End of request
}
Meanwhile in your main method you call
[self myCustomMethodWithCompletionBlock:^(NSArray *allCategories) {
self.allCategories = allCategories;
//Do other stuff you need to with that variable since now you are
//sure the value will be set unless the operation failed
}];
I had the same problem a few days ago. My problem was my array seems nil, array allocations in viewdidload method may be your request run before viewDidLoad. Check it with debug if you see the array is nill then alloc array different place.
P.S: I m not expert but may be it's the same problem with me.
Try this:
dispatch_async(dispatch_get_main_queue(), ^{
[self.allCategories addObject:tempObject];
});
Define NSMutableArray with following line.
#property (nonatomic, strong) NSMutableArray * arrData;
initializein viewDidLoad
- (void)viewDidLoad {
[super viewDidLoad];
self.arrData = [NSMutableArray array];
}
call following method with any UIButton action for see output OR working behavior
- (void) TestMethod {
dispatch_queue_t queue = dispatch_queue_create("myQueue", 0);
dispatch_async(queue, ^{
AFHTTPClient *httpClient = [[AFHTTPClient alloc] initWithBaseURL:[NSURL urlWithEncoding:#"https://www.google.co.in/?gws_rd=ssl"]];
[httpClient registerHTTPOperationClass:[AFJSONRequestOperation class]];
[httpClient setDefaultHeader:#"Accept" value:#"application/json"];
[httpClient setParameterEncoding:AFJSONParameterEncoding];
NSMutableURLRequest *request = [httpClient requestWithMethod:#"GET" path:#"" parameters:nil];
[request setTimeoutInterval:180];
[AFJSONRequestOperation addAcceptableContentTypes:[NSSet setWithObject:#"text/html"]];
dispatch_semaphore_t sema = dispatch_semaphore_create(0);
AFJSONRequestOperation *operation = [AFJSONRequestOperation JSONRequestOperationWithRequest:request success:^(NSURLRequest *request, NSHTTPURLResponse *response, id JSON)
{
[self.arrData addObject:[NSDictionary dictionaryWithObjectsAndKeys:#"test",#"t3da",#"adsf",#"afds", nil]];
dispatch_semaphore_signal(sema);
} failure:^ (NSURLRequest *request, NSURLResponse *response, NSError *error, id json){
[self.arrData addObject:[NSDictionary dictionaryWithObjectsAndKeys:#"test",#"t3da",#"adsf",#"afds", nil]];
dispatch_semaphore_signal(sema);
}];
[operation start];
dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER);
DLog(#"arrData = %#",self.arrData);
});
}

How to Fetch data out from block in AFNetworking ios?

Creating first app with webservices, I am using AFNetworking for webservices. Everything is working fine but i have no idea , that how to fetch data out from block which i am getting in response. This is what i have done so far
+(WebServices *)sharedManager{
static WebServices *managerServices = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
managerServices = [[self alloc] init];
});
return managerServices;
}
-(NSArray *)firstPostService{
//1
NSURL *url = [NSURL URLWithString:BaseURLString];
//2
AFHTTPSessionManager *manager = [[AFHTTPSessionManager alloc] initWithBaseURL:url];
manager.responseSerializer = [AFHTTPResponseSerializer serializer];
NSDictionary *param = #{#"request" : #"get_pull_down_menu" , #"data" : #"0,0,3,1"};
[manager POST:#"person.php" parameters:param success:^(NSURLSessionDataTask *task, id responseObject) {
[self methodUsingJsonFromSuccessBlock:responseObject];
} failure:^(NSURLSessionDataTask *task, NSError *error) {
UIAlertView *av = [[UIAlertView alloc] initWithTitle:#"Error retrieving data" message:[error localizedDescription] delegate:nil cancelButtonTitle:#"Ok" otherButtonTitles:nil, nil];
[av show];
}];
if (list.count == 0) {
NSLog(#"Nothing in array yet!!");
}
else{
NSLog(#"Object 1 is : %#", [list objectAtIndex:1]);
}
return list;
}
- (void)methodUsingJsonFromSuccessBlock:(id)json {
// use the json
NSString *string = [NSString stringWithUTF8String:[json bytes]];
NSLog(#"This is data : %#", string);
list = [string componentsSeparatedByString:#"\n"];
NSLog(#"After sepration first object: %#", [list objectAtIndex:1]);
//NSLog(#"json from the block : %#", json);
}
What i understand reading from different blogs and tuts, that block is a separate thread and what every i do finishes with it. I read some where that this is normally use for it
dispatch_async(dispatch_get_main_queue(), ^{
data = [string componentsSeparatedByString:#"\n"];
//WHERE DATA IS __block NSArray * data = [[NSArray alloc] init];
});
and i was returning it in the of the function(firstPostService) but nothing happen. i still get an empty array outside the block. Kindly help me , suggest me some good reading stuff. Thanking you all in advance.
You say:
I need this data to my view controller i am trying to return in dispatch part but it is not allowing. Is it possible to get data into my viewcontroller class ?
Yes, it's possible. But, no, firstPostService should not return the results. It can't because it returns immediately, but the POST completion blocks won't be called until much later. There's nothing to return by the time firstPostService returns.
At the end of your original question, you said:
What i understand reading from different blogs and tuts, that block is a separate thread and what every i do finishes with it. I read some where that this is normally use for it
dispatch_async(dispatch_get_main_queue(), ^{
data = [string componentsSeparatedByString:#"\n"];
//WHERE DATA IS __block NSArray * data = [[NSArray alloc] init];
});
This is not the appropriate pattern of __block local variable. You generally use that __block pattern when dealing with some block that runs synchronously (for example the block of an enumeration method). But while you can use __block variable with asynchronous block, you almost never do (and it doesn't quite make sense to even try to do it). When you use appropriate completion block patterns, there's no need for any __block variable.
So, let's go back to your original code sample: So, you should take a page from AFNetworking and employ completion blocks yourself. When the AFNetworking POST method wanted to return data to your code asynchonously, it used a completion block pattern, instead. Thus, if your own firstPostService wants to pass back data asynchronously, it should do the same.
For example:
#interface WebServices ()
#property (nonatomic, strong) AFHTTPSessionManager *manager;
#end
#implementation WebServices
// note, use `instancetype` rather than actually referring to WebServices
// in the `sharedManager` method
+ (instancetype)sharedManager
{
static id sharedMyManager = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
sharedMyManager = [[self alloc] init];
});
return sharedMyManager;
}
// I'd also suggest that you init the `AFHTTPSessionManager` only once when this
// object is first instantiated, rather than doing it when `firstPostService` is
// called
- (instancetype)init
{
self = [super init];
if (self) {
NSURL *url = [NSURL URLWithString:BaseURLString];
self.manager = [[AFHTTPSessionManager alloc] initWithBaseURL:url];
self.manager.responseSerializer = [AFHTTPResponseSerializer serializer];
}
return self;
}
// Notice:
//
// 1. This now has a return type of `void`, because when it instantly returns,
// there is no data to return.
//
// 2. In order to pass the data back, we use the "completion handler" pattern.
- (void)firstPostServiceWithCompletionHandler:(void (^)(NSArray *list, NSError *error))completionHandler {
NSDictionary *param = #{#"request" : #"get_pull_down_menu" , #"data" : #"0,0,3,1"};
[self.manager POST:#"person.php" parameters:param success:^(NSURLSessionDataTask *task, id responseObject) {
NSArray *list = [self methodUsingJsonFromSuccessBlock:responseObject];
if (completionHandler) {
completionHandler(list, nil);
}
} failure:^(NSURLSessionDataTask *task, NSError *error) {
[[[UIAlertView alloc] initWithTitle:#"Error retrieving data" message:[error localizedDescription] delegate:nil cancelButtonTitle:#"Ok" otherButtonTitles:nil, nil] show];
if (completionHandler) {
completionHandler(nil, error);
}
}];
// // none of this code belongs here!!! You are dealing with asynchronous methods.
// // the `list` has not been returned by the time you get here!!! You shouldn't even
// // be using instance variable anyway!
//
// if (list.count == 0) {
//
// NSLog(#"Nothing in array yet!!");
// }
// else{
// NSLog(#"Object 1 is : %#", [list objectAtIndex:1]);
//
// }
// return list;
}
- (NSArray *)methodUsingJsonFromSuccessBlock:(NSData *)data {
// note, do not use `stringWithUTF8String` with the `bytes` of the `NSData`
// this is the right way to convert `NSData` to `NSString`:
NSString *string = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
NSLog(#"This is string representation of the data : %#", string);
// Note, retire the `list` instance variable, and instead use a local variable
NSArray *list = [string componentsSeparatedByString:#"\n"];
NSLog(#"After sepration first object: %#", [list objectAtIndex:1]);
return list;
}
#end
Then, you could invoke that like so:
[[WebServices sharedManager] firstPostServiceWithCompletionHandler:^(NSArray *list, NSError *error) {
if (error) {
// handle the error here
} else {
// use the `list` results here
}
}];
// NOTE, DO NOT USE `list` HERE. By the time you get here, `list` has not been
// returned. Only use it in the above block.
//
// In fact, you can see that if you put a `NSLog` or breakpoint here, and again, above
// where it says "use the `list` results` here", you'll see that it's running the code
// inside that block _after_ this code down here!
I'd suggest you tackle the above first, to first make sure you completely understand the proper asynchronous technique of the completion block pattern. We don't want to complicate things quite yet. Make sure you're getting the sort of data you wanted before you proceed to what I will describe below.
But, once you've grokked the above, it's time to look at your JSON parsing. You make several reference to JSON, but if that's what it really is, then using componentsSeparatedByString is not the right way to parse it. You should use NSJSONSerialization. Or even better, you can let AFNetworking do that for you (right now, you're making it more complicated than it needs to be and your results will not be formatted correctly).
Above, I kept your methodUsingJsonFromSuccessBlock in the process, but if you're really dealing with JSON, you should eliminate that method entirely. Let AFNetworking do this for you.
You should eliminate the line that says:
responseSerializer = [AFHTTPResponseSerializer serializer];
The default serializer is AFJSONResponseSerializer which is what you want to use if handling JSON requests.
The methodUsingJsonFromSuccessBlock is then no longer needed because AFNetworking will do the JSON conversion for you. So firstPostServiceWithCompletionHandler should look like:
- (void)firstPostServiceWithCompletionHandler:(void (^)(NSArray *list, NSError *error))completionHandler {
NSDictionary *param = #{#"request" : #"get_pull_down_menu" , #"data" : #"0,0,3,1"};
[self.manager POST:#"person.php" parameters:param success:^(NSURLSessionDataTask *task, id responseObject) {
if (completionHandler) {
completionHandler(responseObject, nil);
}
} failure:^(NSURLSessionDataTask *task, NSError *error) {
[[[UIAlertView alloc] initWithTitle:#"Error retrieving data" message:[error localizedDescription] delegate:nil cancelButtonTitle:#"Ok" otherButtonTitles:nil, nil] show];
if (completionHandler) {
completionHandler(nil, error);
}
}];
}

No shared download link from boxItem in BoxFolderPickerViewController

How can I get the download_url or shared link from the item returned below? The file id and name, size and date modified are available, but my requirement is to return a download_url or shared link.
- (void)folderPickerController:(BoxFolderPickerViewController *)controller didSelectBoxItem:(BoxItem *)item
{
[self dismissViewControllerAnimated:YES completion:^{
// NSLog("%#",item);
// NSLog(#"%#", item.rawResponseJSON);
if ([BoxSDK sharedSDK].OAuth2Session.isAuthorized){
NSLog(#"authorized");
BoxAPIJSONFailureBlock failure = ^(NSURLRequest *request, NSHTTPURLResponse *response, NSError *error, NSDictionary *JSONDictionary){
[self boxError:error];
};
NSDictionary *const queryParametersDictionary = #{#"fields" : #"item_collection"};
BoxFilesRequestBuilder *builder = [[BoxFilesRequestBuilder alloc] initWithQueryStringParameters:queryParametersDictionary];
BoxAPIJSONOperation *operation= [[BoxSDK sharedSDK].filesManager fileInfoWithID:item.modelID
requestBuilder:builder
success:nil
failure:failure];
}else{NSLog(#"fail");}
}];
Not sure why you'd need download_url. Do you want to expose it to some other app or user?
If you don't see shared_link in your item rawResponseJSON (debugger: po [file rawResponseJSON][#"shared_link"]), then it is highly likely that file is not shared. You need to share it first.
Here is how you do it, given you already have BoxItem
BoxFileBlock success = ^(BoxFile *file) {
dispatch_async(dispatch_get_main_queue(), ^{
self.item = file;
NSDictionary *sharedLinkInfo = [self.item sharedLink];
// here is your urlString of the sharedLink
NSString *urlString = [sharedLinkInfo objectForKey:#"url"];
});
};
BoxFilesRequestBuilder *builder = [[BoxFilesRequestBuilder alloc] init];
builder.sharedLink = [[BoxSharedObjectBuilder alloc] init];
builder.sharedLink.access = BoxAPISharedObjectAccessOpen;
builder.sharedLink.canDownload = BoxAPISharedObjectPermissionStateEnabled;
builder.sharedLink.canPreview = BoxAPISharedObjectPermissionStateEnabled;
[self.sdk.filesManager editFileWithID:self.item.modelID
requestBuilder:builder
success:success
failure:failure];
Ideally you also want to provide failure block (omitted here)
Also make sure that you're using latest SDK. It had a small bug fix for sharedObjectFileBuilder that went in yesterday.

AFHTTPSessionManager with multiple requests in rapid succession (AFNetworking 2.0)

i am new to iOS programming, still learning.
EDIT: !!!!!! Everything in my code works. My question is about the delegation pattern i use,
if i am generating problems in the background that i have no idea of, or if there is a better way to handle my situation in AFNetworking...
I have created an API for my app by subclassing AFHTTPSessionManager.
My API creates a singleton and returns it and supplies public functions for various requests. And those functions create parameter lists, and make GET requests on the server like this:
- (void)getCharacterListForKeyID:(NSString *)keyID vCode:(NSString *)vCode sender:(id)delegate
{
NSMutableDictionary *parameters = [NSMutableDictionary dictionary];
parameters[#"keyID"] = keyID;
parameters[#"vCode"] = vCode;
[self GET:#"account/Characters.xml.aspx" parameters:parameters success:^(NSURLSessionDataTask *task, id responseObject) {
self.xmlWholeData = [NSMutableDictionary dictionary];
self.errorDictionary = [NSMutableDictionary dictionary];
NSXMLParser *XMLParser = (NSXMLParser *)responseObject;
[XMLParser setShouldProcessNamespaces:YES];
XMLParser.delegate = self;
[XMLParser parse];
if ([delegate respondsToSelector:#selector(EVEAPIHTTPClient:didHTTPRequestWithResult:)]) {
[delegate EVEAPIHTTPClient:self didHTTPRequestWithResult:self.xmlWholeData];
}
} failure:^(NSURLSessionDataTask *task, NSError *error) {
if ([delegate respondsToSelector:#selector(EVEAPIHTTPClient:didFailWithError:)]) {
[delegate EVEAPIHTTPClient:self didFailWithError:error];
}
}];
}
I was using a normal protocol/delegate method earlier. But once i make calls this API more than once like this: (IT WAS LIKE THIS:)
EVEAPIHTTPClient *client = [EVEAPIHTTPClient sharedEVEAPIHTTPClient];
client.delegate = self;
[client getCharacterListForKeyID:self.keyID vCode:self.vCode];
Previous call's delegate was being overwritten by next. So i changed to above style. Passing sender as an argument in the function:
EVEAPIHTTPClient *client = [EVEAPIHTTPClient sharedEVEAPIHTTPClient];
[client getCharacterListForKeyID:self.keyID vCode:self.vCode sender:self];
And i pass this sender to GET request's success and failure blocks.
What i wonder is : "Is this a good programming practice ?". Passing objects to blocks like this should be avoided if possible ? Is there any other more elegant way in AFHTTPSessionManager to handle this type of work (making same GET request over and over with different parameters and returning results to the respective request owners) more elegantly ?
Delegation pattern falters when it comes to simplicity and asynchronous request processing. You should be using blocks, here's an example
Your server class:
static NSString *const kNews = #"user_news/"; // somewhere above the #implementation
- (NSURLSessionDataTask *)newsWithPage:(NSNumber *)page
lastNewsID:(NSNumber *)lastNewsID
completion:(void (^)(NSString *errMsg, NSArray *news, NSNumber *nextPage))completionBlock {
return [self GET:kNews
parameters:#{#"page" : page,
#"news_id" : lastNewsID
}
success:^(NSURLSessionDataTask *task, id responseObject) {
NSArray *news = nil;
NSNumber *nextPage = nil;
NSString *errors = [self errors:responseObject[#"errors"]]; // process errors
if ([responseObject[#"status"] boolValue]) {
news = responseObject[#"news"];
nextPage = responseObject[#"next_page"];
[self assignToken];
}
completionBlock(errors, news, nextPage);
}
failure:^(NSURLSessionDataTask *task, NSError *error) {
NSString *errors = [self errors:error];
completionBlock(errors, nil, nil);
}];
}
The caller
- (void)dealloc {
[_task cancel]; // you don't want this task to execute if user suddenly removes your controller from the navigation controller's stack
}
- (void)requestNews {
typeof(self) __weak wself = self; // to avoid the retain cycle
self.task = [[GSGServer sharedInstance] newsWithPage:self.page
lastNewsID:self.lastNewsID
completion:^(NSString *errMsg, NSArray *news, NSNumber *nextPage) {
if (errMsg) {
[GSGAppDelegate alertQuick:errMsg]; // shortcut for posting UIAlertView, uses errMsg for message and "Error" as a title
return;
}
[wself.news addObjectsFromArray:news];
wself.lastNewsID = [wself.news firstObject][#"id"];
wself.page = nextPage;
[wself.tableView reloadData];
}];
}

Using AFNetworking NSOperations to download a number of files serially.....runs out of memory

Note: I'm using ARC.
I have some code that makes 1 request to an http server for a list of files (via JSON). It then parses that list into model objects which it uses to add a download operation (for downloading that file) to a different nsoperationqueue and then once it's done adding all of those operations (queue starts out suspended) it kicks off the queue and waits for all the operations to finish before continuing. (Note: this is all done on background threads so as not to block the main thread).
Here's the basic code:
NSURLRequest* request = [NSURLRequest requestWithURL:parseServiceUrl];
AFHTTPRequestOperation *op = [[AFHTTPRequestOperation alloc] initWithRequest:request];
op.responseSerializer = [AFJSONResponseSerializer serializer];
[op setCompletionBlockWithSuccess:^(AFHTTPRequestOperation *operation, id responseObject) {
//NSLog(#"JSON: %#", responseObject);
// Parse JSON into model objects
NSNumber* results = [responseObject objectForKey:#"results"];
if ([results intValue] > 0)
{
dispatch_async(_processQueue, ^{
_totalFiles = [results intValue];
_timestamp = [responseObject objectForKey:#"timestamp"];
NSArray* files = [responseObject objectForKey:#"files"];
for (NSDictionary* fileDict in files)
{
DownloadableFile* file = [[DownloadableFile alloc] init];
file.file_id = [fileDict objectForKey:#"file_id"];
file.file_location = [fileDict objectForKey:#"file_location"];
file.timestamp = [fileDict objectForKey:#"timestamp"];
file.orderInQueue = [files indexOfObject:fileDict];
NSNumber* action = [fileDict objectForKey:#"action"];
if ([action intValue] >= 1)
{
if ([file.file_location.lastPathComponent.pathExtension isEqualToString:#""])
{
continue;
}
[self downloadSingleFile:file];
}
else // action == 0 so DELETE file if it exists
{
if ([[NSFileManager defaultManager] fileExistsAtPath:file.localPath])
{
NSError* error;
[[NSFileManager defaultManager] removeItemAtPath:file.localPath error:&error];
if (error)
{
NSLog(#"Error deleting file after given an Action of 0: %#: %#", file.file_location, error);
}
}
}
[self updateProgress:[files indexOfObject:fileDict] withTotal:[files count]];
}
dispatch_sync(dispatch_get_main_queue(), ^{
[_label setText:#"Syncing Files..."];
});
[_dlQueue setSuspended:NO];
[_dlQueue waitUntilAllOperationsAreFinished];
[SettingsManager sharedInstance].timestamp = _timestamp;
dispatch_async(dispatch_get_main_queue(), ^{
callback(nil);
});
});
}
else
{
dispatch_async(dispatch_get_main_queue(), ^{
callback(nil);
});
}
} failure:^(AFHTTPRequestOperation *operation, NSError *error) {
NSLog(#"Error: %#", error);
callback(error);
}];
[_parseQueue addOperation:op];
and then the downloadSingleFile method:
- (void)downloadSingleFile:(DownloadableFile*)dfile
{
NSURLRequest* req = [NSURLRequest requestWithURL:dfile.downloadUrl];
AFHTTPRequestOperation* reqOper = [[AFHTTPRequestOperation alloc] initWithRequest:req];
reqOper.responseSerializer = [AFHTTPResponseSerializer serializer];
[reqOper setCompletionBlockWithSuccess:^(AFHTTPRequestOperation* op, id response)
{
__weak NSData* fileData = response;
NSError* error;
__weak DownloadableFile* file = dfile;
NSString* fullPath = [file.localPath substringToIndex:[file.localPath rangeOfString:file.localPath.lastPathComponent options:NSBackwardsSearch].location];
[[NSFileManager defaultManager] createDirectoryAtPath:fullPath withIntermediateDirectories:YES attributes:Nil error:&error];
if (error)
{
NSLog(#"Error creating directory path: %#: %#", fullPath, error);
}
else
{
error = nil;
[fileData writeToFile:file.localPath options:NSDataWritingFileProtectionComplete error:&error];
if (error)
{
NSLog(#"Error writing fileData for file: %#: %#", file.file_location, error);
}
}
[self updateProgress:file.orderInQueue withTotal:_totalFiles];
}
failure:^(AFHTTPRequestOperation* op, NSError* error)
{
[self updateProgress:dfile.orderInQueue withTotal:_totalFiles];
NSLog(#"Error downloading %#: %#", dfile.downloadUrl, error.localizedDescription);
}];
[_dlQueue addOperation:reqOper];
}
What I'm seeing is a constant spike in memory as more files get downloaded. It's like the responseObject or maybe even the whole completionBlock is not being let go of.
I've tried making the responseObject __weak as well as fileData. I've tried adding an autoreleasepool and I've tried making the actual file domain object __weak too but still memory climbs and climbs.
I've run Instruments and not seen any leaks persay but it never gets to a point where all the files have been downloaded before it runs out of memory with a big fat "can't allocate region" error. Looking at allocations, I see a bunch of connection:didFinishLoading and connection:didReceiveData methods that never seem to be let go of, however. I can't seem to debug it further than that though.
My question: Why is it running out of memory? What is not getting deallocated and how can I get it to do such?
There is a few things going on here. The biggest is that you are downloading the entire file, storing it in memory, and then writing it out to disk when the download is complete. Even with just one file of 500 MB, you will run out of memory.
The correct way to do this is using an NSOutputStream with asynchronous downloads. The key is to write out the data as soon as it arrives. It should look like this:
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data
{
[self.outputStream write:[data bytes] maxLength:[data length]];
}
Also of note, you are creating your weak references inside the block, not outside. Because of that, you are still creating a retain cycle and leaking memory. When you create weak references, it should look like this.
NSOperation *op = [[NSOperation alloc] init];
__weak NSOperation *weakOp = op;
op.completion = ^{
// Use only weakOp within this block
};
Lastly, your code is using #autoreleasepool. NSAutoreleasePool, and the ARC equivalent #autoreleasepool are only useful in very limited situations. As a general rule, if you aren't absolutely sure you need one, you don't.
With the help of a friend, I was able to figure out the problem.
The problem was actually in the first block of code:
[_dlQueue waitUntilAllOperationsAreFinished];
Apparently , waiting for all operations to finish meant none of those operations would be released either.
Instead of that, I ended up adding a final operation to the queue that would do the final processing and callback and memory is much more stable now.
[_dlQueue addOperationWithBlock:^{
[SettingsManager sharedInstance].timestamp = _timestamp;
dispatch_async(dispatch_get_main_queue(), ^{
callback(nil);
});
}];
What kind of file you are downloading? If you are working with Images or videos you nee to clear URLCache as when you doneload images it create CFDATA and some information in cache and it does not cleared out. You need to clear it explicitly when your single file download completed. It will never caught as a leak also.
NSURLCache *sharedCache = [[NSURLCache alloc] initWithMemoryCapacity:0 diskCapacity:0 diskPath:nil];
[NSURLCache setSharedURLCache:sharedCache];
[sharedCache release];
If you are using ARC replace
[sharedCache release];
with
sharedCache = nil;
Hope It may help you.

Resources