Introduction to Swift

Apr 24 2024 · Swift 5.10, iOS 17, Xcode 15

Lesson 03: Classes & Structures

Memory Demo

Episode complete

Play next episode

Next
Transcript

You’ve learned about initializers and the init function, which prepares the objects inside a struct or a class with their initial values. In this demo, you’ll learn about the deinit function for reference types which is responsible for the de-initialization of the object and cleaning it up when it’s being cleaned from memory. Only classes have a deinit; structs do not. Later in the demo, you’ll learn about the different sizes of data types and how much memory they allocate when you create instances from them.

Open Xcode on your Mac and create a new playground. Start by creating a class type named ExampleClass.

class ExampleClass {

}

You don’t need to add any variables to it. To monitor its life cycle, create a constructor that prints a message to say the instance is created:

init() {
  print("Instance Created")
}

Just like init, there is a deinit function that executes when the instance is getting deleted from memory. It allows you to do some cleanup if you need to. Add this function inside the class right after init:

deinit {
  print("Instance Deleted")
}

Remember that constructors can receive parameters, as you’ve seen in the previous demo. But deinit doesn’t have the round brackets because it can’t receive parameters and you never call it manually. Now, to see how your code works, create a code block. Add some print statements in the beginning to make it easy to track progress:

do {
  print("Start Scope - 1")
}
print("End Scope - 1")

Run the playground and look at the printed messages.

Create an instance of ExampleClass inside the do statement.

var variable1 = ExampleClass()

Run the playground. The value gets created in the scope and as the scope ends, deinit is called right before the print statement.

This is a straightforward example for allocation and deallocation of a reference type inside a scope.

There’s a case when an object doesn’t get removed from memory even when the scope ends. When you create an instance and return it at the end of the function, the object stays in memory and its life is owned by the variable that received the function’s return.

Add this code to create a function that returns an instance of ExampleClass:

func getInstance() -> ExampleClass {
  print("getInstance() called")
  return ExampleClass()
}

This function prints a message before returning the instance, so you can track the order in the console when you run the playground.

Next, add this code to create a new scope:

do {
  print("Start Scope - 2")
  var variable1 = getInstance()
  print("Function call finished")
}
print("End Scope - 2")

This matches the first scope example. Run the playground. Notice that the deletion message is after “Function call finished”. This means the function finished and the object was still alive in memory. It was deleted when the scope ended.

The last example covers a scope in a scope. Add the following to your playground:

do {
  print("Start Scope - 3")
  var variable1 = ExampleClass()
  do {
    print("Start Scope - 3 : Inner Scope - 1 \(variable1)")
    variable1 = ExampleClass()
  }
  print("End Scope - 3 : Inner Scope - 1")
}
print("End Scope - 3")

You might wonder about the string interpolation of \(variable1). This is just so the playground doesn’t warn you that Variable 'variable1' was written to, but never read. You don’t need to read the variable for this demo, but the playground doesn’t know that.

The first scope creates a variable with an instance of ExampleClass, then the inner scope changes its value with another instance.

Run the playground.

Notice that inside the inner scope, an instance is created and right after, an instance is deleted. This is because when variable1 is set to the new value, the reference count on the previous value becomes zero, so the instance is removed from memory. The second value doesn’t get removed by the end of the inner scope because the variable belongs to the outer scope and thus remains alive.

Remember that it’s not about where the instance is created, it’s about the variable holding it. An instance gets deleted when all the variables using it are deleted, not when the scope where you create them ends.

In the next part of the demo, you’ll measure the number of bytes that data types reserve in memory when you create an instance of them.

Add this Swift code to your playground:

let boolValue = false
MemoryLayout.size(ofValue: boolValue)

You create a value of type Bool; it doesn’t matter what initial value you give it. Then, you use the function size(ofValue:) in the type MemoryLayout. The function gives the number of bytes allocated for the data type of the variable provided. In this case, it’s Bool.

Run the playground and look at the right pane of its window.

The size for Bool is one. It needs a single byte in memory for storage.

Add the same code for Int, Float, and Double:

let intValue: Int = 10
MemoryLayout.size(ofValue: intValue)

let floatValue: Float = 10
MemoryLayout.size(ofValue: floatValue)

let doubleValue: Double = 10
MemoryLayout.size(ofValue: doubleValue)

Run the playground.

The number for Int is eight, Float is four, and Double is eight.

There are more versions of Int that have a different memory requirement: Int8, Int16, Int32, and Int64.

Add similar code as before but for those versions of Int:

let int8Value: Int8 = 10
MemoryLayout.size(ofValue: int8Value)

let int16Value: Int16 = 10
MemoryLayout.size(ofValue: int16Value)

let int32Value: Int32 = 10
MemoryLayout.size(ofValue: int32Value)

let int64Value: Int64 = 10
MemoryLayout.size(ofValue: int64Value)

Run the playground to see the results on the right pane.

The memory requirements are one for Int8, two for Int16, four for Int32, and eight for Int64.

Notice that Int64 has the same memory requirement as the regular Int. This actually depends on the CPU architecture you have. Older iPhones and computers had 32-bit processors. One byte is 8 bits. So 32 bits equals 4 bytes. Today’s devices have processors that handle 64 bits, which equals 8 bytes.

Int uses the device architecture for its memory size. When both 32-bit and 64-bit devices were common and you created an app to work on both, it was sometimes important to ensure your code was exactly the same on both kinds of devices. Leaving it to the device to decide how much memory to allocate or which type to use caused problems. That’s why those explicit Int types are defined in Swift.

An Int8 can only hold a number that is in the hundreds. The largest value an Int16 can hold is in the tens of thousands. Int64 can hold numbers in the quintillions, 19 digits. If you’re ever in a situation where you’re developing for a device that has a tiny amount of memory, like a temperature sensor, it’s important to choose variable sizes to match the values you expect them to hold. In normal, day-to-day apps, you can just use Int; there’s plenty of room on a modern iOS device.

So how much memory would your own data types that you define in your code require? Add those two structs to your playground:

struct TwoIntsStruct {
  var intValue1: Int = 0
  var intValue2: Int = 0
}

struct FourIntsStruct {
  var intValue1: Int = 0
  var intValue2: Int = 0
  var intValue3: Int = 0
  var intValue4: Int = 0
}

The first, as its name describes, is a struct with two Int variables, and the second is with four Ints.

Add this to the playground:

MemoryLayout.size(ofValue: TwoIntsStruct())
MemoryLayout.size(ofValue: FourIntsStruct())

Run the playground. TwoIntsStruct gives a value of 16 and FourIntsStruct gives 32. Each gives a value equal to the sum of what’s inside them. But how much memory would a struct with no variables take? Add an empty struct and check its memory size:

struct EmptyStruct {}

MemoryLayout.size(ofValue: EmptyStruct())

Run the playground. As you’d expect, it doesn’t need any memory. It gives a value of zero. Now that you have a good idea how value types define their memory capacity, how does it work for reference types? Create similar types to TwoIntsStruct and FourIntsStruct but as classes:

class TwoIntsClass {
  var intValue1: Int = 0
  var intValue2: Int = 0
}

class FourIntsClass {
  var intValue1: Int = 0
  var intValue2: Int = 0
  var intValue3: Int = 0
  var intValue4: Int = 0
}

MemoryLayout.size(ofValue: TwoIntsClass())
MemoryLayout.size(ofValue: FourIntsClass())

Run the Playground. Both the types give a value of eight.

For reference types, the variable itself is the same size as Int. The contents of the variable is the address of a memory location where the actual value is stored. It doesn’t matter how much memory the value itself is using, the reference is always eight for a 64-bit CPU, or four for a 32-bit CPU.

Try the same for a class with no variables:

class EmptyClass {}
MemoryLayout.size(ofValue: EmptyClass())

Run the playground. EmptyClass gives a value of eight just like the others.

See forum comments
Cinema mode Download course materials from Github
Previous: Memory Next: Conclusion