I have an nsthread, a while loop within this one. It's getting objects from a "thread-safe" queue in the main method. When I leave the UIViewController, which contains this nsthread object, I call the nsthread cancel method, but it doesn't stop, because it's locked by the "queueLock" NSCondition. When I go back to this UIViewController a new nsthread will be created and gets the objects form the queue, but the previous thread still exits and both of them try to use the same object from the queue, and this causes a memory management problem. My question is how should I stop this thread, when I leave the UIViewController.
NSThread main method:
NSAutoreleasePool* pool = [[NSAutoreleasePool alloc] init];
while ([self isCancelled] == NO) {
RenderRequest *renderRequest = [queue get];
[self doRender:renderRequest];
[renderRequest release];
}
[pool drain];
This is the get method of the queue class:
- (id) get {
id toRet = nil;
[queueLock lock];
#try {
while ([queueContents count] == 0) {
[queueLock wait];
}
toRet = [queueContents lastObject];
[queueContents removeLastObject];
}
#finally {
[queueLock unlock];
return toRet;
}
}
Thanks!
I wrote a simple demo, hope this can help you :)
demo.h
#import <Foundation/Foundation.h>
#interface test : NSObject
{
NSCondition *queueCondition;
NSThread *queueThread;
NSMutableArray *queueTask;
NSTimer *timer;
}
- (id)init;
#end
demo.m
#import "demo.h"
#interface demo (PrivateMethods)
- (void)threadTest;
- (void)cancelThread;
- (void)addTask;
#end
#implementation demo
- (id)init
{
self = [super init];
if (self) {
if (!queueThread) {
if (!queueCondition) {
queueCondition = [[NSCondition alloc] init];
}
if (!queueTask) {
queueTask = [[NSMutableArray alloc] initWithCapacity:5];
}
queueThread = [[NSThread alloc] initWithTarget:self selector:#selector(threadTest) object:nil];
[queueThread start];
[self performSelector:#selector(cancelThread) withObject:nil afterDelay:10];
if (!timer) {
timer = [[NSTimer scheduledTimerWithTimeInterval:1 target:self selector:#selector(addTask) userInfo:nil repeats:YES] retain];
}
}
}
return self;
}
- (void)dealloc
{
[queueThread release];
[queueCondition release];
[queueTask release];
[timer invalidate];
[timer release];
[super dealloc];
}
- (void)threadTest
{
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
while (![[NSThread currentThread] isCancelled]) {
[queueCondition lock];
[queueCondition wait];
if ([queueTask count] == 0) {
[queueCondition unlock];
continue;
}
NSString *str = nil;
while ((str = [queueTask lastObject])) {
NSLog(#"getTask: %#", [queueTask lastObject]);
[queueTask removeLastObject];
}
[queueCondition unlock];
}
NSLog(#"threadTest end");
[pool drain];
}
- (void)addTask
{
[queueCondition lock];
if (!queueTask) {
queueTask = [[NSMutableArray alloc] initWithCapacity:5];
}
[queueTask addObject:#"new task"];
[queueCondition signal];
NSLog(#"add: new task");
[queueCondition unlock];
}
- (void)cancelThread
{
[timer invalidate];
[queueThread cancel];
[queueCondition lock];
[queueCondition signal];
[queueCondition unlock];
}
#end
- (id) get
{
id toRet = nil;
[queueLock lock];
#try
{
while ([queueContents count] == 0)
{
[queueLock wait];
if ([self isCancelled]) return nil; // stop waiting
}
toRet = [queueContents lastObject];
[queueContents removeLastObject];
}
#finally
{
[queueLock unlock];
return toRet;
}
}
thread main
NSAutoreleasePool* pool = [[NSAutoreleasePool alloc] init];
while ([self isCancelled] == NO)
{
RenderRequest *renderRequest = [queue get];
if (renderRequest == nil) break; // should stop
[self doRender:renderRequest];
[renderRequest release];
}
[pool drain];
then you can cancel the thread and notify queueLock to stop waiting
Related
I am trying to write some code to scan for bluetooth devices for a while, and then return the array of discovered peripherals through a block-based callback.
Blocking code should not be an issue as the code will be called asynchronously.
After reading up on the API documentation my initial plan of attack was to write an implementation for CBCentralManagerDelegate, use an init method to give it a block-based callback to call once the CBManagerState is PoweredOn, and then initialize this class with a callback that triggers the scanning and extracts the discovered Peripherals.
The issue is... it doesn't work. Except when it does.
Now I could work out a workaround to reach my goal, but for the sake of learning and understanding I am very interested in where exactly the issue originates from.
typedef void (^SomeBlock)(CBCentralManager*);
#interface TEST : NSObject <CBCentralManagerDelegate>
#property CBCentralManager* manager;
#property SomeBlock onPoweredOn;
#property NSMutableArray<CBPeripheral*>* peripherals;
- (void) init: (SomeBlock) onPoweredOn;
- (void) startScan;
- (void) stopScan;
#end
#implementation TEST
- (void) init: (SomeBlock) onPoweredOn {
NSLog(#"%#", #"init");
self.onPoweredOn = onPoweredOn;
self.manager = [CBCentralManager alloc];
dispatch_queue_attr_t attr = DISPATCH_QUEUE_CONCURRENT;
dispatch_queue_t queue =dispatch_queue_create("BTManagerHandler", attr);
self.manager = [self.manager initWithDelegate: self queue: queue];
}
- (void) startScan {
NSLog(#"%#", #"startScan");
[self.manager scanForPeripheralsWithServices: nil options: nil];
}
- (void) stopScan {
NSLog(#"%#", #"stopScan ");
[self.manager stopScan];
}
- (void) centralManagerDidUpdateState: (nonnull CBCentralManager *) manager {
NSLog(#"%#", #"centralManagerDidUpdateState:");
switch (manager.state) {
case CBManagerStateUnknown:
NSLog(#"%#", #"CBManagerStateUnknown:");
break;
case CBManagerStateResetting:
NSLog(#"%#", #"CBManagerStateResetting:");
break;
case CBManagerStateUnsupported:
NSLog(#"%#", #"CBManagerStateUnsupported:");
break;
case CBManagerStateUnauthorized:
NSLog(#"%#", #"CBManagerStateUnauthorized:");
break;
case CBManagerStatePoweredOff:
NSLog(#"%#", #"CBManagerStatePoweredOff:");
break;
case CBManagerStatePoweredOn:
NSLog(#"%#", #"CBManagerStatePoweredOn:");
if (self.onPoweredOn != nil) self.onPoweredOn(manager);
break;
}
}
- (void) centralManager: (nonnull CBCentralManager*) central didDiscoverPeripheral: (nonnull CBPeripheral*) peripheral advertisementData: (nonnull NSDictionary<NSString*, id>*) advertisementData RSSI: (nonnull NSNumber*) RSSI {
NSLog(#"%#", #"centralManager:didDiscoverPeripheral:advertisementData:RSSI:");
if (self.peripherals == nil) self.peripherals = [NSMutableArray array];
for (CBPeripheral* _peripheral in self.peripherals) {
if (peripheral.identifier == _peripheral.identifier) return;
}
[self.peripherals addObject: peripheral];
}
#end
+ (void) discoverDevices {
TEST* test = nil;
#try {
test = [TEST alloc];
SomeBlock onPoweredOn = ^(CBCentralManager* manager) {
NSLog(#"%#", #"_onPoweredOn_");
[test startScan];
[NSThread sleepForTimeInterval: 10.0];
[managerHandler stopScan];
NSArray<CBPeripheral*>* discoveredPeripherals = managerHandler.peripherals;
// do stuff with discoveredPeripherals
};
[test init: onPoweredOn];
} #catch(NSException* e) {
// exception handling
} #finally {
// cleanup
}
}
I would expect the above code to work, but it doesn't.
The 'onPoweredOn' callback and the 'startScan' method are called correctly, but the 'centralManager:didDiscoverPeripheral:advertisementData:RSSI:' method is never called.
After some trial and error I found that the following works:
+ (void) discoverDevices {
TEST* test = nil;
#try {
test = [TEST alloc];
SomeBlock onPoweredOn = ^(CBCentralManager* manager) {
NSLog(#"%#", #"_onPoweredOn_");
[test startScan];
};
[test init: onPoweredOn];
[NSThread sleepForTimeInterval: 10.0];
[managerHandler stopScan];
NSArray<CBPeripheral*>* discoveredPeripherals = managerHandler.peripherals;
// do stuff with discoveredPeripherals
} #catch(NSException* e) {
// exception handling
} #finally {
// cleanup
}
}
After some more trial and error I narrowed it down to one line of code:
+ (void) discoverDevices {
TEST* test = nil;
#try {
test = [TEST alloc];
SomeBlock onPoweredOn = ^(CBCentralManager* manager) {
NSLog(#"%#", #"_onPoweredOn_");
[test startScan];
[NSThread sleepForTimeInterval: 10.0]; // <<=== this line! ===
};
[test init: onPoweredOn];
[NSThread sleepForTimeInterval: 10.0];
[managerHandler stopScan];
NSArray<CBPeripheral*>* discoveredPeripherals = managerHandler.peripherals;
// do stuff with discoveredPeripherals
} #catch(NSException* e) {
// exception handling
} #finally {
// cleanup
}
}
This suggests that that using a [NSThread sleepForTimeInterval:] blocks the discovery of bluetooth devices... but tat seems illogical to me because the same code works without the block-based callback:
+ (void) discoverDevices {
TEST* test = nil;
#try {
test = [TEST alloc];
[test init: nil];
[NSThread sleepForTimeInterval: 1.0];
[test startScan];
[NSThread sleepForTimeInterval: 10.0];
[managerHandler stopScan];
NSArray<CBPeripheral*>* discoveredPeripherals = managerHandler.peripherals;
// do stuff with discoveredPeripherals
} #catch(NSException* e) {
// exception handling
} #finally {
// cleanup
}
}
Conclusion: combining CBCentralManager, block-based callbacks and [NSThread sleepForTimeInterval:] leads to unexpected behaviour??
but why? what's so special about this specific combination?
This problem, when i swipe screen API Calls are to be made, separate calls for separate swipe i.e. Left, Right, Top, Bottom. Also there has to be Stop API which has to be sent in 1 minute delay of swipe.
Example Camera moving on swipe for 1 second and stops:
Swipe Left -> Left() GET API is called with AFNetworking -> (So Camera starts moving to left) -> Soon after Left API is triggered -> 1 second delay -> Stop GET API is called with AFNetworking. Repeats same method for Right(), Top(), Bottom()
I am not able to achieve this smoothly, I use this logic.
- (void)didSwipe:(UISwipeGestureRecognizer*)swipe{
if (swipe.direction == UISwipeGestureRecognizerDirectionLeft) {
[self performSelector:#selector(moveRight) withObject:self afterDelay:0.0];
[self performSelector:#selector(stop:) withObject:self afterDelay:1.0];
} else if (swipe.direction == UISwipeGestureRecognizerDirectionRight) {
[self performSelector:#selector(moveLeft) withObject:self afterDelay:0.0];
[self performSelector:#selector(stop:) withObject:self afterDelay:1.0];
} else if (swipe.direction == UISwipeGestureRecognizerDirectionUp) {
[self performSelector:#selector(moveDown) withObject:self afterDelay:0.0];
[self performSelector:#selector(stop:) withObject:self afterDelay:1.0];
} else if (swipe.direction == UISwipeGestureRecognizerDirectionDown) {
[self performSelector:#selector(moveUp) withObject:self afterDelay:0.0];
[self performSelector:#selector(stop:) withObject:self afterDelay:1.0];
}
}
This are Method calls
-(void) moveRight
{
NSMutableDictionary *parameters = [[NSMutableDictionary alloc] init];
[parameters setValue:#"MoveRight" forKey:#"direction"];
[self changeDirection:parameters];
}
-(void) moveLeft
{
NSMutableDictionary *parameters = [[NSMutableDictionary alloc] init];
[parameters setValue:#"MoveLeft" forKey:#"direction"];
[self changeDirection:parameters];
}
-(void) moveUp
{
NSMutableDictionary *parameters = [[NSMutableDictionary alloc] init];
[parameters setValue:#"MoveUp" forKey:#"direction"];
[self changeDirection:parameters];
}
-(void) moveDown
{
NSMutableDictionary *parameters = [[NSMutableDictionary alloc] init];
[parameters setValue:#"MoveDown" forKey:#"direction"];
[self changeDirection:parameters];
}
-(void) stop
{
NSMutableDictionary *parameters = [[NSMutableDictionary alloc] init];
[parameters setValue:#"Stop" forKey:#"direction"];
[self changeDirection:parameters];
}
Now ChangeDirection Method
-(void) changeDirection:(NSDictionary *) parameters
{
dispatch_queue_t myQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0);
dispatch_async(myQueue, ^{
[RestHandler afnetworkingGetApiCall():parameters :^(id response, NSString *respMsg)
{
}];
dispatch_async(dispatch_get_main_queue(), ^{
});
});
}
Hope I am able to explain my question. Let me know if its not clear
Cheers.
i think, you have to create model for each response. then for each call you have to create common operation queue.
In the UIViewController viewDidAppear event, I want to get some data from web service. And the code like:
- (void)viewDidAppear:(BOOL)animated
{
[super viewDidAppear:animated];
NSArray *arr = [self getCarList];
}
- (NSArray *)getCarList
{
if (!carList) {
ARequset *req = [[ARequset alloc] init];
[NetService sendRequest:req respClass:[Resp class] success:^(BaseResponse *response)
{
//after finished
self.requestFinished = YES;
} fail:^(NSInteger errcode, NSString *errmsg) {
self.requestFinished = YES;
}];
while (!self.requestFinished) {
[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
}
}
return carList;
}
when run in to [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]]; the request success block will not be performed, and the UI become no response.
but if i change the - (void)viewDidAppear:(BOOL)animated like this, all goes well.
- (void)viewDidAppear:(BOOL)animated
{
[super viewDidAppear:animated];
[self performSelector:#selector(getCarList) withObject:self afterDelay:1];
}
Don't do that kind of syntax
The NetService request works asynchronously, the result of the request is passed
in the block. You have to process the BaseResponse object and update your UI or whatever you want to do with the data.
Polling is bad habit and consumes system resources unnecessarily.
Apart from that you're telling the currently running runloop of UIApplication to run which blocks it.
Do something like this
- (void)getCarList
{
if (!carList) {
ARequset *req = [[ARequset alloc] init];
[NetService sendRequest:req respClass:[Resp class] success:^(BaseResponse *response)
{
//after finished
self.carList = [self doSomethingWithTheResponse:response];
dispatch_async(dispatch_get_main_queue(), ^(void) {
// Update UI
});
} fail:^(NSInteger errcode, NSString *errmsg) {
dispatch_async(dispatch_get_main_queue(), ^(void) {
// show alert
});
}];
}
}
}
Edit: Alternatively use a delegate like pattern to handle the asynchronous behavior.
Instead of the synchronous method
- (void)methodToGetCarList
{
NSArray *cars = [self getCarList];
[self doSomethingWithCars:cars];
}
use this
- (void)methodToGetCarListAsynchronously
{
[self getCarList];
}
and the delegate method
- (void)didReceiveCars:(NSArray *)cars errorMessage:(NSString *)error
{
if (error) {
// do error handling
} else {
[self doSomethingWithCars:cars];
}
}
the getCarList method looks like
- (void)getCarList
{
if (!carList) {
ARequset *req = [[ARequset alloc] init];
[NetService sendRequest:req respClass:[Resp class] success:^(BaseResponse *response)
{
//after finished
self.carList = [self doSomethingWithTheResponse:response];
[self didReceiveCars:self.carList errorMessage:nil];
} fail:^(NSInteger errcode, NSString *errmsg) {
[self didReceiveCars:nil errorMessage:errmsg];
}];
}
} else {
[self didReceiveCars:self.carList errorMessage:nil];
}
}
The code does not consider potential issues if the response returns in a background thread.
I will explain scenario.
I have a NSOperation subclass. In this class , I am reading data from multiple bluetooth devices.
I am creating an object of NSOperation class in ViewController A and get data using delegate methods in NSoperation subclass.
Now, I want to read data from Viewcontroller B without creating an object of NSoperation.
Please check my NSOperation Subclass
NOPerationSubclass.h
`
#protocol NOPerationSubclassDelegate`;
#interface NOPerationSubclass : NSOperation{
BOOL executing;
BOOL finished;
}
#property id<NOPerationSubclassDelegate> delegate;
- (id)initWithConnectDevice:(ConnectDevice *)cDevice toPeripheral:(CBPeripheral *)peripheral;
#end
#protocol NOPerationSubclassDelegate
-(void)updateUIFromOperation:(NOPerationSubclass *)operation;
#end
NOPerationSubclass.m
- (id)initWithConnectDevice:(ConnectDevice *)cDevice toPeripheral:(CBPeripheral *)peripheral{
if (self = [super init]) {
executing = NO;
finished = NO;
self.connectDevice = cDevice;
[self.connectDevice setDelegate:self];
self.connectedPeripheral = peripheral;
dataDic = [[NSMutableDictionary alloc] init];
}
return self;
}
-(BOOL)isConcurrent{
return YES;
}
- (BOOL)isExecuting {
return executing;
}
- (BOOL)isFinished {
return finished;
}
-(void) terminateOperation {
[self willChangeValueForKey:#"isFinished"];
[self willChangeValueForKey:#"isExecuting"];
finished = YES;
executing = NO;
[self didChangeValueForKey:#"isExecuting"];
[self didChangeValueForKey:#"isFinished"];
}
- (void)start {
#autoreleasepool {
if (self.isCancelled){
[self willChangeValueForKey:#"isFinished"];
finished = YES;
[self didChangeValueForKey:#"isFinished"];
return;
}
timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:#selector(timerFired:) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] run];
}
-(void)timerFired:(id)sender{
if (self.isCancelled){
[self willChangeValueForKey:#"isFinished"];
finished = YES;
[self didChangeValueForKey:#"isFinished"];
return;
}
[connectDevice calldiscoverServicesForPeripheral:connectedPeripheral];
}
-(void)getDataFromPeripheral:(CBPeripheral *)peripheral Data:(NSString *)data{
[dataDic setValue:[peripheral.identifier UUIDString] forKey:#"identifier"];
[dataDic setValue:data forKey:#"data"];
[[[AppDelegate app] devicesDataArray] addObject:dataDic];
[(NSObject *)self.delegate performSelectorOnMainThread:#selector(updateUIFromOperation:) withObject:dataDic waitUntilDone:NO];
NSLog(#"PERIPHERAL DATA::+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++%#",peripheral.name);
}
And, I am calling this NSOpeartion class from ViewController A like this
NOPerationSubclass *queue = [[NOPerationSubclass alloc] initWithConnectDevice:connectDevices toPeripheral:peripheral];
queue.delegate = self;
[[[AppDelegate app] mainOperationQueue] addOperation:queue];
You can use a shared instance class, this is what I always do:
Database.h
#import <Foundation/Foundation.h>
#interface Database : NSObject
#property (nonatomic, readonly) NSArray* myTable;
+(Database*) sharedInstance;
#end
Database.m
#import "Database.h"
#implementation Database
Database* _db = nil;
+(Database*) sharedInstance {
if (!_db)
_db = [[Database alloc] init];
return _db;
}
-(id) init {
self = [super init];
// Do loading here
return self;
}
#end
Then whenever you want to access the data:
[Database sharedInstance].myTable;
I have few different questions about NSOperation and NSOperationQueue and I know guys that yours answers will help me;
I have to load a big amount of images and I have created my own loader based on NSOperation, NSOperationQueue and NSURLConnection (asynchronous loading);
Questions:
If I set maxConcurrentOperationCount (for example 3) for queue (NSOperationQueue), does it mean that only 3 operations performed in the same time even queue has 100 operations?
When I set property maxConcurrentOperationCount for queue sometimes "setCompletionBlock" doesn't work and count (operationCount) only increases; Why?
MyLoader:
- (id)init
{
self = [super init];
if (self) {
_loadingFiles = [NSMutableDictionary new];
_downloadQueue = [NSOperationQueue new];
_downloadQueue.maxConcurrentOperationCount = 3;
_downloadQueue.name = #"LOADER QUEUE";
}
return self;
}
- (void)loadFile:(NSString *)fileServerUrl handler:(GetFileDataHandler)handler {
if (fileServerUrl.length == 0) {
return;
}
if ([_loadingFiles objectForKey:fileServerUrl] == nil) {
[_loadingFiles setObject:fileServerUrl forKey:fileServerUrl];
__weak NSMutableDictionary *_loadingFiles_ = _loadingFiles;
MyLoadOperation *operation = [MyLoadOperation new];
[operation fileServerUrl:fileServerUrl handler:^(NSData *fileData) {
[_loadingFiles_ removeObjectForKey:fileServerUrl];
if (fileData != nil) {
handler(fileData);
}
}];
[operation setQueuePriority:NSOperationQueuePriorityLow];
[_downloadQueue addOperation:operation];
__weak NSOperationQueue *_downloadQueue_ = _downloadQueue;
[operation setCompletionBlock:^{
NSLog(#"completion block :%i", _downloadQueue_.operationCount);
}];
}
}
MyOperation:
#interface MyLoadOperation()
#property (nonatomic, assign, getter=isOperationStarted) BOOL operationStarted;
#property(nonatomic, strong)NSString *fileServerUrl;
#property(nonatomic, copy)void (^OnFinishLoading)(NSData *);
#end
#implementation MyLoadOperation
- (id)init
{
self = [super init];
if (self) {
_executing = NO;
_finished = NO;
}
return self;
}
- (void)fileServerUrl:(NSString *)fileServerUrl
handler:(void(^)(NSData *))handler {
#autoreleasepool {
self.fileServerUrl = fileServerUrl;
[self setOnFinishLoading:^(NSData *loadData) {
handler(loadData);
}];
[self setOnFailedLoading:^{
handler(nil);
}];
self.url = [[NSURL alloc] initWithString:self.fileServerUrl];
NSMutableURLRequest *request = [[NSMutableURLRequest alloc]
initWithURL:self.url
cachePolicy:NSURLRequestReloadIgnoringLocalCacheData
timeoutInterval:25];
[request setValue:#"" forHTTPHeaderField:#"Accept-Encoding"];
self.connection = [[NSURLConnection alloc] initWithRequest:request delegate:self startImmediately:NO];
[self.connection scheduleInRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes];
[self.connection start];
_data = [[NSMutableData alloc] init];
}
}
- (void)main {
#autoreleasepool {
[self stop];
}
}
- (void)start {
[self setOperationStarted:YES];
[self willChangeValueForKey:#"isFinished"];
_finished = NO;
[self didChangeValueForKey:#"isFinished"];
if ([self isCancelled])
{
[self willChangeValueForKey:#"isFinished"];
_finished = YES;
_executing = NO;
[self didChangeValueForKey:#"isFinished"];
}
else
{
[self willChangeValueForKey:#"isExecuting"];
_finished = NO;
_executing = YES;
[self didChangeValueForKey:#"isExecuting"];
}
}
- (BOOL)isConcurrent {
return YES;
}
- (BOOL)isExecuting {
return _executing;
}
- (BOOL)isFinished {
return _finished;
}
- (void)cancel {
[self.connection cancel];
if ([self isExecuting])
{
[self stop];
}
[super cancel];
}
#pragma mark -NSURLConnectionDelegate
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data {
[_data appendData:data];
}
- (void)connectionDidFinishLoading:(NSURLConnection *)connection {
if ([self OnFinishLoading]) {
[self OnFinishLoading](_data);
}
if (![self isCancelled]) {
[self stop];
}
}
- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error {
;
if (![self isCancelled]) {
[self stop];
}
}
- (void)stop {
#try {
__weak MyLoadOperation *self_ = self;
dispatch_async(dispatch_get_main_queue(), ^{
[self_ completeOperation];
});
}
#catch (NSException *exception) {
NSLog(#"Exception! %#", exception);
[self completeOperation];
}
}
- (void)completeOperation {
if (![self isOperationStarted]) return;
[self willChangeValueForKey:#"isFinished"];
[self willChangeValueForKey:#"isExecuting"];
_executing = NO;
_finished = YES;
[self didChangeValueForKey:#"isExecuting"];
[self didChangeValueForKey:#"isFinished"];
}
You must start the connection in the Operation's start method, and not in fileServerUrl:handler:.
I would remove this method altogether, and only provide an init method with all required parameters where you can completely setup the operation. Then, in method start start the connection.
Additionally, it's not clear why you override main.
Modifying the state variables _executing and _finished could be more concise and more clear (you don't need to set them initially, since the are already initialized to NO). Only set them in the "final" method completeOperation including KVO notifications.
You also do not need a #try/#catch in stop, since function dispatch_async() does not throw Objective-C exceptions.
Your cancel method is not thread safe, and there are also a few other issues. I would suggest the following changes:
#implementation MyOperation {
BOOL _executing;
BOOL _finished;
NSError* _error; // remember the error
id _result; // the "result" of the connection, unless failed
completion_block_t _completionHandler; //(your own completion handler)
id _self; // strong reference to self
}
// Use the "main thread" as the "synchronization queue"
- (void) start
{
// Ensure start will be called only *once*:
dispatch_async(dispatch_get_main_queue(), ^{
if (!self.isCancelled && !_finished && !_executing) {
[self willChangeValueForKey:#"isExecuting"];
_executing = YES;
[self didChangeValueForKey:#"isExecuting"];
_self = self; // keep a strong reference to self in order to make
// the operation "immortal for the duration of the task
// Setup connection:
...
[self.connection start];
}
});
}
- (void) cancel
{
dispatch_async(dispatch_get_main_queue, ^{
[super cancel];
[self.connection cancel];
if (!_finished && !_executing) {
// if the op has been cancelled before we started the connection
// ensure the op will be orderly terminated:
self.error = [[NSError alloc] initWithDomain:#"MyOperation"
code:-1000
userInfo:#{NSLocalizedDescriptionKey: #"cancelled"}];
[self completeOperation];
}
});
}
- (void)completeOperation
{
[self willChangeValueForKey:#"isExecuting"];
self.isExecuting = NO;
[self didChangeValueForKey:#"isExecuting"];
[self willChangeValueForKey:#"isFinished"];
self.isFinished = YES;
[self didChangeValueForKey:#"isFinished"];
completion_block_t completionHandler = _completionHandler;
_completionHandler = nil;
id result = self.result;
NSError* error = self.error;
_self = nil;
if (completionHandler) {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
completionHandler(result, error);
});
}
}
- (void)connectionDidFinishLoading:(NSURLConnection *)connection {
if ([self onFinishLoading]) {
[self onFinishLoading](self.result);
}
[self completeOperation];
}
- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error {
if (self.error == nil) {
self.error = error;
}
[self completeOperation];
}
In answer to your questions:
Yes, a maxConcurrentOperationCount of three means that only three will run at a time. Doing network requests like this is perfect example of when you'd want to use maxConcurrentOperationCount, because failure to do so would result in too many network requests trying to run, most likely resulting in some of the connections failing when using a slower network connection.
The main issue here, though, is that you're calling your operation's fileServerUrl method (which is starting the connection) from MyLoader. You've disconnected the request from the operation's start (defeating the purpose of maxConcurrentCount of 3 and possibly confusing the state of the operation).
The start method should be initiating the connection (i.e. don't start the request until one of those three available concurrent operations is available). Furthermore, since you cannot pass the URL and the handler to the start method, you should move your logic that saves those values to a customized rendition of your init method.
There are other minor edits we might suggest to your operation (main not needed, operationStarted is a little redundant, simplify the _executing/_finished handling, etc.), but the starting of the connection in fileServerUrl rather than being initiated by the start method is the key issue.
Thus:
- (id)initWithServerUrl:(NSString *)fileServerUrl
handler:(void(^)(NSData *))handler
{
self = [super init];
if (self) {
_executing = NO;
_finished = NO;
// do your saving of `fileServerURL` and `handler` here, e.g.
self.fileServerUrl = fileServerUrl;
self.OnFinishLoading:^(NSData *loadData) {
handler(loadData);
}];
[self setOnFailedLoading:^{
handler(nil);
}];
}
return self;
}
- (void)startRequest {
self.url = [[NSURL alloc] initWithString:self.fileServerUrl];
NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:self.url
cachePolicy:NSURLRequestReloadIgnoringLocalCacheData
timeoutInterval:25];
[request setValue:#"" forHTTPHeaderField:#"Accept-Encoding"];
self.connection = [[NSURLConnection alloc] initWithRequest:request delegate:self startImmediately:NO];
[self.connection scheduleInRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes];
[self.connection start];
_data = [[NSMutableData alloc] init];
}
- (void)start {
if ([self isCancelled])
{
[self willChangeValueForKey:#"isFinished"];
_finished = YES;
[self didChangeValueForKey:#"isFinished"];
return;
}
[self setOperationStarted:YES]; // personally, I'd retire this and just reference your `executing` flag, but I'll keep it here for compatibility with the rest of your code
[self willChangeValueForKey:#"isExecuting"];
_executing = YES;
[self didChangeValueForKey:#"isExecuting"];
[self startRequest];
}
For the first question, the answer is yes, if set 3 as a max number of operations, only 3 can be running togheter.
The second is bit strange problem and I'm not totally sure that this answer will be correct. When you leave operations to an NSOperationQueue, you can't be sure on which thread they will be executed, this lead a huge problem with async connection. When you start an NSURLConnection as usual you receive the delegate callbacks without a problem, that is because the connection is running on a thread with a living run loop. If you start the connection on a secondary thread, callbacks will be called on that thread, but if you don't keep the run loop alive they will be never received.That's where probably my answer isn't correct, GCD should take care of living run loops, because GCD queues runs on living threads. But if not, the problem could be that operations are started on a different thread, the start method is called, but the callbacks are never called. Try to check if the thread is always the main thread.