Debugging in Android Studio

Tips and tricks for squashing bugs faster on Android

David Herman
Android Developers

--

As developers, we all know that there are days we spend more time in the debugger than in the code editor. With this in mind, I recently took the opportunity to see what tricks and tips members of our Android Studio team had for speeding up debugging. I’ve gathered together some of the best that I think will save you time and be easy to incorporate into your debugging flow.

While your app is likely much different than the hypothetical sample game app we’re using here, these debugging tips will apply to any app.

If you’d rather watch a live demo, you can find a video version of this article on YouTube.

Log filtering and folding

Let me start with a tip for that debugging classic: printf statements. Take a game that logs its frames-per-second rate every second and the user’s final score at the end of each game. In the Logcat window this gives:

Logcat window

There’s quite a lot of information in the output that you might not care about, such as the date and thread IDs. You can easily configure what is displayed. From the logcat toolbar, click the Settings icon and, in the Configure Logcat Header dialog, deselect information you no longer want to see.

Configure Logcat Header dialog

You now get much cleaner, more relevant log output, like this:

log output

However, this still leaves a lot of clutter obscuring the high score messages. To focus on these messages use the logcat search. Enter part of the debug message into Search to filter the logcat window.

logcat window

If this is a search you use regularly, you can save it by adding a custom filter from Edit Filter Configuration.

Filter configuration

Then, add the details of your filter.

Another way to reduce logging clutter is to use the fold lines feature, which groups and collapses lines that are similar. Select some text from a log Item, right-click, and select Fold Lines Like This.

When the Console dialog opens, click OK and similar messages containing the selected text are collapsed.

If you need to look at this information later, you can click on the lines to expand them. There are also gutter handles that enable you to expand and collapse folded lines.

Attaching the debugger

You usually start a debugging session using the Debug button or menu option. However, if you started an app by running it, you can attach a debugger to the running app without needing to restart it. To do this, click Attach Debugger to Android Process.

In the Choose Process dialog, highlight the process you want to attach the debugger to and click OK. Now, you’ll start hitting your breakpoints as you normally would in a debug session.

Moving breakpoints

If you find that you have set a breakpoint at an inappropriate place, rather than clearing and resetting the breakpoint, you can drag it to the line that you care about. This is useful because it preserves the settings on the breakpoint, including the things I’m going to mention next.

Conditional breakpoints

You may need to chase down a bug that relates to a particular type of event in your app or game. For example, in a game you are developing, you may want to stop when a player collides with the object that uses up their last bit of health. You set a breakpoint on collisions but now the code stops on every collision. To avoid this you can set a conditional breakpoint.

To set a conditional breakpoint, right-click on a breakpoint and add a condition. The condition can be any code expression that equates to a Boolean. When the code hits the line, if the expression evaluates to true the breakpoint activates.

Here, in logic when the player is just about to collide with an object, setting a condition of player.health == 1 enables you to catch the last time the player collides with the object before their health drops to 0.

Dependent breakpoints

It’s not unusual for an app to have code that can be triggered from many different paths. If you have a bug that only happens along one particular path, setting a breakpoint in the code can result in a lot of unnecessary breaks. To get around this, you can use dependent breakpoints, which trigger only after another specified breakpoint was hit. For example, you can create a breakpoint that is triggered only in the path you care about and then use it as a dependency, so your other breakpoint only triggers in the path of interest.

To set a dependent breakpoint, right-click on the second breakpoint and open the More menu. In Disable until breakpoint is hit, choose the breakpoint you want to depend on.

You’ll notice that the breakpoint icon changes:

Now, you only stop here after the previous breakpoint is hit.

You can also use this feature where you have a conditional breakpoint elsewhere and want to avoid copying and pasting that condition to a new location.

Suspend thread

If you’re debugging a multithreaded app, you will notice that, by default, breakpoints suspend all threads. However, you may not want this behavior. For example, perhaps you want to verify that you can block on a thread and the rest of your app still works, or you want your UI to keep rendering while you investigate a background task.

To suspend only the current thread, open the breakpoint options and select Thread in the Suspend settings.

Evaluate and log

Sometimes, rather than stopping at a breakpoint, you want to see some information associated with the app state. You might add printlns to the code to accomplish this. Rather than using this approach, which requires a recompile, you can use the breakpoint itself to evaluate and log.

To do this, in the breakpoint options disable Suspend and enable Evaluate and log.

You can now add any code expression, and it will be evaluated and logged to the console.

If you just want to quickly verify that your breakpoint was triggered and don’t care about specifics, use the “Breakpoint hit” message to log that the breakpoint has been hit. There is even a quick way to create this sort of breakpoint: just press the Shift key and click in the gutter.

Disabling breakpoints

If you want to quickly disable a breakpoint, rather than right-clicking and deselecting Enabled, you can either middle-click, or press the Alt key (Option on Mac) and left-click, to toggle a breakpoint on and off.

Breakpoint groups

You’ve been working on a bug, creating breakpoints, but find yourself making little progress. So you switch to working on another bug. Soon, however, you start hitting the breakpoints from the first bug. Hitting unrelated breakpoints can be frustrating and take you out of your debugging flow.

You can make your life a lot easier by using breakpoint groups.

When you hit the first unwanted breakpoint, right-click and open More. You now see a list of all the breakpoints. Multi-select all the breakpoints associated with your first bug.

Right-click the selected breakpoints and click Move to group then Create new. Name the group, perhaps after the bug that you were working on. Now you can easily toggle all the breakpoints on and off, with a click.

Also, when you’re done with the bug, you can use the group to delete all the breakpoints.

Drop frame

Sometimes, when you’re walking through suspended code it’s possible that you accidentally step over a method rather than into it. If you’re running Android 10 or higher you can now backtrack by clicking Drop Frame in the debugger toolbar.

This feature pulls you out of the current method and puts you back to the point before it started, giving you a second opportunity to step into the method.

This feature is not a “time machine”. If you were in the middle of a long function and it had done a lot of intermediate work — for example, modified the state of the current class — that work doesn’t get undone when you drop the frame.

Mark object

There are times when you want to follow the lifecycle of a specific class instance, like this example, with an item that has a hashcode @10140.

You might be tempted to pull out a piece of paper and write down 10140, so you can identify the object when it appears again. But, alternatively, you can right-click the item, click Mark Object, and label it.

Now, anywhere that this marked object appears in any of the debugging windows, it’s labeled and easy to find. Here, we labelled the object “myItem”:

What makes this even better for tracking an object is the ability to see it in the Watches window, even if you’re in a totally different context where that item isn’t otherwise reachable. Wherever you are, as long as you’ve hit a breakpoint, in the Watches window, add your label followed by “_DebugLabel” (don’t worry about remembering this, it will auto complete).

Now, you can follow the class item anywhere in the Watches window to see its state.

You can also combine this feature with conditional breakpoints. For example, you can set a breakpoint, right-click it, and set the condition to check against a labeled object.

Now, rather than stepping over multiple breakpoints until the particular item instance is in scope, the code breaks in the right place for you.

Evaluate expression

While the Variables and Watches windows are useful to keep tabs on explicit values, you sometimes want to explore your code more freely, which is where the feature to evaluate expressions comes in. When you’re at a breakpoint, access this feature from Evaluate expression in the debugger toolbar.

In the Expression text input, enter any expression and press Evaluate to evaluate it. Also, if you evaluate an object, after evaluation you can browse the object in the Result section.

The evaluate expression dialog may open in single-line mode. You can easily expand it to multiple lines by clicking Expand.

Now you can enter complex, multiline expressions. These can include variables and if statements, among others.

Apply changes

When you have a conditional breakpoint where you evaluate an expression, even if you don’t stop at that breakpoint, the debugger still has to do the evaluation. If you are running some code in a really tight loop, such as animation processing in a game, this can cause the application to stutter. Although conditional breakpoints are useful, this may be an edge case where you can’t rely on them.

One way to get around this problem is by adding the conditional expression to the code and use a no op expression so you can attach the breakpoint.

Having done this, you might decide to restart your app and click Debug. But, rather than that, when running on Android 8 and above, you can use Apply Code Changes.

Now your code is patched with the embedded expression. However, you’ll see in the Frames window that the method you’ve updated is marked as Obsolete.

This is because new code has been patched but your debugger is still pointing at old code. You can use the drop frame feature to leave the old method and step into the new one.

Although we didn’t need it in this case, there is a second option, Apply Changes and Restart Activity. Unlike Apply Code Changes, this option also restarts the activity, which is useful if you modified layout resources or the code that you’re trying to debug is, for example, in an onCreate method.

Analyze stacktrace

Despite all of these tips and tricks, unfortunately, you’re probably going to have bugs in your code and get bug reports. When you receive these bug reports, the reporter may have included a text copy of the exception’s stack. You can turn these into meaningful information in Android Studio using, from the Analyze menu, Analyze Stack Trace or Thread Dump.

This tool provides a place to paste a stack trace, but it will be auto-populated with any text in your clipboard.

Click OK and a fully annotated version of the stack trace is added to the console.

You can see at a glance what is from your code base (highlighted in blue above) versus the code that you probably don’t need to pay attention to (highlighted in gray). And, you can click on the links to quickly jump through your code base.

Final words

These are just some of the tips and tricks you can use to speed up your debugging. A few that we didn’t have time to go into detail here include:

  • In debug mode, click on a line number in the gutter to run to that line.
  • Ctrl+drag makes a copy of a breakpoint.
  • You can set breakpoints on the closing brace of a function.
  • You can set breakpoints on fields and properties (called “field watchpoints”).
  • You can set a breakpoint on an interface method to break on all its implementations.

There are also a several links related to this topic that you might want to check out, including:

--

--

David Herman
Android Developers

I am a team lead on the Android Studio team (at Google). I’ve worked on profilers and improving data binding integration.