Memory Management in Swift: Pointers, Object Creation, Copying, Binding, and Collections

Written by bugorbn | Published 2024/01/15
Tech Story Tags: programming | ios-development | ios-app-development | swift | ios-apps | swift-programming | ios-programming | mobile-app-development

TLDRvia the TL;DR App

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.

1. Pointers

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:

  • Pointers that operate on an array of elements. The name contains Buffer;
  • Pointers that operate on a single element. The name does not contain Buffer;

In total, all possible combinations of pointers look like this:

  • UnsafePointer<T>;
  • UnsafeMutablePointer<T>;
  • UnsafeBufferPointer<T>;
  • UnsafeMutableBufferPointer<T>;
  • UnsafeRawPointer;
  • UnsafeMutableRawPointer;
  • UnsafeRawBufferPointer;
  • UnsafeMutableRawBufferPointer;

2. Creating objects

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.

3. Copying

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.

4. Binding

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.

5. Collections

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 Twitter if you have any questions. Also, you can always buy me a coffee.


Also published here.


Written by bugorbn | Senior iOS Developer | Founder of Flow: Trip & Travel Tracker | #ObjC | #Swift | #UIKit | #SwiftUI
Published by HackerNoon on 2024/01/15