19. Apr 2022iOS

Swift: Improve your memory management - Strong, weak and unowned explained

In this article, we will show and explain how memory management works in practice and how to properly manage memory space in Swift. Understanding this mechanism, we can influence the lifetime of objects in application.

Oleksandr LunevskyiiOS developer

In this article, we will show and explain how memory management works in practice and how to properly manage memory space in Swift. Understanding this mechanism, we can influence the lifetime of objects in application.

As a modern programming language, Swift basically takes care of memory management in iOS applications by allocating and freeing memory. This is due to a mechanism called Automatic Reference Counting, or ARC for short.

The Swift tries to prevent situations that can lead to memory leaks. We can imagine a scenario where objects that occupy some part of the memory will not deallocate, but will occupy the free memory space we need. In the end, due to the use of a large amount of memory, the application simply crashes.

Strong, weak or unowned references

While writing our code, we often worry about the presence of retain cycles in our code. Very often we are faced with a misunderstanding of the purpose of using strong, weak or unowned references. The essence of using these keywords is to avoid retain cycles, but sometimes we do not fully understand where and what method to use.

In order to solve a problem, when Swift does not know which of the references can be removed and which not, there is a certain way called strong and weak references. All instance references created are strong by default. In a situation where two objects point to each other with strong references, Swift cannot decide which of the references can be removed.

To resolve this problem, some references can be transformed to weak references. Weak references are defined using the Weak & Unowned keywords.

Closure Escaping

In our practice, we most often work with processing data from the server. In this case, we all encounter closures more often. Working with closures is quite specific due to the fact that memory leaks can occur in this case. We will try to explain the principle of interaction with closures and the correct use of memory using intelligible examples.

A closure can capture a value from its scope in three different ways: using Strong, Weak or Unowned references. We often use these keywords in our practice, mainly to avoid Strong reference cycles.

An object exists as long as it is remembered.

Strong capturing

The first step is to understand the origin of strong reference. Roughly speaking, this is a reference that protects the objects it refers to from deallocating by increasing the retain count. As we already know, as long as one object has a strong reference to another object, it will not be deallocated. This is important to remember in order to understand the use of reference types. We can say that strong references are used almost everywhere in the Swift language.

Until we explicitly specify a capture method, Swift uses a Strong capture. This means that the closure captures the used external values (values from its scope) and never allows them to be deallocated.

DispatchQueue.main.asyncAfter(deadline: .now() + 4) {
		print(self.greetingValue)
}

 

In our case, the constant is used inside the closure, which means that Swift will automatically ensure that it exists as long as the closure itself exists. This is how the Strong capture works.

Example from project:

UIView.animate(withDuration: 0.2) {
		self.collectionView.contentInset.bottom = self.collectionViewBottomInset
}

 

Weak capturing

Swift gives us the ability to create a capture type to define exactly how the values used will be captured. An alternative to a strong capture is a weak one.

Weak reference is a pointer to an object that allows the object to be deallocated by ARC. Also, weak reference resets the pointer to an object when it has been successfully deallocated from memory. This means that when a weak reference is accessed, the result will be either an object or nil.

In Swift, all weak references are optionals. This comes from the fact that an optional reference can and will change to nil when the object is no longer strongly referenced. Its use leads to the following results:

  • weak captured values are not held by the closure and thus can be deallocated and transformed to nil
  • based on the first point, weak captured values in Swift will always be optional

The most appropriate place for weak variables is where a retain cycle can occur. This happens when two objects have strong references to each other. If variable is declared outside the scope of the closure, variable reference inside the scope of the closure creates strong reference to that object. So we will modify our example to use a weak capture and see what exactly changes:

DispatchQueue.main.asyncAfter(deadline: .now() + 4) { [weak self] in
		print(self?.greetingValue ?? "Hello World")
}

[weak self] provides the capture type. This is the part of the closure syntax where we define the way values should be captured. Here we are saying that self (our ViewController) should be captured weakly, that means that value can be changed to nil at any time, so we can define default value - "Hello World". With this procedure, we have broken the strong reference cycle.

Example from project:

DispatchQueue.main.async { [weak self] in
		self?.showMatchDetail(with: deepLinkValue.itemId)
}

Unowned capturing

An alternative to weak capture is unowned capture. The principle of operation of weak and unowned references is similar, but not the same. Unowned references, like weak ones, do not increase the retain count of the object.

Like weak references, unowned references have their upsides: they allow immutability, and since they can't be manually set to nil, our code won't work in unexpected ways.

The use of unowned capturing is perfectly safe, only if used correctly.

profileButton.publisher(for: .touchUpInside)
		.sink { [unowned self] _ in showProfile() }
		.store(in: &cancellables)

As we can see, the difference is that in Swift an unowned reference is not optional. This makes it easier to manage objects with an unowned reference. But the easy way, as we know, is not always the right one. This principle is very similar to using force-unwrap with optional values. As with force-unwrap, if we are 100% sure that the reference will not be nil, then unowned can be used. If not, then weak.

Example from project:

ErrorHandler.showCommonEmptyView(on: error, in: self) { [unowned self] in
		refreshCart()
}

Conclusion

It would be very good if the developers dealt with unlimited memory and never worried about its rational use. Unfortunately, this is far from the case, and we are forced to behave like temporary users of memory, use it, and then return it back...

Thank you for reading and hope you have learned something new and useful for yourself.

Oleksandr LunevskyiiOS developer