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 5 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!

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.

Adam Eberbach

Contributors

Adam Eberbach

Author

Over 300 content creators. Join our team.