Tải bản đầy đủ
Chapter 17. Code Quality and Distribution

Chapter 17. Code Quality and Distribution

Tải bản đầy đủ

In addition, you can make the debugger automatically jump in the moment the appli‐
cation crashes, allowing you to figure out the cause of the crash.
To add a breakpoint to your application, simply click inside the gray area at the left of
the code. When you do, a small blue arrow will appear, representing the point at
which the program will stop (Figure 17-1).

Figure 17-1. A breakpoint
If you run the application and trigger the code that has the breakpoint, your program
will pause and Xcode will appear, showing the debug view (Figure 17-2).

478

| Chapter 17: Code Quality and Distribution

Figure 17-2. The program, stopped in the debugger
When the debugger is active, a number of things appear:
• The Debug Inspector, at the left of the Xcode window, shows a stack trace, indi‐
cating where in the program the execution has stopped, and which methods were
called to reach this point.
• The debug view appears, and is split into two sections:
— On the left, the list of all local variables is displayed. From here, you can see
the current value of all local variables, as well as access the current object’s
properties in the self variable.
— On the right, the LLDB console appears. From here, you can type commands
for the debugger to interpret. The most useful command is po, which causes
the debugger to print the value of the specified expression.
At the top of the debug view, you can find buttons that control the execution of the
debugger (see Figure 17-3). The most important are the first six:
• The first button closes the debug view.
• The second button enables or disables breakpoints.
Debugging

|

479

• The third button resumes execution of the program.
• The fourth button moves to the next line of code.
• The fifth button steps into the next method call.
• The sixth button continues until the current method returns, and then stops.

Figure 17-3. The debug view’s controls
The debugger is an essential tool for diagnosing problems in your app. Don’t hesitate
to stick a breakpoint in to figure out what your code is actually doing!

Instruments
The Instruments tool tracks the activity of an application. You can monitor just about
every single aspect of an application, from high-level metrics like how much data it’s
transferring over the network down to low-level information about the OpenGL
commands that the app executed in a single frame.
If your app is running slowly, Instruments lets you figure out which part of your
application is responsible for taking up the majority of the time; if your app is con‐
suming too much memory, you can work out what’s responsible for allocating it.
There are two ways to use Instruments. First, you can get a high-level summary of the
behavior of your app in Xcode (see Figure 17-4); if you need more information, you
can launch the separate Instruments app.
To access the high-level summary of how your app is performing, simply run it and
go to the debug navigator. Underneath the app’s name, you’ll find four entries—CPU,
Memory, Disk, and Network—showing the current performance status of the app:
how much of the system’s CPU capacity it’s using, how much total memory, how
much data is being read and written to disk, as well as how much network traffic the
app is getting. When you select these, you’ll be shown a more detailed picture of the
selected aspect.
If you’re testing on a Mac, or on an iOS device—that is, not the
simulator—then you’ll also see energy consumption data. If you’re
on a Mac, you’ll also see iCloud usage data.

480

|

Chapter 17: Code Quality and Distribution

Figure 17-4. Performance data in Xcode
You’ll notice a button labeled “Profile in Instruments” at the top-right corner of the
view. If you click this, Xcode will offer to transfer control of the application to Instru‐
ments, allowing you to gather a more detailed view of the application.
You can use Instruments to profile both the simulator and a real
device. However, the simulator has different performance charac‐
teristics than real devices, and real users don’t use the simulator.
Always test the performance of your app on an actual device before
shipping to the App Store.

To demonstrate, let’s profile the Notes application to identify performance hotspots
when viewing image attachments.
1. Launch the Notes application and select the CPU element in the debug inspector.
2. Click the “Profile in Instruments” button.
3. Xcode will ask if you want to transfer the current session to Instruments, or stop
the current session and launch a new one in Instruments (Figure 17-5). Either
option is useful for our purposes.

Instruments

|

481

Figure 17-5. Transferring the application
4. Instruments will launch, showing the CPU Usage tool (Figure 17-6).

Figure 17-6. Instruments, using the CPU Usage tool
As you use the application, the CPU usage will be logged. We’ll now perform some
tests to determine which methods are taking up most of the time.
1. Open a document. Once the document is open, go to Instruments and press the
Pause button.
2. Look at the Call Tree pane, which takes up the majority of the bottom section of
the window. This window shows the amount of CPU time taken up by each
482

|

Chapter 17: Code Quality and Distribution

thread; additionally, you can dive into each thread to find out which methods
took up the most CPU time.
The less time spent on the CPU, the better your performance.

When you’re tuning the performance of your application, there’s not much sense in
wading through the huge collection of methods that you didn’t write. To that end, we
can filter this view to show only the code that you have control over.
1. Find the Display Settings button, at the top of the panel in the bottom right of the
screen. Click it, and you’ll see a collection of options to control how the data is
displayed.
2. Turn off everything except Hide System Libraries. When you do this, the Call
List will be reduced to just your methods. Additionally, they’ll be ordered based
on how much each time each method took (see Figure 17-7).

Figure 17-7. Instruments, after the display has been filtered
The content of the detail area, which is the lower half of the screen, depends on
which instrument you’re working with. For the CPU Usage instrument, the col‐
umns in the Detail Area’s Call Tree view are:
Running Time
The total amount of time taken by the current row, including any of the
methods that it calls.
Self (ms)
The total amount of time taken by the current now, not including any of the
methods it calls.
Symbol Name
The name of the method in question.

Instruments

|

483

You’ll notice that main is taking up the majority of the time. This makes
sense, because main is the function that kicks off the entirety of the applica‐
tion. If you open up the list of methods, you’ll see the methods that main
calls; each one can in turn be opened.
Given that our goal is to improve the performance of opening a document,
we want to find the most time-consuming method, and optimize that.
3. Expand the topmost method in the list. Continue doing this until there’s nothing
else left to expand.
Hold the Option key and click on the arrow, and all rows will
be expanded.

You’ll notice that the method that takes the majority of the time when opening
the document is labeled “type metadata accessor for AVSpeechSynthesizer” (see
Figure 17-8). This sounds kind of arcane, so let’s back up one level and see if we
can figure out what’s going on.

Figure 17-8. The performance bottleneck in the code
4. Double-click on the method above “type metadata accessor for AVSpeechSynthe‐
sizer”: DocumentViewController.init. You’ll be taken to a view of the source
code, highlighting the line that took the most time to execute: the line that creates
the AVSpeechSynthesizer (Figure 17-9).

Figure 17-9. The offending line of code

484

|

Chapter 17: Code Quality and Distribution

What’s happening here is that AVSpeechSynthesizer does quite a bit of loading
in order to prepare itself for use. It needs to access several hundred megabytes of
speech samples and prepare the language model used for converting text to spo‐
ken audio.
When the DocumentViewController is created, it immediately creates the
AVSpeechSynthesizer. However, it doesn’t technically need to do it right away.
We could instead create the AVSpeechSynthesizer the moment the user asks for

text to be spoken.

The best way to do this is to use a lazy stored property for the AVSpeechSynthe
sizer. A lazy property works just like any other property, except it doesn’t
actually initialize its value until the very first time it’s accessed. If we change the
speechSynthesizer property to be a lazy property, we’ll reduce the amount of
time needed to load the DocumentViewController.
5. Open DocumentViewController.swift and replace the following line of code:
let speechSynthesizer = AVSpeechSynthesizer()

with the following code:
lazy var speechSynthesizer = AVSpeechSynthesizer()

You’re done. Repeat the steps you took earlier: relaunch the app, transfer it to Instru‐
ments, and open a document. The time taken to load a document should be reduced!
This process of measuring the work done by the app, determining the point that
needs changing, and optimizing it can be applied many times, and in different ways.
In this section, we’ve only looked at reducing the time spent on the CPU; however,
you can use the same principles to reduce the amount of memory consumed, data
written to and read from disk, and data transferred over the network.

Testing
While simple apps are easy to test, complex apps get very difficult to properly test. It’s
simple enough to add some code and then check that it works; but the more code you
add, the more you increase the chance that a change in one part of the code will break
something elsewhere. In order to make sure that all of the app works, you need to test
all of the app. However, this has many problems:
• It’s tedious and boring, which means you’ll be less likely to do it thoroughly.
• Because it’s repetitious, you’ll end up testing a feature in the same way every time,
and you may not be paying close attention.
• Some problems appear only if you use the app in a certain way. The more specific
the use case, the less you’ll test it.

Testing

|

485

To address these problems, modern software development heavily relies on automa‐
ted testing. Automated testing solves these problems immediately, by running the
same tests in the same way every time, and by checking every step of the way; addi‐
tionally, automated testing frees up your mental workload a lot.
There are two types of automated tests in Xcode: unit tests and user interface tests.

Unit Testing
Unit tests are small, isolated, independent tests that run to verify the behavior of a
specific part of your code. Unit tests are perfect for ensuring that the output of a
method you’ve written is what you expect. For example, the code that we wrote all the
way back in “JSON Attachments” on page 155 to load a location from JSON is very
straightforward to test: given some valid JSON containing values for lat and lon, we
expect to be able to create a CLLocationCoordinates; additionally, and just as impor‐
tantly, if we give it invalid JSON or JSON that doesn’t contain those values, we should
expect to fail to get a coordinate.
Unit tests are placed inside a unit test bundle. You can choose to either include unit
tests when you create the project, or you can add one to an existing project by open‐
ing the File menu and choosing New→Target, then opening the Tests section and
choosing Unit Tests (see Figure 17-10).

Figure 17-10. Adding a Unit Test bundle to a project
486

|

Chapter 17: Code Quality and Distribution

Test bundles contain one or more test cases; each test case is actually a subclass of
XCTestCase, which itself contains the individual unit tests. A test case looks like this:
func testDocumentTypeDetection() {
// Create an NSFileWrapper using some empty data
let data = NSData()
let document = NSFileWrapper(regularFileWithContents: data)
// Give it a name
document.preferredFilename = "Hello.jpg"
// It should now think that it's an image
XCTAssertTrue(document.conformsToType(kUTTypeImage))
}

The tests inside XCTestCase class are its methods. When Xcode runs the tests, which
we’ll show in a moment, it first locates all subclasses of XCTestCase, and then finds all
methods of each subclass that begin with the word test. Each test is then run: first,
the test case’s setUp method is run, then the test itself, followed by the test case’s tear
Down method.
You’ll notice the use of the XCTAssertTrue functions. This method is one of many
XCTAssert functions, all of which test a certain condition; if it fails, the entire test
fails, and Xcode moves on to the next test. You can find the entire list of XCTAssert
functions in the Xcode testing documentation.
To run the unit test for your current target, press ⌘U, or click the icon at the left of the
top line of a specific test, as shown in Figure 17-11.

Figure 17-11. Running a specific test
Xcode will launch your app, perform the test(s), and report back on which tests, if
any, failed.

UI Testing
To get a complete picture of how your app works, unit tests on their own aren’t
enough. Testing a single isolated chunk of your code, while extremely useful, isn’t
enough to give you confidence that the app itself, with all of its interacting compo‐
nents, is being tested. For example, it’s simply not feasible to write a concise unit test
for “create a document, edit it, and save it.”
Instead, you can use UI tests to verify that the app is behaving the way you want it to
as it’s used. A UI test is a recording of how the user interacts with the user interface;
Testing

|

487

however, these recordings are done in a very clever way. While a UI test is being
recorded, Xcode notes every interaction that you perform, and adds a line of code
that reproduces that step.
The result is code that looks like this (we’ve added comments to describe what’s going
on):
func testCreatingSavingAndClosingDocument() {
// Get the app
let app = XCUIApplication()
// Choose File->New
let menuBarsQuery = XCUIApplication().menuBars
menuBarsQuery.menuBarItems["File"].click()
menuBarsQuery.menuItems["New"].click()
// Get the new 'Untitled' window
let untitledWindow = app.windows["Untitled"]
// Get the main text view
let textView = untitledWindow.childrenMatchingType(.ScrollView)
.elementBoundByIndex(0).childrenMatchingType(.TextView).element
// Type some text
textView.typeText("This is a useful document that I'm testing.")
// Save it by pressing Command-S
textView.typeKey("s", modifierFlags:.Command)
// The save sheet has appeared; type "Test" in it and press Return
untitledWindow.sheets["save"].childrenMatchingType(.TextField)
.elementBoundByIndex(0).typeText("Test\r")
// Close the document
app.windows["Test"].typeKey("w", modifierFlags:.Command)
}

UI tests are run the same way as your unit tests. When they’re run, the system will
take control over your computer and perform the exact steps as laid down in the test.
This ensures that your app is tested in the exact same way, every time.

488

| Chapter 17: Code Quality and Distribution