Category Archives: iOS
NSURLSession – Out of process upload/download
ref – http://www.techotopia.com/index.php/An_iOS_7_Background_Transfer_Service_Tutorial
Background Uploads and Downloads
Adding support for background uploads and downloads is surprisingly easy with NSURLSession. Apple refers to them as out-of-process uploads and downloads as the tasks are managed by a background daemon, not your application. Even if your application crashes during an upload or download task, the task continues in the background.
Enabling background uploads and downloads is nothing more than flipping a switch in your session’s configuration. With a properly configured session object, you are ready to schedule upload and download tasks in the background.
When an upload or download is initiated:
- background daemon comes into existence
- The daemon takes care of the task and sends updates to the application through the delegate protocols declared in the NSURLSession API
- If app stops, daemon continues task
- If download task finishes, daemon informs session.
- The session then invokes the appropriate delegate methods to make sure your application can take the appropriate actions, such as moving the file to a more permanent location.
When the Background Transfer Service gets in action, what is actually happening is that the operating system takes charge of all the download process, performing everything in background threads (daemons).
While a download is in progress, delegates are used to inform the app for the progress, and wakes it up in the background to get more data if needed, such as credentials for logging in to a service. However, even though everything is controlled by the system, users can cancel all downloads at any time through the application.
A reminder that a block parameter has 3 parts. The return, the parameter, and the block name. Like so:
1 2 3 4 |
- (void)doSomethingWithBlock:(void (^)(double, double))block { ... block(21.0, 2.0); } |
So this means that the block is used where it takes 2 doubles, and does not return anything. It is called by using the block name ‘block’.
Handle Events for Background Session
1 2 3 |
- (void)application:(UIApplication *)application handleEventsForBackgroundURLSession:(NSString *)identifier completionHandler:(void (^)())completionHandler |
Apps that (using an NSURLSession with a background configuration) may be launched or resumed in the background in order to handle the
completion of tasks in that session, or to handle authentication.
In other words, when an app uses NSURLSession with background service (whether its a GET request or downloading/uploading), NSURLSession needs to let the app know and to handle the completion of tasks for that session.
We simply strong the completionHandler block in order to call it later to let the system know that we complete all tasks.
NSURLSessionDelegate’s PROTOCOL METHOD will be called. In there you access the appDelegate and call the completionHandler.
Hence when you’re using it, if an application has received an -application:handleEventsForBackgroundURLSession:completionHandler:
message, the session delegate will receive this message to indicate
that all messages previously enqueued for this session have been
delivered.
Declare the session completion handler member variable:
AppDelegate.h
1 2 3 4 5 |
@interface AppDelegate : UIResponder <UIApplicationDelegate> @property (strong, nonatomic) UIWindow *window; //custom block @property (copy) void (^sessionCompletionHandler)(); @end |
Then, have its set property copy the completion handler:
AppDelegate.m
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
@implementation AppDelegate //add the delegate method so that the completion handler reference is stored: //when download is complete, it will notify the app through this delegate method - (void)application:(UIApplication *)application handleEventsForBackgroundURLSession:(NSString *)identifier completionHandler:(void (^)())completionHandler { NSLog(@"AppDelegate.m - handleEventsForBackgroundURLSession: - daemon finished download. Notifies app through this protocol method"); NSLog(@"AppDelegate.m - handleEventsForBackgroundURLSession: - We strong the completionHandler"); self.sessionCompletionHandler = completionHandler; //(void(^)())completionhandler //completionHandler is the blockName // void is the return type // (^) means nullability // () means return type } |
Go to your ViewController and conform to NSURLSessionDelegate.
1 |
@interface ViewController () <NSURLSessionDelegate> |
This is so that we need to implement URLSessionDidFinishEventsForBackgroundURLSession.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
- (void)URLSessionDidFinishEventsForBackgroundURLSession:(NSURLSession *)session { NSLog(@"ViewController.m - URLSessionDidFinishEventsForBackgroundURLSession"); AppDelegate * appDelegate = (AppDelegate *)[[UIApplication sharedApplication] delegate]; if (appDelegate.sessionCompletionHandler) { NSLog(@"ViewController.m - local var block created and stronged appDelegate's sessionCompletionHandler"); void (^completionHandler)() = appDelegate.sessionCompletionHandler; NSLog(@"ViewController.m - appDelegate's sessionCompletionHandler block niled"); appDelegate.sessionCompletionHandler = nil; NSLog(@"ViewController.m - completionHandler called"); completionHandler(); } NSLog(@"URLSessionDidFinishEventsForBackgroundURLSession - Task complete"); } |
Now when your session object’s delegate queue tasks have all been processed, in your AppDelegate, you will receive a
application has received an -application:handleEventsForBackgroundURLSession:completionHandler:
message and your protocol method
1 2 3 |
- (void)application:(UIApplication *)application handleEventsForBackgroundURLSession:(NSString *)identifier completionHandler:(void (^)())completionHandler |
will be called.
Then your session delegate will send a message to protocol method URLSessionDidFinishEventsForBackgroundURLSession to indicate that all messages previously enqueued for this session have been
delivered.
Hence that’s why its the job of your URLSessionDidFinishEventsForBackgroundURLSession method to simply call the completionHandler and clean it up.
If your session delegate sends a message to call protocol method:
1 2 |
- (void)URLSession:(NSURLSession *)session didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential * __nullable credential))completionHandler; |
for authentication purposes, then you need to something similar. Where you give authentication information, then call the completionHandler().
Prepare the UI
1 2 3 4 5 6 7 8 9 10 |
- (void)viewDidLoad { [super viewDidLoad]; // Do any additional setup after loading the view, typically from a nib. _imageView = [[UIImageView alloc] initWithFrame:CGRectMake(0.0f, 0.0f, 320.0f, 480.0f)]; [_imageView setBackgroundColor:[UIColor yellowColor]]; [self.view addSubview:_imageView]; ... ... ... |
Configuration Object
1 2 3 4 5 6 7 8 9 10 11 |
NSLog(@"viewDidLoad - creating a new NSURLSessionConfiguration instance"); NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration backgroundSessionConfigurationWithIdentifier:@"com.ebookfrenzy.transfer"]; NSLog(@"viewDidLoad - specified a unique identifier"); NSLog(@"viewDidLoad - allowsCellularAccess set to YES to allow the transfer to use the cellular connection on the device if WiFi is not available"); configuration.allowsCellularAccess = YES; //For large transfers, this should be set to NO, or the user given //the opportunity to configure this setting to avoid incurring additional //costs on their cellular data plan. |
Session Object
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
- (void)viewDidLoad { [super viewDidLoad]; //UI stuff //Configuration Object NSLog(@"viewDidLoad - session object sets configuration object"); _session = [NSURLSession sessionWithConfiguration:configuration delegate:self delegateQueue:nil]; NSURL * downloadURL = [NSURL URLWithString:DownloadURLString]; NSURLRequest *request = [NSURLRequest requestWithURL:downloadURL]; NSLog(@"viewDidLoad - session calls downloadTaskWithRequest...! Make sure your protocol methods are implemented..! "); _downloadTask = [self.session downloadTaskWithRequest:request]; //default request [_downloadTask resume]; } |
Updating download progress percentages
1 2 3 4 5 6 7 8 |
//Sent periodically to notify the delegate of download progress - (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didWriteData:(int64_t)bytesWritten totalBytesWritten:(int64_t)totalBytesWritten totalBytesExpectedToWrite:(int64_t)totalBytesExpectedToWrite { float progress = (double)totalBytesWritten / (double)totalBytesExpectedToWrite; NSLog(@"URLSession:downloadTask:didWriteData:totalBytesWritten:totalBytesExpectedToWrite - progress is: %f", progress); } |
When you resume a download task
1 2 3 4 5 6 7 8 9 |
-(void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didResumeAtOffset:(int64_t)fileOffset expectedTotalBytes:(int64_t)expectedTotalBytes { NSLog(@"URLSession:downloadTask:didResumeAtOffset:expectedTotalBytes: %lld %lld", fileOffset, expectedTotalBytes); } |
Send last message related to a specific task
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error { if (error == nil) { NSLog(@"URLSession:task:didCompleteWithError: No Error detected, Task %@ completed successfully", task); } else { NSLog(@"URLSession:task:didCompleteWithError: Task %@ completed with error: %@", task, [error localizedDescription]); } _downloadTask = nil; } |
When your download have completed
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
//Sent when a download task that has completed a download. - (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didFinishDownloadingToURL:(NSURL *)downloadURL { NSLog(@"URLSession:downloadTask:didFinishDownloadingToURL: Copying image file"); NSLog(@"URLSession:downloadTask:didFinishDownloadingToURL: - init the file manager"); NSFileManager *fileManager = [NSFileManager defaultManager]; NSArray *URLs = [fileManager URLsForDirectory:NSDocumentDirectory inDomains:NSUserDomainMask]; NSURL *documentsDirectory = [URLs objectAtIndex:0]; NSURL *fromURL = [[downloadTask originalRequest] URL]; NSURL *destinationURL = [documentsDirectory URLByAppendingPathComponent:[fromURL lastPathComponent]]; NSError *error; NSLog(@"URLSession:downloadTask:didFinishDownloadingToURL: - remove previous file if it exists"); // Remove file at the destination if it already exists. [fileManager removeItemAtURL:destinationURL error:NULL]; BOOL success = [fileManager copyItemAtURL:downloadURL toURL:destinationURL error:&error]; if (success) { NSLog(@"URLSession:downloadTask:didFinishDownloadingToURL: - successfully, copied the file to local. Now displaying the image..."); UIImage *image = [UIImage imageWithContentsOfFile:[destinationURL path]]; _imageView.image = image; } else { NSLog(@"URLSession:downloadTask:didFinishDownloadingToURL: - File copy failed: %@", [error localizedDescription]); } } |
NSURLSession Resume/Cancel
NSURLSession basics
ref – http://code.tutsplus.com/tutorials/networking-with-nsurlsession-part-1–mobile-21394
http://code.tutsplus.com/tutorials/networking-with-nsurlsession-part-2–mobile-21581
1) Simple GET of data
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
- (void)viewDidLoad { [super viewDidLoad]; // Do any additional setup after loading the view, typically from a nib. NSLog(@"ViewController.m - viewDidLoad"); NSURLSession *session = [NSURLSession sharedSession]; NSURLSessionDataTask *dataTask = [session dataTaskWithURL:[NSURL URLWithString:@"https://itunes.apple.com/search?term=apple&media=software"] completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) { if(!error && data) { NSDictionary *json = [NSJSONSerialization JSONObjectWithData:data options:0 error:nil]; NSLog(@"Received JSON: %@", json); } else { NSLog(@"uh oh, ERROR IS %@", [error debugDescription]); } }]; [dataTask resume]; //start the task } |
2) Downloading a file using NSURLSessionDownloadDelegate protocol
So we want to download a file. However, we want to see what percentage we are at, as well as when the file has finished downloading.
A session configuration object is nothing more than a dictionary of properties that defines how the session it is tied to behaves. A session has one session configuration object that dictates cookie, security, and cache policies, the maximum number of connections to a host, resource and network timeouts, etc.
Once a session is created and configured by a NSURLSessionConfiguration instance, the session’s configuration cannot be modified. If you need to modify a session’s configuration, you have to create a new session. Keep in mind that it is possible to copy a session’s configuration and modify it, but the changes have no effect on the session from which the configuration was copied.
Once a session is created and configured by a NSURLSessionConfiguration instance, the session’s configuration cannot be modified. A SessionConfiguration is immutable. If you need to modify a session’s configuration, you have to create a new session. Keep in mind that it is possible to copy a session’s configuration and modify it, but the changes have no effect on the session from which the configuration was copied.
- Use a private UIImageView to hold the image
- privately conform to NSURLSessionDownloadDelegate protocol
- Implement protocol methods
ViewController.m
1 2 3 4 5 |
#import "ViewController.h" @interface ViewController () <NSURLSessionDownloadDelegate> @property(nonatomic, strong) UIImageView * myImageView; @end |
Add the image holder to your view hierarchy
1 2 3 4 5 6 7 8 |
- (void)viewDidLoad { [super viewDidLoad]; // Do any additional setup after loading the view, typically from a nib. self.myImageView = [[UIImageView alloc] initWithFrame:CGRectMake(0.0f, 0.0f, 200.0f, 200.0f)]; [self.myImageView setBackgroundColor:[UIColor yellowColor]]; [self.view addSubview:self.myImageView]; |
Then use a NSURLSession to get a task running
Notice that we first get a default Session Configuration object. Then we pass it into the session to be used.
1 2 3 4 5 6 7 8 9 10 11 12 13 |
- (void)viewDidLoad { [super viewDidLoad]; ... ... NSLog(@"ViewController.m - viewDidLoad"); NSURLSessionConfiguration *sessionConfiguration = [NSURLSessionConfiguration defaultSessionConfiguration]; NSURLSession *session = [NSURLSession sessionWithConfiguration:sessionConfiguration delegate:self delegateQueue:nil]; NSURLSessionDownloadTask *downloadTask = [session downloadTaskWithURL:[NSURL URLWithString:@"http://cdn.tutsplus.com/mobile/uploads/2013/12/sample.jpg"]]; [downloadTask resume]; } |
Configuring a session configuration object is as simple as modifying its properties as shown in the example. We can then use the session configuration object to instantiate a session object. The session object serves as a factory for data, upload, and download tasks, with each task corresponding to a single request.
Finally, implement the protocol methods
Make sure you use the main queue, to update your image holder with the data you just downloaded.
Also, if you are to use a progress view to show updates on progress, make sure you dispatch a code block onto the main queue, so that the main thread can run the code and show it.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
- (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didFinishDownloadingToURL:(NSURL *)location { NSData *data = [NSData dataWithContentsOfURL:location]; NSLog(@"ViewController.m - FINISHED DOWNLOADING for task"); dispatch_async(dispatch_get_main_queue(), ^{ //[self.progressView setHidden:YES]; [self.myImageView setImage:[UIImage imageWithData:data]]; }); } - (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didResumeAtOffset:(int64_t)fileOffset expectedTotalBytes:(int64_t)expectedTotalBytes { } - (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didWriteData:(int64_t)bytesWritten totalBytesWritten:(int64_t)totalBytesWritten totalBytesExpectedToWrite:(int64_t)totalBytesExpectedToWrite { float progress = (double)totalBytesWritten / (double)totalBytesExpectedToWrite; dispatch_async(dispatch_get_main_queue(), ^{ //[self.progressView setProgress:progress]; NSLog(@"download progress i: %f", progress); }); } |
http://stackoverflow.com/questions/31254725/transport-security-has-blocked-a-cleartext-http
concurrency, asynchronous, parrallelism
ref – http://stackoverflow.com/questions/4844637/what-is-the-difference-between-concurrency-parallelism-and-asynchronous-methods
Concurrent and parallel are effectively the same principle as you correctly surmise, both are related to tasks being executes simultaneously although I would say that parallel tasks should be truly multitasking, executed at the same time whereas concurrent could mean that the tasks are sharing the execution thread while still appearing to be executing in parallel.
Asynchronous methods aren’t directly related to the previous two concepts, asynchrony is used to present the impression of concurrent or parallel tasking but effectively an asynchronous method call is normally used for a process that needs to do work away from the current application and we don’t want to wait and block our application awaiting the response.
For example, getting data from a database could take time but we don’t want to block our UI waiting for the data. The async call takes a call-back reference and returns execution back to your code as soon as the request has been placed with the remote system. Your UI can continue to respond to the user while the remote system does whatever processing is required, once it returns the data to your call-back method then that method can update the UI (or hand off that update) as appropriate.
From the User perspective it appears like multitasking but it may not be.
EDIT
It’s probably worth adding that in many implementations an asynchronous method call will cause a thread to be spun up but it’s not essential, it really depends on the operation being executed and how the response can be notified back to the system.
global queue and their priorities
http://stackoverflow.com/questions/25052629/ios-gcd-difference-between-any-global-queue-and-the-one-with-background-priorit
- DISPATCH_QUEUE_PRIORITY_HIGH
- DISPATCH_QUEUE_PRIORITY_DEFAULT
- DISPATCH_QUEUE_PRIORITY_LOW
are priority queues with tasks in them.
These queues are performed according to their priority as evident by their names.
Let’s say you have the queue with HIGH priority; tasks in that queue will be executed first; you would do that if one specific task needs to be finished as quick as possible even if it means that other tasks are delayed.
DISPATCH_QUEUE_PRIORITY_LOW – Items dispatched to the queue will run at low priority, i.e. the queue will be scheduled for execution after ALL default priority and high priority queues have been scheduled.
DISPATCH_QUEUE_PRIORITY_BACKGROUND
Let’s say you may have a task A that takes a long time, but it is Ok for that task to wait for other tasks to finish. For example, if there is work to do at NORMAL priority, that work will be done first, and only when there is a spare CPU doing nothing else, then your task A will be performed.
You would give that task A BACKGROUND priority.
Meaning of Global Queue
Other things, like system frameworks, may be scheduling in to it. It’s very easy to starve the priority bands – if there are a lot of DISPATCH_QUEUE_PRIORITY_HIGH tasks being scheduled, tasks at the default priority may have to wait quite a while before executing. And tasks in DISPATCH_QUEUE_PRIORITY_BACKGROUND may have to wait a very long time, as all other priorities above them must be empty.
Don’t abuse the global queue
A lot of developers abuse the global concurrent queue. They want a execute a block, need a queue, and just use that at the default priority. That kind of practice can lead to some very difficult to troubleshoot bugs. The global concurrent queue is a shared resource and should be treated with care. In most cases it makes more sense to create a private queue.
ref – http://stackoverflow.com/questions/9602042/whats-the-difference-between-the-global-queue-and-the-main-queue-in-gcd
thread priorities
-main- : 0.758065
DISPATCH_QUEUE_PRIORITY_HIGH : 0.532258
DISPATCH_QUEUE_PRIORITY_DEFAULT : 0.500000
DISPATCH_QUEUE_PRIORITY_LOW : 0.467742
DISPATCH_QUEUE_PRIORITY_BACKGROUND : 0.000000
self does not hold strong ref to dispatch_async
ref – https://digitalleaves.com/blog/2015/05/demystifying-retain-cycles-in-arc/
Examples of non-cycles
-
123[myArray enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop){[self doSomethingWithObject:obj];}];
The block retains self, but self doesn’t retain the block. If one or the other is released, no cycle is created and everything gets deallocated as it should.
-
Using dispatch_async as the example is misleading as self does not hold a strong reference to it so, there is no threat of a retain cycle.
How you get into trouble
Where you get into trouble is something like:
1 2 3 4 5 6 7 |
//In the interface: @property (strong) void(^myBlock)(id obj, NSUInteger idx, BOOL *stop); //In the implementation: [self setMyBlock:^(id obj, NSUInteger idx, BOOL *stop) { [self doSomethingWithObj:obj]; }]; |
Now, your object (self) has an explicit strong reference to the block. And the block has an implicit strong reference to self. That’s a cycle, and now neither object will be deallocated properly.
Not blocking the UI with FMDB Queue
Not blocking the UI with FMDB Queue
So notifications are coming in rapidly…each one is a thread, and the threads’s data are getting inserted into your FMDB. Your FMDB ensures thread safety because FMDB’s queue uses dispatch_sync:
FMDatabaseQueue.m
1 2 3 4 5 6 7 8 9 |
- (void)inDatabase:(void (^)(FMDatabase *db))block { dispatch_sync(_queue, ^() { FMDatabase *db = [self database]; block(db); ... ... }); |
It gets the block of database code you pass in, and puts it on a SERIAL QUEUE.
The dispatch_sync used here is not a tool for getting concurrent execution, it is a tool for temporarily limiting it for safety.
The SERIAL QUEUE ensures safety by having each block line up one after another, and start their execution ONLY AFTER the previous task has finished executing. This ensures that you are thread safe because they are not writing over each other at the same time.
However there is a problem. Let’s say your main thread is processing a for loop with the inDatabase method. The main thread places the block on FMDB Queue’s serial queue. This means the dispatch_sync that FMDB Queue is using will block your main thread as it processes each task. By definition, dispatch_sync DOES NOT RETURN, until after it has finished its executing task.
Proving FMDB does block
We need to prove that FMDB does indeed block our UI so we first put a UISlider in ViewController for testing purposes. If we are concurrently processing all these incoming notifications in the background, then this UISlider should be responsive.
You put a slider on your UI like so:
1 2 3 4 5 6 7 8 9 10 |
UISlider *slider = [[UISlider alloc] initWithFrame:frame]; [slider addTarget:self action:@selector(sliderAction:) forControlEvents:UIControlEventValueChanged]; [slider setBackgroundColor:[UIColor redColor]]; slider.minimumValue = 0.0; slider.maximumValue = 50.0; slider.continuous = YES; slider.value = 25.0; [self.view addSubview:slider]; |
When you run a simple for loop outside of method say executeUpdateOnDBwithSQLParams:, you are essentially adding a dispatch_sync on your main thread. This will block, and your UI will NOT be responsive.
In order to solve this, we do 2 things:
- Use a concurrent queue and have main thread work on it to ensure concurrency and that the UI is not blocked
- Inside of that concurrent queue, we queue up db jobs to FMDB’s thread safe serial queue
Solution
dispatch_sync does not return until its task is finished. Thus, while the task is executing, the main queue can’t do anything because the dispatch_sync has not returned. That’s the just of the issue.
What we did to solve this issue is to
dispatch_async FMDB tasks on a concurrent queue.
This is the basic setup that enables fmdb to be non-blocking.
1) We set up a block on a concurrent queue first. This ensures that whatever runs inside of that concurrent block will be able to run
concurrently with the main thread.
2) The block starts off with executing its log. Then the PRE-TASK. Then it syncs its own block of the “DB Task”. This sync means that it blocks whatever is trying run with it, on conQueue. Hence, that’s why POST-TASK will run after DB task.
3) Finally, after PRE-TASK, then DB task, finish running, POST-TASK runs.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
// note that conQueue is a concurrent Queue // queue is a serial queue dispatch_async(conQueue, ^{ NSLog(@"--- start block (concurrent queue) ---"); [self taskName:@"PRE-TASK on concurrent queue" countTo:3 sleepFor:1.0f]; dispatch_sync(queue, ^{ //queue up a task on queueA NSLog(@"--- start block (sync queue) ---"); //must finish this line [self taskName:@"DB task" countTo:10 sleepFor:1.0f]; NSLog(@"--- end block (sync queue) ---"); }); [self taskName:@"POST-TASK on concurrent queue" countTo:3 sleepFor:1.0f]; NSLog(@"--- end block (concurrent queue) ---"); }); |
— start block (concurrent queue) —
— Task PRE-TASK on concurrent queue start —
PRE-TASK on concurrent queue – 0
PRE-TASK on concurrent queue – 1
PRE-TASK on concurrent queue – 2
^^^ Task PRE-TASK on concurrent queue END ^^^
— start block (sync queue) —
— Task DB task start —
DB task – 0
DB task – 1
DB task – 2
DB task – 3
DB task – 4
DB task – 5
DB task – 6
DB task – 7
DB task – 8
DB task – 9
^^^ Task DB task END ^^^
— end block (sync queue) —
— Task POST-TASK on concurrent queue start —
POST-TASK on concurrent queue – 0
POST-TASK on concurrent queue – 1
POST-TASK on concurrent queue – 2
^^^ Task POST-TASK on concurrent queue END ^^^
— end block (concurrent queue) —
The dispatch_sync is to simulate FMDB like so:
1 2 3 4 5 6 7 8 9 |
- (void)inDatabase:(void (^)(FMDatabase *db))block { dispatch_sync(_queue, ^() { FMDatabase *db = [self database]; block(db); ... ... }); |
So BOTH tasks
- dispatch_sync is running its db tasks (fmdb)
- free to move your UI (main queue)
are processing on the concurrent queue via dispatch_async.
Thus, that’s how you get FMDB to be non-blocking.
Tidbit: changing dispatch_async to dispatch_sync on the concurrent queue
If you were to change from dispatch_async to dispatch_sync on the concurrent queue “conQueue”, it will block the main queue
when it first starts up because by definition, dispatch_sync means it does not return right away. It will return later
when it runs to the end, but for now, it blocks and you’re not able to move your UI.
Thus, it runs to PRE-TASK, and executes that.
Then it moves down, and runs the “DB task” block via dispatch_async on serial queue “queue”.
The dispatch_async returns immediately, starts executing “DB task” on serial queue “queue”, and then it executes
POST-TASK. Thus, DB task and POST-TASK will be executing together.
After POST-TASK finishes, our concurrent block has ran to the end, and returns (due to dispatch_sync).
At this point, you will be able to move the UI. “DB task” continues its execution because its part of the task that’s still sitting on concurrent queue “conQueue”.
Since its a concurrent queue, it will be processing this task that’s still sitting on its queue, and you will be able to move around the UI because nothing is blocking anymore.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
dispatch_sync(conQueue, ^{ NSLog(@"--- start block (concurrent queue) ---"); [self taskName:@"PRE-TASK on concurrent queue" countTo:3 sleepFor:1.0f]; dispatch_async(queue, ^{ //queue up a task on queueA NSLog(@"--- start block (sync queue) ---"); //must finish this line [self taskName:@"DB task" countTo:10 sleepFor:1.0f]; NSLog(@"--- end block (sync queue) ---"); }); [self taskName:@"POST-TASK on concurrent queue" countTo:3 sleepFor:1.0f]; NSLog(@"--- end block (concurrent queue) ---"); }); |
Other details when you have time
1 2 3 4 5 6 7 8 9 10 11 12 13 |
-(void)insertPortfolioWithPfNum:(NSString*)portfolioNum performance:(NSString*)performance aum:(NSString*)aum ccy:(NSString*)currency chartJSONString:(NSString*)json andTime:(NSString*)time { dispatch_async(concurrencyQueue, ^{ NSLog(@"inserting portoflio %@", portfolioNum); [self executeUpdateOnDBwithSQLParams: INSERT_INTO_PORTFOLIOS, @"portfolio", portfolioNum, performance, aum, currency, json, time]; }); } |
where concurrencyQueue is created and initialized like so:
1 2 3 4 5 6 7 8 9 10 11 12 |
static dispatch_queue_t concurrencyQueue; //concurrency queue @interface DatabaseFunctions() {} @end -(instancetype)init { ... ... concurrencyQueue = dispatch_queue_create("com.epam.halo.queue", DISPATCH_QUEUE_CONCURRENT); ... ... } |
But what about the database writing that is dispatch_sync on the serial queue? Wouldn’t that block?
No. The dispatch_sync on the serial queue only blocks against thread(s) that touches FMDB Queue’s serial queue. In this case, it would be FMDBQueue’s serial queue’s own thread, and the concurrent queue’s thread.
max of 64 threads running concurrently
ref – http://stackoverflow.com/questions/34849078/main-thread-does-dispatch-async-on-a-concurrent-queue-in-viewdidload-or-within
Note that when you are using custom concurrent queues, you are only allowed to use 64 concurrent working threads at once. Hence, that’s why when your main thread calls on the concurrent queue and queue up tasks, the system starts blocking your UI after 64 tasks on the queue.
The workaround is to put the task of placing db tasks onto the concurrent queue onto your main queue like so:
1 2 3 4 5 6 7 8 9 10 |
-(void)run_async_with_UI:(void (^)(void)) task { dispatch_async(dispatch_get_main_queue(), ^() { dispatch_async(concurrencyQueue, ^{ task(); }); }); } |
Then simply call the utility method run_async_with_UI and place your database calls in there.
Proof of concept
The dispatch_sync(serialQueue,….) is essentially the FMDB Queue.
We just added dispatch_async(concurrencyQueue…). Now, you can see that we are manipulating the database in a thread-safe manner, in the background, without clogging up the UI.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
for ( int i = 0; i < 30; i++) { dispatch_async(concurrencyQueue, ^() { NSLog(@"concurrentQueue inserts task %d <--", i); //dispatch_sync function WILL NOT CONTINUE enqueueing further tasks until //this block has been executed //won't return until this block is finished //hence its blocks dispatch_sync(serialQueue, ^() { //this is to simulate writing to database NSLog(@"serialQueue - START %d---------", i); [NSThread sleepForTimeInterval:8.0f]; NSLog(@"serialQueue - FINISHED %d--------", i); }); NSLog(@"concurrentQueue END task %d -->", i); }); } |
result:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
concurrentQueue inserts task 1 <-- 2016-08-17 10:58:48.477 trash[6381:704826] concurrentQueue inserts task 0 <-- 2016-08-17 10:58:48.477 trash[6381:704838] concurrentQueue inserts task 3 <-- 2016-08-17 10:58:48.477 trash[6381:704830] concurrentQueue inserts task 2 <-- 2016-08-17 10:58:48.478 trash[6381:704816] serialQueue - START 1--------- 2016-08-17 10:58:48.478 trash[6381:704839] concurrentQueue inserts task 4 <-- 2016-08-17 10:58:48.478 trash[6381:704840] concurrentQueue inserts task 5 <-- 2016-08-17 10:58:48.478 trash[6381:704841] concurrentQueue inserts task 6 <-- 2016-08-17 10:58:48.479 trash[6381:704842] concurrentQueue inserts task 7 <-- 2016-08-17 10:58:48.479 trash[6381:704843] concurrentQueue inserts task 8 <-- 2016-08-17 10:58:48.479 trash[6381:704844] concurrentQueue inserts task 9 <-- 2016-08-17 10:58:48.480 trash[6381:704845] concurrentQueue inserts task 10 <-- ... ... |
So, the dispatch_async throws all the tasks onto the concurrent queue without returning (aka blocking). That’s why all the task blocks log “concurrentQueue inserts task n”.
The task the dispatch_async throws onto the serialQueue will start executing immediately via dispatch_sync. Dispatch_sync by definition means it won’t return until its block has finished executing. Hence, this means that “concurrentQueue’s END task n” message won’t be logged until after the block on serialQueue has been executed”.
Notice how serialQueue FINISHED 1, then concurrentQueue logs END task 1.
serialQueue FINISHED 0, then concurrentQueue END task 0…
its because dispatch_sync does not return until it has finished executing.
Once it returns, it continues onto the “concurrentQueue END task n” log message.
In other words, due to dispatch_sync, line 10-16 must be run, before line 20 is run.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
ncurrentQueue inserts task 27 <-- 2016-08-17 14:12:34.938 trash[6685:772616] concurrentQueue inserts task 28 <-- 2016-08-17 14:12:34.938 trash[6685:772617] concurrentQueue inserts task 29 <-- 2016-08-17 14:12:42.933 trash[6685:772590] serialQueue - FINISHED 2-------- 2016-08-17 14:12:42.933 trash[6685:772590] concurrentQueue END task 2 --> 2016-08-17 14:12:42.933 trash[6685:772558] serialQueue - START 0--------- 2016-08-17 14:12:50.933 trash[6685:772558] serialQueue - FINISHED 0-------- 2016-08-17 14:12:50.933 trash[6685:772558] concurrentQueue END task 0 --> 2016-08-17 14:12:50.933 trash[6685:772570] serialQueue - START 1--------- 2016-08-17 14:12:58.938 trash[6685:772570] serialQueue - FINISHED 1-------- 2016-08-17 14:12:58.938 trash[6685:772591] serialQueue - START 3--------- 2016-08-17 14:12:58.938 trash[6685:772570] concurrentQueue END task 1 --> 2016-08-17 14:13:06.943 trash[6685:772591] serialQueue - FINISHED 3-------- 2016-08-17 14:13:06.944 trash[6685:772591] concurrentQueue END task 3 --> 2016-08-17 14:13:06.944 trash[6685:772592] serialQueue - START 4--------- 2016-08-17 14:13:14.947 trash[6685:772592] serialQueue - FINISHED 4-------- 2016-08-17 14:13:14.947 trash[6685:772592] concurrentQueue END task 4 --> 2016-08-17 14:13:14.947 trash[6685:772593] serialQueue - START 5--------- 2016-08-17 14:13:22.952 trash[6685:772593] serialQueue - FINISHED 5-------- 2016-08-17 14:13:22.953 trash[6685:772593] concurrentQueue END task 5 --> 2016-08-17 14:13:22.953 trash[6685:772594] serialQueue - START 6--------- 2016-08-17 14:13:30.958 trash[6685:772594] serialQueue - FINISHED 6-------- 2016-08-17 14:13:30.958 trash[6685:772594] concurrentQueue END task 6 --> 2016-08-17 14:13:30.958 trash[6685:772595] serialQueue - START 7--------- 2016-08-17 14:13:38.963 trash[6685:772595] serialQueue - FINISHED 7-------- 2016-08-17 14:13:38.963 trash[6685:772595] concurrentQueue END task 7 --> 2016-08-17 14:13:38.963 trash[6685:772596] serialQueue - START 8--------- 2016-08-17 14:13:46.965 trash[6685:772596] serialQueue - FINISHED 8-------- |
Another important note is that notice serialQueue has started to execute. But by definition, dispatch_sync blocks and does not return until the current executing task is finished…so how does concurrentQueue keeps inserting?
The reason is that the blocks on serialQueue is running in the background. The dispatch_sync that’s not returning happens in the background, and thus, does not affect the UI. The enqueueing of the “db simulate write” onto the serialQueue is done on the background queue concurrentQueue.
Say we switch it
So now we dispatch_sync a block onto a queue, it will not return until it finishes enqueueing. The key point here is that “due to dispatch_async throwing the task onto the serialQueue and returning immediately”, enqueueing will be:
lots of fast enqueueing of blocks onto the concurrent queue, thus, logging of
line 5, and line 20.
example:
1. block task 1 goes onto concurrent queue via dispatch_sync, WILL NOT RETURN UNTIL WHOLE TASK BLOCK IS FINISHED
2. “simulate DB write” task block goes onto serial Queue via dispatch_async, RETURNS RIGHT AWAY.
3. block task 1 finished, thus RETURNS control to concurrent queue.
4. block task 2 goes onto concurrent queue via dispatch_sync, WILL NOT RETURN UNTIL WHOLE TASK BLOCK IS FINISHED
5. “simulate DB write” task block goes onto serial Queue via dispatch_async, RETURNS RIGHT AWAY.
6. block task 2 finished, thus RETURNS control to concurrent queue.
…
…
etc.
continues until the serialQueue, being a background queue, starts processing its first block. Hence it will display:
serialQueue – START 0———
serialQueue – FINISHED 0——–
Hence, the situation is that all the tasks of putting “simulate write tasks onto the serial Queue” are enqueued onto the concurrent queue quickly.
Then, when the serial Queue decides to execute its first task, thats’ when it does its first “DB write simulate”. This DB write simulate does not block UI because its being done in the background.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
for ( int i = 0; i < 30; i++) { dispatch_sync(concurrencyQueue, ^() { NSLog(@"concurrentQueue inserts task %d <--", i); //dispatch_sync function WILL NOT CONTINUE enqueueing further tasks until //this block has been executed //won't return until this block is finished //hence its blocks dispatch_async(serialQueue, ^() { //this is to simulate writing to database NSLog(@"serialQueue - START %d---------", i); [NSThread sleepForTimeInterval:8.0f]; NSLog(@"serialQueue - FINISHED %d--------", i); }); NSLog(@"concurrentQueue END task %d -->", i); }); } |
result:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
concurrentQueue inserts task 0 <-- 2016-08-17 11:35:30.898 trash[6447:722065] concurrentQueue END task 0 --> 2016-08-17 11:35:30.898 trash[6447:722150] serialQueue - START 0--------- 2016-08-17 11:35:30.898 trash[6447:722065] concurrentQueue inserts task 1 <-- 2016-08-17 11:35:30.898 trash[6447:722065] concurrentQueue END task 1 --> 2016-08-17 11:35:30.898 trash[6447:722065] concurrentQueue inserts task 2 <-- 2016-08-17 11:35:30.898 trash[6447:722065] concurrentQueue END task 2 --> 2016-08-17 11:35:30.899 trash[6447:722065] concurrentQueue inserts task 3 <-- 2016-08-17 11:35:30.899 trash[6447:722065] concurrentQueue END task 3 --> 2016-08-17 11:35:30.899 trash[6447:722065] concurrentQueue inserts task 4 <-- 2016-08-17 11:35:30.899 trash[6447:722065] concurrentQueue END task 4 --> 2016-08-17 11:35:30.899 trash[6447:722065] concurrentQueue inserts task 5 <-- 2016-08-17 11:35:30.899 trash[6447:722065] concurrentQueue END task 5 --> 2016-08-17 11:35:30.899 trash[6447:722065] concurrentQueue inserts task 6 <-- 2016-08-17 11:35:30.899 trash[6447:722065] concurrentQueue END task 6 --> 2016-08-17 11:35:30.900 trash[6447:722065] concurrentQueue inserts task 7 <-- 2016-08-17 11:35:30.900 trash[6447:722065] concurrentQueue END task 7 --> 2016-08-17 11:35:30.900 trash[6447:722065] concurrentQueue inserts task 8 <-- 2016-08-17 11:35:30.900 trash[6447:722065] concurrentQueue END task 8 --> 2016-08-17 11:35:30.900 trash[6447:722065] concurrentQueue inserts task 9 <-- 2016-08-17 11:35:30.900 trash[6447:722065] concurrentQueue END task 9 --> 2016-08-17 11:35:30.900 trash[6447:722065] concurrentQueue inserts task 10 <-- 2016-08-17 11:35:30.901 trash[6447:722065] concurrentQueue END task 10 --> 2016-08-17 11:35:30.901 trash[6447:722065] concurrentQueue inserts task 11 <-- 2016-08-17 11:35:30.901 trash[6447:722065] concurrentQueue END task 11 --> 2016-08-17 11:35:30.901 trash[6447:722065] concurrentQueue inserts task 12 <-- 2016-08-17 11:35:30.901 trash[6447:722065] concurrentQueue END task 12 --> 2016-08-17 11:35:30.901 trash[6447:722065] concurrentQueue inserts task 13 <-- |
Then after all the tasks are being enqueued onto the concurrent queue…the serialQueue processing task one by one.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
concurrentQueue inserts task 28 <-- 2016-08-17 11:35:30.905 trash[6447:722065] concurrentQueue END task 28 --> 2016-08-17 11:35:30.905 trash[6447:722065] concurrentQueue inserts task 29 <-- 2016-08-17 11:35:30.906 trash[6447:722065] concurrentQueue END task 29 --> 2016-08-17 11:35:38.900 trash[6447:722150] serialQueue - FINISHED 0-------- 2016-08-17 11:35:38.900 trash[6447:722150] serialQueue - START 1--------- 2016-08-17 11:35:46.904 trash[6447:722150] serialQueue - FINISHED 1-------- 2016-08-17 11:35:46.904 trash[6447:722150] serialQueue - START 2--------- 2016-08-17 11:35:54.908 trash[6447:722150] serialQueue - FINISHED 2-------- 2016-08-17 11:35:54.908 trash[6447:722150] serialQueue - START 3--------- 2016-08-17 11:36:02.910 trash[6447:722150] serialQueue - FINISHED 3-------- 2016-08-17 11:36:02.910 trash[6447:722150] serialQueue - START 4--------- 2016-08-17 11:36:10.915 trash[6447:722150] serialQueue - FINISHED 4-------- 2016-08-17 11:36:10.916 trash[6447:722150] serialQueue - START 5--------- 2016-08-17 11:36:18.921 trash[6447:722150] serialQueue - FINISHED 5-------- 2016-08-17 11:36:18.921 trash[6447:722150] serialQueue - START 6--------- 2016-08-17 11:36:26.926 trash[6447:722150] serialQueue - FINISHED 6-------- 2016-08-17 11:36:26.926 trash[6447:722150] serialQueue - START 7--------- 2016-08-17 11:36:34.930 trash[6447:722150] serialQueue - FINISHED 7-------- |
Protected: FMDatabaseQueue benchmarks
instance variable strong references?
ref – http://stackoverflow.com/questions/16461343/ivars-references-strong-weak-or-what
Default behavior of instance variable
Instance variable maintains a strong reference to the objects by default
Why don’t we specify weak/strong for iVar like properties?
Local variables and non-property instance variables maintain a strong references to objects by default. There’s no need to specify the strong attribute explicitly, because it is the default.
A variable maintains a strong reference to an object only as long as that variable is in scope, or until it is reassigned to another object or nil.
If you don’t want a variable to maintain a strong reference, you can declare it as __weak, like this:
1 |
NSObject * __weak weakVariable; |