Through our extensive analysis at NimbleDroid, we’ve picked up on a few tricks that help prevent monolithic lags in Android apps, boosting fluidity and response time. One of the things we’ve learned to watch out for is the dreaded ClassLoader.getResourceAsStream, a method that allows an app to access a resource with a given name. This method is pretty popular in the Java world, but unfortunately it causes gigantic slowdown in an Android app the first time it is invoked.
Of all the apps and SDKs we’ve analyzed (and we’ve analyzed a ton), we’ve seen that over 10% of apps and 20% of SDKs are slowed down significantly by this method. What exactly is going on? Let’s take an in-depth look in this post.
Another example is TuneIn 13.6.1, which is delayed by 1447ms.
Here TuneIn calls getResourceAsStream twice, and the second
call is much faster (6ms).
Here are some more apps that suffer from this problem:
Again, more than 10% of the apps we analyzed suffer from this issue.
SDKs That Call getResourceAsStream
For brevity, we use SDKs to refer to both libraries that are attached
to certain services, such as Amazon AWS, and those that aren’t, such as
Joda-Time.
Oftentimes, an app doesn’t call getResourceAsStream directly;
instead, the dreaded method is called by one of the SDKs used by the app.
Since developers don’t typically pay attention to an SDK’s internal
implementation, they often aren’t even aware that their app contains the
issue.
Here is a partial list of the most popular SDKs that call
getResourceAsStream:
mobileCore
SLF4J
StartApp
Joda-Time
TapJoy
Google Dependency Injection
BugSense
RoboGuice
OrmLite
Appnext
Apache log4j
Twitter4J
Appcelerator Titanium
LibPhoneNumbers (Google)
Amazon AWS
Overall, 20% of the SDKs we analyzed suffer from this issue - the list above
covers only a small number of these SDKs because we don’t have space
to list them all here. One reason that this issue plagues so many SDKs is
that getResourceAsStream() is pretty fast outside of Android,
despite the method’s slow implementation in Android.
Consequently, many Android apps are affected by this issue because many
Android developers come from the Java world and want to use familiar
libraries in their Android apps (e.g., Joda-Time instead of
Dan Lew’s Joda-Time-Android).
Why getResourceAsStream Is So Slow in Android
A logical thing to be wondering right now is why this method takes so long
in Android. After a long investigation, we discovered that the first time
this method is called, Android executes three very slow operations: (1) it
opens the APK file as a zip file and indexes all zip entries; (2) it opens
the APK file as a zip file again and indexes all zip entries; and (3)
it verifies that the APK is correctly signed. All three operations are
cripplingly slow, and the total delay is proportional to the size of the
APK. For example, a 20MB APK induces a 1-2s delay. We describe our
investigation in greater detail in
Appendix.
Recommendation: Avoid calling ClassLoader.getResource*(); use
Android's Resources.get*(resId) instead.
Recommendation: Profile your app to see if any SDKs call
ClassLoader.getResource*(). Replace these SDKs with more efficient
ones, or at the very least don't do these slow calls in the main
thread.
Appendix: How We Pinpointed the Slow Operations in getResourceAsStream
To really understand the issue here, let’s investigate some actual
code. We will use branch
android-6.0.1_r11
from AOSP. We’ll begin by taking a look at
the ClassLoader code:
Everything looks pretty straightforward here. First we find a path for
resources, and if it’s not null, we open a stream for it. In this case,
the path is java.net.URL class, which has method openStream().
Ok, let’s check out the getResource() implementation:
Still nothing interesting. Let’s dive into findResource():
So findResource() isn’t implemented. ClassLoader is an abstract class,
so we need to find the subclass that is actually implemented in real
apps. If we open android
docs,
we see that Android provides several concrete implementations of the
class, with PathClassLoader being the one typically used.
Let’s build AOSP and trace the call to getResourceAsStream and getResource in order to determine which ClassLoader is used: