Upload audio - http streaming - objective-c

I'm trying to upload a linear file under http in streaming mode.
The idea is do this steps simultaneously,
1) Thread 1: record an audio file and store it in a temp file
2) Thread 2: Take n bytes from temp file and send it to an http server.
How can I write an http stream?, On CFHTTPStream I did not see write methods, only read :s
Do I need use sockets?
Thanks!!!
My actual code is
CFWriteStreamRef stream;
NSString *strUrl = #"myurl";
NSURL *url = [[[NSURL alloc] initWithString:strUrl] retain];
CFStringRef requestMethod = CFSTR("GET");
CFHTTPMessageRef message= CFHTTPMessageCreateRequest(kCFAllocatorDefault, requestMethod, (CFURLRef)url, kCFHTTPVersion1_1);
CFHTTPMessageSetHeaderFieldValue(message, CFSTR("Content-Type"),
CFSTR("multipart/form-data"));
stream = ?? //CFReadStreamCreateForHTTPRequest(NULL, message);
CFRelease(message);
//other headers...
if (CFWriteStreamSetProperty(stream, kCFStreamPropertyHTTPShouldAutoredirect, kCFBooleanTrue) == false)
{
NSLog(#"Error");
return NO;
}
//
// Open the stream
//
if (!CFWriteStreamOpen(stream))
{
CFRelease(stream);
NSLog(#"Error");
return NO;
}
CFStreamClientContext context = {0, self, NULL, NULL, NULL};
CFWriteStreamSetClient(stram, kCFStreamEventHasBytesAvailable | kCFStreamEventErrorOccurred | kCFStreamEventEndEncountered,
RSWriteStreamCallBack,
&context);
CFWriteStreamScheduleWithRunLoop(stream, CFRunLoopGetCurrent(), kCFRunLoopCommonModes);

The solution is create an subclass of NSInputStream, and implement the methods open, close, read, hasBytesAvailable and don't forget - (NSStreamStatus)streamStatus.
Last method is invoked from http to know if we are open, close or we was finished (NSStreamStatusAtEnd) to send (there are others status, but this are the most important).
I use a tmp file like buffer because I have to send a lot of data, but, perhaps, a data memory buffer could be better.
Finally I implement other class where use my custom NSInputStream, here is the code:
NSURL *url = [NSURL URLWithString:#"url"];
NSMutableURLRequest *req = [NSMutableURLRequest requestWithURL:url];
[req setHTTPMethod:#"POST"];
//set headers if you have to do for example:
NSString *contentType = [NSString stringWithFormat:#"multipart/form-data"];
[req setValue:contentType forHTTPHeaderField:#"Content-Type"];
//Create your own InputStream
instream = [[CustomStream alloc] init];
[req setHTTPBodyStream:instream];
//I remove instream later
NSURLConnection *aConnection = [[NSURLConnection alloc] initWithRequest:req delegate:self startImmediately:NO];
[aConnection scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
[aConnection start];

Related

may be NSURLSession or NSMutableURLRequest does not release memory calling through loop

My Problem :- NSURLSession does not release previous call API Memory of 5MB Chunk
I am calling APIs in do while loop to upload 500MB video. I have to send every 5MB chunk with different APIs not in one API.
For Example 500MB Video and create 100 chunks and send using NSURLSession so calls 100 times but NSURLSession does not release previous call API Memory of 5MB Chunk
(1) I have created 5MB Chunk.
(2) read File using NSFileHandle with 5MB Chunk using OffSet
(3) change URL for all chunk and call api (necessary to send all chunk at different URL)
I do not want convert video in NSData in (500MB) i want to send chunk through API
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
//dispatch_group_t group = dispatch_group_create();
__block NSUInteger counterFailure = 0; // PSM Anks calling blob by url fails 4 time, exit for funtion
arrBlobIds = [[NSMutableArray alloc]init];
__block NSInteger intBlockIdCount = 100000; // PSM Anks blobid to assign id to every blob
__block NSUInteger offset = 0; // PSM Anks offset to start posution to read data
NSUInteger length = [[[NSFileManager defaultManager] attributesOfItemAtPath:[urlOfGallaryVideo path] error:nil] fileSize]; // PSM anks total lenght of media
NSUInteger intChunkSize = (5000 * 1024); // PSM anks chunk size
while (offset < length){
//dispatch_group_enter(group);
NSLog(#"offset 1 : %lu",(unsigned long)offset);
// PSM Anks Creat Chunk from file according to length
NSUInteger intThisChunkSize = length - offset > intChunkSize ? intChunkSize : length - offset;
//NSData* chunk = [NSData dataWithBytesNoCopy:(char *)[myBlob bytes] + offset length:intThisChunkSize freeWhenDone:NO];
__block NSFileHandle *fileHandle = [NSFileHandle fileHandleForReadingAtPath:[urlOfGallaryVideo path]];
[fileHandle seekToFileOffset:offset];
__block NSData *dataChunk = [fileHandle readDataOfLength:intThisChunkSize];
// PSM Anks Convert block id in Base 64 encode
NSData *dataBlockId = [[NSString stringWithFormat:#"%ld",intBlockIdCount] dataUsingEncoding:NSUTF8StringEncoding];
NSString *base64BlockId = [dataBlockId base64EncodedStringWithOptions:0];
NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:[NSURL URLWithString:[NSString stringWithFormat:#"%#&comp=block&blockid=%#",[dictAzureSAS objectForKey:#"uri"],base64BlockId]]];
[request setHTTPMethod:#"PUT"];
[request setHTTPBody:dataChunk];
//[request setValue:[NSString stringWithFormat:#"%lu",(unsigned long)dataChunk.length] forHTTPHeaderField:#"Content-Length"]; // Do not need
//[request setValue:strVideoMIMEType forHTTPHeaderField:#"Content-Type"]; // Do not need
[request addValue:#"BlockBlob" forHTTPHeaderField:#"x-ms-blob-type"];
//NSLog(#"request : %#",request);
//NSLog(#"dataChunk.length : %lu \n url for blob %# \n request %#",(unsigned long)dataChunk.length,[NSURL URLWithString:[NSString stringWithFormat:#"%#&comp=block&blockid=%#",[dictAzureSAS objectForKey:#"uri"],base64BlockId]],request);
NSLog(#"dataChunk.length : %lu",(unsigned long)dataChunk.length);
NSURLSessionConfiguration *config = [NSURLSessionConfiguration defaultSessionConfiguration];
config.URLCache = [[NSURLCache alloc] initWithMemoryCapacity:0 diskCapacity:0 diskPath:nil];
config.timeoutIntervalForRequest = 20.0;
config.URLCredentialStorage = nil;
config.requestCachePolicy = NSURLRequestReloadIgnoringLocalCacheData;
///NSURLSession *session = [NSURLSession sessionWithConfiguration:config];
config = nil;
//NSURLSession *session = [NSURLSession sessionWithConfiguration:[NSURLSessionConfiguration defaultSessionConfiguration]];
NSURLSession *session = [NSURLSession sessionWithConfiguration:[NSURLSessionConfiguration defaultSessionConfiguration] delegate:self delegateQueue:[NSOperationQueue mainQueue]];
NSURLSessionDataTask *dataTaskForUpload = [session dataTaskWithRequest:request completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
NSLog(#"Finished with status code: %li", (long)[(NSHTTPURLResponse *)response statusCode]);
NSLog(#"response: %#", response);
NSLog(#"error: %# %#", error,error.description);
if(data != nil) // PSM anks Check Data is nil otherwise app crashed
{
NSMutableArray *jsonList = [NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingMutableContainers error:nil];
NSLog(#"jsonList: %#", jsonList);
}
dataChunk = nil;
fileHandle = nil;
if(error == nil)
{
/*
// PSM Anks First Add Then increment
[arrBlobIds addObject:base64BlockId];
intBlockIdCount++;
offset += intThisChunkSize;
counterFailure = 0;
*/
}
else
{
/*
counterFailure++;
offset = intThisChunkSize;
if(counterFailure >= 4)
{
NSLog(#"Enter counter Failure %lu",(unsigned long)counterFailure);
counterFailure = 0;
[self stopLoader];
[CommonAlertViewMsgs cannotConnectServer:self];
return ;
}
*/
}
//dispatch_group_leave(group);
dispatch_semaphore_signal(semaphore);
[session finishTasksAndInvalidate];
}];
[dataTaskForUpload resume];
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
NSLog(#"offset 2 : %lu",(unsigned long)offset);
}
Your problem is probably that your NSData objects are being put in an autorelease pool, which is never getting drained until after your main dispatch_async block completes. You can probably fix the immediate problem by adding an #autoreleasepool to your while loop; i.e.
while (offset < length) #autoreleasepool {
However, your dispatch_semaphore_wait at the end is blocking a dispatch queue, which is generally discouraged. What I would recommend would be, in addition to adding the #autoreleaspool to the while loop, to use a dispatch group instead of the semaphore, and to use dispatch_group_notify at the end instead of dispatch_group_wait. This will cause your main dispatch_async block to complete, which will release any autoreleased objects which have accumulated in it, and then the block you pass to dispatch_group_notify will be called once all your operations are complete.
EDIT: Knowing a little more about what you're trying to do, here is an alternative that will run the processes one at a time, while still not blocking the dispatch queue:
(pseudocode)
- (void)sendRequestWithOffset:length:otherParameters: {
send the url request {
do what you do
if newOffset < length {
[self sendRequestWithOffset:newOffset length:length otherParameters:whatever];
} else {
hooray, we're done
}
}
}
It's sort of like a recursive call (but not really, since we won't accumulate stack frames). Basically it's an asyncronous version of your while loop; your tasks occur one at a time, there's no blockage of dispatch queues, and since each dispatch queue has its own autorelease pool, you won't get buildup of autoreleased objects either and your memory usage should stay reasonable.
If you want to minimize your memory footprint while the uploads run, you should:
Try #autoreleasepool as advised by Charles;
Reuse one NSURLSession rather than recreating a new NSURLSession each time inside your loop; and
Use file-based upload tasks (e.g. uploadTaskWithRequest:fromFile:) rather than dataTask.
Note, with file-based upload tasks, you can still configure your NSURLRequest like you are now, but don't set httpBody, but instead supply that as the fromFile parameter of the above method.
Once you have this memory issue behind you, you can pursue other approaches to improve performance, too, but let's not get ahead of ourselves.

argument-passing from iOS to WCF service

I need to pass information from the application to the server, specifically a and b in text format. Here is the code the WCF service:
public class iOSService
{
// To use HTTP GET, add [WebGet] attribute. (Default ResponseFormat is WebMessageFormat.Json)
// To create an operation that returns XML,
// add [WebGet(ResponseFormat=WebMessageFo rmat.Xml)],
// and include the following line in the operation body:
// WebOperationContext.Current.Outgoin gResponse.ContentType = "text/xml";
[OperationContract]
public void DoWork()
{
// Add your operation implementation here
return;
}
// Add more operations here and mark them with [OperationContract]
[OperationContract]
public string iOSTest2(string a, string b)
{
string res = "";
try
{
res=(int.Parse(a) + int.Parse(b)).ToString();
}
catch (Exception exp)
{
res = "Not a number";
}
return res;
}
}
After receiving a and b, the server adds them, and sends back their sum.
And here is my code to send parameters to the server of an iOS application:
- (IBAction)test:(id)sender {
NSArray *propertyNames = [NSArray arrayWithObjects:#"23", #"342", nil];
NSArray *propertyValues = [NSArray arrayWithObjects:#"a", #"b", nil];
NSDictionary *properties = [NSDictionary dictionaryWithObjects:propertyNames forKeys:propertyValues];
NSMutableArray * arr;
arr=[[NSMutableArray alloc]initWithObjects:properties, nil];
NSLog(#"%#",arr);
NSError * error;
NSData *jsonData2 = [NSJSONSerialization dataWithJSONObject:arr options:NSJSONWritingPrettyPrinted error:&error];
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:#"http://www.mathforyou.net/iOSservice.svc/iOSTest2"]];
NSString *jsonString = [[NSString alloc] initWithData:jsonData2 encoding:NSUTF8StringEncoding];
[request setValue:#"appliction/json" forHTTPHeaderField:#"Content-Type"];
[request setHTTPMethod:#"POST"];
[request setHTTPBody:jsonData2];
NSLog(#"JSON String: %#",jsonString);
NSError *errorReturned = nil;
NSURLResponse *theResponse =[[NSURLResponse alloc]init];
NSData *data = [NSURLConnection sendSynchronousRequest:request returningResponse:&theResponse error:&errorReturned];
if (errorReturned) {
//...handle the error
NSLog(#"error");
}
else {
NSString *retVal = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
NSLog(#"%#",retVal);
}
}
But the server answers me the following message:
"ExceptionDetail":null,"ExceptionType":null,"Message":"The server was unable to process the request due to an internal error. For more information about the error, either turn on IncludeExceptionDetailInFaults (either from ServiceBehaviorAttribute or from the <serviceDebug> configuration behavior) on the server in order to send the exception information back to the client, or turn on tracing as per the Microsoft .NET Framework SDK documentation and inspect the server trace logs.","StackTrace":null
And here is the logs from Xcode:
2013-03-01 17:38:28.657 2231233122[3442:c07] (
{
a = 23;
b = 342;
}
)
2013-03-01 17:38:28.659 2231233122[3442:c07] JSON String: [
{
"a" : "23",
"b" : "342"
}
]
2013-03-01 17:38:29.054 2231233122[3442:c07] {"ExceptionDetail":null,"ExceptionType":null,"Message":"The server was unable to process the request due to an internal error. For more information about the error, either turn on IncludeExceptionDetailInFaults (either from ServiceBehaviorAttribute or from the <serviceDebug> configuration behavior) on the server in order to send the exception information back to the client, or turn on tracing as per the Microsoft .NET Framework SDK documentation and inspect the server trace logs.","StackTrace":null}
P.S. I asked a friend to help me, but since he does not know objective-c wrote a program just for C++ and the code works, so the problem is not in the server. Here is the code:
string data = "{\"a\":\"560\",\"b\":\"90\"}";
byte[] bd = Encoding.UTF8.GetBytes(data);
HttpWebRequest wr = (HttpWebRequest)HttpWebRequest.Create("http://www.mathforyou.net/IOSService.svc/iOSTest2");
wr.ContentType = "application/json";
wr.Method = "POST";
wr.ContentLength = bd.Length;
Stream sw = wr.GetRequestStream();
sw.Write(bd, 0, bd.Length);
sw.Close();
HttpWebResponse resp = (HttpWebResponse)wr.GetResponse();
Stream s = resp.GetResponseStream();
byte[] bres = new byte[resp.ContentLength];
s.Read(bres, 0, bres.Length);
string ans = Encoding.UTF8.GetString(bres);
Console.WriteLine(ans);
Please help, I'm worn out.
If you copied and pasted the code, it looks like you might have misspelled your Content-Type value. It should be application/json, you have it as appliction/json.
Also, I'm not sure if this matters, but you're also not setting the content length explicitly. I am not sure if setHTTPBody: does that for you.
I'm not sure if you still need this, but try the following:
[request addValue:#"application/json; charset=utf-8" forHTTPHeaderField:#"Content-Type"];
[request setValue:jsonString forHTTPHeaderField:#"json"]; //<-- I added this line
[request setHTTPMethod:#"POST"];
[request setHTTPBody:jsonData2];

Downloading multiple files in batches in iOS

I have an app that right now needs to download hundreds of small PDF's based on the users selection. The problem I am running into is that it is taking a significant amount of time because every time it has to open a new connection. I know that I could use GCD to do an async download, but how would I go about doing this in batches of like 10 files or so. Is there a framework that already does this, or is this something I will have to build my self?
This answer is now obsolete. Now that NSURLConnection is deprecated and NSURLSession is now available, that offers better mechanisms for downloading a series of files, avoiding much of the complexity of the solution contemplated here. See my other answer which discusses NSURLSession.
I'll keep this answer below, for historical purposes.
I'm sure there are lots of wonderful solutions for this, but I wrote a little downloader manager to handle this scenario, where you want to download a bunch of files. Just add the individual downloads to the download manager, and as one finishes, it will kick off the next queued one. You can specify how many you want it to do concurrently (which I default to four), so therefore there's no batching needed. If nothing else, this might provoke some ideas of how you might do this in your own implementation.
Note, this offers two advantages:
If your files are large, this never holds the entire file in memory, but rather streams it to persistent storage as it's being downloaded. This significantly reduces the memory footprint of the download process.
As the files are being downloaded, there are delegate protocols to inform you or the progress of the download.
I've attempted to describe the classes involved and proper operation on the main page at the Download Manager github page.
I should say, though, that this was designed to solve a particular problem, where I wanted to track the progress of downloads of large files as they're being downloaded and where I didn't want to ever hold the entire in memory at one time (e.g., if you're downloading a 100mb file, do you really want to hold that in RAM while downloading?).
While my solution solves those problem, if you don't need that, there are far simpler solutions using operation queues. In fact you even hint at this possibility:
I know that I could use GCD to do an async download, but how would I go about doing this in batches of like 10 files or so. ...
I have to say that doing an async download strikes me as the right solution, rather than trying to mitigate the download performance problem by downloading in batches.
You talk about using GCD queues. Personally, I'd just create an operation queue so that I could specify how many concurrent operations I wanted, and download the individual files using NSData method dataWithContentsOfURL followed by writeToFile:atomically:, making each download it's own operation.
So, for example, assuming you had an array of URLs of files to download it might be:
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
queue.maxConcurrentOperationCount = 4;
for (NSURL* url in urlArray)
{
[queue addOperationWithBlock:^{
NSData *data = [NSData dataWithContentsOfURL:url];
NSString *filename = [documentsPath stringByAppendingString:[url lastPathComponent]];
[data writeToFile:filename atomically:YES];
}];
}
Nice and simple. And by setting queue.maxConcurrentOperationCount you enjoy concurrency, while not crushing your app (or the server) with too many concurrent requests.
And if you need to be notified when the operations are done, you could do something like:
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
queue.maxConcurrentOperationCount = 4;
NSBlockOperation *completionOperation = [NSBlockOperation blockOperationWithBlock:^{
[[NSOperationQueue mainQueue] addOperationWithBlock:^{
[self methodToCallOnCompletion];
}];
}];
for (NSURL* url in urlArray)
{
NSBlockOperation *operation = [NSBlockOperation blockOperationWithBlock:^{
NSData *data = [NSData dataWithContentsOfURL:url];
NSString *filename = [documentsPath stringByAppendingString:[url lastPathComponent]];
[data writeToFile:filename atomically:YES];
}];
[completionOperation addDependency:operation];
}
[queue addOperations:completionOperation.dependencies waitUntilFinished:NO];
[queue addOperation:completionOperation];
This will do the same thing, except it will call methodToCallOnCompletion on the main queue when all the downloads are done.
By the way, iOS 7 (and Mac OS 10.9) offer URLSession and URLSessionDownloadTask, which handles this quite gracefully. If you just want to download a bunch of files, you can do something like:
NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration defaultSessionConfiguration];
NSURLSession *session = [NSURLSession sessionWithConfiguration:configuration];
NSString *documentsPath = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES)[0];
NSFileManager *fileManager = [NSFileManager defaultManager];
for (NSString *filename in self.filenames) {
NSURL *url = [baseURL URLByAppendingPathComponent:filename];
NSURLSessionTask *downloadTask = [session downloadTaskWithURL:url completionHandler:^(NSURL *location, NSURLResponse *response, NSError *error) {
NSString *finalPath = [documentsPath stringByAppendingPathComponent:filename];
BOOL success;
NSError *fileManagerError;
if ([fileManager fileExistsAtPath:finalPath]) {
success = [fileManager removeItemAtPath:finalPath error:&fileManagerError];
NSAssert(success, #"removeItemAtPath error: %#", fileManagerError);
}
success = [fileManager moveItemAtURL:location toURL:[NSURL fileURLWithPath:finalPath] error:&fileManagerError];
NSAssert(success, #"moveItemAtURL error: %#", fileManagerError);
NSLog(#"finished %#", filename);
}];
[downloadTask resume];
}
Perhaps, given that your downloads take a "significant amount of time", you might want them to continue downloading even after the app has gone into the background. If so, you can use backgroundSessionConfiguration rather than defaultSessionConfiguration (though you have to implement the NSURLSessionDownloadDelegate methods, rather than using the completionHandler block). These background sessions are slower, but then again, they happen even if the user has left your app. Thus:
- (void)startBackgroundDownloadsForBaseURL:(NSURL *)baseURL {
NSURLSession *session = [self backgroundSession];
for (NSString *filename in self.filenames) {
NSURL *url = [baseURL URLByAppendingPathComponent:filename];
NSURLSessionTask *downloadTask = [session downloadTaskWithURL:url];
[downloadTask resume];
}
}
- (NSURLSession *)backgroundSession {
static NSURLSession *session = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration backgroundSessionConfiguration:kBackgroundId];
session = [NSURLSession sessionWithConfiguration:configuration delegate:self delegateQueue:[NSOperationQueue mainQueue]];
});
return session;
}
#pragma mark - NSURLSessionDownloadDelegate
- (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didFinishDownloadingToURL:(NSURL *)location {
NSString *documentsPath = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES)[0];
NSString *finalPath = [documentsPath stringByAppendingPathComponent:[[[downloadTask originalRequest] URL] lastPathComponent]];
NSFileManager *fileManager = [NSFileManager defaultManager];
BOOL success;
NSError *error;
if ([fileManager fileExistsAtPath:finalPath]) {
success = [fileManager removeItemAtPath:finalPath error:&error];
NSAssert(success, #"removeItemAtPath error: %#", error);
}
success = [fileManager moveItemAtURL:location toURL:[NSURL fileURLWithPath:finalPath] error:&error];
NSAssert(success, #"moveItemAtURL error: %#", error);
}
- (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didResumeAtOffset:(int64_t)fileOffset expectedTotalBytes:(int64_t)expectedTotalBytes {
// Update your UI if you want to
}
- (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didWriteData:(int64_t)bytesWritten totalBytesWritten:(int64_t)totalBytesWritten totalBytesExpectedToWrite:(int64_t)totalBytesExpectedToWrite {
// Update your UI if you want to
}
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error {
if (error)
NSLog(#"%s: %#", __FUNCTION__, error);
}
#pragma mark - NSURLSessionDelegate
- (void)URLSession:(NSURLSession *)session didBecomeInvalidWithError:(NSError *)error {
NSLog(#"%s: %#", __FUNCTION__, error);
}
- (void)URLSessionDidFinishEventsForBackgroundURLSession:(NSURLSession *)session {
AppDelegate *appDelegate = (id)[[UIApplication sharedApplication] delegate];
if (appDelegate.backgroundSessionCompletionHandler) {
dispatch_async(dispatch_get_main_queue(), ^{
appDelegate.backgroundSessionCompletionHandler();
appDelegate.backgroundSessionCompletionHandler = nil;
});
}
}
By the way, this assumes your app delegate has a backgroundSessionCompletionHandler property:
#property (copy) void (^backgroundSessionCompletionHandler)();
And that the app delegate will set that property if the app was awaken to handle URLSession events:
- (void)application:(UIApplication *)application handleEventsForBackgroundURLSession:(NSString *)identifier completionHandler:(void (^)())completionHandler {
self.backgroundSessionCompletionHandler = completionHandler;
}
For an Apple demonstration of the background NSURLSession see the Simple Background Transfer sample.
If all of the PDFs are coming from a server you control then one option would be to have a single request pass a list of files you want (as query parameters on the URL). Then your server could zip up the requested files into a single file.
This would cut down on the number of individual network requests you need to make. Of course you need to update your server to handle such a request and your app needs to unzip the returned file. But this is much more efficient than making lots of individual network requests.
Use an NSOperationQueue and make each download a separate NSOperation. Set the maximum concurrent operations property on your queue to however many downloads you want to be able to run simultaneously. I'd keep it in the 4-6 range personally.
Here's a good blog post that explains how to make concurrent operations.
http://www.dribin.org/dave/blog/archives/2009/05/05/concurrent_operations/
What came as a big surprise is how slow dataWithContentsOfURL is when downloading multiple files!
To see it by yourself run the following example:
(you don't need the downloadQueue for downloadTaskWithURL, its there just for easier comparison)
- (IBAction)downloadUrls:(id)sender {
[[NSOperationQueue new] addOperationWithBlock:^{
[self download:true];
[self download:false];
}];
}
-(void) download:(BOOL) slow
{
double startTime = CACurrentMediaTime();
NSURLSessionConfiguration* config = [NSURLSessionConfiguration defaultSessionConfiguration];
static NSURLSession* urlSession;
if(urlSession == nil)
urlSession = [NSURLSession sessionWithConfiguration:config delegate:nil delegateQueue:nil];
dispatch_group_t syncGroup = dispatch_group_create();
NSOperationQueue* downloadQueue = [NSOperationQueue new];
downloadQueue.maxConcurrentOperationCount = 10;
NSString* baseUrl = #"https://via.placeholder.com/468x60?text=";
for(int i = 0;i < 100;i++) {
NSString* urlString = [baseUrl stringByAppendingFormat:#"image%d", i];
dispatch_group_enter(syncGroup);
NSURL *url = [NSURL URLWithString:urlString];
[downloadQueue addOperationWithBlock:^{
if(slow) {
NSData *urlData = [NSData dataWithContentsOfURL:url];
dispatch_group_leave(syncGroup);
//NSLog(#"downloaded: %#", urlString);
}
else {
NSURLSessionDownloadTask* task = [urlSession downloadTaskWithURL:url completionHandler:^(NSURL * _Nullable location, NSURLResponse * _Nullable response, NSError * _Nullable error) {
//NSLog(#"downloaded: %#", urlString);
dispatch_group_leave(syncGroup);
}];[task resume];
}
}];
}
dispatch_group_wait(syncGroup, DISPATCH_TIME_FOREVER);
double endTime = CACurrentMediaTime();
NSLog(#"Download time:%.2f", (endTime - startTime));
}
There is nothing to "build". Just loop through the next 10 files each time in 10 threads and get the next file when a thread finishes.

json-framework - Token 'start of array' not expected after outer-most array or object

Feeding the json-parser with the this data: http://mapadosradares.com.br/api/get_initial_load yields this error: Token 'start of array' not expected after outer-most array or object
Here is my code:
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data {
NSLog(#"Connection didReceiveData of length: %u", data.length);
// Printing the received data
size_t length = [data length];
unsigned char aBuffer[length];
[data getBytes:aBuffer length:length];
//aBuffer[length - 1] = 0;
NSLog(#"\n\n\n\n%s\n\n\n\n", aBuffer);
SBJsonStreamParserStatus status = [parser parse:data];
if (status == SBJsonStreamParserError) {
NSLog(#"Parser error: %#", parser.error);
} else if (status == SBJsonStreamParserWaitingForData) {
NSLog(#"Parser waiting for more data");
}
}
As far as I can tell the JSON is perfectly fine. Any thoughts?
UPDATE:
Here's the parser initalization:
- (void) getInitialLoad
{
adapter = [[SBJsonStreamParserAdapter alloc] init];
parser = [[SBJsonStreamParser alloc] init];
adapter.delegate = self;
parser.delegate = adapter;
NSString *url = #"http://mapadosradares.com.br/api/get_initial_load";
NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:url]
cachePolicy:NSURLRequestUseProtocolCachePolicy timeoutInterval:60.0];
connection = [[NSURLConnection alloc] initWithRequest:request delegate:self];
}
Are you properly initializing the parser between requests? You haven't shown your code, but it seems like this would be a reasonable error to expect if you ran two successive calls to the feed through the parser.
By the way, I ran the feed output through the excellent JSON parser at http://jsonlint.com and it does appear to be fine.

Losing responseData in NSURLConnection wrapper

I'm having some trouble implementing a NSURLConnection wrapper. I'm creating an NSURLConnection with POST data, and the connection appears to receive a response as well as data. The didReceiveData callback logs the responseData length, which is 2000 something bytes. However, by the time didFinishLoading fires, responseData contains 0 bytes. Any tips on where to look for what could be modifying the contents of responseData? It gets reset in didReceiveResponse, but didReceiveResponse does not appear to be called in between didReceiveData and didFinishLoading.
Here is some output from the log:
Current language: auto; currently objective-c 2012-01-24 13:35:40.020
PSIdea[24007:11903] didReceiveResponse: responseData length:(0)
warning: Attempting to create USE_BLOCK_IN_FRAME variable with block
that isn't in the frame. 2012-01-24 13:35:40.604 PSIdea[24007:11903]
didReceiveData. responseData length:(2233) 2012-01-24 13:35:40.604
PSIdea[24007:11903] didFinishLoading: responseData length:(0)
2012-01-24 13:35:41.881 PSIdea[24007:11903] responseData as string:
2012-01-24 13:35:41.882 PSIdea[24007:11903] responseData as
dictionary:
Here is the relevant code:
NetworkController.m
-(void)postRequestToURL:(NSURL*)url withData:(NSData*)data withContentType:(NSString*)contentType
{
NSMutableURLRequest* request = [NSMutableURLRequest requestWithURL:url];
[request setHTTPMethod:#"POST"];
if (!contentType)
{
contentType = #"application/x-www-form-urlencoded";
}
[request setValue:contentType forHTTPHeaderField:#"Content-Type"];
[request setHTTPBody:data];
[[NSURLConnection alloc] initWithRequest:request delegate:self];
}
-(void)connection:(NSConnection*)conn didReceiveData:(NSData*)data
{
[_responseData appendData:data];
NSLog(#"didReceiveData. responseData length:(%d)", _responseData.length);
}
-(void)connection:(NSConnection*)conn didReceiveResponse:(NSURLResponse *)response
{
if (_responseData == NULL) {
_responseData = [[NSMutableData alloc] init];
}
[_responseData setLength:0];
NSLog(#"didReceiveResponse: responseData length:(%d)", _responseData.length);
}
-(void)connection:(NSConnection*)conn didFailWithError:(NSError *)error
{
NSLog([NSString stringWithFormat:#"Connection failed: %#", error.description]);
}
PSINetworkController.m (subclass)
-(void)connectionDidFinishLoading:(NSURLConnection*)connection
{
NSLog(#"didFinishLoading: responseData length:(%d)", _responseData.length);
NSString *responseString = [[NSString alloc] initWithData:_responseData encoding:NSUTF8StringEncoding];
NSLog(#"responseData as string: %#", responseString);
SBJsonParser* parser = [[SBJsonParser alloc] init];
NSDictionary* dict = [parser objectWithData:_responseData];
NSLog(#"responseData as dictionary:");
for (id key in dict) {
NSLog(#"%#=%#", key, [dict objectForKey:key]);
}
[_delegate connection:connection receivedResponse:dict];
}
Thanks.
EDIT:
Also, I seem to have stumbled across a solution. The problem does have something to do with the way _responseData is declared.
Declaring response data as an atomic property leads to the response data being reset just as before.
#property (retain) NSMutableData* responseData;
However, simply declaring the variable in the interface seems to eliminate the problem - the data persists from didReceiveData to didFinishLoading.
#interface NetworkController : NSObject
{
NSMutableData* _responseData;
}
It is my understanding that declaring properties simply generates setters and getters, but I'm failing to see how this relates in this situation. Can anyone explain?
EDIT: I neglected to mention that this project is using ARC.
if you have declared responseData as retain if your .h file, you should change the code to:
-(void)connection:(NSConnection*)conn didReceiveResponse:(NSURLResponse *)response
{
if (self._responseData) {
self._responseData = [[NSMutableData alloc] init] autorelease];
}
//further code
}
this should resolve your issue