My iPhone app has to load 3 data sets when it is first opened. I have 3 view controllers, one for each data set. I notice that when I am on my real iPhone and first open the app and touch a view controller there may be a very long pause, I am assuming while the data is being loaded, but I am not sure.
Here is the relevant code in my AppDelegate:
#import "AppDelegate.h"
#import "MasterViewController.h"
#implementation AppDelegate
- (void)application:(UIApplication *)application performFetchWithCompletionHandler: (void (^)(UIBackgroundFetchResult))completionHandler
{
// Background Fetch for Employees
EmployeeDatabase *tmpEmployeeDatabase = [[EmployeeDatabase alloc] init];
[tmpEmployeeDatabase updateEmployeeData];
completionHandler(UIBackgroundFetchResultNewData);
}
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
// Set Background Fetch Interval
[application setMinimumBackgroundFetchInterval: UIApplicationBackgroundFetchIntervalMinimum];
// Take Database Name and get DatabasePath for later use
self.databaseName = #"employees.db";
self.databaseNameLocations = #"locations.db";
self.databaseNameContacts = #"contacts.db";
NSArray *documentPaths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *documentDir = [documentPaths objectAtIndex:0];
self.databasePath =[documentDir stringByAppendingPathComponent:self.databaseName];
self.databasePathLocations =[documentDir stringByAppendingPathComponent:self.databaseNameLocations];
self.databasePathContacts =[documentDir stringByAppendingPathComponent:self.databaseNameContacts];
// See if we need to initialize the employee db
EmployeeDatabase *tmpEmployeeDatabase = [[EmployeeDatabase alloc] init];
if (![tmpEmployeeDatabase checkIfDatabaseExists]) {
[tmpEmployeeDatabase updateEmployeeData];
}
// See if we need to initialize the contact db
ContactsDatabase *tmpContactsDatabase = [[ContactsDatabase alloc] init];
if (![tmpContactsDatabase checkIfDatabaseExists]) {
[tmpContactsDatabase updateContactsData];
}
// See if we need to initialize the Locations db
LocationDatabase *tmpLocationDatabase = [[LocationDatabase alloc] init];
if (![tmpLocationDatabase checkIfDatabaseExists]) {
[tmpLocationDatabase updateLocationData];
}
return YES;
}
#pragma mark - Application's Documents directory
// Returns the URL to the application's Documents directory.
- (NSURL *)applicationDocumentsDirectory {
return [[[NSFileManager defaultManager] URLsForDirectory:NSDocumentDirectory inDomains:NSUserDomainMask] lastObject];
}
#end
And here is where I call one of the web services and load the data:
#define kBgQueue dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)
- (void)callWebService {
AFHTTPRequestOperationManager *manager = [AFHTTPRequestOperationManager manager];
manager.requestSerializer = [AFJSONRequestSerializer serializer];
[manager.requestSerializer setAuthorizationHeaderFieldWithUsername:#"xxxxxx" password:#"xxxxxxxxxxxx"];
manager.responseSerializer = [AFJSONResponseSerializer serializer];
AFHTTPRequestOperation *operation = [manager GET: #"https://xxxx/mobile/mobilede.nsf/restPeople.xsp/People"
parameters: [self jsonDict]
success: ^(AFHTTPRequestOperation *operation, id responseObject)
{
NSMutableArray *employees = (NSMutableArray *)responseObject;
FMDatabase *db = [FMDatabase databaseWithPath:self.employeeDatabasePath];
[db open];
for (NSDictionary *dict in employees) {
BOOL success = [db
executeUpdate:
#"INSERT INTO employees "
"(id,fstNme,midNme,lstNme,fulNme,locNbr,supID,"
"mrkSeg,geoLoc,lvl,vp,ema,ofcPhn,mobPhn) VALUES "
"(?,?,?,?,?,?,?,?,?,?,?,?,?,?);",
[dict objectForKey:#"id"], [dict objectForKey:#"fstNme"],
[dict objectForKey:#"midNme"], [dict objectForKey:#"lstNme"],
[dict objectForKey:#"fulNme"], [dict objectForKey:#"locNbr"],
[dict objectForKey:#"supId"], [dict objectForKey:#"mrkSeg"],
[dict objectForKey:#"geoLoc"], [dict objectForKey:#"lvl"],
[dict objectForKey:#"vp"], [dict objectForKey:#"ema"],
[dict objectForKey:#"ofcPhn"],[dict objectForKey:#"mobPhn"], nil];
if (success) {
} // Only to remove success error
}
}
failure:
^(AFHTTPRequestOperation * operation, NSError * error) {
NSLog(#"Error: %#", error);
}
];
[operation start];
}
[EDIT]
I forgot the part of the code where I called the
[EDIT]
One possible mistake is that I am using the same background queue for each of the three processes? See the #define kBgQueue at the top of this bit of code.
What is the best practice to handle this? Should I NOT put this on a background queue and alert the user to wait?
[Thank you. I changed this and recompiled. The first time the app starts the interface will freeze at some point, and you cannot do anything for 12 seconds or so, and then it comes out of it. Subsequently there are no pauses. As an example, when I first open the app I can get to my first view, Employees by Name, and if I touch it to list them it might go into the the list but be blank. So I will touch the navigation backward and then it stops for 12 seconds (or so) and then it will return to the main menu, and when I go back in there are the employees. And it never stops from then on out. I cannot figure out why if this is on the background que that it is holding up the interface. What could I run to try to determine when this is happening?]
If your app could not work without data put data processing in background and show some activity indicator to user. If your app could work without data let user do whatever he wants to do and after data is being loaded just reload UI with new data.
Related
I'm new to iOS and have been trying to load JSON data in UITableView (within a UIViewController, which are in a Tab bar controller). The JSON data is huge and I have tried using AFNetworking library as well. But still the data takes more that 15 secs. to load.
Here is what I've tried:
Approach #2
#implementation AppDelegate
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
// Override point for customization after application launch.
[self saveData];
return YES;
}
-(void) saveData
{
NSString * userType= #"3" ;
NSString * userID= #"33" ;
NSString * URLString = [NSString stringWithFormat:#"http://<some_url>/%#/%#/",userType,userID];
AFHTTPSessionManager *manager = [AFHTTPSessionManager manager];
manager.responseSerializer =[AFHTTPResponseSerializer serializer];
[manager GET:leadsURLString parameters:nil progress:nil success:^(NSURLSessionTask *task, NSData* responseObject) {
NSError * error = nil;
NSArray * lData = [NSJSONSerialization JSONObjectWithData:responseObject options:NSJSONReadingAllowFragments error:&error];
NSLog(#"Leads:%#",lData);
if (error != nil) {
NSLog(#"JSON Error: %#", [error localizedDescription]);
}
// Write the file .
NSArray * paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString * docDirectory = [paths objectAtIndex:0];
NSString * lFilePath = [NSString stringWithFormat:#"%#",[docDirectory stringByAppendingPathComponent:#"leads"]]; // File
if (![leadsData writeToFile:lFilePath atomically:YES]) {
NSLog(#"Couldn't save leads data.");
}
} failure:^(NSURLSessionTask *operation, NSError *error) {
NSLog(#"Error: %#", error);
}];
}
#end
Code for tableView
#implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// Uncomment the following line to preserve selection between presentations.
// self.clearsSelectionOnViewWillAppear = NO;
// Uncomment the following line to display an Edit button in the navigation bar for this view controller.
// self.navigationItem.rightBarButtonItem = self.editButtonItem;
NSArray * paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString * docDirectory = [paths objectAtIndex:0];
NSString * projFilePath = [NSString stringWithFormat:#"%#",[docDirectory stringByAppendingPathComponent:#"leads"]]; // file
if ([[NSFileManager defaultManager] fileExistsAtPath:projFilePath])
{
_projects = [[NSMutableArray alloc] initWithContentsOfFile:projFilePath];
}
else
{
_projects = [NSMutableArray arrayWithObject:[NSDictionary dictionaryWithObject:#"No Project" forKey:#"project_name"]];
}
[self.tableView reloadData];
}
Initially tried loading data when the view controllers load, took too long.
Then tried loading data in Appdelegate and saving it, and loading it when the view controllers load, still took long enough.
Tried loading data in Appdelegate and then setting it in the respective view controllers, but couldn't achieve it.
Could anyone suggest the best way to accomplish it?
if data is much larger(as you mentioned in comment) then you can manage your api something like you can send last index to server and server returns data from that index and you can define total number of records should be return in one call. then you can frequently call api as user scrolls your table view!
For example you could consider facebook app's time line or Quora app's read tab!
Try to follow below steps to reduce the time.
Step: 1 Try to load data from below check how time it will take.
AFHTTPSessionManager *managerFB = [AFHTTPSessionManager manager];
[managerFB GET:WEB_SERIVCE_URL parameters:dicData progress:^(NSProgress * _Nonnull downloadProgress) {
<#code#>
} success:^(NSURLSessionDataTask * _Nonnull task, id _Nullable responseObject) {
NSLog(#"%#",responseObject);
} failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) {
<#code#>
}];
Step: 2: Don’t try to save data into local file. Create one global variable in AppDelegate and stored directly into it.
Step: 3: Try to call service in background thread, so your main application UI won’t be stuck.
[self performSelector:#selector(yourMethodName) withObject:nil afterDelay:0.2];
Also, you can check your web-service response time in Postman which is extension of Chrome. So you can get response time from service.
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.
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);
}
}];
}
I am using AFNetworking for downloading files having size between 1 to 4 gbs,.
Currently while downloading such a huge files I pause the current download when app enters in background state and resume when it gets active.
But what happens wrong in my case is that, first time while downloading when I minimize the app I pause it and when I again maximize app after 20 to 30 mins I resume it and download continues from where it was left paused last time. But it works only first time, second time when I again minimize the app with same download it gets paused and when I again maximizes it , it stuck at the same point showing some wrong values for progress and current transfer speed and it never moves forward or never continues current download.
Strange behaviour??
I have tried both old and new (2.0) versions but no luck.
Can you guess what is happening wrong in my case?
Or
Please suggest me some good alternatives to using AFNetworking.
UPDATE
Method called to download file
-(void) downloadTracksFromProgramArray:(NSArray*) programs
{
if (programs.count == 0) {
return;
}
queue = [[NSOperationQueue alloc] init];
[queue setMaxConcurrentOperationCount:1];
queueSize = 0;
urlString = [programs objectAtIndex:0];
NSString *filename = [urlString lastPathComponent];
// 11-09-12
// remove query string from aws
NSString *string1 = [NSHomeDirectory() stringByAppendingPathComponent:[NSString stringWithFormat:#"Documents/%#",[filename lastPathComponent]]] ;
// remove query string from aws
NSArray *jaysarray = [string1 componentsSeparatedByString:#"?"];
NSString *downloadPath1 = [NSString stringWithFormat:#"%#",[jaysarray objectAtIndex:0]];
extract_file_path_after_download = downloadPath1;
NSLog(#"%#",[jaysarray objectAtIndex:0]);
// NSLog(#"%#",[jaysarray objectAtIndex:1]);
current_downloading_file_path = [downloadPath1 copy];
NSLog(#"download url %#",[NSURL URLWithString:urlString]);
NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:urlString]];
operation = [[AFHTTPRequestOperation alloc] initWithRequest:request];
operation.outputStream = [NSOutputStream outputStreamToFileAtPath:downloadPath1 append:NO];
//handle successful completion of each track download
[operation setCompletionBlockWithSuccess:^(AFHTTPRequestOperation *operation, id responseObject) {
NSLog(#"Successfully downloaded file to %#", downloadPath1);
//if ([[queue operations] count] == 0) {
NSNotification *success = [NSNotification notificationWithName:#"AudioDone" object:[NSNumber numberWithBool: YES]];
[[NSNotificationCenter defaultCenter] postNotification:success];
queueSize = 0;
//} else {
//send total track info
//get total queue size by the first success and add 1 back
if (queueSize ==0) {
queueSize = [[queue operations] count] +1.0;
}
float progress = (float)(queueSize-[[queue operations] count])/queueSize;
NSNumber * totProgress = [NSNumber numberWithFloat:progress];
NSLog(#"Total Progress: %#", totProgress);
current_downloading_file_path = #"";
//Commented by rakesh biradar - becoz #"TotalProgress" notification method does not do anything(memory).
//NSNotification * totalProgressNotification = [NSNotification notificationWithName:#"TotalProgress"
// object:totProgress];
//[[NSNotificationCenter defaultCenter] postNotification:totalProgressNotification];
//}
NSLog(#"QueueCount: %d", [[queue operations] count]); //[[self sharedQueue] operationCount]);
} failure:^(AFHTTPRequestOperation *operation, NSError *error) {
//deletes the partial downloaded file from document folder
if(([current_downloading_file_path length] > 0) && [[NSFileManager defaultManager] fileExistsAtPath:current_downloading_file_path])
[[NSFileManager defaultManager] removeItemAtPath:current_downloading_file_path error:nil];
current_downloading_file_path = #"";
NSLog(#"Error: %#", error);
}];
//Send progress notification
[operation setDownloadProgressBlock:^(NSUInteger bytesWritten, long long totalBytesWritten, long long totalBytesExpectedToWrite) {
//NSLog(#"Sent %lld of %lld bytes, %#", totalBytesWritten, totalBytesExpectedToWrite, path);
float percentDone = ((float)((int)totalBytesWritten) / (float)((int)totalBytesExpectedToWrite));
//NSLog(#"Percent: %f", percentDone);
NSDictionary *userInfo = [NSDictionary dictionaryWithObjects: [NSArray arrayWithObjects:filename, [NSNumber numberWithFloat: percentDone],[NSNumber numberWithLongLong:totalBytesWritten],[NSNumber numberWithLongLong:totalBytesExpectedToWrite],[NSNumber numberWithUnsignedInteger:bytesWritten],nil]
forKeys:[NSArray arrayWithObjects:#"message", #"percent",#"totalBytesWritten",#"totalBytesExpectedToWrite",#"bytesWritten", nil]];
NSNotification * progress = [NSNotification notificationWithName:#"DownloadingAudio" object:nil userInfo:userInfo];
[[NSNotificationCenter defaultCenter] postNotification:progress];
}];
[queue addOperation:operation];
//[self enqueueHTTPRequestOperation:operation];
//NSLog(#"Operation Queue: %#", [self sharedQueue]);
}
Method when app goes in background
- (void)applicationWillResignActive:(UIApplication *)application {
// Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state.
// Use this method to pause ongoing tasks, disable timers, and throttle down OpenGL ES frame rates. Games should use this method to pause the game.
if (operation)
{
NSLog(#"%#",operation);
//[self saveCustomObject:operation];
[operation pause];
}
}
Method called when app becomes active
- (void)applicationDidBecomeActive:(UIApplication *)application {
// Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface.
// Handle the user leaving the app while the Facebook login dialog is being shown
// For example: when the user presses the iOS "home" button while the login dialog is active
if (operation)
{
//operation = [self loadCustomObjectWithKey:#"myEncodedObjectKey"];
NSLog(#"%#",operation);
[operation resume];
}
[FBAppCall handleDidBecomeActive];
}
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.