How to Use Instruments in Xcode-(转载)

This is a blog post by iOS Tutorial Team member Matt Galloway, founder of SwipeStack, a mobile development team based in London, UK. You can also find me on Google+.

Learn how to troubleshoot and optimize your code with Xcode Instruments!

Learn how to troubleshoot and optimize your code with Xcode Instruments!

At this point in your iOS development career, you’ve probably written an app or two, and you are no doubt wondering what you can do to make your apps even better.

Besides improving your app by adding features, there is one thing that all good app developers should do… instrument their code!

This tutorial will show you how to use the most important features of the tool called Instruments that ships with Xcode. It allows you to check your code for performance issues, memory leaks, or other problems.

In this tutorial you’re going to learn:

  • How to detect and fix memory management issues in your code using the allocations and leaks instruments, and
  • How to determine hot-spots in your code using the time profiler instrument and how to make your code more efficient.

Note: This tutorial assumes that you are competent in Objective-C and iOS programming. If you are a complete beginner to iOS programming, you may wish to check out some of the other tutorials on this site. This tutorial makes use of a storyboard, so make sure you’re familiar with the concept; a good place to start is with the tutorial on this site.

This tutorial will be using Xcode 4.5, so make sure you’re fully updated to the latest version, which is available through the Mac App Store.

All set? Get ready to dive into the fascinating world of Instruments! :]

Getting Started

For this tutorial you won’t go through the process of creating an application from scratch; instead, a sample project has been provided for you. Your task is to go through the application and improve it using Instruments as your guide — very similar to how you would go about optimizing your own apps!

Download the starter project then unzip it and open it up in Xcode.

Build and run the app, perform a search, click the result, and you’ll see something like the following:

The starter project for this tutorial

Browse through the application and check out the basic functions. As you can see, the core feature of the app is to search and display photos on Flickr. There is a search bar at the top of the app, and when you perform a search, a new row of results appear in the table.

This new row in the table displays the search term, and the number of results found in parentheses. If you tap on a cell, the search results expand and present you with another table, displaying the image titles along with preview images.

If you tap on one of the preview results, the app takes you to a full screen view of the image. From that view, you can then rotate the image if desired.

So far so good! :] You can see that the app works as designed. You might be tempted to think that once the UI looks great the app is ready for store submission. However, you’re about to see the value that using Instruments can add to your app.

The remainder of this tutorial will show you how to find and fix the issues that still exist in the app. You’ll see how Instruments can make debugging problems a whole lot easier ! :]

Time for Profiling

Lots of developers start out with a vague idea that their app should go fast – and it’s a worthy aim. Then they read about something called “premature optimisation” and wonder how this terrible thing that the greybeard programmers frown at might be avoided. In the worst case, the novice developers forget about optimisation altogether!

To some extent, you can leave optimisation out of your app development process; only ten years ago, mobile devices were incredibly limited, and even the use of floating point numbers was forbidden because it made code size larger and calculations move at glacial speed.

Now, you hold an incredible amount of power in your pocket, complete with 3D hardware good enough to beat the best desktop hardware of not so long ago. But you can’t always depend on hardware and processor speed to gloss over the inefficient bits in your app.

So what is “profiling”, anyway? Profiling is a means of measuring. The output of a profiling session provides insight into which parts of your code are used most often; in turn, that tells you which parts of the code you should try to improve.

You can spend a week fine-tuning an interesting algorithm, but if that code only occupies 0.5% of total execution time, nobody will ever notice the difference, no matter how much you improved it. If instead you spent the effort optimising the loop where your program spends 90% of its time, and made only a 10% improvement, chances are your update will attract five star reviews because it will feel so much faster!

The first lesson of optimisation is: find the right places to do it! :]

Premature optimisation, you will have guessed by now, is spending time optimising the bits that really don’t matter in the end.

The first instrument you’ll look at is the “Time Profiler”. At measured intervals, the execution of the program is halted, and a stack trace is performed on each running thread. Think of it as pressing the pause button in Xcode’s debugger.

Here’s a sneak preview of the Time Profiler:



This screen displays the call stack of each thread. Each level, or frame, as it is called, is a different method the program’s execution path has followed to arrive at the point where the CPU is currently executing the code – that is, frame 0.

The time spent in each method can then be determined from the number of times the profiler is stopped in each method.

For instance, if 100 samples are done at 1 millisecond intervals, and a particular method is found to be at the top of the stack in 10 samples, then you can deduce that approximately 10% of the total execution time — 10 milliseconds — was spent in that method. It’s a fairly crude approximation, but it works!

So without any further ado, time to get instrumenting!

From Xcode’s menu bar, select ProductProfile, or press ⌘I. This will build the app and launch Instruments. You will be greeted with a selection window that looks like this:

These are all different templates that come with Instruments.

Select the Time Profiler instrument and click Profile. This will launch the iOS simulator and start the app. You may be asked for your password to authorise Instruments to analyse other processes — fear not, it’s safe to provide here! :]

In the Instruments window, you can see the time counting up, and a little arrow moving from left to right above the graph in the center of the screen. This indicates that the app is running.

Now, start using the app. Search for some images, and drill down into one or more of the search results. You have probably noticed that going into a search result is tediously slow, and scrolling through a list of search results is also incredibly annoying – it’s a terribly clunky app!

Well, you’re in luck, for you’re about to embark on fixing it! However, you’re first going to get a quick run down on what you’re looking at in Instruments.

First, make sure the view selector in the toolbar has all three options selected, like so:

That will ensure that all panels are open. Now study the screenshot below and the explanation of each section beneath it:


  1. These are the recording controls. The middle red button will stop & start the app currently being profiled when it is clicked. This is actually stopping and starting the app — not pausing it.
  2. This is the run timer and run navigator. The timer counts how long the app being profiled has been running. The arrows move between runs. If you stop and then restart the app using the recording controls, that would start a new run. The display would then show “Run 2 of 2”, but you could get back to the data of the first run by first stopping your current run, then pressing the left arrow to go back.
  3. This is called a track. In the case of the time profiler template you selected, there’s just one instrument so there’s just one track. You’ll learn more about the specifics of the graph shown here later in the tutorial.
  4. This is the extended detail panel. In the case of the time profiler instrument, it’s used to show stack traces, as that is what Instruments is recording.
  5. This is the detail panel. It shows the main information about the particular instrument you’re using. In this case, it’s showing the methods which are “hottest” — that is, the ones that have used up the most CPU time.

    If you click on the bar at the top which says “Call Tree” (the left hand one) and select “Sample List”, then you are presented with a different view of the data. This view is showing every single sample. Click on a few samples, and you’ll see the captured stack trace appear in the extended detail panel.

  6. This is the options panel. You’ll be learning more about these options shortly.

Now onto fixing the clunky UI! :]

Drilling Deep

Perform an image search, and drill into the results. I personally like searching for “dog”, but choose whatever you wish – you might be one of those cat people! :]

Now, scroll up and down the list a few times so that you’ve got a good amount of data in the time profiler. You should notice the numbers in the middle of the screen changing and the graph filling in; this tells you that CPU cycles are being used.

You really wouldn’t expect any UI to be as clunky as this; no table view is ready to ship until it scrolls like butter! To help pinpoint the problem, you need to set some options on the time profile perspective.

Under the Call Tree section on the left, select Separate by ThreadInvert Call TreeHide System Libraries and Show Obj-C Only. It will look like this:

Here’s what each option is doing to the data displayed in the table to the right:

  • Separate by Thread: Each thread should be considered separately. This enables you to understand which threads are responsible for the greatest amount of CPU use.
  • Invert Call Tree: With this option, the stack trace is considered from top to bottom. This means that you will see the methods in the table that would have been in frame 0 when the sample was taken. This is usually what you want, as you want to see the deepest methods where the CPU is spending its time.
  • Hide Missing Symbols: If the dSYM file cannot be found for your app or a system framework, then instead of seeing method names (symbols) in the table, you’ll just see hex values. These correspond to the address of the instruction within the binary code. If this option is selected, then these are hidden, and only fully resolved symbols are displayed. This helps to declutter the data presented.
  • Hide System Libraries: When this option is selected, only symbols from your own app are displayed. It’s often useful to select this option, since usually you only care about where the CPU is spending time in your own code – you can’t do much about how much CPU the system libraries are using!
  • Show Obj-C Only: If this is selected, then only Objective-C methods are displayed, rather than any C or C++ functions. There are none in your program, but if you were looking at an OpenGL app, it might have some C++, for example.
  • Flatten Recursion: This option treats recursive functions (ones which call themselves) as one entry in each stack trace, rather than multiple.
  • Top Functions: Enabling this makes Instruments consider the total time spent in a function as the sum of the time directly within that function, as well as the time spent in functions called by that function. So if function A calls B, then A’s time is reported as the time spent in A PLUS the time spent in B. This can be really useful, as it lets you pick the largest time figure each time you descend into the call stack, zeroing in on your most time-consuming methods.

Although some values may be slightly different, the order of the entries should be similar to the table below once you have enabled the options above:

Well, that certainly doesn’t look too good. The vast majority of time is spent in the table cell that sets the photo. That shouldn’t come as too much of a shock to you, as the table scrolling was the clunkiest part of the UI, and that’s when the table cells are constantly being updated.

To find out more about what’s going on within that method, double click on the row. Doing so will bring up the following view:

Well that’s interesting, isn’t it! Almost three-quarters of the time spent in the setPhoto: method is spent creating the image data for the photo!

Now you can see what the problem is. NSData’s dataWithContentsOfURL blocks (that is, does not return) until the data has been downloaded. Since this request goes out to the internet to grab the data, each call could take up to a few seconds to return. This method is run on the main thread, and therefore the entire UI is blocked from updating whilst the image data is downloaded.

To solve this, a class has been provided called ImageCache which allows asynchronous downloading of images on a background thread. The code exists in the PhotoCell class.

You could now switch to Xcode and manually find the file, but Instruments has a handy “Open in Xcode” button right in front of your eyes. Locate it in the panel just above the code and click it:

There you go! Xcode opens up at exactly the right place. Boom!

Now, comment out the two lines which grab the NSData and set the image, and uncomment the block of code below. The setPhoto method will then look like this:

- (void)setPhoto:(FlickrPhoto *)photo {
    _photo = photo;
 
    self.textLabel.text = photo.title;f
 
//    NSData *imageData = [NSData dataWithContentsOfURL:_photo.thumbnailUrl];
//    self.imageView.image = [UIImage imageWithData:imageData];
 
    [[ImageCache sharedInstance] downloadImageAtURL:_photo.thumbnailUrl
                                  completionHandler:^(UIImage *image) {
                                      self.imageView.image = image;
                                      [self setNeedsLayout];
                                  }];
}

Re-run the app in Instruments by pressing ProductProfile (or ⌘I – remember, those shortcuts will really save you some time).

Notice that this time you are not asked for which instrument to use. This is because you still have a window open for this app, and Instruments assumes you want to run again with the same options.

Perform a few more searches, and notice that this time the UI is not quite so clunky! The images now load asynchronously and are cached in the background, so once they’ve been downloaded once they do not have to be downloaded again.

Looks great! Is it time to ship it? Not yet! :]

Allocations, Allocations, Allocations

The next instrument covered in this tutorial is the allocations instrument. This gives you detailed information about all the objects that are being created and the memory that backs them; it also shows you retained counts of each object.

The easiest way to start afresh with a new instruments profile is to just close Instruments and start again. Close Instruments, go back to Xcode and select ProductProfile again. Then select Allocations from the picker and click Profile.

Allocations

Instruments will open once again and you’ll be greeted with the following:



This time you’ll notice two tracks. One is called Allocations, and one is called VM Tracker. The Allocations track will be discussed in detail in this tutorial; the VM tracker is also very useful, but is a bit more complicated.

So what bug are you going to track down next? :]

There’s something hidden in the project that you probably don’t know is there. You’ve likely heard about memory leaks. But what you may not know is that there are actually two kinds of leaks.

The first is the true memory leak, where an object has not yet been deallocated, but is no longer referenced by anything. Therefore the memory can never be re-used.

The second kind of leak is a bit more tricky. It’s called “unbounded memory growth”. This happens where memory continues to be allocated and is never given a chance to be deallocated.

If this continues forever, then at some point the system’s memory will be filled and you’ll have a big memory problem on your hands. In iOS this means that the app will be killed by the system watch dog. This will not lead your app to five-star ratings! :]

Set up a scenario where you can detect unbounded memory growth. First, make 10 different searches in the app (but do not drill into the results yet). Make sure the searches have some results! Now let the app settle a bit by waiting a few seconds.

You should have noticed that the graph in the allocations track has been rising. This is telling you that memory is being allocated. It’s this feature that will guide you to finding unbounded memory growth.

What you’re going to perform is a “heap shot analysis”. To do this, press the button called “Mark Heap”. You’ll find the button on the left side of the detail panel:



Press it and you will see a red flag appear in the track, like so:



The purpose of heap shot analysis is to perform an action multiple times, and see if memory is growing in an unbounded fashion. Drill into a search, wait a few seconds for the images to load, and then go back to the main page. Then mark the heap again. Do this repeatedly for different searches.

After a drilling into a few searches, Instruments will look like this:



At this point, you should be getting suspicious. Notice how the blue graph is going up with each search that you drill into. If you continue this for all 10 searches, you’ll end up with a graph that looks like this:



Well, that certainly isn’t good. But wait, what about memory warnings? You know about those, right? Memory warnings are iOS’s way of telling an app that things are getting tight in the memory department, and you need to clear out some memory.

It’s possible that this growth is not just due to your app; it could be something in the depths of UIKit that’s holding onto memory. Give the system frameworks and your app a chance to clear their memory first before pointing a finger at either one.

Simulate a memory warning by selecting HardwareSimulate Memory Warning in the iOS simulator’s menu bar. You’ll notice that memory usage dips a little, but certainly not back to where it should be. So there’s still unbounded memory growth happening somewhere.

The reason for doing a heap shot after each iteration of drilling into a search is that you can see what memory has been allocated between each shot. Take a look in the detail panel and you’ll see a bunch of heap shots.

Hit Me With Your Best Shot

The first is the baseline shot. Open that up you’ll see all the objects that were allocated and still resident at the time that heap shot was taken. Subsequent heap shots will contain just the objects between the previous heap shot and the current one.

Look at the “Heap Growth” column and you’ll see that there is definitely growth occurring somewhere. Open up one of the heap shots and you’ll see this:



Wow, that’s a lot of objects! Where do you start?

The best thing to do is to look through the list for the classes that you use in your app directly. In this case, HTTPHeaderDict, CGRegion, CGPath, CFNumber, etc can all be ignored for now.

However, the one that stands out is UIImage, as that’s certainly something that is dealt with in your app. Click on the arrow on the left of UIImage to display the full list. Select one and look at the extended detail panel:

This shows you a stack trace at the point when this specific UIImage object was created. The parts of the stack trace in grey are in system libraries; the parts in black are in your app’s code. To get more context for this trace, double click on the only black frame that is in a method of the ImageCache class. This will take you to the code for that method, which looks like this:

- (void)downloadImageAtURL:(NSURL*)url completionHandler:(ImageCacheDownloadCompletionHandler)completion {
    UIImage *cachedImage = [self imageForKey:[url absoluteString]];
    if (cachedImage) {
        completion(cachedImage);
    } else {
        dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
            NSData *data = [NSData dataWithContentsOfURL:url];
            UIImage *image = [UIImage imageWithData:data];
            [self setImage:image forKey:[url absoluteString]];
            dispatch_async(dispatch_get_main_queue(), ^{
                completion(image);
            });
        });
    }
}

Instruments is pretty useful, but it can help you no further in this case! You’re now going to have to work through the code yourself in order to understand what’s going on.

Take a look through the method above, and you’ll see it calling a method called setImage:forKey:. This method caches an image in case it is used again later on in the app. Ah! Well that certainly sounds like it could be a problem! :]

Take a look at the implementation of that method:

- (void)setImage:(UIImage*)image forKey:(NSString*)key {
    [_cache setObject:image forKey:key];
}

This adds an image to a dictionary which is keyed on the URL the image came from. But if you look through the code, you’ll notice that the image is never cleared from that dictionary!

That’s where your unbounded memory growth is coming from! Everything is working as it should, but the app never removes things from the cache — it only ever adds them!

To fix the problem, all you need to do is have ImageCache listen to the memory warning notification that UIApplication fires. When ImageCache receives this, it must be a good citizen and clear its cache.

To make ImageCache listen to the notification, modify the init method to look like this:

- (id)init {
    if ((self = [super init])) {
        _cache = [NSMutableDictionary new];
        [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(memoryWarning:) name:UIApplicationDidReceiveMemoryWarningNotification object:nil];
    }
    return self;
}

This registers the UIApplicationDidReceiveMemoryWarningNotification to execute thememoryWarning: method. Now implement that method as below:

- (void)memoryWarning:(NSNotification*)note {
    [_cache removeAllObjects];
}

All that memoryWarning does above is remove all objects in the cache. This will ensure that nothing is holding onto the images any more and they will be deallocated.

To test this fix, fire up Instruments again (from Xcode with ⌘I) and repeat the steps you followed previously. Don’t forget to simulate a memory warning at the end!

Note: Make sure you launch from Xcode, triggering a build, rather than just hitting the red button in Instruments, in order to make sure you’re using the lastest code.

This time the heap shot analysis should look like this:



This time, the memory usage dropped dramatically after the memory warning. There’s still some memory growth overall, but nowhere near as much as before.

The reason there’s still some growth is really due to the system libraries, and there’s not much you can do about those. It appears that the system libraries are not freeing all of their memory, which may be by design or may be a bug. All you can do in your app is free up as much memory as possible, and you’ve already done that! :]

Well done! One more issue patched up! It must be time to ship by now! Oh, wait – there’s still the issue of the first type of leak that you haven’t yet addressed.

Call the Plumber — You’ve Got a Leak!

The next instrument you’ll be looking at is the Leaks instrument. This is used to find the first kind of leak mentioned earlier – the kind that occurs when an object is no longer referenced by anything, and eats up memory.

Detecting leaks is understandably a very complex affair, but the leaks tool remembers all objects that have been allocated and periodically scans through each object to determine if any cannot be accessed from any other object.

Close Instruments, go back to Xcode and select ProductProfile. Select the Leaks template and click Profile:

Leaks

Instruments will fire up with 2 tracks – Allocations and Leaks. The allocations track is the same one that comes in the allocations template you used in the previous section.

You’ll only be concerned with the leaks track in this section, so click on that track to highlight it. Notice that the details you see in the rest of the Instruments window now change to reflect the state of that track.

In the detail panel, you’ll see that Automatic Snapshotting is turned on. This means that leaks are detected every so often automatically.

The interval between snapshots can be changed, but the default of 10 seconds is good enough for your purposes right now. You can force a snapshot at any time you wish by pressing the Snapshot Now button.

Back to your app! Perform a search and drill into the results. Then tap on one of the result preview rows to open the full screen viewer. Press the Rotate button at the top left, then press it again.

Go back to Instruments and wait for a moment. If you’ve done the above steps correctly, you’ll notice a leak has appeared! Your Instruments window will look like this:

Go back to the simulator and press Rotate a few more times. Then go back to Instruments and wait. Another few leaks should appear, looking like this:



Where’s the leak coming from? If the extended detail panel (on the right) isn’t open, remember the view selector control can get you there:

Open up the CGContext list in the extended detail panel. Select one of the CGContext elements in the list and look at the extended detail panel, which shows the stack trace that caused the object to be created, as below:



Once again, the frames that relate to your code are shown in black. Since there is only one, double click on it to see the code for that method.

The method in question is rotateTapped:, which is the handler called when the Rotate button is tapped. This method rotates the original image and creates a new image, as below:

- (void)rotateTapped:(id)sender {
    UIImage *currentImage = _imageView.image;
    CGImageRef currentCGImage = currentImage.CGImage;
 
    CGSize originalSize = currentImage.size;
    CGSize rotatedSize = CGSizeMake(originalSize.height, originalSize.width);
 
    CGContextRef context = CGBitmapContextCreate(NULL,
                                                 rotatedSize.width,
                                                 rotatedSize.height,
                                                 CGImageGetBitsPerComponent(currentCGImage),
                                                 CGImageGetBitsPerPixel(currentCGImage) * rotatedSize.width,
                                                 CGImageGetColorSpace(currentCGImage),
                                                 CGImageGetBitmapInfo(currentCGImage));
 
    CGContextTranslateCTM(context, rotatedSize.width, 0.0f);
    CGContextRotateCTM(context, M_PI_2);
    CGContextDrawImage(context, (CGRect){.origin=CGPointZero, .size=originalSize}, currentCGImage);
 
    CGImageRef newCGImage = CGBitmapContextCreateImage(context);
    UIImage *newImage = [UIImage imageWithCGImage:newCGImage];
 
    self.imageView.image = newImage;
}

Again, Instruments can only give you a hint here as to where the problem lies; it can’t tell you exactly where the leak is. It’s only able to show you where the leaked object was created. It’s then your job to work out what the problem is! :]

You may think that ARC should have taken care of all the memory management, and that there couldn’t possibly be a leak in the code…right?

Recall that ARC only deals with Objective-C objects. It doesn’t manage the retain and release of CoreFoundation objects which are not Objective-C objects.

Ah, now it’s starting to become obvious what the problem is — the CGContextRef and CGImageRef objects are never released! To fix that, add the following two lines of code at the end of the rotateTapped method:

CGImageRelease(newCGImage);
CGContextRelease(context);

These two calls are required to balance out the retain counts of these two objects. The moral of this story is that you still need to know about reference counting — even if you use ARC in your project!

From within Xcode, use ⌘I again to build and run the app in Instruments.

Look at the app again in Instruments using the leaks instruments and see if that leak’s been fixed. If you’ve followed the above steps correctly, the leak should be gone!

Well done! SHIP IT! :]

Where to Go From Here?

Now that you have this knowledge under your belt, go and instrument your own code and see what interesting thigs appear! Also, try to make Instruments a part of your usual development work flow.

You should be running your code through Instruments relatively often, and performing a full sweep of your app before release to ensure that you’ve caught as many memory management and performance issues as possible.

Now go and make some awesome – and efficient – apps! :]

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值