(UPDATED) this is the problem in a nutshell: in iOS I want to read a large file, do some processing on it (in this particular case encode as Base64 string() and save to a temp file on the device. I set up an NSInputStream to read from a file, then in
(void)stream:(NSStream *)stream handleEvent:(NSStreamEvent)eventCode
I'm doing most of the work. For some reason, sometimes I can see the NSInputStream just stops working. I know because I have a line
NSLog(#"stream %# got event %x", stream, (unsigned)eventCode);
in the beginning of (void)stream:(NSStream *)stream handleEvent:(NSStreamEvent)eventCode and sometimes I would just see the output
stream <__NSCFInputStream: 0x1f020b00> got event 2
(which corresponds to the event NSStreamEventHasBytesAvailable) and then nothing afterwards. Not event 10, which corresponds to NSStreamEventEndEncountered, not an error event, nothing! And also sometimes I even get a EXC_BAD_ACCESS exception which I have no idea at the moment how to debug. Any help would be appreciated.
Here is the implementation. Everything starts when I hit a "submit" button, which triggers:
- (IBAction)submit:(id)sender {
[p_spinner startAnimating];
[self performSelector: #selector(sendData)
withObject: nil
afterDelay: 0];
}
Here is sendData:
-(void)sendData{
...
_tempFilePath = ... ;
[[NSFileManager defaultManager] createFileAtPath:_tempFilePath contents:nil attributes:nil];
[self setUpStreamsForInputFile: [self.p_mediaURL path] outputFile:_tempFilePath];
[p_spinner stopAnimating];
//Pop back to previous VC
[self.navigationController popViewControllerAnimated:NO] ;
}
Here is setUpStreamsForInputFile called above:
- (void)setUpStreamsForInputFile:(NSString *)inpath outputFile:(NSString *)outpath {
self.p_iStream = [[NSInputStream alloc] initWithFileAtPath:inpath];
[p_iStream setDelegate:self];
[p_iStream scheduleInRunLoop:[NSRunLoop currentRunLoop]
forMode:NSDefaultRunLoopMode];
[p_iStream open];
}
Finally, this is where most logic occurs:
- (void)stream:(NSStream *)stream handleEvent:(NSStreamEvent)eventCode {
NSLog(#"stream %# got event %x", stream, (unsigned)eventCode);
switch(eventCode) {
case NSStreamEventHasBytesAvailable:
{
if (stream == self.p_iStream){
if(!_tempMutableData) {
_tempMutableData = [NSMutableData data];
}
if ([_streamdata length]==0){ //we want to write to the buffer only when it has been emptied by the output stream
unsigned int buffer_len = 24000;//read in chunks of 24000
uint8_t buf[buffer_len];
unsigned int len = 0;
len = [p_iStream read:buf maxLength:buffer_len];
if(len) {
[_tempMutableData appendBytes:(const void *)buf length:len];
NSString* base64encData = [Base64 encodeBase64WithData:_tempMutableData];
_streamdata = [base64encData dataUsingEncoding:NSUTF8StringEncoding]; //encode the data as Base64 string
[_tempFileHandle writeData:_streamdata];//write the data
[_tempFileHandle seekToEndOfFile];// and move to the end
_tempMutableData = [NSMutableData data]; //reset mutable data buffer
_streamdata = [[NSData alloc] init]; //release the data buffer
}
}
}
break;
case NSStreamEventEndEncountered:
{
[stream close];
[stream removeFromRunLoop:[NSRunLoop currentRunLoop]
forMode:NSDefaultRunLoopMode];
stream = nil;
//do some more stuff here...
...
break;
}
case NSStreamEventHasSpaceAvailable:
case NSStreamEventOpenCompleted:
case NSStreamEventNone:
{
...
}
}
case NSStreamEventErrorOccurred:{
...
}
}
}
Note: when I posted this first, I was under a wrong impression the issue had something to do with using GCD. As per Rob's answer below I removed the GCD code and the issue persists.
First: in your original code, you were not using a background thread, but the main thread (dispatch_async but on the main queue).
When you schedule NSInputStream to run on the default runloop (so, the runloop of the main thread), the events are received when the main thread is in the default mode (NSDefaultRunLoopMode).
But: if you check, the default runloop changes mode in some situations (for example, during an UIScrollView scroll and some other UI updates). When the main runloop is in a mode different than the NSDefaultRunLoopMode, your events are not received.
Your old code, with the dispatch_async, was almost good (but move the UI Updates on the main thread). You have to add only few changes:
dispatch in the background, with something like this:
:
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT,0);
dispatch_async(queue, ^{
// your background code
//end of your code
[[NSRunLoop currentRunLoop] run]; // start a run loop, look at the next point
});
start a run loop on that thread. This must be done at the end (last line) of the dispatch async call, with this code
:
[[NSRunLoop currentRunLoop] run]; // note: this method never returns, so it must be THE LAST LINE of your dispatch
Try and let me know
EDIT - added example code:
To be more clear, I copy-paste your original code updated:
- (void)setUpStreamsForInputFile:(NSString *)inpath outputFile:(NSString *)outpath {
self.p_iStream = [[NSInputStream alloc] initWithFileAtPath:inpath];
[p_iStream setDelegate:self];
// here: change the queue type and use a background queue (you can change priority)
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT,0);
dispatch_async(queue, ^ {
[p_iStream scheduleInRunLoop:[NSRunLoop currentRunLoop]
forMode:NSDefaultRunLoopMode];
[p_iStream open];
// here: start the loop
[[NSRunLoop currentRunLoop] run];
// note: all code below this line won't be executed, because the above method NEVER returns.
});
}
After making this modification, your:
- (void)stream:(NSStream *)stream handleEvent:(NSStreamEvent)eventCode {}
method, will be called on the same thread where you started the run loop, a background thread: if you need to update the UI, it's important that you dispatch again to the main thread.
Extra informations:
In my code I use dispatch_async on a random background queue (which dispatch your code on one of the available background threads, or start a new one if needed, all "automagically"). If you prefer, you can start your own thread instead of using a dispatch async.
Moreover, I don't check if a runloop is already running before sending the "run" message (but you can check it using the currentMode method, look NSRunLoop reference for more informations). It shouldn't be necessary because each thread has only one associated NSRunLoop instance, so sending another run (if already running) does nothing bad :-)
You can even avoid the direct use of runLoops and switch to a complete GCD approach, using dispatch_source, but I've never used it directly so I can't give you a "good example code" now
NSStream requires a run loop. GCD doesn't provide one. But you don't need GCD here. NSStream is already asynchronous. Just use it on the main thread; that's what it's designed for.
You're also doing several UI interactions while on a background thread. You can't do that. All UI interactions have to occur on the main thread (which is easy if you remove the GCD code).
Where GCD can be useful is if reading and processing the data is time consuming, you can hand that operation to GCD during NSStreamEventHasBytesAvailable.
Related
My app uses NSInputStream like below:
inputStream.delegate = self;
[inputStream scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
[readStream open];
and delegate:
- (void)stream:(NSStream *)theStream handleEvent:(NSStreamEvent)streamEvent
It works fine, but all other requests that i do, it queued until first is finished.
I can do one per time and there is no way to do multiple concurrent requests.
There is a solution ?
Thank you
This solution not work for me :
https://stackoverflow.com/a/15346292/1376961
UPDATE:
Was my server can't handle multiple connections from the same source.
You will need to create your streams in separate threads to enable them to work simultaneously. I assume you have a method that sets up the inputStream you referred to:
- (void)openStreamInNewThread {
[NSThread detachNewThreadSelector:#selector(openStream) toTarget:self withObject:nil];
}
- (void)openStream {
NSInputStream *inputStream;
// stream setup
[inputStream scheduleInRunLoop:[NSRunLoop currentRunLoop]
forMode:NSRunLoopCommonModes];
}
Note that [NSRunLoop currentRunLoop] will return the runloop of the current thread. So you have the newly created stream running in a separate thread loading data simultaneously with other streams in their own threads.
You can try to schedule each stream in its own run loop. Below is a refined method from the mock class designed to unit-test my POSInputStreamLibrary:
static const NSTimeInterval kRunLoopCycleInterval = 0.01f;
static const uint64_t kDispatchDeltaNanoSec = 250000000;
- (POSRunLoopResult)launchNSRunLoopWithStream:(NSInputStream *)stream delegate:(id<NSStreamDelegate>)streamDelegate {
stream.delegate = streamDelegate;
__block BOOL breakRunLoop = NO;
__block dispatch_semaphore_t doneSemaphore = dispatch_semaphore_create(0);
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
[stream scheduleInRunLoop:runLoop forMode:NSDefaultRunLoopMode];
if ([stream streamStatus] == NSStreamStatusNotOpen) {
NSLog(#"%#: opening stream...", [NSThread currentThread]);
[stream open];
}
while ([runLoop runMode:NSDefaultRunLoopMode beforeDate:[NSDate dateWithTimeIntervalSinceNow:kRunLoopCycleInterval]] && !breakRunLoop)
{}
NSLog(#"%#: We are done!", [NSThread currentThread]);
dispatch_semaphore_signal(doneSemaphore);
});
POSRunLoopResult result = dispatch_semaphore_wait(doneSemaphore, dispatch_time(DISPATCH_TIME_NOW, kDispatchDeltaNanoSec)) == 0 ? POSRunLoopResultDone : POSRunLoopResultTimeout;
if (POSRunLoopResultTimeout == result) {
breakRunLoop = YES;
dispatch_semaphore_wait(doneSemaphore, DISPATCH_TIME_FOREVER);
}
return result;
}
Each time I create a new NSInputStream, I add it to a block object, and then store the block object in an NSMutableArray.
I posted code that streams video from one iOS to another:
https://app.box.com/s/94dcm9qjk8giuar08305qspdbe0pc784
Build this app with Xcode 11; run it on two iOS 11 devices.
Touch the Camera icon on one of the two devices to start streaming live video.
If you don't have two devices, you can run the app in a simulator; however, stream from the real device only (the camera is not available on the simulator).
I'm trying to run a download in a background thread as to not block the main UI thread on iOS, what I did was create a new thread using
[NSThread detachNewThreadSelector:#selector(startDownload) toTarget:downloadObject withObject:nil];
Then the following code runs on a background thread:
NSURL* urlForCalendar = [NSURL URLWithString:#"http://www.apple.com/"];
urlRequest = [NSURLRequest requestWithURL:urlForCalendar];
urlConnection = [[NSURLConnection alloc] initWithRequest:urlRequest delegate:self startImmediately:NO];
NSRunLoop* runLoop = [NSRunLoop currentRunLoop];
[urlConnection scheduleInRunLoop:runLoop forMode:NSRunLoopCommonModes];
[urlConnection start];
However, the delegate callbacks are never called.
EDIT:
For anyone who might come across a similar problem in the future, after a bit of trying to figure out why it wasn't working, I wasn't running the loop. So the last 3 lines of code should actually be:
NSRunLoop* runLoop = [NSRunLoop currentRunLoop];
[urlConnection scheduleInRunLoop:runLoop forMode:NSRunLoopCommonModes];
[urlConnection start];
[runLoop run];
You don't run the run loop of the thread you created, so the connection you add to the run loop is never serviced and you never get any callbacks.
Generally you just want to handle the callbacks on the main thread and then push the result to a background thread if heavy processing is required.
You can do what you're currently doing though so long as you run the run loop and tidy up properly once the download is complete.
I am using NSInputStream to grab some resources from web like this:
CFHTTPMessageRef request = CFHTTPMessageCreateRequest(kCFAllocatorDefault, CFSTR("GET"), (__bridge CFURLRef)url, kCFHTTPVersion1_1);
CFReadStreamRef readStream = CFReadStreamCreateForHTTPRequest(kCFAllocatorDefault, request);
CFRelease(request);
_inputStream = CFBridgingRelease(readStream);
_inputStream.delegate = self;
[_inputStream scheduleInRunLoop:[NSRunLoop mainRunLoop] forMode:NSDefaultRunLoopMode];
[_inputStream open];
And then to offload the actual work to other thread:
- (void)stream:(NSStream *)aStream handleEvent:(NSStreamEvent)eventCode
{
NSLog(#"stream: %# handleEvent: %d", aStream, eventCode);
dispatch_async(_custom_serial_queue, ^{
switch(eventCode) {
case NSStreamEventOpenCompleted: {
NSLog(#"Stream has opened");
break;
}
// rest of switch
});
}
I see Stream has opened log and then in 95% of cases my main thread freezes
This happens only on iPhone4 and iOS6.1, on iPhone5s and iOS7 and iPad3 iOS6.1 everything is fine.
Could you please suggest what is wrong with this code.
EDIT 1: I have tracked down problem to some piece of code(in 3rd party lib) which uses __sync_add_and_fetch function. It seems it causes the issue but I still need to use it.
EDIT 2: I have changed __sync_add_and_fetch function by OSAtomicAdd32 but it hasn't changed anything.
EDIT 3: I have checked the problem on other devices. Works fine on iPhone4s iOS7, iPad2 iOS6.1. It seems the problem is only relevant to 1 core devices.
(UPDATED) I am trying to read a large file ( a video or a picture) and send it to a remote server via a SOAP request. I need to encode the data as a Base64 string. I am trying to do this as follows:
Create a template xml for the SOAP request that will go "around" the base64 encoded data
push the first part of the SOAP xml into a buffer
open the video file and encode it in chunks and push each encoded chunk into the buffer
finally, push the second part of the SOAP xml
To be able to "enqueue" parts as above, I am trying to use GCDAsyncSocket with its buffering capabilities. I figure that since GCDAsyncSocket operates on TCP level, I need to write the HTTP POST header myself. So, there are many moving parts which I only vaguely understand and I might be doing it all incorrectly. But my socket never seems to even take off and I am not even sure how to debug it. Here is my relevant code, try to see if you spot any obvious errors:
NSString *soapBody = ...; //Create the SOAP xml here with the part in the middle reserved for the Base64 encoded data (marked with string "CUT_HERE";
NSArray *soapBodyArray = [soapBody componentsSeparatedByString:#"CUT_HERE"];
self.p_soapBodyPart1 = [soapBodyArray[0] dataUsingEncoding:NSUTF8StringEncoding];
self.p_soapBodyPart2 = [soapBodyArray[1] dataUsingEncoding:NSUTF8StringEncoding];
socketQueue = dispatch_queue_create("socketQueue", NULL);//socketQueue is an ivar
self.p_socket = [[GCDAsyncSocket alloc] initWithDelegate:self delegateQueue:socketQueue];//create the socket
NSError *err = nil;
if (![p_socket connectToHost:myURL onPort:80 error:&err]) // Asynchronous!
{
NSLog(#"I goofed: %#", err);
return;
}
NSString* httpHeader = [NSString stringWithFormat:#"POST %# HTTP/1.1\r\nHost: %#\r\nAccept-Encoding: gzip, deflate\r\nContent-Type: text/xml\r\nAccept-Language: en-us\r\nAccept: */*\r\nSOAPAction: http://tempuri.org/myAction\r\nConnection: keep-alive\r\nUser-Agent: ...\r\n\r\n", webserviceOperationsPath, webserviceHost];//Create the HTTP POST header
[p_socket writeData:[httpHeader dataUsingEncoding:NSUTF8StringEncoding] withTimeout:-1 tag:1]; //enqueue the HTTP header
[p_socket writeData:self.p_soapBodyPart1 withTimeout:-1 tag:2]; //enqueue the first part of the SOAP xml
[self setUpStreamsForInputFile: [self.p_mediaURL path]];//set up NSInputStream to read from media file and encode it as Base64
The socket seems to always connect all right, which I see using this delegate method:
- (void)socket:(GCDAsyncSocket *)sock didConnectToHost:(NSString *)host port:(uint16_t)port
{
NSLog(#"Socket Connected");
}
setUpStreamsForInputFile method (that is called in the first code listing above):
- (void)setUpStreamsForInputFile:(NSString *)inpath {
self.p_iStream = [[NSInputStream alloc] initWithFileAtPath:inpath];
[p_iStream setDelegate:self];
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT,0);
dispatch_async(queue, ^ {
[p_iStream scheduleInRunLoop:[NSRunLoop currentRunLoop]
forMode:NSDefaultRunLoopMode];
[p_iStream open];
[[NSRunLoop currentRunLoop] run];
});
}
Now, the NSInputStream setup in the previous method will send events to this delegate:
- (void)stream:(NSStream *)stream handleEvent:(NSStreamEvent)eventCode {
switch(eventCode) {
case NSStreamEventHasBytesAvailable:
{
if (stream == self.p_iStream){
if(!self.p_tempMutableData) {
self.p_tempMutableData = [NSMutableData data];
}
uint8_t buf[24000];
unsigned int len = 0;
len = [p_iStream read:buf maxLength:24000];//read a chunk from the file
if(len) {
[p_tempMutableData appendBytes:(const void *)buf length:len];
NSString* base64encData = [Base64 encodeBase64WithData:self.p_tempMutableData];//encode the chunk
self.p_streamEncData = [base64encData dataUsingEncoding:NSUTF8StringEncoding];
[p_socket writeData:self.p_streamEncData withTimeout:-1 tag:3];//write the encoded chunk to the socket
}
}
break;
}
case NSStreamEventEndEncountered:
{
[stream close];
[stream removeFromRunLoop:[NSRunLoop currentRunLoop]
forMode:NSDefaultRunLoopMode];
stream = nil;
[p_socket writeData:self.p_soapBodyPart2 withTimeout:-1 tag:4];//write the second part of SOAP xml
break;
}
... //some other events handled here
}
}
The socket is supposed to output things to the log with this delegate
- (void)socket:(GCDAsyncSocket *)sock didWriteDataWithTag:(long)tag
{
if (tag == 1)
NSLog(#"HTTP Header Written");
else if (tag == 2)
NSLog(#"Soap Part 1 written");
else if (tag == 3)
NSLog(#"File written");
else if (tag == 4)
NSLog(#"Soap Part 2 written");
}
but this happens kind of randomly. For example, sometimes I see the first 2 if's called and sometimes not. When I do and it reaches the third "if" (the one where I am writing the actual encoded data), it writes it only 2 or 3 times and that's it - too few times, I think, given the size of the file. I never see it reach the last "if", where it should write the last part of SOAP xml.
Would appreciate any help! Thanks in advance.
Further update (3/19/13)
Today testing the socket I am no longer getting the write events at all, which tells me that it is random and I am doing something terribly wrong. Today the connection opens but then times out at some point, as I can see with the following delegate method:
- (void)socketDidDisconnect:(GCDAsyncSocket *)sock withError:(NSError *)err
{ // This method is executed on the socketQueue (not the main thread)
dispatch_async(dispatch_get_main_queue(), ^{
#autoreleasepool {
NSLog(#"socketDidDisconnect:withError: \"%#\"", err);
}
});
}
which returns
socketDidDisconnect:withError: "Error Domain=NSPOSIXErrorDomain Code=60 "Operation timed out" UserInfo=0x1cd89b00 {NSLocalizedFailureReason=Error in connect() function, NSLocalizedDescription=Operation timed out}"
while I am still running writes of Base64 data in the stream delegate above.
How can I get NSURLConnection to call it's delegate methods from a different thread instead of the main thread. I'm trying to mess around with the scheduleInRunLoop:forMode:but doesn't seem to do what I want.
I have to download a large file and it interrupts the main thread so frequently that some rendering that is happening starts getting choppy.
NSURLRequest * request = [NSURLRequest requestWithURL:url];
NSURLConnection * connection = [[NSURLConnection alloc] initWithRequest:request delegate:self startImmediately:NO];
NSRunLoop * loop = [NSRunLoop currentRunLoop];
NSLog(#"loop mode: %#",[loop currentMode]);
[connection scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes];
[connection start];
The other thing I don't see much of is "Modes" There are only two modes documented so not much really to test with.
Any ideas?
Thanks
There are several options:
In your implementation of the delegate methods, make use of dispatch_async.
Start the schedule the connection on a background thread.
You can do the latter like this:
// all the setup comes here
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSRunLoop *loop = [NSRunLoop currentRunLoop];
[connection scheduleInRunLoop:loop forMode:NSRunLoopCommonModes];
[loop run]; // make sure that you have a running run-loop.
});
If you want a guarantee on which thread you're running, replace the call to dispatch_get_global_queue() appropriately.
If you want to perform downloads on a separate thread, I'm pretty sure these are the droids you're looking for...
- (void) dispatchRequest{
self->finished = NO;
NSMutableURLRequest* request = //Formulate your request
NSThread* download_thread = [[NSThread alloc] initWithTarget:self selector:#selector(downloadThreadLoop:) object:request];
[download_thread start];
}
- (void) downloadThreadLoop:(NSMutableURLRequest*) request{
NSURLConnection* connection = [[NSURLConnection alloc] initWithRequest:request delegate:self];
while(!self->finished]){
//This line below is the magic!
[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
}
}
- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response{
//...
}
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data{
//...
}
- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error{
//...
}
- (void)connectionDidFinishLoading:(NSURLConnection *)connection{
//...
self->finished = YES;
}
If you truly need to do the download in a new thread, it may be easier to detachNewThreadSelector:toTarget:withObject:, setup (and destroy) an NSAutoreleasePool, and then use one of the synchronous selectors like NSData's dataWithContentsOfURL:. This will not make use of the asynchronous NSURLConnectionDelegate.
Because this call is synchronous, it will not return until the file has been downloaded, which will block the main thread, but because you're in a new thread, it won't. Please note that this is typically discouraged behavior. Is there other code happening in the main thread that can be optimized?
NSURLConnection is already doing the download off of the main thread asynchronously. If I understand your question, you would like the delegate messages to be sent on a thread other than the main thread? You can't do that as you can't modify the internal implementation of NSURLConnection. I can think of two ways to simulate this.
Create a sublcass of NSURLConnection (e.g. MyURLConnection) that assigns itself as own delegate. Note that this creates an intentional retain cycle so be careful. MyURLConnection should define a new delegate that supports NSURLConnectionDelegate. Let's call this finalDelegate. When MyURLConnection handles it's own delegate messages, forward or dispatch them to finalDelegate on whatever thread you like.
Similar to option #1 but without the subclass. Handle the NSURLConnection delegate methods on the main thread and forward/dispatch them to whatever thread you like.
The main difference is if you want a reusable subclass that behaves this way or it's a one off implementation.
EDIT: added suggestion on how to run code in the background
If you are going to process the response in the background I would either use operations or grand central dispatch. No need to mess around with run loops and creating threads. Check out Apple's Concurrency Programming Guide.