I've been investigating NSProgress but have found the existing documentation, class reference and tutorials to be lacking. I'm mainly wondering if my NSProgress is applicable to my use case. The class reference documentation alternatively refers to suboperations or subtasks, I may be mistaken but I interpreted suboperations to mean a case where an NSOperation manages a group of other NSOperations. An example of my use case is as follows:
Create an Upload All Items in Group operation for each group that exists.
Add each of these operations to an NSOperationQueue.
Each Upload All Items in Group operation will create an Upload Item operation for each item in their group. These all get added to an NSOperationQueue managed by the operation.
I would have expected NSProgress to support this, and allow me to propagate progress from the nested operations (Upload Item operation) to the parent operation, and then finally to the main thread and the UI. But I've had difficulty implementing this, it seems as though NSProgress is meant more for long operations that execute all their code on one background thread, but have separate "sections" that make it easy to determine when progress has been made, if this is the case then the use of the term suboperation is a bit misleading as it brings to mind the use of nested NSOperations.
Thank you for any help you can provide, and let me know if additional details are needed.
NSProgress knows nothing about NSOperations -- the two things are orthogonal -- but that doesn't mean it can't be used with them. The idea behind nesting NSProgress "tasks" is that the inner task doesn't know anything about the outer task, and the outer task doesn't need direct access to the inner task's NSProgress to pull in updates for it. I cooked up a little example:
// Outer grouping
NSProgress* DownloadGroupsOfFiles(NSUInteger numGroups, NSUInteger filesPerGroup)
{
// This is the top level NSProgress object
NSProgress* p = [NSProgress progressWithTotalUnitCount: numGroups];
for (NSUInteger i = 0; i < numGroups; ++i)
{
// Whatever DownloadFiles does, it's worth "1 unit" to us.
[p becomeCurrentWithPendingUnitCount: 1];
DownloadFiles(filesPerGroup);
[p resignCurrent];
}
return p;
}
// Inner grouping
void DownloadFiles(NSUInteger numberOfFiles)
{
NSProgress* p = [NSProgress progressWithTotalUnitCount: numberOfFiles];
NSOperationQueue* opQueue = [[NSOperationQueue alloc] init];
// Make the op queue last as long as the NSProgress
objc_setAssociatedObject(p, NULL, opQueue, OBJC_ASSOCIATION_RETAIN);
// For each file...
for (NSUInteger i = 0; i < numberOfFiles; ++i)
{
// Whatever this DownloadOperation does is worth 1 "unit" to us.
[p becomeCurrentWithPendingUnitCount: 1];
// Make the new operation
MyDownloadOperation* op = [[MyDownloadOperation alloc] initWithName: [NSString stringWithFormat: #"File #%#", #(i+1)]];
[opQueue addOperation: op];
[p resignCurrent];
}
}
// And then the DownloadOperation might look like this...
#interface MyDownloadOperation : NSOperation
#property (nonatomic, readonly, copy) NSString* name;
- (id)initWithName: (NSString*)name;
#end
#implementation MyDownloadOperation
{
NSProgress* _progress;
NSString* _name;
}
- (id)initWithName:(NSString *)name
{
if (self = [super init])
{
_name = [name copy];
// Do this in init, so that our NSProgress instance is parented to the current one in the thread that created the operation
_progress = [NSProgress progressWithTotalUnitCount: 1];
}
return self;
}
- (void)dealloc
{
_name = nil;
_progress = nil;
}
- (void)main
{
// Fake like we're doing something that takes some time
// Determine fake size -- call it 768K +- 256K
const NSUInteger size = 512 * 1024 + arc4random_uniform(512*1024);
const NSUInteger avgBytesPerSec = 1024 * 1024;
const NSTimeInterval updatePeriod = 1.0/60.0;
// Make sure all the updates to the NSProgress happen on the main thread
// in case someone is bound to it.
dispatch_async(dispatch_get_main_queue(), ^{
_progress.totalUnitCount = size;
_progress.completedUnitCount = 0;
});
NSUInteger bytesRxd = 0;
do
{
// Sleep for a bit...
usleep(USEC_PER_SEC * updatePeriod);
// "Receive some data"
NSUInteger rxdThisTime = updatePeriod * avgBytesPerSec;
// Never report more than all the bytes
bytesRxd = MIN(bytesRxd + rxdThisTime, size);
// Update on the main thread...
dispatch_async(dispatch_get_main_queue(), ^{
[_progress setCompletedUnitCount: bytesRxd];
});
} while (bytesRxd < size);
}
#end
One thing to note is that if NSProgress is being used to convey status to the UI, then you will want to make sure that every time you update the NSProgress object, you do so from the main thread, otherwise you'll get lots of weird crashes.
Alternately you could just use NSURLConnection to download files, and then have a delegate like this:
#interface MyURLConnectionProgressReporter : NSObject <NSURLConnectionDownloadDelegate>
#property (nonatomic, readwrite, assign) id<NSURLConnectionDownloadDelegate> delegate;
#end
NSProgress* DownloadABunchOfFiles(NSArray* arrayOfURLs)
{
arrayOfURLs = arrayOfURLs.count ? arrayOfURLs : #[ [NSURL URLWithString: #"http://www.google.com"] ];
NSProgress* p = [NSProgress progressWithTotalUnitCount: arrayOfURLs.count];
for (NSURL* url in arrayOfURLs)
{
[p becomeCurrentWithPendingUnitCount: 1];
MyURLConnectionProgressReporter* delegate = [[MyURLConnectionProgressReporter alloc] init];
NSURLConnection* conn = [[NSURLConnection alloc] initWithRequest: [NSURLRequest requestWithURL: url] delegate: delegate];
[conn start];
[p resignCurrent];
}
return p;
}
#implementation MyURLConnectionProgressReporter
{
NSProgress* _progress;
}
static void EnsureMainThread(dispatch_block_t block);
- (id)init
{
if (self = [super init])
{
_progress = [NSProgress progressWithTotalUnitCount: 1];
EnsureMainThread(^{
_progress.kind = NSProgressKindFile;
[_progress setUserInfoObject:NSProgressFileOperationKindDownloading forKey:NSProgressFileOperationKindKey];
});
}
return self;
}
- (id)forwardingTargetForSelector:(SEL)aSelector
{
id retVal = [super forwardingTargetForSelector:aSelector];
if (!retVal && [self.delegate respondsToSelector: _cmd])
{
retVal = self.delegate;
}
return retVal;
}
- (void)p_updateWithTotalBytesWritten:(long long)totalBytesWritten expectedTotalBytes:(long long) expectedTotalBytes
{
// Update our progress on the main thread...
EnsureMainThread(^{
if (!expectedTotalBytes)
_progress.totalUnitCount = -1;
else
_progress.totalUnitCount = MAX(_progress.totalUnitCount, expectedTotalBytes);
_progress.completedUnitCount = totalBytesWritten;
});
}
- (void)connection:(NSURLConnection *)connection didWriteData:(long long)bytesWritten totalBytesWritten:(long long)totalBytesWritten expectedTotalBytes:(long long) expectedTotalBytes
{
// Update our progress
[self p_updateWithTotalBytesWritten: totalBytesWritten expectedTotalBytes: expectedTotalBytes];
// Then call on through to the other delegate
if ([self.delegate respondsToSelector: _cmd])
{
[self.delegate connection:connection didWriteData:bytesWritten totalBytesWritten:totalBytesWritten expectedTotalBytes:expectedTotalBytes];
}
}
- (void)connectionDidResumeDownloading:(NSURLConnection *)connection totalBytesWritten:(long long)totalBytesWritten expectedTotalBytes:(long long) expectedTotalBytes
{
// Update our progress
[self p_updateWithTotalBytesWritten: totalBytesWritten expectedTotalBytes: expectedTotalBytes];
// Then call on through to the other delegate
if ([self.delegate respondsToSelector: _cmd])
{
[self.delegate connectionDidResumeDownloading:connection totalBytesWritten:totalBytesWritten expectedTotalBytes:expectedTotalBytes];
}
}
- (void)connectionDidFinishDownloading:(NSURLConnection *)connection destinationURL:(NSURL *) destinationURL
{
// We're done, so we want (_progress.completedUnitCount == _progress.totalUnitCount)
EnsureMainThread(^{
_progress.completedUnitCount = _progress.totalUnitCount;
});
if ([self.delegate respondsToSelector: _cmd])
{
[self.delegate connectionDidFinishDownloading:connection destinationURL:destinationURL];
}
}
static void EnsureMainThread(dispatch_block_t block)
{
if (!block)
return;
else if ([NSThread isMainThread])
block();
else
dispatch_async(dispatch_get_main_queue(), block);
}
#end
Hope that helps.
Related
I'm trying to create a DownloadOperation subclass of NSOperation to download data asynchronously. Everything seemed to be working fine until I tried to add cancelling support. Basically, the completion handler of the operation's NSURLSessionDownloadTask seems to be called after the operation has been released. It will crash with EXC_BAD_ACCESS at the line weakSelf.state = kFinished.
The full sample project is here: https://github.com/angstsmurf/DownloadOperationQueue. Press Command+. after running to crash.
#import "DownloadOperation.h"
typedef enum OperationState : NSUInteger {
kReady,
kExecuting,
kFinished
} OperationState;
#interface DownloadOperation ()
#property NSURLSessionDownloadTask *task;
#property OperationState state;
#end
#implementation DownloadOperation
// default state is ready (when the operation is created)
#synthesize state = _state;
- (void)setState:(OperationState)state {
#synchronized(self) {
if (_state != state) {
[self willChangeValueForKey:#"isExecuting"];
[self willChangeValueForKey:#"isFinished"];
_state = state;
[self didChangeValueForKey: #"isExecuting"];
[self didChangeValueForKey: #"isFinished"];
}
}
}
- (OperationState)state {
#synchronized (self) {
return _state;
}
}
- (BOOL)isReady { return (self.state == kReady); }
- (BOOL)isExecuting { return (self.state == kExecuting); }
- (BOOL)isFinished { return (self.state == kFinished); }
- (BOOL)isAsynchronous {
return YES;
}
- (instancetype)initWithSession:(NSURLSession *)session downloadTaskURL:(NSURL *)downloadTaskURL completionHandler:(nullable void (^)(NSURL * _Nullable, NSURLResponse * _Nullable, NSError * _Nullable))completionHandler {
self = [super init];
if (self) {
__unsafe_unretained DownloadOperation *weakSelf = self;
// use weak self to prevent retain cycle
_task = [[NSURLSession sharedSession] downloadTaskWithURL:downloadTaskURL
completionHandler:^(NSURL * _Nullable localURL, NSURLResponse * _Nullable response, NSError * _Nullable error) {
/*
if there is a custom completionHandler defined,
pass the result gotten in downloadTask's completionHandler to the
custom completionHandler
*/
if (completionHandler) {
completionHandler(localURL, response, error);
}
/*
set the operation state to finished once
the download task is completed or have error
*/
weakSelf.state = kFinished;
}];
}
return self;
}
- (void)start {
/*
if the operation or queue got cancelled even
before the operation has started, set the
operation state to finished and return
*/
if (self.cancelled) {
self.state = kFinished;
return;
}
// set the state to executing
self.state = kExecuting;
NSLog(#"downloading %#", self.task.originalRequest.URL.absoluteString);
// start the downloading
[self.task resume];
}
-(void)cancel {
[super cancel];
// cancel the downloading
[self.task cancel];
}
#end
As pointed out in the comments by Scott Thompson, the correct keyword to use for the weakSelf variable is __weak, not __unsafe_unretained.
I have always been nervous when it comes to blocks and GCD because my mind tells me that it looks very complex!
I am getting a crash inside a block which ideally looks alright to me:
#pragma mark -
-(void)fetchOrdersListWithInfoDict:(NSDictionary*)infoDict completionBlock:(CompletionBlock)completionBlock errorBlock:(ErrorBlock)errorBlock
{
__weak VTVehicleServiceNetworkManager *weakSelf = self;
TaskBlock fetchOrdersListTaskBlock = ^()
{
__block __strong HLOrdersDataProvider *ordersDataProvider = nil;
NSBlockOperation *fetchOrdersOperation = [NSBlockOperation blockOperationWithBlock:[^{
ordersDataProvider = [[HLOrdersDataProvider alloc] init];
[ordersDataProvider performFetchOrdersListWithInfoDict:infoDict
completionBlock:completionBlock
errorBlock:errorBlock];
} copy]];
[weakSelf.dataOperationQueue addOperation:fetchOrdersOperation];
};
[self fetchDataWithTaskBlock:[fetchOrdersListTaskBlock copy]
errorBlock:^(NSError *error) {
errorBlock(error);
}];
}
I was able to trace out the zombie object but I am not able to figure out why is this object turning out to be a zombie. Here is the snapshot from profile:
I have gone through the following guides (1, 2) to see if I can find out what I am doing wrong but I was no where near to find out what is going wrong.
Any help and reference text to what I am doing wrong will help.
Edit:
I have tried what #Jerimy has suggested and in fact my code which I have posted earlier was exactly the same as required: Declaring and initializing ordersDataProvider inside the block operation itself. But since it was crashing at the same point I tried to declare it outside the block just to see if it addresses the crash.
Below is the new code I tested:
#pragma mark -
-(void)fetchOrdersListWithInfoDict:(NSDictionary*)infoDict completionBlock:(CompletionBlock)completionBlock errorBlock:(ErrorBlock)errorBlock
{
__weak VTVehicleServiceNetworkManager *weakSelf = self;
completionBlock = [completionBlock copy];
errorBlock = [errorBlock copy];
TaskBlock fetchOrdersListTaskBlock = ^()
{
NSBlockOperation *fetchOrdersOperation = [NSBlockOperation blockOperationWithBlock:^{
HLOrdersDataProvider *ordersDataProvider = [[HLOrdersDataProvider alloc] init];
[ordersDataProvider performFetchOrdersListWithInfoDict:infoDict
completionBlock:completionBlock
errorBlock:errorBlock];
}];
[weakSelf.dataOperationQueue addOperation:fetchOrdersOperation];
};
[self fetchDataWithTaskBlock:[fetchOrdersListTaskBlock copy]
errorBlock:^(NSError *error) {
errorBlock(error);
}];
}
The crash from Profile:
There is not much from the stack trace as well, SDMHTTPRequest is a library and am very sure there is nothing wrong there, and the HLOrdersDataProvider is the zombie object which I was able to trace out in Instruments app:
EDIT 2
Adding the interface and implementation of HLOrdersDataProvider for more details:
#interface HLOrdersDataProvider : HLDataProvider
-(void)performFetchOrdersListWithInfoDict:(NSDictionary*)infoDict completionBlock:(CompletionBlock)completionBlock errorBlock:(ErrorBlock)errorBlock;
#end
#implementation HLOrdersDataProvider
-(void)performFetchOrdersListWithInfoDict:(NSDictionary*)infoDict completionBlock:(CompletionBlock)completionBlock errorBlock:(ErrorBlock)errorBlock
{
// Using SDMConnectivity
NSString *queryString = [infoDict valueForKey:#"$filter"];
NSString *appendStringForEndpoint = [kRowsetsKeyword stringByAppendingFormat:#"?%#", queryString];
[self fetchDataFromServerWithEndPointAppendString:appendStringForEndpoint
completionBlock:completionBlock
errorBlock:errorBlock];
}
#pragma mark - Service Agent related
-(NSString*)collectionName
{
return [NSString stringWithString:kRowsetsKeyword];
}
-(void)requestFinished:(SDMHttpRequest *)request
{
NSError *error = nil;
// Let's parse the response and send the results back to the caller
NSString *collectionName = [self collectionName];
NSData *responseData = [request responseData];
NSArray *entitiesArray = [self parseODataEntriesWithData:responseData
withCollectionName:collectionName
error:&error];
if (error)
[self triggerFailureBlockWithArgument:error];
else
[self triggerCompletionBlockWithArgument:entitiesArray];
}
#end
Also, HLOrdersDataProvider is inherited from HLDataProvider so below is the interface and implementation of this class too:
#import <Foundation/Foundation.h>
//#import "SDMHttpRequestDelegate.h"
#import "SDMRequestBuilder.h"
#import "SDMHttpRequest.h"
#import "SDMParser.h"
#import "HLConstant.h"
#import "HLConnectionData.h"
#interface HLDataProvider : NSObject <SDMHttpRequestDelegate>
#property (copy, atomic) CompletionBlock completionBlock;
#property (copy , atomic) ErrorBlock errorBlock;
#property (copy, atomic) CompletionBlockWithDataFetchStatus completionBlockWithDataFetchStatus;
+ (id)sharedInstance;
- (NSMutableArray*)parseODataEntriesWithData:(NSData*)data withCollectionName:(NSString*)collectionName;
- (NSMutableArray*)parseODataEntriesWithData:(NSData*)data withCollectionName:(NSString*)collectionName error:(NSError**)outError;
- (NSMutableArray*)parseJSONEntriesWithData:(NSData*)data;
-(NSArray*)fetchEntriesFromDatabaseWithEntityName:(NSString*)entityName relationshipObjects:(NSMutableArray*)relationships;
-(void)updateDatabaseWithEntries:(NSMutableArray*)scanEntries;
-(void)updateDatabaseWithJSONEntries:(NSMutableArray*)scanEntries;
-(id)parsedOdataResultFromEntries:(NSMutableArray*)entries;
-(void)fetchDataFromServerWithEndPointAppendString:(NSString*)appendStr completionBlock:(CompletionBlock)inCompletionBlock errorBlock:(ErrorBlock)inErrorBlock;
-(NSString*)collectionName;
-(void)triggerCompletionBlockWithArgument:(id)inArg;
-(void)triggerFailureBlockWithArgument:(NSError*)inArg;
#end
#implementation HLDataProvider
+ (id)sharedInstance
{
//Subclassess will override this method
return nil;
}
-(NSMutableArray*)parseODataEntriesWithData:(NSData*)data withCollectionName:(NSString*)collectionName
{
return [self parseODataEntriesWithData:data
withCollectionName:collectionName
error:NULL];
}
-(NSMutableArray*)parseODataEntriesWithData:(NSData*)data withCollectionName:(NSString*)collectionName error:(NSError**)outError
{
NSMutableArray *entriesArray = nil;
#try {
entriesArray = sdmParseODataEntriesXML(data,
[[[HLConnectionData metaDataDocument] getCollectionByName:collectionName] getEntitySchema],
[HLConnectionData serviceDocument]);
}
#catch (NSException *exception) {
NSLog(#"Got exception: %#", exception);
if (outError)
{
*outError = [NSError errorWithDomain:#"Vehicle Service"
code:-1001
userInfo:[NSDictionary dictionaryWithObject:exception forKey:NSLocalizedDescriptionKey]];
}
}
#finally {
}
return entriesArray;
}
- (NSMutableArray*)parseJSONEntriesWithData:(NSData*)data
{
NSError *error = nil;
id object = [NSJSONSerialization
JSONObjectWithData:data
options:0
error:&error];
NSMutableArray *resultArray = nil;
if(error) { /* JSON was malformed, act appropriately here */ }
if([object isKindOfClass:[NSDictionary class]])
{
resultArray = [NSMutableArray arrayWithObject:object];
}
else if ([object isKindOfClass:[NSArray class]])
{
resultArray = [NSMutableArray arrayWithArray:object];
}
return resultArray;
}
#pragma mark -
#pragma mark - Data Fetch - Server - SDMConnectivity
-(void)fetchDataFromServerWithEndPointAppendString:(NSString*)appendStr completionBlock:(CompletionBlock)inCompletionBlock errorBlock:(ErrorBlock)inErrorBlock;
{
self.errorBlock = inErrorBlock;
self.completionBlock = inCompletionBlock;
id<SDMRequesting> request = nil;
//NSString *clientStr = #"&sap-client=320&sap-language=EN";
NSString *urlStr =[NSString stringWithFormat:#"%#/%#",[HLConnectionData applicationEndPoint], appendStr];
urlStr = [urlStr stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
[SDMRequestBuilder setRequestType:HTTPRequestType];
request=[SDMRequestBuilder requestWithURL:[NSURL URLWithString:urlStr]];
[request setUsername:kUserName];
/*Set Password in SDMRequesting object*/
[request setPassword:kPassword];
[request setRequestMethod:#"GET"];
[request setTimeOutSeconds:kTimeoutInterval];
/*set the Delegate. This class must adhere to SDMHttpRequestDelegate to get the callback*/
[request setDelegate:self];
/*Call startAsynchronous API to request object to retreive Data asynchrnously in the call backs */
[request startSynchronous];
}
-(void)updateDatabaseWithEntries:(NSMutableArray*)scanEntries
{
//Subclasses will override this
}
-(void)updateDatabaseWithJSONEntries:(NSMutableArray*)scanEntries
{
//Subclasses will override this
}
-(id)parsedOdataResultFromEntries:(NSMutableArray*)entries
{
//Subclasses will override this
return nil;
}
-(void)deleteExistingEntriesFromCoredata
{
//Subclasses will override this
}
-(NSArray*)fetchEntriesFromDatabaseWithEntityName:(NSString*)entityName relationshipObjects:(NSMutableArray*)array
{
//Subclasses will overide this method
return nil;
}
#pragma mark - SDMHTTPRequestDelegate methods
- (void)requestStarted:(SDMHttpRequest*) request
{
}
- (void)requestFinished:(SDMHttpRequest*) request
{
// For service doc and metadata we instantiate HLDataProvider, so we send this raw SDMHTTPRequest object as-is. For other service agents like HLOrdersDataProvider we send the parsed information, check the subclass' implementation of -requestFinished: method
[self triggerCompletionBlockWithArgument:request];
}
-(void)triggerCompletionBlockWithArgument:(id)inArg
{
self.completionBlock(inArg);
}
- (void)requestFailed:(SDMHttpRequest*) request
{
[self triggerFailureBlockWithArgument:request.error];
}
-(void)triggerFailureBlockWithArgument:(NSError*)inArg
{
self.errorBlock(inArg);
}
- (void)requestRedirected:(SDMHttpRequest*) request
{
}
#pragma mark - Service Agent related
-(NSString*)collectionName
{
// Should be overridden by the subclasses
return nil;
}
The referenced code is very convoluted (you're doing things in a very complicated way).
First off, you should not be creating the copy of your blocks in the caller. Make the copy in the callee if necessary (ie: to copy it to heap instead of using the stack-allocated block if you are going to call it after the stack has been popped). In almost all APIs using blocks, it is not the caller's responsibility to ensure that a block is on heap.
There is no reason for your ordersDataProvider variable to be declared in the scope of fetchOrdersListTaskBlock because it is only ever used inside of fetchOrdersOperation's block.
I don't immediately see the cause of your crash, but I suspect that simplifying your code will help reveal the problem. Perhaps the issue is in HLOrdersDataProvider's initializer.
Try something like:
-(void)fetchOrdersListWithInfoDict:(NSDictionary*)infoDict completionBlock:(CompletionBlock)completionBlock errorBlock:(ErrorBlock)errorBlock
{
completionBlock = [completionBlock copy];
errorBlock = [errorBlock copy];
__weak VTVehicleServiceNetworkManager *weakSelf = self;
TaskBlock fetchOrdersListTaskBlock = ^{
NSBlockOperation *fetchOrdersOperation = [NSBlockOperation blockOperationWithBlock:^{
HLOrdersDataProvider *ordersDataProvider = [[HLOrdersDataProvider alloc] init];
[ordersDataProvider performFetchOrdersListWithInfoDict:infoDict
completionBlock:completionBlock
errorBlock:errorBlock];
}];
[weakSelf.dataOperationQueue addOperation:fetchOrdersOperation];
};
[self fetchDataWithTaskBlock:fetchOrdersListTaskBlock
errorBlock:errorBlock];
}
Or better yet, re-design your class to work like this (I don't see a need for NSBlockOperation in your example):
-(void)fetchOrdersListWithInfoDict:(NSDictionary*)infoDict completionBlock:(CompletionBlock)completionBlock errorBlock:(ErrorBlock)errorBlock
{
completionBlock = [completionBlock copy];
errorBlock = [errorBlock copy];
TaskBlock fetchOrdersListTaskBlock = ^{
HLOrdersDataProvider *ordersDataProvider = [[HLOrdersDataProvider alloc] init];
[ordersDataProvider performFetchOrdersListWithInfoDict:infoDict
completionBlock:completionBlock
errorBlock:errorBlock];
};
[self fetchDataWithTaskBlock:fetchOrdersListTaskBlock
errorBlock:errorBlock];
}
I have an async method called getCount: which goes to a web URL, counts some stuff, and invokes a callback with the count when it's done.
I have another method which is synchronous and needs to take those results, puts them into a message, and returns that message. Here are the two together:
- (NSString *)describe {
__block bool gotCount = NO;
[self getCount:^(int count) {
NSLog(#"Got the count: %i", count);
_count = count; // _count is an ivar of the object with this method.
gotCount = YES;
}];
// Pause here until the count has been fetched.
while (!gotCount) {
[NSThread sleepForTimeInterval:0.05];
}
return [NSString stringWithFormat:#"The count is %i", _count];
}
My callback is never called when I try this. It never prints
Got the count 0
or any other value for count in this scenario.
If I comment out the while loop, that message does get printed out. So I know that the getCount: method works, there's just something wrong with my loop waiting for it to arrive.
I need getCount: to remain asynchronous (there's other places it gets used where that's more important) and I need describe to remain synchronous. How can I handle this?
one possible thing: if your describe method is in the main thread then you call getCount method also from the main thread and all web callbacks are in the main thread. BUT you block the main thread with the thread sleep -> you can not get call back from the web to get a count.
Edited:
try to call getCount method from another thread. use e.g.
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[self getCount:^(int count) {
NSLog(#"Got the count: %i", count);
_count = count; // _count is an ivar of the object with this method.
gotCount = YES;
}];
});
Edited 2:
I tried this code and it works fine -> something probably wrong with the threads in your getCount method.
- (NSString *)describe {
__block bool gotCount = NO;
__block NSInteger _count;
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[NSThread sleepForTimeInterval:5.00];
_count = 5;
gotCount = YES;
});
// Pause here until the count has been fetched.
while (!gotCount) {
[NSThread sleepForTimeInterval:0.05];
}
return [NSString stringWithFormat:#"The count is %li", _count];
}
a way that would work but is QUITE the hack (which apple employed in older APIs on the mac all the time) would be to run the runloop while waiting:
note: this relies on the callback being on the same queue as the describe method
see: JSON async request [SAME ISSUE]
a self contained WORKING example:
#import <Foundation/Foundation.h>
#interface T : NSObject
- (NSString *)describe;
#end
#implementation T {
int _count;
}
- (void)getCount:(void (^)(int c)) handler {
dispatch_async(dispatch_get_global_queue(0,0), ^ {
sleep(5);
dispatch_sync(dispatch_get_main_queue(), ^{
handler(55);
});
});
}
- (NSString *)describe {
__block bool gotCount = NO;
[self getCount:^(int count) {
NSLog(#"Got the count: %i", count);
_count = count; // _count is an ivar of the object with this method.
gotCount = YES;
}];
// Pause here until the count has been fetched.
while (!gotCount) {
[[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:0.1]];
}
return [NSString stringWithFormat:#"The count is %i", _count];
}
#end
int main(int argc, char *argv[]) {
#autoreleasepool {
T *t = [T new];
NSLog(#"describe: %#", [t describe]);
}
}
I've started using Objective-c for iOS programming. I switched over from Java, and I wanted to know if there were any existing libraries like the Java Collections Framework for Obj-c, more specifically a priority queue implementation. I've done some searches, but have been unable to come up with anything.
UPDATE: I found this, but would have no idea how to use it myself: http://www.ohloh.net/p/pqlib
I was unable to find an implementation of a priority queue, so I went ahead and made my own. I'm not sure how robust it is, but I hope it might point others in the right direction.
PriorityQueue.h
//
// PriorityQueue.h
//
#import <Foundation/Foundation.h>
#import "comparable.h"
//Implements a priority queue. All objects in queue must implement the comparable protocol and must be all of the same type. The queue can be explicity typed at initialization, otherwise the type of the first object entered will be the type of the queue
#interface PriorityQueue : NSObject{
NSMutableArray *queue;
Class type;
}
- (id)init;
- (id)initWithObjects:(NSSet *)objects;
- (id)initWithCapacity:(int)capacity;
- (id)initWithCapacity:(int)capacity andType:(Class)oType; //Queue will reject objects not of that type
#pragma mark - Useful information
- (BOOL)isEmpty;
- (BOOL)contains:(id<comparable, NSObject>)object;
- (Class)typeOfAllowedObjects; //Returns the type of objects allowed to be stored in the queue
- (int) size;
#pragma mark - Mutation
- (void)clear;
- (BOOL)add:(id<comparable, NSObject>)object;
- (void)remove:(id<comparable, NSObject>)object;
#pragma mark - Getting things out
- (id)peek;
- (id)poll;
- (id)objectMatchingObject:(id<comparable, NSObject>)object;
- (NSArray *)toArray;
#pragma mark -
- (void)print;
#end
PriorityQueue.m
//
// PriorityQueue.m
//
#import "PriorityQueue.h"
#define INITIAL_CAPACITY 50
#implementation PriorityQueue
#pragma mark - Initialization
- (id)init{
return [self initWithCapacity:INITIAL_CAPACITY andType:nil];
}
- (id)initWithObjects:(NSSet *)objects{
self = [self initWithCapacity:INITIAL_CAPACITY andType:nil];
for (id<comparable, NSObject>object in objects){
[self add:object];
}
return self;
}
- (id)initWithCapacity:(int)capacity{
return [self initWithCapacity:capacity andType:nil];
}
- (id)initWithCapacity:(int)capacity andType:(Class)oType{
self = [super init];
if(self){
queue = [[NSMutableArray alloc] init];
type = oType;
}
return self;
}
#pragma mark - Useful information
- (BOOL)isEmpty{
if(queue.count == 0){
return YES;
}
else{ return NO;}
}
- (BOOL)contains:(id<comparable, NSObject>)object{
//Search the array to see if the object is already there
for(id<comparable> o in queue){
if([o isEqual:object]){
return YES;
}
}
return NO;
}
- (Class)typeOfAllowedObjects{
NSLog(#"Allowed Types: %#", type);
return type;
}
- (int) size{
return [queue count];
}
#pragma mark - Mutation
//Mutation
- (void)clear{
[queue removeAllObjects];
}
//A "greater" object (compareTo returns 1) is at the end of the queue.
- (BOOL)add:(id<comparable, NSObject>)object{
//Make sure the object's type is the same as the type of the queue
if(type == nil){
// NSLog(#"Type is nil");
type = [object class];
}
if([object class] != type){
NSLog(#"ERROR: Trying to add incorrect object");
return NO;
}
if([queue count] == 0){
[queue addObject:object];
return YES;
}
for(int i = 0; i < [queue count]; i++){
if([object compareTo:queue[i]] < 0){
[queue insertObject:object atIndex:i];
return YES;
}
}
[queue addObject:object];
return YES;
}
- (void)remove:(id<comparable, NSObject>)object{
[queue removeObject:object];
}
#pragma mark - Getting things out
- (id)peek{
return queue[0];
}
- (id)poll{
//Get the object at the front
id head = queue[0];
//Remove and return that object
[queue removeObject:head];
return head;
}
- (id)objectMatchingObject:(id<comparable, NSObject>)object{
//Search the array to see if the object is already there
for(id<comparable> o in queue){
if([o isEqual:object]){
return o;
}
}
return nil;
}
- (NSArray *)toArray{
return [[NSArray alloc] initWithArray:queue];
}
#pragma mark -
- (NSString *)description{
return [NSString stringWithFormat:#"PriorityQueue: %# allows objects of type %#", queue, type];
}
- (void)print{
NSLog(#"%#", [self description]);
}
#end
Comparable.h
//
// comparable.h
//
#import <Foundation/Foundation.h>
//NOTE: Class must check to make sure it is the same class as whatever is passed in
#protocol comparable
- (int)compareTo:(id<comparable, NSObject>)object;
- (BOOL)isEqual:(id<comparable, NSObject>)object;
#end
See https://mikeash.com/pyblog/using-evil-for-good.html where Mike implements Objective-C wrapper for C++ STD priority queue.
CFBinaryHeap can be used as a Priority Queue and is described as such in the docs: https://developer.apple.com/library/mac/documentation/CoreFoundation/Reference/CFBinaryHeapRef/
The downsides seem to be:
1) There is no remove or update element capability. As far as I can tell you can only remove the min element.
2) It is very C-like and not very pleasant to use in Objc or Swift.
My approach supporting value updates. As CFBinaryHeap does not support updating values I put them on an invalidation list and once being extracted the object is inserted again and a new extraction is made.
/**
Objective-C wrapper around CFBinaryHeap implementing a priority queue and extended by updating a previous value
*/
NS_ASSUME_NONNULL_BEGIN
#interface BEPriorityQueue<ObjectType, ValueType> : NSObject
- (void)dispose;
#property (nonatomic, readonly) NSUInteger count;
- (void)insert:(ObjectType)object value:(ValueType)value;
- (void)update:(ObjectType)object value:(ValueType)value;
/** returns and removes object with lowest value (highest priority */
- (ObjectType)extractMinimum;
- (BOOL)containsObject:(ObjectType)object;
- (id)valueForObject:(id)object;
- (void)removeAllObjects;
#end
NS_ASSUME_NONNULL_END
With this implementation:
NS_ASSUME_NONNULL_BEGIN
#interface BEPriorityQueue()
- (CFComparisonResult)compareObject:(id)object1 with:(id)object2;
#end
static CFComparisonResult BEPriorityQueueCompareItems(const void *ptr1, const void *ptr2, void *info)
{
id object1 = (__bridge id)ptr1;
id object2 = (__bridge id)ptr2;
BEPriorityQueue* queue = (__bridge id)info;
return [queue compareObject:object1 with:object2];
}
static const void *BEPriorityQueueItemRetain(CFAllocatorRef allocator, const void *ptr) {
return CFRetain(ptr);
}
static void BEPriorityQueueItemRelease(CFAllocatorRef allocator, const void *ptr) {
CFRelease(ptr);
}
#implementation BEPriorityQueue
{
BOOL _disposed;
CFBinaryHeapRef _binaryHeapRef;
NSMapTable* _objectToValue;
NSMutableSet* _invalidated;
}
- (instancetype)init
{
self = [super init];
if (self)
{
CFBinaryHeapCallBacks callbacks = (CFBinaryHeapCallBacks) {
.version = 0,
.retain = &BEPriorityQueueItemRetain,
.release = &BEPriorityQueueItemRelease,
.copyDescription = &CFCopyDescription,
.compare = &BEPriorityQueueCompareItems
};
CFBinaryHeapCompareContext compareContext = (CFBinaryHeapCompareContext) {
.version = 0,
.info = (__bridge void *)(self),
.retain = NULL,
.release = NULL,
.copyDescription = NULL,
};
_binaryHeapRef = CFBinaryHeapCreate(NULL, 0, &callbacks, &compareContext);
_objectToValue = [NSMapTable strongToStrongObjectsMapTable];
_invalidated = [NSMutableSet set];
}
return self;
}
- (void)dealloc
{
[self dispose];
if (_binaryHeapRef != NULL)
{
CFRelease(_binaryHeapRef);
_binaryHeapRef = NULL;
}
}
- (void)dispose
{
[self removeAllObjects];
_disposed = YES;
}
#pragma mark internal
- (CFComparisonResult)compareObject:(id)object1 with:(id)object2
{
id value1 = [_objectToValue objectForKey:object1];
id value2 = [_objectToValue objectForKey:object2];
return (CFComparisonResult)[value1 compare:value2];
}
#pragma mark interface
- (NSUInteger)count
{
BEEnsureFalse(_disposed);
return (NSUInteger)CFBinaryHeapGetCount(_binaryHeapRef);
}
- (id)extractMinimum
{
BEEnsureFalse(_disposed);
const void *ptr = NULL;
if (!CFBinaryHeapGetMinimumIfPresent(_binaryHeapRef, &ptr))
return nil;
id object = (__bridge id)ptr;
id value = [_objectToValue objectForKey:object];
CFBinaryHeapRemoveMinimumValue(_binaryHeapRef);
[_objectToValue removeObjectForKey:object];
// if the objects was invalidated, it may no longer be the minimum
// therefore reinsert the object and extract again
if ([_invalidated containsObject:object])
{
[_invalidated removeObject:object];
[self insert:object value:value];
return [self extractMinimum];
}
return object;
}
- (void)insert:(id)object value:(id)value
{
BEEnsureFalse(_disposed);
BEEnsureIsNotNil(object);
BEEnsureIsNotNil(value);
BEEnsureTrue([value respondsToSelector:#selector(compare:)]); // <NSComparable>
[_objectToValue setObject:value forKey:object]; // first to be available furing insertion compare
CFBinaryHeapAddValue(_binaryHeapRef, (__bridge void *)object);
}
- (void)update:(id)object value:(id)value
{
BEEnsureFalse(_disposed);
BEEnsureIsNotNil(object);
BEEnsureTrue([value respondsToSelector:#selector(compare:)]); // <NSComparable>
[_objectToValue setObject:value forKey:object]; // first to be available during insertion compare
[_invalidated addObject:object];
}
- (BOOL)containsObject:(id)object
{
BEEnsureFalse(_disposed);
return CFBinaryHeapContainsValue(_binaryHeapRef, (__bridge void *)object);
}
- (id)valueForObject:(id)object
{
return [_objectToValue objectForKey:object];
}
- (void)removeAllObjects
{
CFBinaryHeapRemoveAllValues(_binaryHeapRef);
[_objectToValue removeAllObjects];
[_invalidated removeAllObjects];
}
#end
NS_ASSUME_NONNULL_END
I came into a situation where I had to write a loop with a good amount if iterations and in this loop I had a NSData object that I had to associate with a key. This lead me to search for a simple objective-c _KeyValuePair_ class but coulnt not find one so I wrote my own. Now I'm curious to see if there is any benefit over just using an NSMutableDictinoary holding just 1 key and value. After trying both throughout my project I can't tell much difference on the App UI side or with Instruments Time Profiler.
So my questions are:
Could a single kvpair class be more efficient than a NSMutableDictionary
Does a NSMutableDict allocate any larger amount of space by default then this does
Is there actually a standard single key value pair class that I just missed
Some code:
for (int i = 0, count = [photoUrls count]; i < count; ++i) {
// Example usage of the kvp class
NSMutableDictionary *imageRequest = [[NSMutableDictionary alloc] init];
JHKeyValuePair *kvPair = [[JHKeyValuePair alloc] initWithKey:#"DAILY" andValue:[NSNumber numberWithInt:i];
[imageRequest setObject:self forKey:#"delegate"];
[imageRequest setObject:kvPair forKey:#"userInfo"];
[kvPair release];
[imageRequest setObject:[dailySpecialData objectForKey:#"IMAGE_URL"] forKey:#"url"];
[imageDownloader addDownloadRequestToQueue:imageRequest];
[imageRequest release];
}
JHKeyValuePair.h
#interface JHKeyValuePair : NSObject {
id key;
id value;
}
#property (nonatomic, retain) id key;
#property (nonatomic, retain) id value;
- (id)initWithKey:(id)aKey andValue:(id)aValue;
#end
JHKeyValuePair.m
#import "JHKeyValuePair.h"
#implementation JHKeyValuePair
#synthesize key;
#synthesize value;
- (id)initWithKey:(id)aKey andValue:(id)aValue {
if ((self = [super init])) {
key = [aKey retain];
value = [aValue retain];
}
return self;
}
- (void)dealloc {
[key release], key = nil;
[value release], value = nil;
[super dealloc];
}
- (id)copyWithZone:(NSZone *)zone {
JHKeyValuePair *copy = [[JHKeyValuePair allocWithZone:zone] init];
[copy setKey:self.key];
[copy setValue:self.value];
return copy;
}
- (BOOL)isEqual:(id)anObject {
BOOL ret;
if (self == anObject) {
ret = YES;
} else if (![anObject isKindOfClass:[JHKeyValuePair class]]) {
ret = NO;
} else {
ret = [key isEqual:((JHKeyValuePair *)anObject).key] && [value isEqual:((JHKeyValuePair *)anObject).value];
}
return ret;
}
#end
Edit to fix the initial explanation. Seems I got sidetracked mid-sentance and never came back to finish it.
If you really want to get speed you are doing a lot of unnecessary retain releases that probably aren't necessary every time you set your key/values. If you use a struct and some basic c code you can achieve something a little quicker but you sacrifice the simple and consistent memory management you get from doing it the objective c way.
typedef struct {
id key;
id value;
} Pair;
BOOL isEqual(Pair a, Pair b); //...
// You will need to clean up after yourself though:
void PairRelease(Pair p) {
[p.key release];
[p.value release];
}