Jack, Jill & building Android apps by hand
This seems to be the overarching theme of my posts. I renounce modern IDEs, go command-line and claim it’s good for you. But this time it’s serious.
I’m scared of magic.
Mysterious background processes performing tasks I’m unaware of and don’t understand. IDEs reading my mind and generating code behind my back. “Here, try some”, they’ll say. And I will try some indeed. Heck, I will even like it. But the mystery of it all will make it hard to swallow.
One such mystery for me has been the Android build process. Even outside the IDE, turning Java and XML into APK via Gradle was just enough magic to make me uncomfortable.
Whenever possible, I like to strip away the magic and do things by hand. Even if only as an exercise.
This was one such exercise.
The goal
To build and deploy an Android app by hand, using only the SDK command-line tools.
The rules
- No IDEs
- No Gradle
And for extra credit:
- Let’s use the Jack toolchain
- Let’s build for Android N
The project
For this task, I created a simple, single-activity application. Not a paragon of Android good practices mind you, but just enough to walk us through the major steps of the build:
- One activity
- One drawable resource for the app icon
- One layout resource with an
ImageView
- One external dependency - Picasso
For demonstration purposes I’ve adopted a simple, flat project folder structure:
handbuilt-android-project/
├── AndroidManifest.xml
├── gen/
├── lib/
│ └── picasso-2.5.2.jar
├── out/
├── res/
│ ├── drawable-xhdpi/
│ │ └── icon.png
│ └── layout/
│ └── activity_main.xml
└── src/
└── pl/
└── czak/
└── handbuilt/
└── MainActivity.java
When we’re done:
gen/
will contain the generatedR.java
class.out/
will contain the products of our build
The complete source code (with a slightly more advanced folder structure and simple dependency management built in) is available on GitHub. For the tl;dr version, just check out the Makefile.
If you wish to follow along, we need to do some setup beforehand.
Prerequisites
Download the latest SDK tools package (24.4.1 as of this writing), unzip it and
set $ANDROID_HOME
to the root folder. Then launch the SDK Manager using:
$ $ANDROID_HOME/tools/android
Before the SDK is fully usable, we need to download additional components:
- Tools
- Platform tools
- Build tools
- Android N Preview SDK
- Android N System Image (if you want to test on an emulator)
I live on the edge ;) so I go with preview versions of everything:
Once everything is installed, make sure the following commands are available in
your $PATH
:
$JAVA_HOME/bin/java
$JAVA_HOME/bin/jarsigner
$ANDROID_HOME/platform-tools/adb
$ANDROID_HOME/tools/build-tools/24.0.0-preview/aapt
$ANDROID_HOME/tools/build-tools/24.0.0-preview/zipalign
The Jack compiler is contained in the build tools under
$ANDROID_HOME/build-tools/24.0.0-preview/jack.jar
. For convenience, I use the
following alias to invoke it:
$ alias jack='java -jar "$ANDROID_HOME/build-tools/24.0.0-preview/jack.jar"'
The build
The complete build consists of 7 distinct steps, described in the following subsections:
Step 1. Generate R.java
In Android Studio, this happens continuously behind the scenes and is perhaps
the most magical part of the process. You add a new layout XML and a second
later you can reference it in your code as R.layout.activity_main
. This is
because the IDE regenerates the R.java
source file whenever your resources
change.
In our exercise, we need to do it manually. This is the first job for aapt
:
$ aapt package -f \
-M AndroidManifest.xml \
-I "$ANDROID_HOME/platforms/android-N/android.jar" \
-S res/ \
-J gen/ \
-m
This invocation takes our AndroidManifest.xml
and the resources under res/
and generates the following source file:
/* AUTO-GENERATED FILE. DO NOT MODIFY.
*
* This class was automatically generated by the
* aapt tool from the resource data it found. It
* should not be modified by hand.
*/
package pl.czak.handbuilt;
public final class R {
public static final class attr {
}
public static final class drawable {
public static final int icon=0x7f020000;
}
public static final class id {
public static final int image=0x7f040000;
}
public static final class layout {
public static final int activity_main=0x7f030000;
}
}
Step 2. Compile
The complete source code now consists of MainActivity.java
and the newly
generated R.java
. We can now compile it using Jack:
$ jack --classpath "$ANDROID_HOME/platforms/android-N/android.jar" \
--import lib/picasso-2.5.2.jar \
--output-dex out/ \
src/ gen/
We pass in N’s android.jar
for classpath and import the Picasso JAR. We also
specify both folders containing our Java files.
As a result, we get out/classes.dex
ready for packaging.
Step 3. Package
We’re ready to start building the APK. I split the packaging process into two steps. First, we create the initial package file with the manifest and resources:
$ aapt package -f \
-M AndroidManifest.xml \
-I "$ANDROID_HOME/platforms/android-N/android.jar" \
-S res/ \
-F out/handbuilt.apk
Once again, this is performed with the Android Asset Packaging Tool (aapt
).
This time however, we’re using the -F
switch to create out/handbuilt.apk
.
Now we add our compiled classes.dex
. Note that aapt
is sensitive to relative
paths and classes.dex
needs to reside in root of the package. Thus, we perform
this step from within the out/
folder:
$ cd out/
$ aapt add handbuilt.apk classes.dex
We’ve provided all the required contents of the APK. Before we install it however, it needs to be signed.
Step 4. Sign
Every Android APK needs to be signed before it can be installed on a device or an emulator. This is true whether you’re just trying out a debug build in development or preparing your final APK for public release. When working in the IDE this is kept out of sight and there’s no mention of any keys until you want to publish your app in Google Play.
For development purposes, the SDK provides a standard debug key in
~/.android/debug.keystore
. The key can be used as follows:
- Keystore password:
android
- Key password:
android
- Key alias:
androiddebugkey
With this in mind, we turn to JDK’s jarsigner
to perform the task:
$ jarsigner -verbose \
-keystore ~/.android/debug.keystore \
-storepass android \
-keypass android \
out/handbuilt.apk \
androiddebugkey
The APK is now ready to upload. However, let’s perform one important optimization first.
Step 5. Zipalign
Zipaligning is a process which takes an APK and ensures that all
uncompressed data (e.g. images and resources.arsc
) starts with a particular
alignment relative to the start of the file. This way they can be mmap
ed
without moving any bytes around. This is an important performance optimization.
Luckily, it’s very simple:
$ zipalign -f 4 out/handbuilt.apk out/handbuilt-aligned.apk
Our final APK is handbuilt-aligned.apk
.
Step 6. Upload
Now launch an emulator (as an exercise for the reader – do this from the command line as well) and let’s deploy:
$ adb install -r out/handbuilt-aligned.apk
The app is ready to be launched. Sure we could just tap the icon in the drawer. But where’s the fun in that?
Step 7. Run
By now we’re commandline ninjas after all. So let’s launch the app the right way:
$ adb shell am start pl.czak.handbuilt/.MainActivity
Done.
Notes
- Jack simplifies the compilation process quite a bit. What previously took
multiple steps (
javac
,dx
, ProGuard) is now taken care of by this single tool. - Android Studio generates one more class behind the scenes -
BuildConfig.java
. For this simple exercise I’ve omitted it. - The Gradle build system uses a different naming scheme - the package before
zipaligning would be called
handbuilt-unaligned.apk
and the final APK would behandbuilt.apk
. This is just a convention and I’m breaking it here. Sue me. (Please don’t sue me). - A typical build process would also incorporate Jill – Jack’s helper tool for
converting libraries into Jack’s expected
jayce
format. Again, I’ve omitted it in the examples here - as of version 1.2-rc2 Jack seems to acceptandroid.jar
andpicasso-2.5.2.jar
just fine. To speed up consecutive builds, you would want to “pre-jill” both of these just like you would “pre-dex” libraries with the old toolchain. - Some Java 8 features are now available to Android
developers. If you want to use those, you need to specify
-D jack.java.source.version=8
to Jack. Also note that Jack is the only option for using Java 8 asdx
won’t accept Java 8.class
files.
Summary
Once again, I’m pleased to see the magic behind the Android build process is not
that bad. Will I be using make
instead of gradle
from now on? Most probably
not. I’m even starting to miss Android Studio a bit. And it’s not such a scary
place anymore.