Swift. ARC
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 referenceperson2 = person1
// retain(Person(Maxim)), 2 strong referencesperson3 = person1
// retain(Person(Maxim)), 3 strong referencesperson1 = nil
// release(Person(Maxim)), 2 strong referencesperson2 = nil
// release(Person(Maxim)), 1 strong referenceperson3 = 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 personperson = 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 personperson = 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
- https://docs.swift.org/swift-book/LanguageGuide/AutomaticReferenceCounting.html
- https://stackoverflow.com/questions/26553924/what-is-the-difference-in-swift-between-unownedsafe-and-unownedunsafe
- https://clang.llvm.org/docs/AutomaticReferenceCounting.html
- https://enterra.ru/blog/garbage_collector_in_ios/
- https://habr.com/ru/post/209288/
- http://macbug.ru/cocoa/memarc
- http://macbug.ru/cocoa/mempool
- https://proswift.ru/pamyat-i-autoreleasepool-dlya-ciklov