• Memory Leaks in Swift: Unit Testing and other tools to avoid them.
  • By Leandro Perez
  • The Nuggets translation Project
  • Permanent link to this article: github.com/xitu/gold-m…
  • Translator: RickeyBoy
  • Proofread by: Swants, Talisk

Memory leak in Swift

Avoid it by unit testing, etc

In this article, we’ll explore memory leaks and learn how to detect them using unit tests. Let’s start with a quick example:

describe("MyViewController"){
    describe("init") {
        it("must not leak") {let vc = LeakTest{
                return MyViewController()
            }
            expect(vc).toNot(leak())
        }
    }
}
Copy the code

Here’s a test from SpecLeaks.

Important: I’ll explain what a memory leak is, discuss circular references, and some other things you probably already know. If you just want to read about unit testing leaks, skip to the last chapter.

A memory leak

In practice, memory leaks are the most common problem we developers face. As the app grew, we developed one function after another, which also brought the problem of memory leakage.

A memory leak is a memory fragment that is no longer used, but is held permanently. It is memory junk that not only takes up space but also causes problems.

Memory that was allocated at some point, but is not released, and is no longer held by your app, is leaked memory. Because it is no longer referenced, there is no way to release it now, nor can it be used again.

Apple Official Documentation

Whether we are new or experienced, we will create a memory leak at some point, no matter how experienced we are. In order to create a clean, crash-free application, it is important to eliminate memory leaks because they are dangerous.

Memory leaks are dangerous

Memory leaks not only increase the memory footprint of your app, but also introduce harmful side effects and even crashes.

Why is the memory footprint growing? It is a direct consequence of the object not being released. These objects are pure memory junk, and as the operations that create them are performed, they take up more and more memory. Too much memory garbage! This can lead to a memory warning and eventually the app crashes.

Explaining harmful side effects requires a little more detail.

Suppose an object starts listening for a notification in the init method when it is created. Every time it listens for a notification, it puts something into a database, plays a video, or posts an event to an analytics engine. Since the object needs to be balanced, we must stop listening for notifications when it is released, which is implemented in deinit.

What happens if such an object leaks?

This object is never released, and it never stops listening for notifications. Each time a notification is published, the object responds. If the user repeatedly creates the object in question, there will be multiple duplicate objects. All of these objects respond to this notification and affect each other.

In this case, a crash is probably the best that can happen.

A large number of leaked objects repeatedly respond to app notifications, changing the database and user interface, causing the state of the entire APP to fail. You can see The importance of this kind of problem in The Pragmatic Programmer’s Dead Programs Tell No Lies article.

Memory leaks can undoubtedly lead to a very poor user experience and low scores on the App Store.

Where do memory leaks occur?

For example, third-party SDKS or frameworks can leak memory, or even apple-created classes such as CALayer or UILabel. In these cases, there is nothing we can do but wait for the SDK to update or discard the SDK.

But memory leaks are more likely to be caused by our own code. The number one cause of memory leaks is circular references.

To avoid memory leaks, we must understand memory management and circular references.

A circular reference

The word loop comes from the days when Objective-C used manual reference counting. Before we were able to use automatic reference counting and Swift, and all the convenient things we can now do for value types, we used Objective-C and manual reference counting. You can learn about manual reference counting and automatic reference counting in this article.

During that time, we needed to learn more about memory processing. It is important to understand what allocation, copy, reference means, and how to balance these operations (such as release). The basic rule is that whenever you create an object, you own it and you are responsible for releasing it.

Things are much simpler now, but there are still concepts to learn.

In Swift, when an object pair is strongly associated with another object, it is referred to. When I say objects, I mean reference types, basically classes.

Structs and enumerations are value types. Circular references are unlikely to occur with only value types. When capturing and storing value types (structs and enumerations), there are no problems with references mentioned earlier. Values are copied, not referenced, although values can also hold references to objects.

When an object refers to a second object, it is owned. The second object will persist until it is released. This is called a strong reference. The second object is not destroyed until you set the property to nil.

class Server {
}

class Client {
    var server : Server //Strong association to a Server instance
    
    init (server : Server) {
        self.server = server
    }
}
Copy the code

Strong correlation.

A holds B and B holds A creates A circular reference.

A 👉 B + A 👈 B = 🌀

class Server { var clients : Func add(Client :Client){self.client.append (Client)}} class Client {var server: func add(Client :Client){self.client.append (Client)}} class Client {var server: Init (Server: Server) {self.server = Server self.server.add(client:self) // This line generates a circular reference -> memory leak}}Copy the code

Circular reference.

In this example, neither client nor server will be able to free memory.

In order to be freed from memory, an object must first release all of its dependencies. Because the object itself is a dependency, it cannot be released. Similarly, when an object has a circular reference, it is not released.

A circular reference can be broken when one of its references is weak or undirected (unowned) **. Sometimes loops have to exist because the code we’re writing needs to be interrelated. But the problem is that not all associations are strong, at least one of them must be weak.

class Server { var clients : [Client] func add(client:Client){ self.clients.append(client) } } class Client { weak var server : Server! // init (server: server) {self.server = server self.server.add(client:self)}Copy the code

Weak references can break circular references.

How to break circular references

Swift provides two ways to resolve the strong reference loop caused by using reference types: Weak and Unowned.

Using Weak and Unowned in circular references allows one instance to reference another instance without being strongly held. This allows instances to refer to each other without creating a strong reference loop.

Apple ‘s Swift Programming Language

Weak: A variable can optionally not hold the objects it references. A weak reference is a variable that does not hold its reference object. Weak references can be nil.

Unowned: Similar to weak references, an owner-reference does not strongly hold an instance of its reference. But unlike weak references, an undirected reference must always have a value. Because of this, an ownerless reference is always defined as a non-optional type. An ownerless reference cannot be nil.

The timing of the use of both

When a closure and the instances it captures refer to each other, the captured values in the closure are defined as unreferenced, so that they are always freed from memory at the same time.

Conversely, when an instance caught in a closure is defined as a weak reference, the captured reference has the potential to become nil in the future. A weak reference is always an optional type that automatically becomes nil when the referenced instance is freed from memory.

Apple ‘s Swift Programming Language

class Parent {
    var child : Child
    var friend : Friend
    
    init (friend: Friend) {
        self.child = Child()
        self.friend = friend
    }
    
    func doSomething() {
        self.child.doSomething( onComplete: { [unowned self] in  
              //The child dies with the parent, so, when the child calls onComplete, the Parent will be alive
              self.mustBeAlive() 
        })
        
        self.friend.doSomething( onComplete: { [weak self] in// The friend might outlive the Parent. The Parent might die and later the friend calls onComplete. self? .mightNotBeAlive() }) } }Copy the code

Contrast weak references with undirected references.

It’s not uncommon to forget to use weak Self when writing code. We often introduce memory leaks when writing closures, such as when using functional code such as flatMap and Map, or when writing code related to message listeners and brokers. You can read more about memory leaks in closures in this article.

How to eliminate memory leaks?

  1. Don’t create memory leaks. Gain a deeper understanding of memory management. Define complete for the projectCode style.And strictly abide by it. If you’re serious and follow your code style, it’s notweak selfIt will also be easy to spot. Code reviews can also be of great help.
  2. Use Swift Lint. This is a great tool that forces you to follow a code style and follow the first rule. It can help you spot problems at compile time, such as proxy variable declarations that are not declared as weak references, which could result in circular references.
  3. Detect and visualize memory leaks at run time. If you know how many instances of a particular object exist at a given time, you can use a LifetimeTracker. This is a great tool to run in development mode.
  4. Review apps regularly. Memory analysis tools in Xcode are very useful and can be found in this article. Not so long ago Instruments was a way to do that, and it’s a great tool.
  5. Unit test memory leaks using SpecLeaks. This third-party library uses Quick and Nimble to easily test for memory leaks. You can learn more about it in the following chapters.

Unit test for memory leaks

Once we know what loops and weak references are, we can write tests for circular references by using weak references to detect loops. By making a weak reference to an object, we can test whether the object has a memory leak.

Because a weak reference does not hold the instance it refers to, it is likely that a weak reference will still point to that instance when it is freed from memory. Therefore, automatic reference counting sets weak references to nil when the object referenced by weak references is freed.

Suppose we want to know if X has a memory leak, and we create a weak reference to it called leakReference. ARC sets leakReference to nil if X is freed from memory. So, if X has a memory leak, leakReference will never be set to nil.

func isLeaking() -> Bool {
   
    var x : SomeObject? = SomeObject()
  
    weak var leakReference = x
  
    x = nil
    
    if leakReference == nil {
        return false// There is no memory leak}else{
        return true// Memory leak occurred}}Copy the code

Tests whether an object has a memory leak.

If X does have a memory leak, the weak reference leakReference points to the instance where the memory leak occurred. On the other hand, if the object does not leak, it will no longer exist after it is set to nil. In this case, leakReference will be nil.

Swift by Sundell elaborates on the differences between different memory leaks, which helped me tremendously both with this article and SpecLeaks. Another good book takes a similar approach.

Based on this theory, I wrote SpecLeacks, an extension based on Quick and Nimble to detect memory leaks. The core is to write unit tests to detect memory leaks without a lot of redundant boilerplate code.

SpecLeaks

Using Quick and Nimble together makes it easier to write more user-friendly and readable unit tests. SpecLeaks just adds a little more functionality to these two frameworks, making it easier to write unit tests to detect if any objects are leaking memory.

If you don’t know much about unit tests, this screenshot may give you an idea of what the unit tests do:

You can write unit tests to instantiate some objects and try them out. You define the desired outcome, and what it takes to meet expectations to pass the test and make the test result green. If the end result does not match the expectations originally defined, the test will fail and appear red.

Test for memory leaks during initialization

This is the easiest test to detect a memory leak, just initialize an instance and see if it has a memory leak. Sometimes, when the object registers listening events, or has proxy methods, or registers notifications, these tests can detect some memory leaks:

describe("UIViewController") {let test = LeakTest{
        return UIViewController()
    }

    describe("init") {
        it("must not leak"){
            expect(test).toNot(leak())
        }
    }
}
Copy the code

Test the initialization phase.

Test memory leaks in viewController

A viewController may start leaking memory after its subviews have finished loading. A lot of things can happen after that, but using this simple test you can be sure there are no memory leaks in the viewDidLoad method.

describe("a CustomViewController") {
    let test = LeakTest{
        let storyboard = UIStoryboard.init(name: "CustomViewController", bundle: Bundle(for: CustomViewController.self))
        return storyboard.instantiateInitialViewController() as! CustomViewController
    }

    describe("init + viewDidLoad()") {
        it("must not leak"){
            expect(test).toNot(leak())
            //SpecLeaks will detect that a view controller is being tested 
            // It will create it's view so viewDidLoad() is called too } } }Copy the code

Test init and viewDidLoad on a viewController.

With SpecLeaks you don’t need to manually call the View on the viewController in order for the viewDidLoad method to be called. SpecLeaks will do this for you when you test a subclass of UIViewController.

Tests for memory leaks when methods are called

Sometimes initializing an instance is not enough to determine if a memory leak has occurred, because a memory leak can occur when a method is called. In this case, you can test for memory leaks while the operation is being executed, like this:

describe("doSomething") {
    it("must not leak") {let doSomething : (CustomViewController) -> () = { vc in
            vc.doSomething()
        }

        expect(test).toNot(leakWhen(doSomething))
    }
}
Copy the code

Detects if the custom viewController is leaking memory when the doSomething method is called.

To summarize

Memory leaks can cause a lot of problems, they can lead to poor user experience, crashes, and poor reviews in the App Store, and we need to eliminate them. A good code style, good practices, a thorough understanding of memory management, and unit testing all help.

But unit testing doesn’t guarantee that in-memory testing won’t happen at all, you can’t cover all method calls and state, and testing everything that exists that interacts with other objects is impossible. In addition, sometimes you have to simulate dependencies to find memory leaks that might occur with the original dependencies.

Unit tests do reduce the likelihood of memory leaks, and SpeakLeaks makes it very easy to detect and find memory leaks in closures such as flatMap or any other runaway closure that holds Self. The same is true if you forget to declare the proxy as weak reference.

I made extensive use of RxSwift, as well as faltMap, map, SUBSCRIBE, and a few other functions that require transitive closures. In these cases, a lack of weak or unowned often leads to memory leaks, which can be easily detected using SpecLeaks.

Personally, I always try to add such tests to all my classes. For example, whenever I create a viewController, I create a SpecLeaks code for it. Sometimes the viewController will have a memory leak while loading the view, which can be easily detected by such tests.

So what do you think? Would you write unit tests to detect memory leaks? Can you write tests?

I hope you enjoyed reading this article and feel free to reply to me if you have any suggestions or questions! Feel free to try SpeckLeaks 🙂


Thank you Flawless App.


The Nuggets Translation Project is a community that translates quality Internet technical articles from English sharing articles on nuggets. The content covers Android, iOS, front-end, back-end, blockchain, products, design, artificial intelligence and other fields. If you want to see more high-quality translation, please continue to pay attention to the Translation plan of Digging Gold, the official Weibo, Zhihu column.