Why Is ClassLoader.getResourceAsStream So Slow in Android?
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.
Example Slowdowns in Top Apps
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
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
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
Here is a partial list of the most popular SDKs that call
- Google Dependency Injection
- Apache log4j
- 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
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).
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
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:
We get what we expected, dalvik.system.PathClassLoader. However, checking the methods of PathClassLoader, we don’t find an implementation for findResource. This is because findResource() is implemented in the parent of PathClassLoader - BaseDexClassLoader.
Let’s find pathList:
And now DexPathList:
Let’s check out
Element is just a static inner class in DexPathList. Inside there is much more interesting code:
Let’s stop and think for a bit. We know that the APK file is just a zip file. As we see here:
We try to find ZipEntry by a given name. If we do this successfully, we return the corresponding URL. This can be a slow operation, but if we check the implementation of getEntry, we see that it’s just iterating over LinkedHashMap:
This isn’t a super fast operation, but it can’t take too long.
We missed one thing though - before working with zip files, they should be opened. If we look once again at the DexPathList.Element.findResource() method implementation, we will find the maybeInit() call. Let’s check it out:
Here it is! This line
opens a zip file for reading:
This constructor initializes a LinkedHashMap object called entries. (To investigate more about the internal structure of ZipFile, check this out.) Obviously, as our APK file gets larger, we will need more time to open the zip file.
We’ve found the first slow operation of getResourceAsStream. The journey so far has been interesting (and complicated), but still only the beginning. If we patch the source code like the following:
We see that the zip file operation cannot account for all the delay in getResourceAsStream: url.openStream() takes much longer than getResource(), so let’s investigate further.
Following the call stack of url.openStream(), we get to /libcore/luni/src/main/java/libcore/net/url/JarURLConnectionImpl.java
Let’s check connect() first:
Nothing interesting, so let’s dive deeper.
Calling getUseCaches() should return true because:
Let’s look at the openJarFile() method:
As you can see, here we open a JarFile, not a ZipFile. However, JarFile extends ZipFile. Here we’ve found the second slow operation in getResourceAsStream - Android needs to open the APK file again as a ZipFile and index all its entries.
Opening the APK file as a zip file twice doubles the overhead, which is already significantly noticeable. However, this overhead still doesn’t account for all observed overhead. Let’s look at the JarFile constructor:
So here we find the third slow operation. All APK files are signed, so JarFile will execute the “verify” path. This verification process is cripplingly slow. While further discussion regarding verification is outside the scope of this post, you can learn more about it here.
To summarize, ClassLoader.getResourceAsStream is slow because of three slow operations: (1) opening the APK as a ZipFile; (2) opening the APK as JarFile which requires opening the APK as ZipFile again; (3) verifying that the JarFile is properly signed.
Q: Is ClassLoader.getResource*() slow for both Dalvik and ART?
A: Yes. We checked 2 branches, android-6.0.1_r11 with ART and android-4.4.4_r2 with Dalvik. The slow operations in getResource*() are present in both versions.
Q: Why doesn’t ClassLoader.findClass() have a similar slowdown?
A: Android extracts DEX files from an APK during installation. Therefore, to find a class, there is no need to open the APK as a ZipFile or JarFile.
Specifically, if we go to class DexPathList we will see
There is no ZipFile or JarFile involved.
Q: Why doesn’t Android’s Resources.get*(resId) have this issue?
A: Android has its own way to index and load resources, avoiding the costly ZipFile and JarFile operations.