How many times have you been in the middle of using a new shiny app, only to have it crash on you?
This is the fifth in a series of posts that will investigate how the exception handling mechanism works in Java and Android, and how crash reporting SDKs can capture diagnostic information, so that you’re not flying blind in production.
We previously learnt how to capture other sources of errors on Android, such as caught exceptions and ANRs. Our goal now is to handle obfuscated stacktraces from a minified Android app, by uploading ProGuard mapping files to an error reporting API.
Obfuscating your app has several advantages. Your app will tend to be smaller, as obfuscated symbols require less space in Dex files than regular naming conventions for identifiers. Obfuscated code is also harder to understand, which is a benefit if you wish to make life harder for anyone attempting to reverse engineer your production APK.
An additional benefit is that obfuscation is usually coupled with a minification process that removes dead code, and optimises any remaining Java bytecode to make your app faster. If you’re interested in learning more about optimisations made by Android, Jake Wharton has written a very comprehensive series of blog posts which cover optimisations performed by D8 and R8.
Android uses R8 as the default obfuscation tool and code shrinker. Without a bit of extra work, this can cause some serious problems when it comes to debugging stacktraces. This is what a stacktrace may look like before obfuscation:
— CODE language-kotlin —
java.lang.RuntimeException: Whoops!
at com.foo.mylib.RequestInterceptor.interceptRequest(RequestInterceptor.kt:5)
at com.foo.mylib.HttpClient.doGet(HttpClient.kt:9)
at com.foo.mylib.HttpClient.makeRequest(HttpClient.kt:5)
at com.foo.mylib.DownloadManager.downloadFile(DownloadManager.kt:5)
And this is what it will look like after obfuscation:
— CODE language-kotlin —
java.lang.RuntimeException: Whoops!
at com.a.a.c.a(Unknown Source:5)
at com.a.a.b.b(Unknown Source:9)
at com.a.a.b.a(Unknown Source:5)
at com.a.a.a.a(Unknown Source:5)
I know which one I’d prefer to debug!
Fortunately, it’s possible to reverse the obfuscation process if we retain the mapping file. This contains a map where each obfuscated symbol is the key, and the value for each entry is the original symbol information. In a production app, this will contain thousands of entries, but a simplified mapping file may look something like the following:
— CODE language-kotlin —
com.foo.mylib.DownloadManager -> com.a.a.a:
5:6:void downloadFile(java.lang.String) -> a
3:3:void <init>() -> <init>
com.foo.mylib.HttpClient -> com.a.a.b:
5:6:void makeRequest(java.lang.String) -> a
9:10:void doGet(java.lang.String) -> b
3:3:void <init>() -> <init>
com.foo.mylib.RequestInterceptor -> com.a.a.c:
5:5:void interceptRequest(java.lang.String) -> a
3:3:void <init>() -> <init>
Looking back to the first line of our obfuscated stacktrace, we can obviously see that com.a.a.c
corresponds to com.foo.mylib.RequestInterceptor
. We can then look up line number and method information in a similar way, and deobfuscate the first frame:
— CODE language-kotlin —
java.lang.RuntimeException: Whoops!
at com.foo.mylib.RequestInterceptor.interceptRequest(RequestInterceptor:5)
at com.a.a.b.b(Unknown Source:9)
at com.a.a.b.a(Unknown Source:5)
at com.a.a.a.a(Unknown Source:5)
It’s then simply a case of deobfuscating the rest of the frames to gain the original stacktrace:
— CODE language-kotlin —
java.lang.RuntimeException: Whoops!
at com.foo.mylib.RequestInterceptor.interceptRequest(RequestInterceptor:5)
at com.foo.mylib.HttpClient.doGet(HttpClient:9)
at com.foo.mylib.HttpClient.makeRequest(HttpClient:5)
at com.foo.mylib.DownloadManager.downloadFile(DownloadManager:5)
Decoding each stacktrace manually is a bit painful, particularly if you receive several thousands of them each day. It also means you need to retain correct mapping file for every single build of your APK.
Bugsnag deobfuscates Android errors by automatically uploading any mapping files and using the information to deobfuscate any errors sent to our error reporting API. This is achieved using a gradle plugin which hooks into the assemble
gradle task, and uploads the mapping file whenever the app is built, whether it be locally or on CI.
Now that we’ve answered the eternal question of why every crash reporting service uses a gradle plugin, it’s time to think about adding some useful metadata to our reports. Read on in the next post.
Hopefully this has helped you learn a bit more about Error Handling on Android. If you have any questions or feedback, please feel free to get in touch.
———
Try Bugsnag’s Android crash reporting.