Great performance is a prerequisite for delivering a compelling and immersive app experience that keeps users engaged. Learn best practices and strategies for characterizing and improving the performance of your code for iPhone, iPad, and Apple Watch.
My name's Ben. I am an iOS Performance Engineer, and today we are going to talk about "Performance on iOS and watchOS." So we are going to start by telling you, why should you think about performance? If you've never thought about performance in your app before, why start now? Hopefully I'll convince you to stay in your seats, and then we can move on to, how should you think about performance? It can be like a very broad and sometimes daunting topic, but we are going to break it down into a couple of categories and give you a few specific strategies for improving the performance of your app in those areas.
And finally, you're hopefully really excited to bring native code to watchOS 2, and we are going to dig into what you can do to get the best user experience in your app on that platform.
So why should you think about performance? The easiest way to sum it up is that performance is a feature. It's a core and central element of giving your users a great experience in your app. It's not an extra or a bonus or something you can get to at the end if you have time.
It's actually something that should be on your mind all the time while you are writing your app. There's a couple of reasons for that.
If your app is really responsive, if it always responds to user input right away, that actually builds the trust of your users.
That lets them know that if they quickly need to access a piece of information or service some interaction in your app, it's not going to keep them waiting, and that makes them really happy and keeps them coming back. If you are going to adopt Multitasking on iPad in iOS 9, not only does your app no longer have full run of the screen, it actually doesn't have full run of the system's resources either.
So a performance problem in your app doesn't just cause a bad experience in your app anymore; it can actually cause a bad experience in another app as well.
You really want to be known as a good neighbor in Multitasking. Apps that are architected to be efficient with system resources, like CPU or memory, don't just feel great when you are using them; they actually save battery power and let your user get through their day, and they really appreciate that.
And finally, iOS 9 supports a huge range of hardware, and performance is a prerequisite of continuing to bring great apps and features to all of your customers.
So hopefully I have convinced you, you don't have to walk out.
How should you think about performance? Well, the very first step when you are building your app is choosing technologies.
This is critical because you need to choose which technologies are going to give your users the best experience in your app. Once you are a little bit of the way into building your app, you can start taking measurements and actually understanding what the user experience is in the critical interactions in your app. Your measurements will inform you how your app is doing today. Once you've got those, you can actually set some goals for what state you want your app to be in before you submit it to the Store.
And finally, once you've got all that, you are ready to start making code changes to improve the performance in your app, and there's a great workflow you can follow to converge on your targets.
So let's start with choosing technologies.
Picking the right tool for the job is a really early and critical aspect of proactively architecting your app for great performance. And the very first step of choosing technologies is to know the technologies. So over the course of this talk, I'm going to reference several other talks from this year and previous years that cover technologies that we have found really helpful for getting great performance in apps. Once you know the universe of technologies that are available to you, then you can start to bring in your knowledge of how your app works and what it needs to do, and pick the best ones for your app.
A great example here is if your app needs to store three strings, it's probably okay to write those out to a plist or put them in user defaults. On the other hand, if your app needs to use 3,000 strings, you might want to consider Core Data. Speaking of Core Data, when you are choosing technologies, I strongly encourage you to consider Apple APIs and frameworks.
We spent a lot of time trying to make those great for you and your users, and we build our own products on them. One benefit of adopting Apple APIs and frameworks is that after the user installs your app, they might install an iOS update, and these frequently contain performance improvements to core APIs and technologies. So the next time they open your app after installing an iOS update, it magically got faster.
So you've chosen some technologies, and you've started building your app.
Let's measure a few things. There's a couple of categories of performance that we can think about measuring.
Let's start with animations. Animations are what make your app feel alive and fluid and let your user know where they are and what's going on.
The easiest way to measure the performance of your animations is the Core Animation instrument.
Responsiveness is all about how quickly you react to user input, and actually, the simplest way of measuring responsiveness may seem a little bit low tech, but it's really powerful, and that is simply instrumenting your actual code. I am going to go into an example of that.
For more complex scenarios that maybe involve several threads or lots of system interactions, there is a great instrument called System Trace.
And finally, memory.
Memory is the most precious resource on mobile devices, and it's really important to make sure your app is using exactly as much memory as it needs and no more.
Again, a simple but powerful way to understand your app's memory use -- which I am going to go into -- is the Xcode debugger.
When you are ready to dig in more, there's a great instrument called Allocations. And if you think you have leaks, there's an instrument to help you track those down as well. So let's go into a code instrumentation example. Here I've got an IBAction that's wired to a button, so when my user taps this, I am going to load an image and put it in my view. And I want to know how long this takes. So I am going to use an API called 'CF absolute time get current.' Now, I don't actually care about the absolute value of the current time, but I do care about the difference between these. This API, although this is Swift and it's type inference and it's wonderful, I will tell you this returns a double, and specifically, that double represents the current time in seconds.
A second is actually a really long time. Your user is really going to feel it if something in your app takes a second.
So we actually find milliseconds to be a more actionable unit of measure here. So we are going to subtract the start time from the end time and multiply the result by a thousand to get our measurement in milliseconds.
It's important to profile the release configuration of your app so that you get all the compiler optimizations that your users will get and so that you understand how your app is actually behaving in the field; however, it's equally important not to submit your performance instrumentation to the App Store. So my suggestion is that you actually create a copy of your release scheme in Xcode and define one additional define so that you can build a release version of your app with the performance instrumentation quickly and easily.
So what types of responsiveness are we interested in measuring? Well, definitely taps and button presses.
Most commonly, you'll be doing these in IBActions. You may also do them in UIView touch handling code. Or you may have a gesture recognizer target. Another aspect of your app's performance that's interesting to understand is what it feels for the user to move around in your app and switch to different views, whether that's using a tab bar or modal views. In this case, we find it interesting to think about the time between 'view will appear' and 'view did appear' because that lets you compare which of your views take longer to get ready to get on screen.
So you've taken some measurements, and you understand how your app is behaving.
How do you set goals for where you want your app to be before you submit it to the Store? Animations feel great, they feel realistic and fluid and alive when they run at 60 frames per second.
I'm not going to talk too much about them this year because there's a great talk from last year you can check out called "Advanced Graphics and Animations in iOS Apps," and there they cover the Core Animation instrument and how you can use it to measure the performance of your animations and also improve it across the full range of hardware.
We are going to spend a lot of time today on responsiveness.
Responsiveness is all about, again, how you react to user input. And we've found that if it takes too much longer than a hundred milliseconds, your user starts to feel it. So your goal for any responsiveness scenario should be 100 milliseconds.
By the way, you want to think about reaching these performance goals on the oldest hardware you intend to support. If you are targeting iOS 9, that might be the original iPad mini, the iPhone 4s, or even the iPad 2.
If you've already got one of those -- or rather, if you've still got one of those -- please keep it, continue to use it, continue testing on it.
And if you don't, well, there's a great refurbished section on the Apple Online Store.
So you've set goals. You've taken measurements. And now you want to proceed with making code changes in your app to improve the performance.
How do we start? First of all, don't guess.
You really would like to profile with tools and act on evidence of what's causing the performance issues in your app. It's charming to think that your intuition will be right all the time, but it's probably closer to a coin toss. Along those lines, avoid premature optimization. Don't complexify your code until you have evidence that the simplest possible implementation is not sufficient for great performance. Frequently, the mechanisms that people introduce to try to proactively stave off performance problems end up causing performance problems of their own. Make one change at a time. You do want to start to hone and develop your intuition for how to approach improving the performance of your app, and it's very hard to understand which thing you did that actually ended up improving the performance of your app, so make one change at a time. And what I am really trying to say here is that there's no magic. This is just like ordinary debugging. So bring the same rigorous, scientific mindset you would to debugging an app crash or a functional issue. So here is the picture I want you to print out, stick on your wall, set as the wallpaper on your Mac.
This is how we go through making code changes to improve performance in an app.
The first thing you need to do is reproduce the issue.
Then profile with tools to get an understanding of which areas of your code are actually contributing to a performance issue. In a sufficiently large code base, your intuition may actually not be correct here, so it's good to collect evidence.
Then, once you've zoomed in on the specific sections of code that are causing less-than-desirable performance in your app, you can measure exactly how much time you are spending there. And finally, you can make a single targeted code change to try and advance towards your goals. Often it's the case that one single change doesn't get you all the way to where you want to be, and in fact, several different changes will combine to eventually get you to your goal. So that's why this is a cycle because you may find that after you change code and reproduce again, it's better, but you are not where you want to be. So you can keep going through the cycle until you are happy.
Profiling and measuring are on that slide, and maybe they seem like they are similar, but actually, they are two discrete steps of improving the performance of your app. Profiling, again, is using tools like the Xcode debugger and Instruments: Time Profiler to get an overview of all the aspects of your code that are contributing to your performance scenario.
Measuring means instrumenting a specific area of your code to understand precisely how much time the user spends waiting there. And again, the 'CF absolute time get current' example I gave is actually a really powerful way to do that.
Again, for more complex scenarios, there's System Trace.
So let's talk about responsiveness.
Responsiveness is all about reacting to user input. And we can't talk about responsiveness without talking about your app's main thread because your app's main thread is where you consume all the user input. That's anything from the touchscreen -- a tap or a scroll -- anything from the other sensors on the device, like an orientation change, and multitasking resizes and other system state events.
If your main thread is mostly dedicated to the task of responding to user input, your app will feel really great all the time.
If you're a little less careful with what you do on your main thread or maybe you do everything on your main thread, it's possible your app will appear hung or frozen. So what should we avoid doing on our main thread? Specific things to watch out for include CPU-intensive work. That can mean parsing a long string that you downloaded from the network, maybe applying a filter to an image, and tasks that depend on an external resource. And I will come back to that. I am not going to spend too much time on CPU-intensive work today because there was a great talk earlier this week from the folks that made Instruments called "Profiling in Depth," and they actually talk about improving the performance of CPU-intensive code in Instruments using Instruments. It's awesome.
So let's go back to tasks that depend on an external resource. Another name for these is a blocking call. It's called that because it prevents your thread from making progress, so you are blocked.
Now, what's a blocking call? Some of you may be familiar with the concept of a syscall.
Any code path that ends up making a syscall is considered a blocking call. As I mentioned, these typically involve resources that are not currently in memory, commonly things like loading things from the disk or fetching stuff over the network.
Occasionally, your main thread will also get blocked because it's waiting for a resource that is available, but it's waiting for someone else to finish using that resource because that resource only allows one client at a time. So, how do you spot blocking calls in your thread? Well, sometimes they jump right out at you.
The word "synchronous" is a synonym for blocking. So that's a clue. When you are reading through your code, that should light up and draw your eyes.
So, great, we spotted this blocking call in my code, 'NSURLConnection send synchronous request.' Well, what do I do now? Well, sometimes there is an asynchronous API -- especially for APIs that specifically call out that they're synchronous in their name -- that you can easily switch over to. In this case, we got lucky, and in fact, this one helpfully has the word "asynchronous" in the name, so we know exactly what we are getting ourselves into.
Unfortunately, it's not quite as simple as search and replace. You are changing the order that your code executes in, and you may have other code that depends on the result of this operation. So unfortunately, some restructuring is required.
But let's say you don't have an async equivalent that you can easily switch to or you want to move a whole section of code off the main thread in one operation. In that case, use Grand Central Dispatch.
Grand Central Dispatch is an Apple technology that manages a global thread pool in your app. It's already there. Even if you don't notice it. If you are familiar with programming with threads on other platforms, Grand Central Dispatch sort of takes out all the confusion and trouble of worrying about starting threads and what state they are in and so on, and lets you express tasks that you'd like to run as closures or blocks.
These closures, once you submit them to Grand Central Dispatch, run on an arbitrary thread in your process. Arbitrary threads are awesome because you didn't have to deal with starting them, and you don't have to think about how many there are, but they have the caveat. Since you don't control which thread your code is running on, any operation you express in a closure or block must be safe to do on any thread. What are some examples? Well, some objects are actually restricted to access on the main thread only. UIKit views and controllers, for example, must be created, modified, and destroyed only on the main thread.
Some objects, like Foundation and Core Graphics objects, allow use from any thread.
However, many of these have the additional stipulation that the caller is responsible for ensuring only one thread accesses them at the time. They don't protect themselves internally. So if you intend to use them for multiple threads, you frequently have to implement the protection yourself, and the preferred way of doing this is a serial GCD queue.
The best way to find out how your objects expect to be treated is to read the headers. Per object, usually near the initializer, there should be a description of exactly how it expects to be accessed by threads in your app. So, let's go back to my example here.
What do I do in this code? I load some data from a file.
I process and filter an image.
And finally, I put it in image view in my view hierarchy.
So right now when the user taps a button in my app, my main thread looks a little bit like this. It does those three things in order one after another.
Simple, straightforward, easy to understand, great.
Unfortunately, should the user happen to try and scroll or rotate right when I am in the middle of doing this, we won't be able to service that input until later.
And the thing with blocking calls is you never really know how long they are going to take. It's sort of like the weather. So the user is going to be kept waiting for some unknown amount of time, and that will make them sad.
So how do we fix this? Well, we can use Grand Central Dispatch, and we can use a Grand Central Dispatch API called 'dispatch async.' Now, 'dispatch async' takes two arguments.
The first thing you need to pass in is which queue you want it to use. As I mentioned, there's already several queues in your app that GCD has created for you. And I am going to get one of them using the 'dispatch get global queue' API.
Because there are several, I have to actually tell GCD which one I want, and here I am going to use a 'Quality of Service' class.
'Quality of Service' is how you tell the system how important the work you are asking it to do is relative to both other work on your app and work across the rest of the system. In this case, because this is the direct result of a user action and the user is probably waiting for the result, I am going to use the 'user-initiated' QOS class. The final argument to 'dispatch async' is a closure containing the code you would like it to run. So, great. I am done. It's off the main thread. Right? Not quite.
As I mentioned, UIKit views and controllers are only safe for use on the main thread, so I can't put them in this closure. So that wasn't really the particularly slow part of my code anyway, right? The first two lines were the blocking calls.
Why don't I just move this back out onto the main thread? Unfortunately, that's not quite going to work either because I have actually changed the order that this code executes in.
This closure is not guaranteed to have run by the time 'dispatch async' returns. Hopefully it will run quite shortly afterwards. But most likely, once GCD has fired the work off to the dispatch queue, it will immediately move on to the next line, and at this point, my image is still likely to be nil. The user will never see their image. That will also make them sad. So how do we fix this? Well, we can actually make another call to 'dispatch async, and this time we'll use a very special queue called the main queue. The main queue is guaranteed to be serviced by the main thread. You can get it using the API dispatch 'get main queue.' What that means is if you have objects that require access on the main thread, you can still put them in a closure and pass it to dispatch. You just have to make sure they run on this queue. So I have done that here and now my imageView is safe and happy.
With this, we've moved that work off the main thread, but then when we've needed to use objects that must be accessed on the main thread, we have done so once the data is ready for them.
And by the way, the original problem we were trying to solve, should the user try and scroll or rotate, that will get serviced right away, and they will be happy.
So what types of blocking calls are you likely to find in your code? They are sneaky and they hide in all kinds of places. As I mentioned, networking. NSURLConnection and friends. It's very easy to, without meaning to, make a synchronous call to the network.
Usually you can switch to asynchronous API, or if you actually want even more control over when your application accesses the network -- and in some cases even let it download things when it's not running -- I encourage you to check out the NSURLSession background session.
Foundation initializers. When you come across these in your code, they don't look that scary. It's one line. How bad can it be? But actually, some of these, like ones with the name 'contents of file' or 'contents of URL,' may end up having to use the disk or other resources to service your request.
And finally, Core Data. Again, they just look like objects, not bad, right? Core Data frequently does a lot of I/O on your behalf.
Luckily, it's pretty easy to move some of your heavy recorded operations to different concurrency modes. And actually, there's great new API in Core Data this year to simplify that and other common bulk operations. You can go learn about it in the session from the other day, "What's New in Core Data." So if you find a blocking call, switch to asynchronous API or use GCD. If you want to know more about GCD, including, again, new API this year that's going to simplify common operations and also the quality of service classes that I mentioned earlier, there's a great talk from just an hour ago called "Building Responsive and Efficient Apps with GCD," and I encourage you to go watch it.
Let's move on to memory.
Like I said, memory is the most precious resource on mobile devices. If you are planning to adopt multitasking in your app, again, you don't have full run of the screen anymore, so it stands to reason that you also don't have full run of the rest of the system's resources. If you are going to bring some of the code from your app to watchOS, it's especially important that its memory footprint be compact. Once again, iOS 9 supports a huge range of hardware. If you want to bring your great apps and features to some of the lower-end devices supported by the OS, memory is extremely important on those systems. And finally, if you are the developer of an extension, think about the fact that your extension may now be called on to run when there are two other apps on the screen.
So memory will be in high demand, and you need to be able to get away with using as little as possible. So let's get a little bit into how memory works on iOS.
Ground rules. There is not enough physical memory on any iOS device to keep all the suspended apps in RAM at the same time.
When we come under memory pressure, we actually have to evict things to make room for the foreground app.
On OS X or a PC operating system, we might write the state of those apps out to disk first, but it turns out that doesn't make sense on mobile devices, so when you get evicted, you are just gone. There's lots more detail here and lots to get into, and there's actually a great talk from a few years ago called "iOS App Performance Memory." The slide template and colors are a little different, but the information is really solid, so please go watch that one if you are interested to learn more about how this works.
But if you've never thought about memory in your app before, it boils down to this -- reclaiming memory takes time. If you've already used up all the available memory in the system and then you need some more, you might be kept waiting while the system evicts things on your behalf.
If you suddenly request a large amount of memory, the system might need to evict several different things to service your request, and that can influence the responsiveness of your app.
Conversely, when you are in the background, if your footprint is very compact, it's actually less likely that you will be one of the things that gets evicted. And so when the user then returns to your app, you will be able to resume instead of launching, and that will feel a lot faster. So if you've never thought about memory before, it's really important as a first step to rationalize your app's memory footprint. And what that means is to think about the types of resources that it uses.
These might be strings; maybe long blobs of JSON or XML that you downloaded from the network; images that, again, came from the network or the user took them with the camera; and again, Core Data managed objects, which use a lot of underlying resources to sort of make the magic happen. Once you've thought about these resources, you can start to group them according to which user interactions depend on each kind, and that helps you establish a mental model of the resources that your app uses.
Once you've done that, we can quickly check our work using the Xcode debugger.
To get into more detail, we would reach for the Allocations and Leaks Instruments. I am not going to go into those today, but please check out this talk from last year called "Improving Your App with Instruments" to get started with those. So let's go back to the Xcode debugger. I have downloaded the Photos framework example app from the Apple Developer website. I've plugged in my phone, opened the Xcode project, and hit build and run. And now I am just going to look at the top left of my Xcode window at the debugger. Zoom in on that.
So, here, right away I can see without touching my phone the first number that's interesting to me.
Now I know that right after my app launches, before the user does anything, I am using about 10 megabytes of memory.
The next data point that I want to collect is that I want to go and do whatever the most common user interaction in my app is, so because this is a Photos app, I am going to open a photo.
And now I've learned that to open a photo, my app needs about another 2.5 megabytes of memory.
At this point, an additional interesting experiment is to repeat the same action over and over. So I might open the same photo or a couple of different photos a few times.
If my memory footprint continues to grow, I may have a memory issue worth investigating.
The final point of interest here is I am going to use the Home button on my device to suspend the app, and I am going to see what happens when it goes into the background. And it looks like it shrank down to actually a little bit smaller than it was right after it launched.
This is a great balance to strike.
You don't want to repeat work that you did during launch on resume, but you also want to stay compact in the background to make sure your users actually get to experience that resume. Important to note that the Photos framework example app didn't actually have to do anything special or magical to achieve that behavior. It's actually just a really straightforward and minimal implementation of Apple technologies, and Apple technologies actually have this behavior often built in, and they manage sort of their underlying resources in response to application lifecycle events automatically. So you don't need to worry about it.
However, if you have large objects or other resources of your own that you'd like to sort of dynamically lose and get back in response to application lifecycle events, the easiest way to do it is to use NSCache.
In some cases, though, you might have things that can't be neatly represented as evictable objects for NSCache, and then you have to actually implement custom code that responds to system lifecycle notifications in your app.
A few that you might be interested in are the 'did enter background' notification. Your app will get this when it's suspended, and this is what NSCache uses to actually shrink when you go into the background.
Another interesting one is the memory warning notification. The system actually sends this before it starts evicting suspended apps to give them a chance to shrink, and maybe they can avoid getting evicted if their footprint goes down. So here is a quick example of that. I am going to use the default NSNotificationCenter. I am going to add an observer for, in this case, the 'did receive memory warning' notification. And all I am going to do is call some 'custom cache purging' code.
Maybe this walks a linked use of C structures and frees some other memory. An important note, if you do register for NSNotificationCenter observer, especially in init, please be sure to remove yourself on deinit.
So memory is actually so important that there's another talk I am going to encourage you to go watch.
It's called "Optimizing Your App for Multitasking on iPad in iOS 9." But actually, even if your app doesn't run on iPad, or you have no plans to support Multitasking, please go watch this talk. They go into a lot of detail about the types of resources that applications use, the patterns they typically access them in, and more information about how to have your app respond to system memory state. It's really great. Last but not least, I hope you are all really excited to bring native code to watchOS 2. When you are thinking about how to build your watchOS 2 app, you have to start with a great design, a design that really focuses on the essential functions of your app and makes them easy, delightful, and accessible to the user.
If you need help with that, there's a great session you can go watch called "Designing for Apple Watch." Once you've got a great design for your Apple Watch app, then you can start to think about which aspects of your iOS app it might make sense to reuse.
This could include actual code or familiar access patterns to APIs and frameworks that are shared between the platforms. Sometimes something you are doing on iOS might actually not make sense on watchOS for performance reasons. And you will end up implementing new mechanisms to achieve the same result on the other platform. watchOS users expect short, simple interactions, and they expect to always see recent and relevant data in Apps, Notifications, and Glances.
What does this mean for you as an app developer? The most likely thing the user will do on watchOS is just launch the app and look at the one thing it shows immediately afterwards.
So what are some things we can do to get great launch time and great responsiveness on watchOS? Focus on minimizing both the amount of network traffic you generate and the amount of work you have to do on the device to do something sensible with it.
If you are accessing a server that you can control and add new APIs to, make sure you are sending appropriately sized and formatted responses down to the Watch.
This can include something as simple as removing unused keys from JSON or XML blobs; resizing images so that the Watch can just display them exactly as they came over the wire and doesn't have to do any extra work; and if your API is accustomed to feeding devices with large screens that can show 10 or 20 records at a time, it may be sending back all that information in one call. But actually, for the Watch, you should send only the appropriate number of records needed to display a single screen.
To show fresh, relevant information all the time, it's important to use your iPhone app to keep the app context updated. The app context is a piece of bidirectional shared state between the platforms. So when the user takes an action on either end that will cause them to expect to see something different on the other end, that can be updated. The API for doing this is 'watch connectivity update application context.' A great time to do this is when your iPhone app gets woken up by background app refresh.
When it's done downloading new information and updating its own snapshot, it can also push that information over to the Watch so that it will be ready next time the user launches.
Finally, in case you're relying on a server that you can't change for some reason, let's say you're hitting a third-party API, you can use the power of your iPhone's network connection and CPU to actually implement an intermediary that formats and sizes responses for the Watch.
The API you would use to do this is 'watch connectivity send message.' So you would send a message to the iPhone, requesting whatever you need, and the iPhone would download it, and do all the operations I mentioned, remove unused keys, reduce the number of records, resize images.
Then it could send a compact and actionable reply to the Watch, again, over WC Session. So wrapping up, performance is a feature. It's an essential aspect of giving your users a great experience in your apps. And it should be on your mind from day one when you are building your apps.
Efficient apps feel great when you are using them, they build your users' trust, and they save battery power.
Please go learn about all the Apple technologies I mentioned, and when you are thinking about building your app, choose the best ones.
Keep your main thread always ready for user input.
Understand when and why your app uses memory.
And to get a great experience on watchOS, download and process a minimal set of information. Here's some great written documentation you can get into if you are starting to get interested in this stuff.
And again, here are the sessions that I mentioned.
The first few are about technologies that we covered this year, and there's a few from previous years as well. Thanks, and have a great Friday. [Applause]
Looking for something specific? Enter a topic above and jump straight to the good stuff.
An error occurred when submitting your query. Please check your Internet connection and try again.