O'Reilly logo

High Performance iOS Apps by Gaurav Vaish

Stay ahead with the world's most comprehensive technology and business learning platform.

With Safari, you learn the way you learn best. Get unlimited access to videos, live online training, learning paths, books, tutorials, and more.

Start Free Trial

No credit card required

Chapter 4. Concurrent Programming

iOS devices have two or three CPU cores (see Table 3-1). This means, even if the main thread (the UI thread) is busy updating the screen, the app can still be doing more computations in the background without the need for any context switch.

In this chapter, we explore various options for making the best use of the available CPU cores, and we’ll learn how to optimize performance using concurrent programming. We will discuss the following topics:

  • Creating and managing threads

  • The Great Central Dispatch (GCD) abstraction

  • Operations and queues

We will cover best practices and techniques for writing thread-safe, highly performant code.

Threads

A thread is a sequence of instructions that can be executed by a runtime.

Each process has at least one thread. In iOS, the primary thread on which the process is started is commonly referred to as the main thread. This is the thread in which all UI elements are created and managed. All interrupts related to user interaction are ultimately dispatched to the UI thread where the handler code is written—your IBAction methods are all executed in the main thread.

Cocoa programming does not allow updating UI elements from other threads. This means that whenever the app executes background threads for long operations such as network or other processing, the code must perform a context switch to the main thread to update the UI—for example, the progress bar indicating the task progress or the label indicating the outcome of the process.

The Cost of Threads

However great it may look to have several threads in the app, each thread has a cost associated with it that impacts app performance. Each thread not only takes some time during creation but also uses up memory in the kernel as well as the app’s memory space.1

Kernel Data Structures

Each thread consumes approximately 1 KB of memory in kernel space. The memory is used to store the data structures and attributes pertaining to the thread. This is wired memory and cannot be paged.

Stack Size

The main thread stack size is 1 MB and cannot be changed. Any secondary thread is allocated 512 KB of stack space by default. Note that the full stack is not immediately created. The actual stack size grows with use. So, even if the main thread has a stack size of 1 MB, at some point in time, the actual stack size may be much smaller.

Before a thread starts, the stack size can be changed. The minimum allowed stack size is 16 KB, and the size must be a multiple of 4 KB. The sample code in Example 4-1 shows how you can configure the stack size before starting a thread.

Example 4-1. Change thread stack size
+(NSThread *)createThreadWithTarget:(id)target selector:(SEL)selector
            object:(id)argument stackSize:(NSUInteger)size {

    if( (size % 4096) != 0) {
        return nil;
    }
    NSThread *t = [[NSThread alloc] initWithTarget:target
        selector:selector object:argument];
    t.stackSize = size;

    return t;
}

Creation Time

A quick test on an iPhone 6 Plus running iOS 8.4 showed average thread creation time (not including the start time) ranged between 4,000–5,000 µs, which is about 4–5 ms.

The time taken to actually start a thread after creation ranged from anywhere between 5 ms to well over 100 ms, averaging about 29 ms. That can be a lot of time, especially if you start multiple threads during app launch.

The elongated time for thread start can be attributed to several context switches that have overheads.

For brevity, the code for these computations has been omitted here. For details, see the computeThreadCreationTime method in the code on GitHub. Figure 4-1 shows the output from that code.

hpia 0401
Figure 4-1. Thread creation time

GCD

The Grand Central Dispatch (GCD) API is comprised of core language features, runtime libraries, and system enhancements for concurrent code execution.

We will not get into the fundamentals of using GCD, as that is not the purpose of this book. You most likely already have a fair background working with GCD constructs, but if you need a review of GCD fundamentals, check out Ray Wenderlich’s “Multithreading and Great Central Dispatch on iOS for Beginners Tutorial”.

However, for completeness, we will run through a quick list of what GCD provides:

  • Task or dispatch queues, which allow execution on the main thread, concurrent execution, and serial execution

  • Dispatch groups, which allow tracking execution of a group of tasks, irrespective of the underlying queue they are submitted on

  • Semaphores

  • Barriers, which allow creating synchronization points in a concurrent dispatch queue

  • Dispatch object and source management, which allow low-level management and monitoring

  • Asynchronous I/O, using either file descriptors or channels

GCD handles thread creation and management well. It also helps you to keep the total number of threads in your app under control and not cause any leaks.

Caution

While most apps will generally perform well using GCD alone, there are specific cases when you should consider using NSThread or NSOperationQueue. In the scenarios where your app has multiple long-running tasks to be executed concurrently, it is better to take control of the thread creation. If your code takes longer to complete, you may soon hit the limit of 64,2,3 the maximum GCD thread pool size.

Be wary about using dispatch_async and dispatch_sync lavishly too, as it can lead to app crashes.4 Although 64 threads might look like a reasonably high number for a mobile app, the app may hit the limit sooner than later.

Operations and Queues

The next set of abstractions available for managing tasks in iOS programming is operations and operation queues.

NSOperation encapsulates a task and its associated data and code, whereas NSOperationQueue controls execution of one or more of such tasks in a FIFO order.

NSOperation and NSOperationQueue both provide control over the number of threads that get created. You control the number of queues formed. You also control the number of threads in each queue, using the maxConcurrentOperationCount property.

These two options sit somewhere in between using NSThread (where it is left to the developer to manage all concurrency) and GCD (where the OS manages concurrency).

Here’s a quick comparison of the NSThread, NSOperationQueue, and GCD APIs:

GCD
  • Highest abstraction.

  • Two queues are available out of the box: main and global.

  • Can create more queues (using dispatch_queue_create).

  • Can request exclusive access (using dispatch_barrier_sync and dispatch_barrier_async).

  • Manages underlying threads.

  • Hard limit on 64 threads created.

NSOperationQueue
  • No default queues.

  • App manages the queues it creates.

  • Queues are priority queues.

  • Operations can have different priorities (use the queuePriority property).

  • Operations can be cancelled using the cancel message. Note that cancel is merely a flag. If an operation is under execution, it may continue to execute.

  • Can wait for an operation to complete (use the waitUntilFinished message).

NSThread
  • Lowest-level construct, gives maximum control.

  • App creates and manages threads.

  • App creates and manages thread pools.

  • App starts the threads.

  • Threads can have priority. OS uses this for scheduling their execution.

  • No direct API to wait for a thread to complete. Use a mutex (e.g., NSLock) and custom code.

Note

NSOperationQueue is multicore-safe. It is safe to use a shared queue and submit tasks from multiple threads without having to worry about queue corruption.

Thread-Safe Code

All throughout our software engineering lives, we are told to always write thread-safe code—meaning that if multiple threads execute the same instruction sets concurrently, there should not be any negative side effects.

There are two broad techniques for achieving this:

  • Do not have a modifiable shared state.

  • If you cannot avoid using a modifiable shared state, make your code thread-safe.

These techniques are easier said than done. There are a number of choices available to accomplish them.

Because an app will have a modifiable shared state, we need to establish best practices for application state management and modifications.

One basic rule that drives these best practices is “Preserve invariants in the code.”5

Atomic Properties

Atomic properties are a great start to making your application state thread-safe. If a property is atomic, the modification or retrieval is guaranteed to be atomic.

This is important because it prevents two threads from simultaneously updating a value, which otherwise could result in a corrupted state. The thread that is modifying the property must complete before the other thread can proceed.

All properties are atomic by default. As a best practice, use atomic explicitly where this is appropriate. To mark a property otherwise, use the nonatomic attribute. Example 4-2 demonstrates both atomic and nonatomic properties.

Example 4-2. Atomic and nonatomic properties
@property (atomic) NSString *firstName; 1
@property (nonatomic) NSString *department; 2
1

Atomic property

2

Nonatomic property

Because atomic properties have overheads, it is advisable not to overuse them. For example, when it can be guaranteed that a property will never be accessed from more than one thread at any time, it is better to mark it nonatomic.

One such scenario is working with IBOutlets. @property (nonatomic, readwrite, strong) IBOutlet UILabel *nameLabel should be preferred over @property (atomic, readwrite, strong) IBOutlet UILabel *nameLabel because we know that UIKit allows manipulating UI elements from only the main thread. Because access will be in one designated thread, marking the property atomic will only add overhead without bringing any value.

Synchronized Blocks

Even if the properties are marked atomic, the eventual code using them may not be thread-safe. An atomic property only prohibits concurrent modification. Assuming that we have an entity HPUser that can be updated using an operation HPOperation, let’s have a look at Example 4-3.

Example 4-3. Using atomic properties across threads
//An entity (partial definition)
@interface HPUser

@property (atomic, copy) NSString *firstName;
@property (atomic, copy) NSString *lastName;

@end


//A service class (declaration omitted for brevity)
@implementation HPUpdaterService

-(void)updateUser:(HPUser *)user properties:(NSDictionary *)properties {
    NSString *fn = [properties objectForKey:@"firstName"];
    if(fn != nil) {
        user.firstName = fn;
    }
    NSString *ln = [properties objectForKey:@"lastName"];
    if(ln != nil) {
        user.lastName = ln;
    }
}

@end

Let’s consider that the updateUser:properties: method is called whenever the user pulls down to refresh and data is available from the server. It may also be called by a sync task that executes periodically.

So, at some point in time, there is a possibility that multiple responses will attempt to update the user profile concurrently—maybe on two cores or just using time-slicing.

Consider the scenario where two responses in different threads try to update the user with the names “Bob Taylor” and “Alice Darji.” Without atomic updates on the properties firstName and lastName, the order of execution is not guaranteed and the final result can be any combination, including “Alice Taylor” and “Bob Darji.”

This example is only demonstrative but enforces the point that atomic properties are not enough to make code thread-safe.

This brings us to the next best practice: all related state updates should be batched in a single transaction.

Use the @synchronized directive to create a mutex and enter a critical section, which can only be executed by one thread at any point in time. The code may be updated as shown in Example 4-4.

Example 4-4. Thread-safe blocks
@implementation HPUpdaterService

-(void)updateUser:(HPUser *)user properties:(NSDictionary *)properties {
    @synchronized(user) { 1
        NSString *fn = [properties objectForKey:@"firstName"];
        if(fn != nil) {
            user.firstName = fn;
        }
        NSString *ln = [properties objectForKey:@"lastName"];
        if(ln != nil) {
            user.lastName = ln;
        }
    }
}

@end
1

Acquire a lock against the user object. All related changes are now handled together with no possibility of race conditions.

With this change, the final name of the user will be either “Bob Taylor” or “Alice Darji.”

Caution

Note that overuse of the @synchronized directive can slow down your app, as only one thread can execute within the critical section at any time.

For our case, we chose user as the object to acquire a lock on. Thus, the updateUser:properties: method can be called from multiple threads for as many users as necessary, and it will execute with high concurrency as long as the user objects are not the same. The result is code implemented for high-concurrency use with guards against data corruption.

Tip

The object on which the lock is acquired is key to well-defined critical sections. As a rule of thumb, select the object whose state will be accessed or modified as the reference for the mutex.

So far, so good. But what should the strategy be for reading the properties? What if you needed to display the full name of the HPUser object while it is being modified?

Locks

Locks are the basic building blocks to enter a critical section. atomic properties and @synchronized blocks are higher-level abstractions available for easy use.

There are three kinds of locks available:

NSLock

This is a low-level lock. Once a lock is acquired, the execution enters the critical section and no more than one thread can execute concurrently. Release the lock to mark the end of the critical section.

Example 4-5 shows an example of using NSLock.

Example 4-5. Using NSLock
@interface ThreadSafeClass () {
 NSLock *lock; 1
}
@end

-(instancetype)init {
 if(self = [super init]) {
  self->lock = [NSLock new]; 2
 }
 return self;
}

-(void)safeMethod {
 [self->lock lock]; 3

 //Thread-safe code 4

 [self->lock unlock]; 5
}
1

The lock is declared as a private field. Another option is to make it a property.

2

Initialize the lock.

3

Acquire the lock to enter the critical section.

4

In the critical section, a maximum of one thread can execute at any time.

5

Release the lock to mark the end of the critical section. Another thread can now acquire the lock.

NSLock must be unlocked from the same thread where it was locked.

NSRecursiveLock

NSLock does not allow lock to be called more than once without first calling unlock. NSRecursiveLock, as the name indicates, does allow lock to be called more than once before it is unlocked. Each lock call must be matched with an equal number of unlock calls before the lock can be considered released for another thread to acquire.

NSRecursiveLock is useful when you have a class with multiple methods that use the same lock to synchronize and one method invokes the other. Example 4-6 shows an example of using it.

Example 4-6. Using NSRecursiveLock
@interface ThreadSafeClass () {
 NSRecursiveLock *lock; 1
}
@end

-(instancetype)init {
 if(self = [super init]) {
  self->lock = [NSRecursiveLock new];
 }
 return self;
}

-(void)safeMethod1 {
 [self->lock lock]; 2

 [self safeMethod2]; 3

 [self->lock unlock]; 6
}

-(void)safeMethod2 {
 [self->lock lock]; 4

 //Thread-safe code

 [self->lock unlock]; 5
}
1

The NSRecursiveLock object.

2

safeMethod1 acquires the lock.

3

It calls method safeMethod2.

4

safeMethod2 acquires a lock on the already-acquired lock.

5

safeMethod2 releases the lock.

6

safeMethod1 releases the lock. Because each lock call is now matched with a corresponding unlock, the lock is now released and ready to be acquired by another thread.

NSCondition

There are cases when there is a need to coordinate execution across threads. For example, a thread may want to wait until another thread has results ready. NSCondition can be used to atomically release a lock and let it be obtained by another waiting thread, while the original thread waits.

A thread can wait on a condition that releases the lock. Another thread can signal the condition by releasing the same lock and awakening the waiting thread.

The standard producer–consumer problem can be solved using NSCondition. Example 4-7 shows the code to implement the solution to this problem.

Example 4-7. Using NSCondition
@implementation Producer

-(instancetype)initWithCondition:(NSCondition *)condition
 collector:(NSMutableArray *)collector { 1
 if(self = [super init]) {
  self.condition = condition;
  self.collector = collector;
  self.shouldProduce = NO;
  self.item = nil;
 }
 return self;
}

-(void)produce {
 self.shouldProduce = YES;
 while(self.shouldProduce) { 2
  [self.condition lock]; 3
  if(self.collector.count > 0) {
   [self.condition wait]; 4
  }
  [self.collector addObject:[self nextItem]]; 5
  [self.condition signal]; 6
  [self.condition unlock]; 7
 }
}
@end

@implementation Consumer

-(instancetype)initWithCondition:(NSCondition *)condition
 collector:(NSMutableArray *)collector { 8
 if(self = [super init]) {
  self.condition = condition;
  self.collector = collector;
  self.shouldConsume = NO;
  self.item = nil;
 }
 return self;
}

-(void)consume {
 self.shouldConsume = YES;
 while(self.shouldConsume) { 9
  [self.condition lock]; 10
  if(self.collector.count == 0) {
   [self.condition wait]; 11
  }
  id item = [self.collector objectAtIndex:0];
  //process item
  [self.collector removeItemAtIndex:0]; 12
  [self.condition signal]; 13
  [self.condition unlock]; 14
 }
}
@end

@implementation Coordinator

-(void)start {
 NSMutableArray *pipeline = [NSMutableArray array];
 NSCondition *condition = [NSCondition new]; 15
 Producer *p = [Producer initWithCondition:condition
   collector:pipeline];
 Consumer *c = [Consumer initWithCondition:condition
   collector:pipeline]; 16
 [[NSThread initWithTarget:self selector:@SEL(startProducer)
  object:p] start];
 [[NSThread initWithTarget:self selector:@SEL(startCollector)
  object:c] start]; 17
  //once done
  p.shouldProduce = NO;
  c.shouldConsume = NO; 18
  [condition broadcst]; 19
}

@end
1

The initializer for the producer needs the NSCondition object to coordinate with and a collector to push produced items to. It is initially set to not produce (shouldProduce = NO).

2

The producer will produce while shouldProduce is YES. Another thread should set it to NO for the producer to stop producing.

3

Obtain the lock on the condition to enter the critical section.

4

If the collector already has some not-consumed items, wait, which blocks the current thread until the condition is signaled.

5

Add the produced nextItem to the collector for it to be consumed.

6

signal another waiting thread, if any. This is an indicator that an item has been produced, and added to the collector, and is available to be consumed.

7

Release the lock.

8

The initializer for the consumer needs the NSCondition object to coordinate with and a collector to push produced items to. It is initially set to not consume (shouldConsume = NO).

9

The consumer will consume while shouldConsume is YES. Another thread should set it to NO for the consumer to stop consuming.

10

Obtain the lock on the condition to enter the critical section.

11

If the collector has no items, wait.

12

Consume the next item in the collector. Ensure that it is removed from the collector.

13

signal another waiting thread, if any. This is an indicator that an item has been consumed and removed from the collector.

14

Release the lock.

15

The Coordinator class readies the input data for the producer and consumer (specifically, the collector and the condition).

16

Set up the producer and consumer.

17

Start production and consumption tasks in different threads.

18

Once completed, set the producer and consumer to stop producing and consuming, respectively.

19

Because the producer and consumer threads may be waiting, broadcast, which is essentially signaling all waiting threads, unlike signal, which affects only one of the waiting threads.

Use Reader–Writer Locks for Concurrent Reads and Writes

We started this section with two choices for achieving thread safety. We discuss best practices to safeguard against concurrent writes in this section and talk about immutable entities in the next section.

We already learned that atomic properties safeguard against inconsistent updates and are overcautious about it. If multiple threads attempt to read a property, the synthesized code allows access to only one thread at a time. Having an atomic property will therefore slow down the app.

This can be a big bottleneck, especially if the state is shared across various components and may need to be accessed from multiple threads. An example of this is a cookie or access token after login. It can change periodically but will be required by all network calls made to the server.

Another use case for such a scenario is the cache. A cache entry can be used anywhere in the app and may be updated upon specific user actions or otherwise.

Essentially, we need a mechanism for concurrent reads but exclusive writes. That brings us to the topic of reader–writer locks. They are also known as multiple readers/single-writer or multireader locks.

A reader–writer lock allows concurrent access for read-only operations, while write operations require exclusive access. This means that multiple threads can read the data in parallel but an exclusive lock is needed to modify the data.

GCD barriers allow creating a synchronization point within a concurrent dispatch queue. When GCD encounters a barrier, the corresponding queue delays the execution of the block until all blocks submitted before the barrier are finished executing. And then, the block submitted via a barrier executes exclusively. We shall call this block a barrier block. Subsequently, the queue continues with its normal execution behavior.

Figure 4-2 demonstrates the effect that barriers have on execution in a multithreaded environment. Blocks 1 through 6 can execute concurrently across multiple threads in the app. However, the barrier block executes exclusively. The only constraint that must be satisfied is that all executions must happen on the same concurrent queue.

hpia 0402
Figure 4-2. Dispatch blocks and barriers

To implement this behavior, we need to follow these steps:

  1. Create a concurrent queue.

  2. Execute all reads using dispatch_sync on this queue.

  3. Execute all writes using dispatch_barrier_sync on the same queue.

You can use the code in Example 4-8 to implement a high-throughput thread-safe model.

Example 4-8. Thread-safe, high-throughput model
//HPCache.h
@interface HPCache

+(HPCache *)sharedInstance;

-(id)objectForKey:(id) key;
-(void)setObject:(id)object forKey:(id)key;

@end

//HPCache.m
@interface HPCache ()

@property (nonatomic, readonly) NSMutableDictionary *cacheObjects;
@property (nonatomic, readonly) dispatch_queue_t queue;

@end

@implementation HPCache

-(instancetype)init {
    if(self = [super init]) {
		_cacheObjects = [NSMutableDictionary dictionary];
		_queue = dispatch_queue_create(kCacheQueueName,
            DISPATCH_QUEUE_CONCURRENT); 1
	}
	return self;
}

+(HPCache *)sharedInstance {
	static HPCache *instance = nil;

	static dispatch_once_t onceToken;
	dispatch_once(&onceToken, ^{
		instance = [[HPCache alloc] init];
	});
	return instance;
}

-(id)objectForKey:(id<NSCopying>)key {
	__block id rv = nil;

	dispatch_sync(self.queue, ^{ 2
		rv = [self.cacheObjects objectForKey:key];
	});

	return rv;
}

-(void)setObject:(id)object forKey:(id<NSCopying>)key {
	dispatch_barrier_async(self.queue, ^{ 3
		[self.cacheObjects setObject:object forKey:key];
	});
}

@end
1

Create a custom DISPATCH_QUEUE_CONCURRENT queue.

2

Use dispatch_sync (or dispatch_async) for operations that do not modify state.

3

Use dispatch_barrier_sync (or dispatch_barrier_async) for operations that may modify state.

Notice that the properties have been marked nonatomic because there is custom code to manage thread safety using a custom queue and barrier.

Use Immutable Entities

This all looks great. But what if there is a need to access state while it is being modified?

For example, what if the cache is being purged but part of the state needs to be used immediately because the user performed an interaction? What if there were a more effective mechanism for state management than multiple components trying to update it simultaneously?

Your team should follow these best practices:

  • Use immutable entities.

  • Support them with an updater subsystem.

  • Allow observers to receive notifications on data changes.

This creates a decoupled, scalable system to manage application state. Let’s go through one of the several possible ways to implement this.

The first step is to clearly define the models. For our case study, we define the following three entities:

HPUser

Represents a user in the system. A user has a unique id, name broken down into firstName and lastName, gender, and dateOfBirth.

HPAlbum

Represents a photo album. A user may have zero or more albums. An album has a unique id, owner, name, creationTime, description, link to coverPhoto (the cover photo of the album), and likes (users that liked the album).

HPPhoto

Represents a photo in an album. An album may have zero or more photos. A photo has a unique id, album to which it belongs, user (the person who uploaded the photo), caption, url, and size (width and height).

Example 4-9 shows the code for the entity definitions.

Example 4-9. Entities for the case study, representing a user, an album, and a photo
@interface HPUser

@property (nonatomic, copy) NSString *userId;
@property (nonatomic, copy) NSString *firstName;
@property (nonatomic, copy) NSString *lastName;
@property (nonatomic, copy) NSString *gender;
@property (nonatomic, copy) NSDate *dateOfBirth;
@property (nonatomic, strong) NSArray *albums;

@end

@class HPPhoto;

@interface HPAlbum

@property (nonatomic, copy) NSString *albumId;
@property (nonatomic, strong) HPUser *owner;
@property (nonatomic, copy) NSString *name;
@property (nonatomic, copy) NSString *description;
@property (nonatomic, copy) NSDate *creationTime;
@property (nonatomic, copy) HPPhoto *coverPhoto;

@end

@interface HPPhoto

@property (nonatomic, copy) NSString *photoId;
@property (nonatomic, strong) HPAlbum *album
@property (nonatomic, strong) HPUser *user;
@property (nonatomic, copy) NSString *caption;
@property (nonatomic, strong) NSURL *url;
@property (nonatomic, copy) CGSize size;

@end

There are multiple ways to define the model and mechanisms to populate the data. Two of the more common options are:

  • Using a custom initializer

  • Using a builder pattern

Each option has its advantages.

Using a custom initializer may mean a long method name, which can result in a nasty call. Think about the method initWithId:firstName:lastName:gender:birthday:. And this is when we have used only a few of the available attributes in our model. The initializer bloats if five more attributes were added.

Custom initializers also pose backward compatibility problems. A newer model with more attributes will never be backward compatible. However, this also ensures that the app using the updated version of the model knows right at compile time that things have changed.

Using a builder means managing an extra class for it. It will only have setter methods. The builder will also need parallel storage (properties or otherwise) to store all the data needed by the model. The builder will, eventually, use an initializer.

Any update to the model will require a corresponding change to the builder and its backing properties.

The builder pattern is preferred, as it enables backward compatibility and does not break the app even if there are more attributes added to the model. The extra attributes in the later versions of the model will continue to have their default values.

Using the second option, the code looks similar to that given in Example 4-10. This is code adapted from Klaas Pieter’s idea of implementing the builder pattern using blocks.

Example 4-10. Immutable entity using builder
//HPUser.h
@interface HPUserBuilder 1

@property (nonatomic, copy) NSString *userId;
@property (nonatomic, copy) NSString *firstName;
@property (nonatomic, copy) NSString *lastName;
@property (nonatomic, copy) NSString *gender;
@property (nonatomic, copy) NSDate *dateOfBirth;
@property (nonatomic, strong) NSArray *albums;

-(HPUser *)build;

@end

@interface HPUser 2

//properties

+(instancetype) userWithBlock:(void (^)(HPUserBuilder *))block;

@end

@interface HPUser () 3

-(instancetype) initWithBuilder:(HPUserBuilder *)builder;

@end


@implementation HPUserBuilder

-(HPUser *) build { 4
    return [[HPUser alloc] initWithBuilder:self];
}

@end

@implementation HPUser
-(instancetype) initWithBuilder:(HPUserBuilder *)builder { 5

    if(self = [super init]) {
        self.userId = builder.userId;
        self.firstName = builder.firstName;
        self.lastName = builder.lastName;
        self.gender = builder.gender;
        self.dateOfBirth = builder.dateOfBirth;
        self.albums = [NSArray arrayWithArray:albums];
    }
    return self;
}

+(instancetype) userWithBlock:(void (^)(HPUserBuilder *))block { 6
    HPUserBuilder *builder = [[HPUserBuilder alloc] init];
    block(builder);
    return [builder build];
}

@end


//Building the object, an example
-(HPUser *) createUser { 7
    HPUser *rv = [HPUser userWithBlock:^(HPUserBuilder *builder) {
        builder.userId = @"id001";
        builder.firstName = @"Alice";
        builder.lastName = @"Darji";
        builder.gender = @"F";

        NSCalendar *cal = [NSCalendar currentCalendar];
        NSDateComponents *components = [[NSDateComponents alloc] init];
        [components setYear:1980];
        [components setMonth:1];
        [components setDay:1];
        builder.dateOfBirth = [cal dateFromComponents:components];

        builder.albums = [NSArray array];
    }];

    return rv;
}
1

The builder.

2

The model with the class method userWithBlock:. Example 4-9 has all the properties declared.

3

Private extension to the model—the custom initializer.

4

Implementation of the build method.

5

Implementation of the custom initializer of the model.

6

Implementation of the userWithBlock: method.

7

A sample use of the builder to create the object.

Note that the preceding code has a few advantages:

  • The model is always backward compatible. A new version of the model-builder with extra attributes will not break the createUser code.

  • The builder can be created directly. The consumer of the model can instantiate the builder and call the build method to create the model object.

  • The builder creation and handling can be left to the core. The consumer of the model can use the class method userWithBlock: and does not need to either instantiate or call the build method by itself.

Have a Central State Updater Service

The next thing that we need an updater service to update is the client state. The updater service may require connecting to the server, validating the update before performing a local update—for example, adding or updating a record, confirming a friend request, or uploading a photo. From the UI perspective, in the interim, you may show a progress bar or some other indicator to keep the user informed about the status of the change of the state.

For our case, let’s have HPUserService, HPAlbumService, and HPPhotoService classes for servicing HPUser, HPAlbum, and HPPhoto objects, respectively.

Updating state is tricky because it is immutable. Paradoxical, isn’t it? One option is to let the state builder take an input state that can be subsequently modified.

To do that for HPUser, we can create a helper initializer on HPUserBuilder that takes an input object.

The code in Example 4-11 shows an updated HPUserBuilder class to support modifications to an earlier created HPUser object, and an HPUserService class to retrieve and update the objects. Similar infrastructure will exist for HPAlbum and HPPhoto entities. This code demonstrates the services for user and album entities for the following two scenarios:

  • Retrieving data from the server resulting in an update to local state

  • Updating local and remote states, for example, upon a user interaction

Example 4-11. Services for user and album objects
//HPUserBuilder.h
@interface HPUserBuilder

-(instancetype) initWithUser:(HPUser *)user;

@end

@interface HPUserBuilder

-(instancetype) initWithUser:(HPUser *)user { 1
    if(self = [super init]) {
        self.userId = builder.userId;
        self.firstName = user.firstName;
        self.lastName = user.lastName;
        self.gender = user.gender;
        self.dateOfBirth = user.dateOfBirth;
        self.albums = user.albums;
    }
    return self;
}

@end

//HPUserService.h
@interface HPUserService

+(instancetype)sharedInstance; 2
-(void)userWithId:(NSString *)id completion:(void (^)(HPUser *))completion;
-(void)updateUser:(HPUser *)user completion:(void (^)(HPUser *))completion;

@end

//HPUserService.m
@interface HPUserService

@property (nonatomic, strong) NSMutableDictionary *userCache; 3

@end

@implementation HPUserService

-(instancetype) init { 4
    if(self = [super init]) {
        self.userCache = [NSMutableDictionary dictionary];
    }
    return self;
}

-(void)userWithId:(NSString *)id completion:(void (^)(HPUser *))completion { 5
    //Check in local cache or fetch from server
    HPUser *user = (HPUser *)[self.userCache objectForKey:id];
    if(user) {
        completion(user);
    }

    [[HPSyncService sharedInstance] fetchType:@"user"
        withId:id completion:^(NSDictionary *data) { 6
        //Use HPUserBuilder, parse data and build
        HPUser *userFromServer = [builder build];
        [self.userCache setObject:userFromServer forKey:userFromServer.userId];
        callback(userFromServer);
    }];
}

-(void)updateUser:(HPUser *)user completion:(void (^)(HPUser *))completion { 7
    //May require update to server
    [[HPSyncService sharedInstance] updateType:@"user"
        //Use HPUserBuilder, parse data and build
        HPUser *updatedUser = [builder build];

        [self.userCache setObject:updatedUser forKey:updatedUser.userId]; 8
        [HPAlbumService updateAlbums:updatedUser.albums]; 9
        completion(updatedUser);
    }];
}

@end
1

HPUserBuilder now has another custom initializer. It takes an HPUser object as a parameter and initializes itself with the values from the user object. The state can be modified using property setters and a new object can finally be built using the build method. Note that although the state has been modified, the old object has not been modified. This also means that if the old object is being used in another entity (e.g., a view controller), it has to be replaced. We will explore state change notifications in the next section.

2

HPUserService follows a singleton pattern here and is available using sharedInstance. The code has been omitted for brevity, but we know how to implement good and safe singletons. It is not advisable to use a singleton entity or service levels, as it results in tight coupling and also interferes with mocking frameworks. A configurable factory is preferred over using singletons. The factory may create a disposable singleton. We will revisit this topic in Chapter 10.

3

As a quick prototype, the service also holds on to the cache of user objects created. However, it is definitely not a good idea to mix state with the cache logic. Always keep the state separate from any other intelligent code. You want to keep the models as dumb as possible.

4

The HPUserService initializer has been overridden to initialize the cache. This is a stopgap solution, as the focus of our discussion is about how immutable objects can serve better than mutable objects whose state can be changed from different parts of the app. In a real-world app, the service object will have access to the state, which can be used as input for any processing or be updated, and to underlying network operations to keep the server in sync.

5

A user with a given id can be retrieved using userWithId:completion:. If the object exists in the local state, it is returned. Otherwise, it may contact the server and retrieve the details. Once ready, the completion callback is used to notify the caller that the object is available.

6

Availability of a sync service, HPSyncService, is assumed here. The service retrieves data from the server. It is also assumed that the server sends a JSON object6 that is deserialized into an NSDictionary. The code for extracting properties and populating the builder has been omitted. Once the data is available, we also update the local cache so that further server trips can be avoided.

7

User state can be updated using the updateUser:completion: method.

8

Updating local state may require syncing changes to the server.

9

Once the server has been notified, the local cache is updated. Because the user object holds albums, the album service is used to update the related albums as well. Specifically, the associated owner object must now point to the updated user object. The old user object must be up for dealloc. Note that the solution presented here is not scalable: what if other entities also need to update themselves? We will fix this problem momentarily.

One of the points to note in the entities is their cross-references. The user has a list of albums, and each album has an owner. Similarly, an album has a list of photos, and each photo has its container album. And we have not even modeled the comments on a photo, which may comprise when the comment was made, the content, and the user who wrote it.

Regardless of whether they are strong or weak, creating immutable objects with such cross-references has been purposefully omitted here. We need the user object to be ready before the album can be created, and vice versa. It is a catch-22 situation.

The way out is to keep the objects mutable unless specifically marked immutable. This is known as popsicle immutability.7 For this, you may have a special method, say, freeze or markImmutable. To be able to use this structure, you will need custom setters that will first check if the object is immutable before allowing any changes.

We can now solve the deadlock. We allow HPAlbum to be modifiable until we set its owner. We create the HPUser object and set the owner of the HPAlbum object. Subsequently, we call the method freeze on the HPAlbum object. After all albums are created, we assign them to albums property of the HPUser object. Finally, we call the method freeze on the HPUser object.

Code to this effect is shown in Example 4-12. HPUser has been updated to have read/write properties and be mutable until it is marked immutable. And guess what—for most common use cases, you will probably never need a builder because the properties are read/write.

Example 4-12. Popsicle-immutable entities
//HPUser.h
@interface HPUser

@property (nonatomic, copy) NSString *userId; 1
@property (nonatomic, copy) NSString *firstName;
-(void) freeze; 2

@end

//HPUser.m
@interface HPUser ()

@property (nonatomic, copy) BOOL frozen; 3

@end

@implementation HPUser

@synthesize userId = _userId; 4
@synthesize firstName = _firstName;

-(void) freeze { 5
    self.frozen = YES;
}

-(void) setUserId:(NSString *)userId { 6
    if(!self.frozen) {
        self->_userId = userId;
    }
}

-(void) setFirstName:(NSString *)firstName {
    if(!self.frozen) {
        self->_firstName = firstName;
    }
}

//... Other setters omitted

@end

//Creating objects
-(HPUser *)sampleUser { 7
    HPUser *user = [[HPUser alloc] init];
    user.userId = @"user-1";
    user.firstName = @"Bob";
    user.lastName = @"Taylor";
    user.gender = @"M";

    HPAlbum *album1 = [[HPAlbum alloc] init];
    album1.owner = user; 8
    album1.name = @"Album 1";
    //... other properties
    [album1 freeze]; 9

    HPAlbum *album2 = [[HPAlbum alloc] init];
    album2.owner = user;
    album2.name = @"Album 2";
    //... other properties
    [album2 freeze]; 10

    user.albums = [NSArray arrayWithObjects:album1, album2, nil];
    [user freeze]; 11

    return user;
}
1

The properties are no longer readonly. They are readwrite (implicit).

2

We add the method freeze, which marks an object immutable. Objects are mutable by default.

3

A flag to track the immutability state of the object.

4

Because we are going write custom setters, we need to @synthesize and tell the compiler about the backing iVar to use.

5

Implementation of the method freeze marks the object immutable.

6

Custom setters. First, check if the object is mutable. If yes, update. If not, do not update. You may want to throw an exception during development time to ensure legitimate invocations and identify any bad code.

7

Sample code to demonstrate the use of the new API.

8

A user is assigned as the album’s owner. At this point, both objects are mutable.

9

HPAlbum object marked immutable.

10

HPUser object marked immutable. Notice how the line just before this can make use of the immutable album objects.

Although the objects may be mutable for a while, we ensure that the mutability is short-lived and restricted only to the thread that created an object. Before the objects are pushed from the creation method to the shared app state, you must ensure that they are marked immutable.

State Observers and Notifications

The previous section left us with an unanswered question: how do we update dependents if an object is updated? Or, put differently, what are the best options to track state changes?

To track changes, you have the following options:

  • KVO

  • The notification center

  • A custom solution

We looked at the first two options briefly in Chapter 2. KVO is great for tracking changes in object properties. But using our approach, this does not work because the objects are immutable and we replace the entire object. As such, the observer will never receive any callbacks.

The notification center is a great option. It serves a useful purpose and would suffice for most of the parts. But the challenge is scaling for the complex scenarios that an app will eventually have—for example, filtering update notifications by album ID or bubbling up the changes to the UI directly if possible.

That’s where a custom solution is needed. And to make that happen, we will switch our style to reactive programming.

Reactive Programming is programming with asynchronous data streams.8 Streams are cheap and ubiquitous, anything can be a stream: variables, user inputs, properties, caches, data structures, etc.

The ReactiveCocoa library enables reactive programming in Objective-C. It not only allows observers on arbitrary state but also has advanced category extensions for bubbling them all the way up to UI elements (UILabel, for example) or responding to interactive views (UIButton, for example).

We will use ReactiveCocoa to notify observers about any model changes. The observers can be created anywhere.

For illustrative purposes, we will add a notification during each user creation and update operation. We will also add an observer in the album service to monitor any changes to the owner (user) and, for completeness, in the UI to monitor changes to the albums list for a user. Example 4-13 shows the relevant parts of the code.

Example 4-13. Observers and notifications
//HPUserService.m
-(RACSignal *)signalForUserWithId:(NSString *)id { 1
    @weakify(self);
    return [RACSignal
        createSignal:^RACDisposable *(id<RACSubscriber> subscriber) { 2
        @strongify(self);
        HPUser *userFromCache = [self.userCache objectForKey:id];
        if(userFromCache) {
            [subscriber sendNext:userFromCache];
            [subscriber sendCompleted];
        } else {
            //Assuming HPSyncService also follows FRP style
            [[[HPSyncService sharedInstance]
                loadType:@"user" withId:id]
                subscribeNext:^(HPUser *userFromServer) {
                    //Also update local cache and notify
                    [subscriber sendNext:userFromServer];
                    [subscriber sendCompleted];
                } error: ^(NSError *error) {
                    [subscriber sendError:error];
                }];
        }

        return nil;
    }]
}

-(RACSignal *)signalForUpdateUser:(HPUser *)user { 3
    @weakify(self);
    return [RACSignal
        createSignal:^RACDisposable *(id<RACSubscriber> subscriber) { 4
        //Update the server
        [[[HPSyncService sharedInstance]
            updateType:@"user" withId:user.userId value:user]
            subscribeNext:^(NSDictionary *data) {
                //Use HPUserBuilder, parse data and build
                HPUser *updatedUser = [builder build];

                @strongify(self);
                var oldUser = [self.userCache objectForKey:updatedUser.userId];
                [self.userCache setObject:updatedUser forKey:updatedUser.userId];
                [subscriber sendNext:updatedUser];
                [subscriber sendCompleted];
                [self notifyCacheUpdatedWithUser:updatedUser old:oldUser]; 5
            } error: ^(NSError *error) {
                [subscriber sendError:error];
            }];
    }];
}

-(void)notifyCacheUpdatedWithUser:(HPUser *)user old:(HPUser *)oldUser { 6
    NSDictionary *tuple = {
        @"old": oldUser,
        @"new": user
    };
    [NSNotificationCenter.defaultCenter
        postNotificationName:@"userUpdated" object:tuple]; 7
}

-(RACSignal *)signalForUserUpdates:(id)object { 8
    return [[NSNotificationCenter.defaultCenter
        rac_addObserverForName:@"userUpdated" object:object] 9
        flattenMap:^(NSNotification *note) {
            return note.object;
        }];
}

//At some other place in app
-(void)retrieveAUser:(NSString *)userId { 10
    [[[HPUserService sharedInstance]
        signalForUserWithId:userId]
        subscribeNext:^(HPUser *user) { 11
            //process user, maybe update UI
        } error:^(NSError *) {
            //show error to user
        }];
}

-(void)updateAUser:(HPUser *)user { 12
    [[[HPUserService sharedInstance]
        signalForUpdateUser:user]
        subscribeNext:^(HPUser *user) { 13
            //process user, maybe update UI
        } error:^(NSError *) {
            //show error to user
        }];
}

//Listening for user updates
-watchForUserUpdates { 14
    [[[HPUserService sharedInstance]
        signalForUserUpdates:self] 15
        subcribeNext:^(NSDictionary *tuple) { 16
            //Do something with the values
            HPUser *oldUser objectForKey:@"old";
            HPUser *newUser objectForKey:@"new";
    }];
}
1

The method signalForUserWithId does not take in a block as a parameter but returns a promise that can be chained. The @weakify and @strongify macros that were first introduced in “Best Practices” have been used here.

2

The code for the signal is pretty much the same as the original code in userWithId: but this time using RACSubscriber and a promise.

It is assumed that the method loadType:withId in the class HPSyncService also returns a promise, an RACSignal.

3

The method signalForUpdateUser: updates an HPUser object.

4

This creates the RACSignal.

5

When the user is updated, you need to not only inform the immediate subscriber, but also notify observers about updates to the cache.

6

notifyCacheUpdatedWithUser:old: broadcasts about user object changes.

7

NSNotificationCenter has been used here for simplicity. This method may not be exposed to the HPUserService users. It is an extension method.

8

The method published (in the HPUserService.h file) is signalForUserUpdates:.

9

It uses the rac_addObserverForName category extension provided by the ReactiveCocoa framework to subscribe to userUpdated notifications. It also extracts the actual NSDictionary, comprised of the old and new user objects from the underlying NSNotification object.

10

The retrieveAUser: method demonstrates sample code to retrieve a user.

11

The subscribeNext: block is where the user object is received.

12

The updateAUser: method demonstrates sample code to update a user.

13

The subscribeNext: block is where the user object is received.

14

The watchForUserUpdates: method shows sample code to watch for changes in the user cache.

15

It uses the method signalForUserUpdates: to listen to notifications about changes to the user cache.

16

The subscribeNext: block is given the NSDictionary of old and new objects.

The advantage is that if in the future the implementation of signalForUserUpdates: changes to not use NSNotificationCenter, it will not result in changes all the way up to watchForUserUpdates:.

The primary motive for using this library is that it already has what we need to implement a decoupled, scalable, self-contained, general-purpose system for observing for changes. More importantly, it provides promises for chaining (using RACSignal) that allow us to write code in a style that is more understandable and maintainable. It also provides simpler solutions for interacting with the UI elements—something that we will use as we continue to build on these concepts in upcoming chapters. In a nutshell, it provides a lot of boilerplate code that we would otherwise have had to write ourselves, and a lot more.

Prefer Async over Sync

In the previous section, we learned that we should prefer promises. This section provides some deeper discussion of asynchronous code.

There is a big and more impactful reason to always prefer async over sync. And it has to do with synchronization. In “Use Reader–Writer Locks for Concurrent Reads and Writes”, we discussed using dispatch barriers and learned about how dispatch_sync can be used for concurrent reads.

Let’s briefly analyze the code in Example 4-14.

Example 4-14. Using dispatch-sync in the real world
//Case A
dispatch_sync(queue, ^() {
    dispatch_sync(queue, ^() {
        NSLog(@"nested sync call");
    });
});

//Case B
-(void) methodA1 {
    dispatch_sync(queue1, ^() {
        [objB methodB];
    });
}

-(void)methodA2 {
    dispatch_sync(queue1, ^() {
        NSLog(@"indirect nested dispatch_sync");
    });
}

-(void) methodB {
    [objA methodA2];
}

In Example 4-14, Case A demonstrates a hypothetical scenario in which a nested dispatch_sync is invoked using the same dispatch queue. This results in a deadlock. The nested dispatch_sync cannot dispatch into the queue because the current thread is already on the queue and will not release the lock.

Case B demonstrates a more likely scenario. A class has two methods (methodA1 and methodA2) that use the same queue. The former method calls a methodB on some object, which in turns calls the latter. The end result is a deadlock. The otherwise useful method dispatch_get_current_queue has long since been deprecated.11

One option is to use the dispatch_queue_set_specific and dispatch_get_specific methods, but you will realize that the code gets murky pretty soon.

For thread-safe, deadlock-free, and maintainable code, using an async style is highly recommended. And there is nothing better than using promises. ReactiveCocoa (see “Functional Reactive Programming and ReactiveCocoa”) introduces the FRP style in Objective-C. dispatch_async does not suffer from this behavior.

Summary

It is impossible to envision any app without concurrent programming. Operations as simple as animation require multitasking. All long-running tasks (such as networking and I/O) must always be done in a background thread.

With an in-depth analysis on various available options (namely threads, GCD, and operations and queues) in hand, you should now be able to select the one that works best in your specific scenario.

Choosing the right option to make your code thread-safe is key to the correctness of the app’s state. Using mutexes to synchronize access to code blocks is as important as creating high-throughput reads with protected writes using reader–writer locks.

Now that you are familiar with the core optimization techniques for memory management, energy use, and concurrent programming discussed in this part of the book, you should be able to optimize the model and business logic layers of your app.

With Safari, you learn the way you learn best. Get unlimited access to videos, live online training, learning paths, books, interactive tutorials, and more.

Start Free Trial

No credit card required