Sponsored Tutorial: Improving Your App’s Performance with Pulse.io

Learn how you can use Pulse.io to notify you of low frame rates, app stalls and more. Let us walk you through all the features in this Pulse.io tutorial. By Adam Eberbach.

Leave a rating/review
Save for later
Share
You are currently viewing page 3 of 4 of this article. Click here to view the first page.

Memory

The final button in the dashboard is Memory. Click it and you’ll see your app’s memory usage as demonstrated below:

before-memory

That feels like a lot of memory for a simple app like yours. iOS is likely to terminate apps using large chunks of memory when resources get low, and making a user restart your app every time they want to use it won’t be a pleasant user experience.

This could be due to the loading of the images as you noted before, so draw a few asterisks next to the item on your fix list that addresses the loading of large images.

Fixing the Issues

Now that you’ve drilled down through the pertinent data points of your app, it’s time to address the issues exposed by Pulse.io.

A likely place to start is the image fetching strategy. Right now the app fetches a thumbnail image for each sight discovered and then pre-fetches the large image in case the user taps on the pin to view it. But not every user will tap on every pin.

What if you could defer the large image fetch until it is actually needed; that is, when the user taps the pin?

Correcting the Image Loading

Find the implementation of thumbnailImage in Sight.m. You’ll see that you make two network requests: one for the thumbnail and one for the large image.

Replace the current implementation of thumbnailImage with the following:

- (UIImage *)thumbnailImage {
  if (_thumbnail == nil) {
    NSString *urlString = [NSString stringWithFormat:@"%@_s.jpg", _baseURLString];
    AFImageRequestOperation* operation
    = [AFImageRequestOperation
       imageRequestOperationWithRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:urlString]] imageProcessingBlock:nil
       success:^(NSURLRequest *request, NSHTTPURLResponse *response, UIImage *image) {
         self.thumbnail = image;
         [_delegate sightDidUpdateAvailableThumbnail:self];
       }
       failure:nil];
    [operation start];
  }
  return _thumbnail;
}

This looks very much like the original method – it contains an AFImageRequestOperation whose success block notifies the delegate MapSightsViewController that the thumbnail is available.

You’ve removed the code that kicks off the full image download. So next, you’ll need to load the large image only when the user drills down into the annotation. Find initiateImageDisplay and replace it with the following code:

- (void)initiateImageDisplay {
  if (_fullImage) {
    [_delegate sightDisplayImage:self];
  } else {
    [_delegate sightBeginningLargeImageDownload];
    NSString *urlString = [NSString stringWithFormat:@"%@_b.jpg", _baseURLString];
    AFImageRequestOperation* operation = [AFImageRequestOperation
      imageRequestOperationWithRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:urlString]]
      imageProcessingBlock:nil success:^(NSURLRequest *request, NSHTTPURLResponse *response, UIImage *image) {
        [_delegate sightsDownloadEnded];
        self.fullImage = image;
        [_delegate sightDisplayImage:self];
      } failure:^(NSURLRequest *request, NSHTTPURLResponse *response, NSError *error) {
        [_delegate sightsDownloadEnded];
    }];
    [operation start];
    return;
  }
}

This loads the image the first time it’s requested and caches it for future requests. That should reduce the number of network requests for images — with the added bonus of reduced memory usage. Correcting both of those issues should help reduce the amount of spinner time users need to suffer through! :]

Since you’re already fixing network related items, you may as well strip the sensitive bits from the http requests while you’re at it.

Fortunately this is a simple one-line fix. Add the following line after the call to monitor: in main.m:

[PulseSDK setURLStrippingEnabled:true];

This prevents all query parameters from being logged in the Network dashboard, which helps keep your Flickr API keys secret.

Fixing Routing Performance

The next thing on your list of things to fix is the algorithm that calculates the route between the various sights. It’s easy to underestimate the complexity of finding the shortest route between an arbitrary number of points. In fact, it is one of the hardest problems encountered in computer science!

Note: Better known as the Travelling Salesman Problem, this type of algorithm is a great example of the class of problems known as NP-hard. In fact, if you find a fast, general solution to this problem while working through this tutorial, there may be a million dollar prize awaiting you!

This app uses a brute force method of finding the shortest route by calculating the complete route many times and saving the shortest one. If you think about it, though, there’s no real requirement to show the shortest route through all points — you can just display any route and let the user vary the route if they feel like it. The time spent waiting for the optimal route just isn’t worth it in this case.

Take a quick look at orderLocationsInRange:byDistanceFromLocation: in PathRouter.m; you can see that it currently orders the discovered paths in a random fashion. A reasonably good route can be found by starting at one point and visiting the next closest point, repeating until all points are visited.

It’s quite unlikely that this is going to be even close to the optimal route, but the potential gains in performance make this approach your best option.

Inside the else clause in this method, replace the call to sortedArrayUsingComparator: (including the block passed to it) with the following code:

NSArray *sortedRemainingLocations = [[self.workingCopy subarrayWithRange:range] sortedArrayUsingComparator:^(id location1, id location2) {
  CLLocationDistance distance1 = [location1 distanceFromLocation:currentLocation];
  CLLocationDistance distance2 = [location2 distanceFromLocation:currentLocation];
  
  if (distance1 > distance2) {
    return NSOrderedDescending;
  } else if (distance2 < distance1) {
    return NSOrderedAscending;
  } else {
    return NSOrderedSame;
  }
}];

Now find orderPathPoints: and take a look at the for loop in there. It currently tries 1000 iterations to find the best route.

But this new algorithm only needs one iteration, because it finds a decent route straight away. 1000 iterations down to 1 - nice one! :]

Find the following lines and remove them:

for (int i = 0; i < 1000; i++) {
  if ([locations count] == 0) continue;

Then find the corresponding closing brace and remove it also. (The brace to remove is just above the line that reads // calculation of the path to all the sights, without blocking the main (UI) thread).

This change cuts the path algorithm down to one iteration and should reduce spinner time even further.

That takes care of the excess spinner time. Next up are those pesky frame rate issues uncovered by Pulse.io.

Fixing the Frame Rate

iOS tries to render a frame once every sixtieth of a second, and your apps should aim for that same performance benchmark. If the code execution to prepare a frame exceeds ~1/60 second (less the actual time to display the frame), then you'll end up with a reduced frame rate.

If you're only slowed down by one or two frames per second most users won't even notice. However, when your frame rate drops to 20 frames/second you can bet most users will find it highly annoying. Using Pulse.io to track your frame rate keeps you ahead of your users and lets you detect slow frame rates before they are noticed by too many users.

One of the changes you made to the app was adding the label Providing Annotation View to a user action. The dashboard showed that slow frame rates were taking place in this specific user action. Pulse.io tells you exactly what your users are experiencing so you don't have to guess whether or not smooth scrolling on older devices is something you need to handle in your app design.

Map views like the one in this app require multiple annotation views to work together to provide smooth scrolling performance. Map Kit includes a reuse API since reusing an annotation is much faster than allocating a new one every time. Your app isn't reusing annotation views at the moment, which might explain at least some of the performance issues.

Open MapSightsViewController.m and find mapView:viewForAnnotation:. Find the following two lines that allocate new annotations:

  
sightAnnotationView = [[MKPinAnnotationView alloc] initWithAnnotation:annotation reuseIdentifier:kSightAnnotationIdentifier];
sightAnnotationView.canShowCallout = YES;

Replace the above lines with the following implementation:

  
sightAnnotationView = [mapview dequeueReusableAnnotationViewWithIdentifier:kSightAnnotationIdentifier];
if (sightAnnotationView == nil) {
  sightAnnotationView = [[MKPinAnnotationView alloc] initWithAnnotation:annotation reuseIdentifier:kSightAnnotationIdentifier];
  sightAnnotationView.canShowCallout = YES;
}

This mechanism is similar to the way table views or collection view cells get reused and should be somewhat familiar. The new implementation attempts to dequeue an existing annotation and only creates a new annotation if it fails to get one from the map view.

While this change has the least dramatic effect of all the changes made so far, you should always reuse objects whenever UIKit offers you the chance.

Now that you've completed all of the items on your fix list, you need to generate some more analytics in Pulse.io to see how your app performance has improved.

Build and run the app; pick several simulated locations and scroll around the map as an average user would. The question is — will the Pulse.io results show some improvement? Or has all your hard work been for naught?

Adam Eberbach

Contributors

Adam Eberbach

Author

Over 300 content creators. Join our team.