February 15, 2022 - 6 min
iOS Threads and Memory Management
Let’s start with threads. If you ever tried running an app from Xcode, you heard about the Main thread. So, what are threads, how many are there, and how do you use them?
A thread is a single sequential flow of control within a program, which means that each process has at least one thread. In iOS, the primary thread on which the process is started is commonly referred to as the Main thread.
This is the thread in which all UI elements are created and managed. All interrupts related to user interaction are ultimately dispatched to the UI thread where the handler code is written — your IBAction methods are all executed in the main thread. The real thing is knowing how to use multiple threads simultaneously, called concurrent programming. This is a fancy way of saying running numerous tasks simultaneously, but I will get to that a bit later.
The first question that pops in my mind is: Can’t you just run multiple tasks on a single thread? Sure, why not, but that is not always a good solution. Small tasks in small numbers can be run together because of today’s hardware. However, when it comes to complicated and more complex tasks, you should use background threads and see that fancy concurrent programming in action.
If you run two tasks simultaneously on the main thread, they would be done using the “criss-cross” technique. Meaning that the thread would complete part of the first task, then part of the second, then back to the first, and again, the second one, until one of them is completed.
Here is an example that should help you understand and visualize the tasks being completed:
You have two mathematical operations (tasks) to complete. The first one will be 1+2+3, and the second one 4+5+6. Let’s say that a thread is the math teacher that will show us the result of these two operations. The thread will begin with the first task 1+2+3 and complete the first part, 1+2. Then, it will solve the first part of the second task, 4+5. Right after solving the first part of the second task, it will go back to the first one and solve the second part, which is now 3+3 ([1+2 = 3, -> 3+3]). The first task is completed, and the thread resolves the second task.
Here is a visualization. I tried my best!
How do you use concurrent programming? It’s pretty simple. You can use two threads instead of just one. Here is a simple example in code:
import UIKit
class ViewController: UIViewController {
override func viewDidLoad()
super.viewDidLoad()
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
DispatchQueue.global(qos: .userInitiated).async {
let result1 = self.performTask1()
print("This is run on a background queue and the result is \(result1)")
DispatchQueue.main.async {
let result2 = self.performTask2()
print("This is run on the main queue, after the previous code in outer block and the result is \(result2)")
}
}
}
func performTask1() -> Int {
return 1+2+3
}
func performTask2() -> Int {
return 4+5+6
}
}
Here you can use the background thread as another math teacher that solves one mathematical operation, while the second teacher, your Main thread, solves the other one. Here is the visualization:
In this example, you can use the background thread to solve a simple mathematical operation. Background threads are usually used when you have to download an image, make an API call or perform a task that will take some time. The question is, how do you know when the task is completed? Well, Swift provides us with a powerful tool called closures.
Closures or completion handlers are a chunk of code that provides further instructions on what to do after the initial task is done. Most likely, you have seen this while presenting another controller on top of the current one:
self.present(vc, animated: true, completion:(() -> Void)?)
For example, after presenting the new view controller, you can print something in the log:
self.present(vc, animated: true) {
print("New view presented")
}
The same goes for background threads. After the API call is completed, you can use the closure to process further the data you obtained, and keep in mind that you should always update the UI from the main thread. Here is an example of downloading an image:
import UIKitclass ViewController: UIViewController { let imageView: UIImageView = UIImageView() override func viewDidLoad() {
super.viewDidLoad()
} override func viewDidAppear(_ animated: Bool) {
print("<strong>Begin of code</strong>")
let url = URL(string: "https://image.shutterstock.com/image-vector/example-red-square-grunge-stamp-600w-327662909.jpg")!
downloadImage(from: url)
print("<strong>End of code. The image will continue downloading in the background and it will be loaded when it ends.</strong>")
} func getData(from url: URL, completion: @escaping (Data?, URLResponse?, Error?) -> ()) {
URLSession.shared.dataTask(with: url, completionHandler: completion).resume()
} func downloadImage(from url: URL) {
print("<strong>Download Started</strong>")
getData(from: url) { data, response, error in
guard let data = data, error == nil else { return }
print(response?.suggestedFilename ?? url.lastPathComponent)
print("<strong>Download Finished</strong>")
<strong>// Always update the UI from the main thread!
</strong>DispatchQueue.main.async() { [weak self] in
self?.imageView.image = UIImage(data: data)
}
}
}
}
Memory management
At first, there was no ARC back in the Objective C days. I used to retain and release objects by ourselves. Nowadays, ARC (Automatic Reference Counting) does this automatically. Xcode takes care of the job in compile time.
Automatic Reference Counting manages object life cycles by keeping track of all valid references to an object with an internal retain count. Once all objects’ references go out of scope or are cleared, and the retain count thus reaches zero, the object and its underlying memory are automatically freed.
If you free or overwrite the data that is still in use, you can cause memory corruption which usually results in your application crashing or, worse, corrupted user data.
When allocated memory is not freed even though it will never be used again, it is known as a memory leak. Leaks cause your application to use ever-increasing amounts of memory, which in turn may result in poor system performance or your application being terminated.
For example, if you for some reason load a video of 200Mb size on one screen, and right after that continue to the second screen without freeing the allocated memory for that video, and then come back to the first screen, again allocating new 2000Mb for the video, you will populate too much memory After doing this a few more times, your app will crash.
To avoid doing these mistakes, here are three awesome rules that you can follow:
- You own the objects you create, and you have to subsequently release them when they are no longer needed.
- Use Retain to gain ownership of an object that you did not create. You have to release these objects too when they are not needed.
- Don’t release the objects that you don’t own.
So as mentioned before, you don’t need to use release and retain in ARC. So, all the view controller’s objects will be released when the view controller is removed. Similarly, all object’s sub-objects will be released when they are released.
However, keep in mind that if other classes have a solid reference to an object of a class, then the whole class won’t be released. So, it is recommended to always use weak properties for delegates.
Xcode provides some tools to help with memory management. To use, you should select the Debug Navigator on the left Toolbar. You should see the Memory window being the second.
If you wish to use more advanced tools, you can open the Instruments app with the Profile in Instruments in the top right corner.
Bonus — Singleton objects
I’m pretty sure that you heard about the singleton objects. Singleton object is a shared resource within a single unique class instance. So, for example, let’s say that you have an object containing some strings (I will call this object Words) used to populate some labels.
You can either create a single instance of the Words object and use it through the app or create a new instance every time you need to populate the label.
Here is an example of using the singleton object:
First, you will create an object, Words, containing the strings that you need to populate the labels.
import Foundationclass Words: NSObject { var firstWord = "Church"
var secondWord = "Palace"
var thirdWord = "Castle"
static let shared = Words()}
Simple, right? Notice that there is a static let shared which you will use. That is your singleton object. Now back to your ViewController that contains three labels. Here is what it should look like:
import UIKitclass ViewController: UIViewController { var firstLabel = UILabel()
var secondLabel = UILabel()
var thirdLabel = UILabel() override func viewDidLoad() {
super.viewDidLoad()
} override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated) firstLabel.text = Words.<strong>shared</strong>.firstWord
secondLabel.text = Words.<strong>shared</strong>.secondWord
thirdLabel.text = Words.<strong>shared</strong>.thirdWord
}
}
Notice that I never made a new instance of the Words object using Words(), but keep in mind that even after populating the labels, your singleton Words object still lives and will continue living its long life until you terminate the app.
Now let’s try doing the same thing without the singleton object. This is what your Words object will look like:
import Foundationstruct Words { var firstWord = "Church"
var secondWord = "Palace"
var thirdWord = "Castle"
}
Much cleaner and simpler, right? Now, how do I use this object to populate the labels? I need to make a new instance of the Words object every time I want to use it. Here is what the ViewController will look like:
import UIKitclass ViewController: UIViewController { var firstLabel = UILabel()
var secondLabel = UILabel()
var thirdLabel = UILabel() override func viewDidLoad() {
super.viewDidLoad()
} override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated) firstLabel.text = Words().firstWord
secondLabel.text = Words().secondWord
thirdLabel.text = Words().thirdWord
}
}
Here you can notice that every time you need a property of an object, you are making a new instance of that object using Words(). This way, you are using the memory constantly, but only when you need it, and after you have used it, the memory will be freed for usage in the future.
Singletons were popular in the Objective C era. However, they are not always a great solution, so use them only if you have to. They also make debugging and unit testing very hard.
As always, I hope this helps, and happy coding. Cheers!
Give Kudos by sharing the post!