Last time, I described how to integrate Clojure into the standard Android
build process. In this post, I will build on this by integrating
ProGuard into the build process in an effort to make applications
smaller and faster.
First, I will give a brief overview of ProGuard and describe how to enable the
ProGuard capability of the Android build process. Next, I will show how to
configure ProGuard so that it can work with your Clojure code. Finally, I will
summarise my recommendations and let you know what to expect next in this
series.
The sample application I will use is available on my Clojure-Android-Examples
repository on GitHub under the slimmed
directory.
About ProGuard
As described on its web site, ‘ProGuard is a free Java class file shrinker,
optimizer, obfuscator, and preverifier.’ What does this mean?
- Minimisation
-
ProGuard can rid your application of unused classes, methods, and
fields, leading to smaller application sizes and load times.
- Optimisation
-
ProGuard can analyse your code and perform a large number of
optimisations, such as removing unnecessary field accesses and method
calls, merging identical code blocks, inlining method invocations, etc.
This should make your code smaller and run faster (depending on VM
implementation).
- Obfuscation
-
ProGuard can remove debug information normally included class files and
rename classes, methods, etc. to make them meaningless. The idea is to
make it harder to reverse engineer a compiled application into source
code.
- Preverification
-
This is something specific to Java 6 or Java ME VMs. As a result, it
does not apply to working with Android.
Now that you have an idea of what ProGuard can do, how can you start using it?
Enabling ProGuard
It is very simple to enable ProGuard when you are creating a release build of
your application using ant release
. All you have to do is edit the
build.properties
file created by Android and add the following line:
proguard.confg = proguard.cfg
If you want to enable ProGuard for debug builds, you will also need to override
the -debug-obfuscation-check
target. I set up my target as follows:
<target name="-debug-obfuscation-check" if="proguard.enabled">
<path id="out.dex.jar.input.ref"/>
<target/>
Line two above ensures that the libraries you are using, such as Clojure, will
be processed in addition to your application’s code.
These two changes will give you the ability to do the following:
- When running
ant install
or ant debug
, your program will be built as usual.
- If you want to create a ProGuard-processed debug build, simply use
ant
-Dproguard.enabled=true install
or ant -Dproguard.enabled=true debug
.
If you want to run ProGuard for every build, you can simply add
proguard.enabled = true
to your build.properties
file.
However, if you try to run ProGuard now, you will find your build fails.
First, it has to be configured.
Configuring ProGuard
The ProGuard configuration bundled with the latest Android SDK is configured
with some sane defaults. However, if you are doing Clojure development, you
will need to make some adjustments in order for ProGuard to work correctly.
The following sections should help guide you to use ProGuard with your
application.
All of these changes will be made to the proguard.cfg
file in the root
directory of your application.
Shrinking your application with ProGuard
ProGuard’s default configuration will not even allow the build process to
complete. To get proper shrinking, you must silence some warnings and ensure
that all of your application’s code is not shrunk away. If you are following
along, you may want to add the following two lines to your proguard.cfg
before continuing any further:
-dontoptimize
-dontobfuscate
Naturally, these disable ProGuard’s optimization and obfuscation functionality.
By disabling them here, you can concentrate on getting one thing working at a
time.
Silencing Clojure-related warnings and notes
Clojure refers to a number of classes that are not available on Android,
particularly the Swing components used from the clojure.inspector
and
clojure.java.browse
namespaces and some java.beans
classes used by
clojure.core/bean
. ProGuard will complain about these missing referenced
classes, but these warnings can be disabled by adding the following lines to
proguard.cfg
:
-dontwarn clojure.inspector**,clojure.java.browse**,clojure.core$bean*
-dontnote clojure.inspector**,clojure.java.browse**
With these two lines in place, the sample application shrinks from an install
size of 4.04MB to only 460KB. Also, instead of taking 4.7 seconds to load, it
crashes almost instantaneously. Why? It's because most of Clojure has been
stripped out.
Keeping Clojure
The reason why Clojure is stripped out is because ProGuard does not understand
how Clojure initialises a namespace. Briefly, when loading a namespace,
Clojure tries to do one of two things:
- Read and evaluate the source file, if available.
- Check to see if the namespace was AOT-compiled by trying to load the
namespace’s initialisation class and its corresponding
load
method.
For example, when trying to load the namespace org.deepbluelambda.example
,
Clojure will look for the file org/deepbluelambda/example.clj
or the class
org.deepbluelambda.example__init
.
Therefore it is necessary to configure ProGuard to keep Clojure’s core
initialization classes. This can be done by adding the following lines to
proguard.cfg
:
-keep class clojure.core__init { public static void load(); }
-keep class clojure.core_proxy__init { public static void load(); }
-keep class clojure.core_print__init { public static void load(); }
-keep class clojure.genclass__init { public static void load(); }
-keep class clojure.core_deftype__init { public static void load(); }
-keep class clojure.core.protocols__init { public static void load(); }
-keep class clojure.gvec__init { public static void load(); }
-keep class clojure.java.io__init { public static void load(); }
You must include these eight lines, otherwise your application will fail to
run. If you use any other Clojure namespaces, you will need to add a
corresponding line. For example, if you use clojure.set
and
clojure.contrib.string
, you will need to add the following lines to your
proguard.cfg
:
-keep class clojure.set__init { public static void load(); }
-keep class clojure.contrib.string__init { public static void load(); }
Taking these steps will help ensure that Clojure itself loads up, but how about
your application?
Keeping your Application
In addition to making sure all of Clojure’s namespaces load properly, you will
need to ensure that all of your application’s namespaces load as well. The
approach is the same; just be sure not to forget any gen-class
ed namespaces.
Furthermore, if you expose superclass methods with gen-class
, as below, you
will need to ensure that ProGuard does not delete those methods.
(ns com.sattvik.android.clojure.slimmed.ClojureBrowser
(:gen-class :extends android.app.Activity
:main false
:exposes-methods {onCreate superOnCreate})
…)
To ensure that the above activity works fine, the following lines need to be
added to the ProGuard configuration:
-keep class com.sattvik.android.clojure.slimmed.ClojureBrowser__init {
public static void load();
}
-keep class com.sattvik.android.clojure.slimmed.ClojureBrowser {
public *** super*(...);
}
The first three lines should be familiar to you, the second three lines will
preserve any public method, with any return and parameter types, so long as its
name starts with ‘super’. Thus, it should be able to handle any number of
:exposes-methods
so long as the exposed method name starts with ‘super’.
Measuring the results
Now that you have successfully shrunk your application, what can you expect?
The following table summarises the results from my sample application:
Metric |
Basic |
Slimmed |
Savings (%) |
Package size (KB) |
1007 |
644 |
36 |
Installed application size (MB) |
4.43 |
2.77 |
37 |
Start time on device A (s) |
~4.7 |
~4.2 |
12 |
Start time on device B (s) |
~3.0 |
~2.3 |
23 |
As can be seen, shrinking definitely helps significantly reduce application
package and installation sizes. The effect on performance tends to be much
more device-dependent.
However, one caveat about the above data. Most of the runtime savings is
realised by the absence of the clojure.set
, clojure.xml
, and clojure.zip
namespaces. In Clojure version 1.2, these namespaces are automatically loaded,
if available. If you actually use and include the namespaces, much of the
runtime savings will disappear. In version 1.3, Clojure will no longer try to
automatically load these namespaces.
Optimizing with ProGuard
The default proguard.cfg
already disables certain optimisations known not to
work on Android. Unfortunately, I find that ProGuard often fails to analyse
Clojure code. During optimization, you may encounter an error similar to the
following:
Unexpected error while performing partial evaluation:
Class = [clojure/java/io$buffer_size]
Method = [invoke(Ljava/lang/Object;)Ljava/lang/Object;]
Exception = [java.lang.IllegalArgumentException] (Stacks have different current sizes [2] and [1])
I have tried using the latest released version of ProGuard, but have found that
it still has problems analysing Clojure code. As a result, I recommend that
you disable optimisation by including the following in your proguard.cfg
:
-dontoptimize
Obfuscating with ProGuard
As with optimization, the default configuration enables obfuscation. Your
application will most likely build, but you will get an error similar to the
following at runtime:
java.lang.IllegalStateException: b already refers to: class clojure.b.a.b in namespace: clojure.core
Unfortunately, I do not see any easy way to resolve this problem. As a result,
for now, I recommend you disable obfuscation as follows:
-dontobfuscate
Final Thoughts
ProGuard can definitely help reduce the size of your application by eliminating
any namespaces you are not using. Additionally, if you are not using
clojure.set
, clojure.xml
, or clojure.zip
, you can expect a runtime
benefit of faster loading when using Clojure 1.2.
Whether or not to enable ProGuard during regular development depends on the
nature of your application. If the number of classes that are removed is large
enough, the time spent shrinking will be offset by the time not spent
processing all of those classes into a Dalvik executable. In some cases, you
may forced to shrink your application, as there is a limit of 65536 method
references per dex file. You may run into this limit if you are using
large libraries (such as the Scala or JRuby core libraries) or many different
libraries.
Unfortunately, at this time it does not seem like optimization and obfuscation
with ProGuard is compatible with Clojure code. There may be ways to work
around some of these problems, such as fine-tuning optimizations or running
your code through ProGuard in stages with different configurations.
Next time
In my next post, I demonstrate how improve application start-up responsiveness
by using a Java bootstrap activity. Additionally, I plan to show how to
exploit the bootstrap activity to use supplemental class loaders to circumvent
the dex file format shortcomings and drastically improve build times.