Swift. ARC

Maxim Krylov
6 min readJul 12, 2020

Here is a quick introduction, along with the full articles list

Automatic Reference Counting (ARC) is a compile-time mechanism that takes over memory management. This mechanism works only for objects that are stored on the heap (reference types)

Before ARC appeared, developers had to manage memory and objects lifetime manually. For these purposes, they used the following functions: retain, release, and autorelease

retain is an instance method that makes reference count of the instance + 1

release is an instance method that makes reference count of the instance — 1. When the count reaches 0, it calls the instance method dealloc to dispose of the instance and free up memory on the heap

autorelease is an instance method that adds the instance to an autorelease pool for having a deferred release call

Today, when code is being compiled, ARC inserts retain, release, and autorelease calls to the code automatically. And, roughly speaking, that is all that ARC does. However, it is smart enough to make it okay, avoiding unnecessary insertions. The code remains quite efficient. So, there is no need to think about manual reference management anymore

The critical point here is that ARC is namely a compile-time mechanism. So, it doesn’t manage memory at runtime or do some magic with the heap there

class Person {
public let name: String
init(name: String) {
self.name = name
print("person is initialized")
}
deinit {
print("person is reinitialized")
}
}
var person1: Person?
var person2: Person?
var person3: Person?
person1 = Person(name: "Maxim")
// person is initialized
// retain(Person(Maxim)), 1 strong reference
person2 = person1
// retain(Person(Maxim)), 2 strong references
person3 = person1
// retain(Person(Maxim)), 3 strong references
person1 = nil
// release(Person(Maxim)), 2 strong references
person2 = nil
// release(Person(Maxim)), 1 strong reference
person3 = nil
// release(Person(Maxim)), 0 strong references
// person is deinitialized

Autorelease Pool

An autorelease pool is an instance of NSAutoreleasePool

It is a mechanism for having deferred release behavior. A pool itself is placed on the stack. It contains the objects, which were added by autorelease method calls. When a pool is being released (removed from the stack), it calls the release method of each item inside (sends a release message to all of them)

Each function has its pool. When a function is called, a pool is created and put onto the top of the stack. Each object created inside the function, which got an autorelease message, is added to the pool. When the function ends, the pool is being released

However, if another pool is manually created inside the function, the objects will be added there because now that pool is placed onto the top of the stack. So, objects are added into the last created pool

In general, there is no need to create pools manually. E.g., UIKit and AppKit create a pool automatically when a touch event starts and release it when the event ends (run loop iteration)

However, it can be useful to create a pool manually to prevent big memory surges in cycles. E.g., heavy objects that are created on each cycle iteration should be released at the end of the iteration

// some function bodyfor _ in 0..<10 {
autoreleasepool {
// UIImage inherits NSObject (Objective-C)
let image = UIImage(contentsOfFile: "image.jpg") // heavy
}
}

Let’s imagine that each image takes ~20Mb in memory. So, without autoreleasepool it will take ~200Mb by the end of the function. But now, it takes ~20Mb within the whole function call. Thus, no significant memory surges there

However, it works only for Objective-C objects (NSObject) that got an autorelease message. For objects that come from Swift, it doesn’t work anyway

Strong Reference Cycles Between Class Instances

// Problemclass Person {
let name: String
init(name: String) { self.name = name }
var apartment: Apartment? // strong reference
deinit { print("person is deinitialized") }
}
// An apartment may not always have a tenant
class Apartment {
let unit: String
init(unit: String) { self.unit = unit }
var tenant: Person? // strong reference
deinit { print("apartment is deinitialized") }
}
var person: Person?
var apartment: Apartment?
person = Person(name: "Maxim")
apartment = Apartment(unit: "1408")
person!.apartment = apartment // the person refers to the apartment
apartment!.person = person // the apartment refers to the person
person = nil
apartment = nil
// no deinit called
// resources will be never deallocated

A weak reference is used when the object’s lifetime is shorter than its holder. It can store only optional values. weak tells ARC not to insert retain for the object (not to make one more strong reference)

Note: Property observers aren’t called when ARC sets a weak reference to nil

// Solution (using weak)class Person {
let name: String
init(name: String) { self.name = name }
var apartment: Apartment? // strong reference
deinit { print("person is deinitialized") }
}
// An apartment may not always have tenant
// Tenant lifetime is shorter than apartment one
class Apartment {
let unit: String
init(unit: String) { self.unit = unit }
weak
var tenant: Person?
deinit { print("apartment is deinitialized") }
}
var person: Person?
var apartment: Apartment?
person = Person(name: "Maxim")
apartment = Apartment(unit: "1408")
person!.apartment = apartment // the person refers to apartment
apartment!.person = person // the apartment refers to person
person = nil
apartment = nil
// the person is deinitialized
// the apartment is deinitialized
// resources are deallocated

An unowned reference is used when the object’s lifetime is the same or longer than its holder. It can store optional and non-optional values. unowned also tells ARC not to insert retain for the object (not to make one more strong reference)

ARC can make it nil, and if you try to get the value, then you will have the runtime error

// Solution (using unowned)class Person {
let name: String
init(name: String) { self.name = name }
var card: CreditCard?
deinit { print("person is deinitialized") }
}
// A credit card must always have customer
// A customer lifetime is the same as credit card one
class CreditCard {
let number: Int
unowned let customer: Person
init(number: Int, customer: Person) {
self.number = number
self.customer = customer
}
deinit { print("card is reinitialized") }
}
var person: Person?person = Person(name: "Maxim")
person!.card = CreditCard(number: 12345, customer: person!)
person = nil
// the person is deinitialized
// the card is deinitialized
// resources are deallocated

weak and unowned can also be applied to variables

weak var person = Person()
unowned var card = CreditCard()

The difference between unowned and unowned(unsafe) (unowned is safe by default) is that Swift for unowned checks at runtime whether the object is alive. So, in this case, it works like implicit unwrapping (!). And if the object doesn’t exist (it is nil), Swift throws an error when trying to get the object value

But when unowned(unsafe) is used, Swift doesn’t verify that the object is still alive, and it can get access to other data stored in this memory region. Thus, in this case, you can get an undefined state. But using unowned(unsafe) can increase performance

The best practice is to avoid using unowned at all because the code anyway becomes unsafe. The best practice is to use weak always and check explicitly that the object is alive

ARC is not a garbage collector. Unlike standard garbage collectors, where the resources are deallocated when the app needs memory, objects in Swift are deallocated once the last strong reference gets nil (release calls dealloc)

Strong Reference Cycles for Closures

A strong reference cycle can also occur if you assign a closure to a property of a class instance, and the body of the closure captures the instance

// Problemclass HTMLElement {
let name: String
let text: String?
lazy var asHTML: () -> String = {
if let text = self.text {
return "<\(self.name)>\(text)</\(self.name)>"
} else {
return "<\(self.name)/>"
}
}
init(name: String, text: String? = nil) {
self.name = name
self.text = text
}
deinit {
print("element is deinitialized")
}
}
var element: HTMLElement? = HTMLElement(
name: "p", text: "something"
)
print(element?.asHTML ?? "")
element = nil
// no deinit called
// resources will be never deallocated

There is a capture list that defines the rules to capture objects within the closure body

// ... [unowned self, weak delegate = self.delegate, someVariable]
// Solutionclass HTMLElement {
let name: String
let text: String?
lazy var asHTML: () -> String = { [weak self] in
if let text = self?.text {
return "<\(self.name)>\(text)</\(self.name)>"
} else {
return "<\(self.name)/>"
}
}
init(name: String, text: String? = nil) {
self.name = name
self.text = text
}
deinit {
print("element is deinitialized")
}
}
var element: HTMLElement? = HTMLElement(
name: "p", text: "something"
)
print(element?.asHTML ?? "")
element = nil
// the element is deinitialized
// resources are deallocated

Resources

--

--