神刀安全网

Memory Leaks and Resource Management in Swift and iOS

Let’s get real for a moment; what truly separates an amateur developer from a seasoned one is their footprint on the system. Less code and less memory while performing the same task at hand is truly where the art comes in. In this sobering post, I’d like to highlight some of the various pitfalls that lead to memory leaks, which inevitably result in crashes. I will also cover some tools and remedies to resolve these issues.

Back to the Basics: Reference Counting

On the machine-level, memory is allocated for our software during runtime. Anytime an object, variable, or data is created, it’s stored in memory by the system. How does the software we write access that memory? This is where references, pointers, and handles come in, which are basically associations to the memory location. It’s the glue between our software and the machine; where the virtual and physical world meet.

For example, say I create an instance and pass it around to other variables:

class Car {     var plate: String          init(plate: String) {         self.plate = plate     } }   var car1 = Car(plate: "ABC123") var car2 = car1 var car3 = car1 

The instance of “ Car(plate: “ABC123”) ” is not actually stored in “car1”. This actually doesn’t make sense when you think about it. The variable “ car1 ” isn’t memory, its text in your source code that represents a location to memory. This is what a reference is, “ car1 ” is actually referencing the memory location of the “ Car(plate: “ABC123”) ” instance.

Even though there are 3 variables, “ car1 “, “ car2 “, and “ car3 “, there is only one instance, “ Car(plate: “ABC123”) “. The 3 variables are referencing that same memory location of the instance. This means the reference count is 3 for that object, therefore reference counting is occurring .

If you nil one variable out:

car3 = nil 

Now the reference count is down to 2. Now if you do it again:

car1 = nil 

The reference count is down to 1. And one more time:

car2 = nil 

Finally, the reference count is zero and now the system deallocates the instance from memory and frees up that resource space.

This is garbage collection and the algorithm Apple has chosen for its Cocoa , Cocoa Touch , and Core Foundation ecosystems. It seems archaic compared to tracing garbage collections algorithms, such as those used in .NET and Java. However, it is the most efficient since memory is freed right away and doesn’t suffer from the performance intensities of sweeping unused memory. These advantages are at the cost of burdening programmers with understanding memory allocation and avoiding reference cycles.

How Memory Leaks Happen

The reference count increment/decrement dance I’ve illustrated used to be a manual process back in the day with Objective-C before iOS5. It was painful; retain , release , and autorelease had to be manually written out by the programmer to allocate and deallocate memory. If you ever forgot or missed deallocating memory for an instance, a memory leak would easily happen.

Then Apple graced us with Automatic Reference Counting (ARC), which took the load off developers having to manually do the reference counting. Reference counting didn’t go away though; it was just abstracted away by simply doing what the developer would do to manage memory. The concept of reference counting lives on and there is another memory leak trap that has to do with reference counting: strong reference cycles .

What is Strong Reference Cycles

Let’s add a driver to our previous example:

class Car {     var plate: String     var driver: Driver?          init(plate: String) {         self.plate = plate     } }   class Driver {     let name: String     var cars: [Car] = []       init(name: String) {         self.name = name     } } 

The “ Car ” class now has a “ Driver ” property. Also, the “ Driver ” class has a property that is an array of “ cars “. This is legitimate since a car can only have one driver, but a driver can have multiple cars.

Now let’s start assigning instances:

var myCar: Car? = Car(plate: "ABC123") //Car instance reference count is 1 var myDriver: Driver? = Driver(name: "Neo") //Driver instance reference count is 1   myCar?.driver = myDriver //Driver instance reference count is 2   if let car = myCar {     myDriver?.cars.append(car) //Car instance reference count is 2 } 

The reference count for the driver and car instances in memory is 2 each. Remember, only one instance is actually stored in memory; the variables and properties only hold a reference to their memory locations.

Now wipe out myCar and myDriver variables and let’s see what happens to the memory deallocation:

myCar = nil //Car instance reference count is 1 myDriver = nil //Driver instance reference count is 1 

You just chopped off access to the instances in memory: “ Car(plate: “ABC123”) ” and “ Driver(name: “Neo”) “. Remember, even though those variables have been terminated, the actual instances still exist in memory. Why did the system not deallocate the instances? Because they have properties in them that reference each other: <<Car instance>>.driver and <<Driver instance>>.cars[0] , so their reference count is not zero. They are keeping each other alive and we have no way to get to them:

Memory Leaks and Resource Management in Swift and iOS

This is a strong reference cycle; those instances will never be released from memory and are inaccessible too.

1. Resolving Strong Reference Cycles Like It’s 1999!

On a basic level, the strong reference cycle can be prevented by cleaning up all the instances in the correct order the old fashioned way. For example, before wiping out the driver variable, clear out the “ driver ” property of the car instance first:

myCar?.driver = nil myCar = nil myDriver = nil 

Problem solved! No more reference cycle and the instances are released from memory since no reference cycle exists that would keep each other alive. Likewise, you can pop off all the cars from the driver instance instead before wiping out the car variable:

myDriver?.cars.removeAll() myCar = nil myDriver = nil 

Problem solved again! Just a different way.

However, this is not a very elegant or scalable solution. This is just a small scenario. What happens if there are dozens of properties? And those properties are managed by other modules? How would I know when and if it’s ok to wipe out the properties and in what order?

2. Resolving Strong Reference Cycles Like a Champ!

Swift offers two keywords that break the strong reference cycle: “ weak ” and “ unowned “. These are decorators on the properties that tell the compiler not to increment the counter for those references. Below is how we would break the strong reference cycle:

class Car {     var plate: String     weak var driver: Driver?          init(plate: String) {         self.plate = plate     }       deinit {         print("Car deallocated")     } }   class Driver {     let name: String     var cars: [Car] = []       init(name: String) {         self.name = name     }          deinit {         print("Driver deallocated")     } }   var myCar: Car? = Car(plate: "ABC123") //Car instance reference count is 1 var myDriver: Driver? = Driver(name: "Neo") //Driver instance reference count is 1   myCar?.driver = myDriver //Driver instance reference count is STILL 1   if let car = myCar {     myDriver?.cars.append(car) //Car instance reference count is 2 }   myCar = nil //Car instance reference count is 1 myDriver = nil //Driver instance reference count is zero, also car instance reference count is zero since nothing else holding it 

Notice the “ driver ” property of the “ Car ” class is decorated with “ weak “. It means when this property creates a reference to an instance, do not increment the instance’s reference count. This same concept gets applied to delegates, where it should almost always be declared as “ weak “.

What about “ unowned “? It does the same thing, except this keyword is used for non-optional properties. It implies that the property will never become nil . A common place this is used is with closures, where strong reference cycles lurk as well. For example, let’s add a closure property to the “ Driver ” class that outputs all the car plates they own:

class Driver {     let name: String     var cars: [Car] = []          lazyvar allPlates: () -> String = { [unowned self] in         return "Car plates are: "             + self.cars.map { $0.plate }.joinWithSeparator(", ")     }       init(name: String) {         self.name = name     } } 

The property called “ allPlates ” doesn’t own “ self “. It is not possible that the property would exist without the instance, so this makes a good candidate for “ [unowned] “.

Note 1: I could have made “ allPlates ” a function, but there are scenarios that making a closure property makes sense if subclassing is overkill; instead of subclassing, overwriting the closure property with another closure is possible.

Note 2: Do not confuse a closure property with a lazy variable:

    lazyvar allPlates: String = {         return "Car plates are: "             + self.cars.map { $0.plate }.joinWithSeparator(", ")     }() 

A strong reference cycle is not possible in this case since it is only executed once and deallocates right after. In addition, if you were to add cars to the driver after this executed, calling this property would not consider new cars so this is not what we want!

Another common area where [unowned self] is used is NSNotificationCenter blocks:

NSNotificationCenter.defaultCenter()     .addObserverForName(UIApplicationWillEnterForegroundNotification,         object: nil,         queue: nil,         usingBlock: { [unowned self] notificationin             self.doSomething()         }) 

However, you do not have a possible strong reference cycle in “ UIView.animation ” because there’s no circular reference:

UIView.animateWithDuration(1.0, animations: { _ in   self.myThing.alpha = 1.0   self.doSomething() }) 

You should be wondering why do you have to handle strong reference cycles in NSNotificationCenter and not  UIView.animateWithDuration . There are some quirks in the Cocoa framework itself that leak objects. A couple of examples are  NSNotificationCenter and WKWebKit . There’s not much you can do about this except to keep your ears to the ground and listen to the community, as well as poking around popular open source libraries that have been battle tested.

Finally, careful not to unnecessarily use [weak/unowned self] , otherwise your objects will be deallocated right away before you get a chance to use it!

3. Resolving Strong Reference Cycles Like a Jedi!

There is one more way to handle strong reference cycle that is a complete mind shift: use structs over classes! Value types keep a unique copy of their data, while reference types share a single copy of their data. This is in-line with Swift’s philosophy of value over reference types. Almost everything in the Swift core library is immutable, about 100-to-1 in the source code! Structs are everywhere and rarely will you see a class in Swift. In fact, the Swift team is undertaking a major update to the Foundation library, where it will wrap many reference types to value types .  Choose a struct over a class any day or you better have a good reason!

Let’s update our “ Car ” and “ Driver ” classes to structs:

struct Car {     var plate: String     var driver: Driver?          init(plate: String) {         self.plate = plate     } }   struct Driver {     let name: String     var cars: [Car] = []          lazyvar allPlates: () -> String = {         return "Car plates are: "             + self.cars.map{ $0.plate }.joinWithSeparator(", ")     }          init(name: String) {         self.name = name     } } 

That’s it! Good-bye strong reference cycles, weak , and unowned ! This is because reference counts are always 1 for value types. There are no references to it. Instead, they are copied when assigned to a new variable:

var myCar: Car? = Car(plate: "ABC123") //Car instance reference count is 1 var myDriver: Driver? = Driver(name: "Neo") //Driver instance reference count is 1   myCar?.driver = myDriver //Driver instance is COPIED, so reference count is 1   if let car = myCar {     myDriver?.cars.append(car) //Car instance is COPIED, so reference count is 1 }   myCar = nil //Car instance deallocated myDriver?.cars[0] //Car instance still lives on, it's a copy! myDriver = nil //Driver instance deallocated 

Using value types over reference type is a world of difference with Objective-C and the recommended approach from the Swift team. It’s safer because many bugs in software arise from unexpected state changes. Of course, there is a place for classes, but as Crusty says : Start with a protocol, then a struct, then a class when necessary.

The implications of this are vast, see be aware:

// Classes var car1: Car? = Car(plate: "ABC123") var car2 = car1 var car3 = car1 print(car1?.plate ?? "") //ABC123 car3?.plate = "XYZ987" print(car2?.plate ?? "") //XYZ987   // Structs var car1: Car? = Car(plate: "ABC123") var car2 = car1 var car3 = car1 print(car1?.plate ?? "") //ABC123 car3?.plate = "XYZ987" print(car2?.plate ?? "") //ABC123 

Notice with classes, updating “ car3 ” affects “ car2 “. However with structs, updating “ car3 ” is self contained and only affects “ car3 “. It’s very functional; inputting a value into function outputs another value without affecting the rest of the application – very modular! So before using value semantics, ensure you’re thinking value semantics and let it work for you.

Other Memory Issues

We only discussed memory leaks in this post, which is inaccessible memory that has no more pointers to it. There are other memory issues that can plague your app that you should be conscious of.

Abandoned Memory

This kind of memory still has references to it, but are wasted since they won’t ever be used again. There is no good reason to access that memory so it’s wasted resources. An example of this is pushing the same view controller over and over again without ever popping/dismissing it. Now you have duplicate view controller instance on the view hierarchy. Not only is this wasted resources, but how do you know which one has the latest changes? Fortunately, this is a rookie mistake and easy to spot.

Cached Memory

Cache is great thing; whether it be images from the web or data from a database, it saves round trips to the server. However, without proper caching mechanisms in place, they can grow large and drown the resources fast. These are also easy to spot using trivial memory profilers. Implementing caching rules for the specific needs of the apps resolve these issues fast as well. There are some basic rules that can be enforced using the native NSCache , or libraries that offer more sophisticated options, such as HanekeSwift .

Clean & Dirty Memory

There are some hooks and triggers iOS provides that help guide you to manage memory. First we must understand there is the concept of clean and dirty memory.

Clean memory is static resources that are loaded into memory that doesn’t change. They can be recreated at any time, such as system frameworks, binaries, mapped files, etc. The system gets rid of these first when under memory pressure, since it can always recreate it when needed.

Dirty memory is those that are changed during runtime. The system can’t recreate these, so it has to use swap space, but iOS doesn’t have a swap mechanism. So dirty memory will always be kept in physical memory until it reach a certain limit. Then the app will be terminated and all its memory will be recycled by system.

Before getting to this point, iOS will send a few warnings to your app so you can act accordingly.

UIViewController – didReceiveMemoryWarning

You’ve probably seen it before, but habitually deleted it. When you create new app or view controller, this is the template Xcode creates for you:

class ViewController:UIViewController {       override func viewDidLoad() {         super.viewDidLoad()         // Do any additional setup after loading the view, typically from a nib.     }       override func didReceiveMemoryWarning() {         super.didReceiveMemoryWarning()         // Dispose of any resources that can be recreated.     }     } 

See the “ didReceiveMemoryWarning ” function? That’s where iOS will trigger it’s warning for your view controller. Ahhhh, that’s what that’s for :). What you’ll want to do there is purge any of the view controller’s resources you don’t necessarily need any more, but can recreate later if so. This is also a good place to purge some cache or database resources so it can be recreated later when needed as well. This is why it is good to create everything in a lazy fashion, so resources are requested as needed, and recreated as needed.

AppDelegate – applicationDidReceiveMemoryWarning

Implement the applicationDidReceiveMemoryWarning method of your application delegate. This is where you will set any app-wide references to objects it can safely free to nil . Also in the application delegate, make use of the  applicationDidEnterBackground  to free any resource so your app isn’t first line to be terminate from the list of background apps.

NSNotificationCenter – UIApplicationDidReceiveMemoryWarningNotification

Subscribe to the NSNotificationCenter if this suits better to free up resources:

NSNotificationCenter.defaultCenter()     .addObserverForName(UIApplicationDidReceiveMemoryWarningNotification,         object: nil,         queue: NSOperationQueue.mainQueue(),         usingBlock: { [unowned self] notificationin             // Clean up         }) 

With one or more of these in place, you can use the simulator to trigger a memory warning by choosing Hardware > Simulate Memory Warning .

Debugging

Xcode Instruments is actually quite useful. It will help you monitor memory usage, find abandoned memory, find memory leaks, find zombies, and more.

To see this in action, clone this sample code that has a strong reference cycle in it. Once you’ve opened it in Xcode, go to Product > Profile . This will open up the Instruments template list, choose “ Leaks “:

Memory Leaks and Resource Management in Swift and iOS

Click the record button and this will run the app in the profiler. Next, click the button I placed in the app labeled “ Trigger Strong Reference Cycle “. The profiler will display green checkmarks, then boom! A red X will appear to indicate there is a strong reference cycle. It may take awhile for the profiler to catch up, but once you see the red X, stop the recording and click it.

You will see the backtrace highlighting the leak and associated classes. You can even select Details > Cycle & Roots and see a nice reference graph:

Memory Leaks and Resource Management in Swift and iOS

For an actual production app, be sure to eventually use a real device instead of the simulator since low-level details may not be the same.

Conclusion

Swift is a gorgeous language and it also has smarts. It manages memory across the board. In Objective-C for example, Automatic Reference Counting (ARC) is not supported in procedural-C code and low-level API’s like Core Graphics. These have been handled in Swift though, so the huge memory leaks that a programmer can have in Objective-C are impossible in Swift. This alone is a big win for Swift. However, this doesn’t mean we’re home-free for any language or platform.

Happy Coding!!

Further Reading

转载本站任何文章请注明:转载至神刀安全网,谢谢神刀安全网 » Memory Leaks and Resource Management in Swift and iOS

分享到:更多 ()

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址