Memory management in Swift is based on automatic reference counting (ARC), which means that an object exists in memory as long as there is at least one strong reference to it. After that, ARC initiates the object deallocation mechanism, depending on the number of existing weak and unowned object references, the deallocation mechanism will be different.
However, in addition to ARC, Swift also supports manual memory management. In this article, I will tell you what are the ways to work with memory that provide create/read/update/delete (CRUD) operations and much more.
Manual memory management can be implemented based on pointers. Pointer types vary depending on the need for unsafe memory access.
The data type is:
Pointers to a piece of memory without an explicit type. Returns the number of bytes. Usually contains Raw
in the name;
Pointers with an explicit type are specified during initialization as a generic parameter. Does not contain Raw
in the name;
Variability is distinguished by:
Pointers to a memory area with the possibility of changing it. The name contains Mutable
;
Pointers to a piece of memory without the possibility of changing it. The name does not contain Mutable
;
The number of elements is distinguished by:
Buffer
;Buffer
;
In total, all possible combinations of pointers look like this:
UnsafePointer<T>
;UnsafeMutablePointer<T>
;UnsafeBufferPointer<T>
;UnsafeMutableBufferPointer<T>
;UnsafeRawPointer
;UnsafeMutableRawPointer
;UnsafeRawBufferPointer
;UnsafeMutableRawBufferPointer
;
Let’s consider the creation of objects in the example of UnsafePointer.
var x: Int = 10
let unsafePointer = UnsafePointer<Int>(&x)
Because UnsafePointer
is an immutable pointer, it can only be initialized by passing it an already initialized object directly.
You can get information about the memory area stored at a given pointer as follows:
unsafePointer.pointee // printed 10
Consider the creation of objects in the example of UnsafeMutablePointer
. Unlike UnsafePointer
, this pointer can be initialized before information is written to the memory area.
let size = MemoryLayout<Int>.size
let unsafeMutablePointer = UnsafeMutablePointer<Int>.allocate(capacity: size)
unsafeMutablePointer.pointee = 5
Now, through the pointee property, you can read and write the allocated memory area. Since we are working with the Int
data type, the capacity area was chosen taking into account the required size of the MemoryLayout
.
You can deallocate a memory area and a pointer to it as follows:
unsafeMutablePointer.deallocate()
unsafeMutablePointer.deinitialize(count: 1)
Consider the creation of elements in the example of UnsafeMutableRawPointer
. Since this pointer is mutable and without explicit typing, it is enough to allocate a memory area for an object and then write data to it. In this case, all operations for this pointer occur byte by byte without a specific data type.
let unsafeMutableRawPointer = UnsafeMutableRawPointer.allocate(byteCount: size, alignment: alignment) // 6000006E44F0
unsafeMutableRawPointer.storeBytes(of: 32, as: Int.self) // 6000006E44F0
unsafeMutableRawPointer.load(as: Int.self) // printed 32
The load
method allows you to read a memory area with a given data type. The storeBytes
method allows you to write to the allocated memory area. At the same time, because of the lack of binding to a specific data type, you can easily put and read data with a completely different type into the allocated area:
unsafeMutableRawPointer.initializeMemory(as: String.self, to: "123") // 6000006E44F0
unsafeMutableRawPointer.load(as: String.self) // printed “123”
unsafeMutableRawPointer.deallocate()
The address 6000006E44F0
was the number 32 with the data type Int
, but we rewrote it to the string "123"
, hello Python.
In addition to standard CRUD operations, pointers also support copying memory from one address to another.
let size = MemoryLayout<Int>.size
let alignment = MemoryLayout<Int>.alignment
let unsafeMutableRawPointer1 = UnsafeMutableRawPointer.allocate(byteCount: size, alignment: alignment)
unsafeMutableRawPointer1.storeBytes(of: 32, as: Int.self) // 32
let unsafeMutableRawPointer2 = UnsafeMutableRawPointer.allocate(byteCount: size, alignment: alignment)
unsafeMutableRawPointer2.storeBytes(of: 40, as: Int.self) // 40
unsafeMutableRawPointer2.copyMemory(from: unsafeMutableRawPointer1, byteCount: size)
unsafeMutableRawPointer2.load(as: Int.self) // 32
Two pointers were created for different memory locations containing the numbers 32 and 40. Thanks to copyMemory
, we were able to copy information from one memory location to another. At the same time, the use of the copyMemory
method allows one-time copying, preserving the further independence of memory sections with different addresses.
It is also possible to bind two different pointers:
let unsafeMutableRawPointer3 = UnsafeMutableRawPointer.allocate(byteCount: size, alignment: alignment)
unsafeMutableRawPointer3.storeBytes(of: 32, as: Int.self)
let unsafeMutableRawPointer4 = unsafeMutableRawPointer3.bindMemory(to: Int.self, capacity: size)
unsafeMutableRawPointer4.pointee // 32
unsafeMutableRawPointer3.storeBytes(of: 40, as: Int.self)
unsafeMutableRawPointer4.pointee // 40
Thanks to bindMemory
, both pointers point to the same memory location and will catch all changes, regardless of which pointer they are written to.
To allocate an area of memory for an array using UnsafeMutableBufferPointer
, the area for each element of the array will be allocated first:
let array: [Int] = [5, 6, 7, 8, 9]
let elementPointer = UnsafeMutablePointer<Int>.allocate(capacity: array.count)
let arrayPointer = UnsafeMutableBufferPointer(start: elementPointer, count: array.count)
Let’s fill in the previously allocated area. The offset between memory locations of different elements will be provided by the advanced(by: Int)
method.
for (index, value) in array.enumerated() {
elementPointer.advanced(by: index).pointee = value
}
In addition to writing, the advanced(by: Int)
method also helps when reading a specific array element by ordinal index:
elementPointer.advanced(by: 4).pointee // 9
UnsafeMutableBufferPointer
supports subscript[index]
to read and write array elements by ordinal index:
arrayPointer[4] = 5
elementPointer.advanced(by: 4).pointee // 5
arrayPointer.deallocate()
On this, the article comes to an end. I will talk about other types of manual memory management, such as Unmanaged objects
, in the next article.
Don’t hesitate to contact me on
Also published here.