For a while now, developing our SDK for Android has relied on Eclipse combined with the no longer supported Android Development Tools plugin. While most of the Android development world has been able to enjoy the delights of the officially supported Android Studio IDE, our heavy reliance on native code coupled with Android Studio’s minimal native support has meant that we’ve been stuck in the dark ages, destined to spend our time cursing at errors that don’t exist, UI that reacts at glacial speeds or whatever other foible Eclipse has decided to throw at us this week.
The Android Gradle experimental plugin alleges to include sufficient enough NDK integration for building JNI applications. Although it’s edging closer to becoming a viable IDE for native Android development, it’s currently plagued with crippling (and often baffling) bugs that will abrupt bring your development to a screeching halt. Heed my warnings and spare yourself the pain, for I, once bright-eyed and full of optimism, have now returned to bask in the tepid glow of Eclipse, a mere shadow of my former self.
Okay, a touch dramatic, maybe.
The experience has been somewhat painful, though. If you’re not deterred and still want to attempt to set sail for the sunny shores of Studio, then I commend you - your courage is laudable - and hopefully this post will serve you well in your efforts. Before you read any further, I urge you to skip ahead to the ‘Blockers’ section. This contains the issues that have most severely impeded our Android Studio efforts and there’s a good chance they’re going to do the same to yours.
If that didn’t scare you, then I’d also like to refer you to Google’s recent Android Studio 2.2 developer preview from their 2016 I/O event. The pertinent section here is the ‘build’ section, where they’ve opted to support external build systems such as ndk-build (don’t get too excited, it doesn’t work) and CMake. That choice alone tells you everything you need to know about the faith they must have in their own Gradle plugins.
One thing I found when attempting to migrate to Android Studio is that the resources for configuring native-heavy projects are somewhat scattered. On top of that, the Gradle plugin is awash with commands and features (internally, we colloquially refer to these as ‘spells’) that only present themselves as viable solutions in the deepest of Stack Overflow searches.
If you haven’t already stumbled across it, I strongly recommend using Xavier Hallade’s blog as a reference for the Gradle components of the Android experimental plugin.
Gradle is Groovy
I’m not just attempting to resurrect adjectives that were popular four decades ago here; Gradle’s DSL is based on Groovy, a programming language that interoperates with Java. In fact, you can often write Java code in your Gradle build files and it’ll compile just fine. This is quite handy if the idiomatic Groovy way of doing things is unclear - you can almost always fall back on a familiar Java method.
For example, we may want to create a list of header search paths to pass to the C++ compiler. We want to make sure that we only include each path once - who wants to trawl through lines upon lines of duplicate entries when they’re checking out out their compiler input to see why MySuperCode.h can’t be found? Usually, when we’re talking about lists of unique values in Java or C++ we’re ubiquitously talking about sets and in Gradle’s DSL it’s perfectly acceptable to use them. What’s more, these types interoperate well with Groovy, meaning you can use some handy Groovy constructs to iterate through them.
On the subject of compiler paths, we found that the C++ builds in Android Studio don’t cope well with relative include paths. It’s not unreasonable to want relative include paths in your compiler input - imagine the pain involved if all paths had to be absolute and you shared code between developers, machines and file systems. Fortunately, you can use Groovy’s
absolutePath on Java File objects to resolve this.
absolutePath is a Groovy convenience that actually calls into Java’s getAbsolutePath method in the File class.
More Architectures, More Problems
One of the biggest hurdles I faced in migration was the inability to build 32-bit and 64-bit configurations from the same Gradle module due to a bug in Android Studio 2.1. In the current release, you’re able to specify which NDK platform version is compiled against but you can only do this as an overall setting - that is, you can’t specify this on a per-configuration basis. This means that if you compiled with version < 21, you won’t be able to build 64-bit configurations (version 21 was the first to offer 64-bit support). However, if you try to compile _all _of you configurations with version >=21, you’ll most likely run into compiler/linker errors for your 32-bit builds.
Fortunately, this is now on Google’s radar and a fix is projected for version 2.2 allowing users to specify per-configuration NDK platform versions. Until then, this remains a problem and here at eeGeo we found it had two main issues:
- We can’t easily build 32-bit and 64-bit builds within Android Studio without modifying a Gradle build file
- We have no means of creating a fat APK (a single APK containing multiple architectures)
Building within Android Studio
Thus far, the best workaround I’ve found is to split the project. We have an app project that serves as the root of our app that contains two sub-projects - arm and arm64. The arm and arm64 projects are identical in setup - they both have their own build.gradle and reference the same files. The one difference is that the arm project specifies a NDK platform version of 9 whilst the arm64 project specifies 21.
Our app’s project split into arm and arm64 sub-projects.
This does introduce a fair amount of DRY-fail and unnecessary inventory (you need to maintain two gradle.build files that differ in only one property) but considering how little you tend to interact with a build.gradle file once your project is up and running it seems a reasonable temporary arrangement.
This approach solves issue 1) above by now allowing us choose whether we want to run the arm or arm64 project from within Android Studio. You’ll need to know which architecture your device is running in order to make sure you’re building the right configuration but if in doubt you can always use
getprop in ADB:
Issue 2) is slightly trickier to deal with. Although the Google Play store supports multi-APKs, it is best practice to publish a single APK. Android Studio comes laden with helpful features for your APK, such as the ability to specify keys for release signing or choosing to zipalign your APK. Unfortunately, once you lose the ability to create a fat APK from within Android Studio itself, those features become somewhat redundant and everything has to be done manually. And by manually, I do, of course, mean with a script.
Since our arm/arm64 project is a child of the app project, everything can be built from the command line by heading to the app directory and running
gradle assembleRelease (assembleRelease is one of the Android plugin tasks that will assemble all configurations in your gradle.build files). In our case, this produces three unsigned, unaligned APK files; armeabi and armeabi-v7a (as specified by the arm configuration) and arm64-v8a (as specified by the arm64 configuration). We need to combine these three into one signed and aligned APK.
The Android SDK contains some handy tools that will help out here. The first is the Android Asset Packaging Tool (aapt), which allows you to create and update zip-compatible archives (which, after all, is what an APK file is). We’re specifically interested in the update functionality here - we want to add the native shared objects from one architecture-specific APK to another. In particular, we’re interested in two files - libmobile-example-app.so and libeegeo-sdk.so. The former is the native code produced by our app build and the latter is our C++ SDK that the app depends on.
We can use
aapt add to combine the APKs as needed. Here’s a snippet of how you can add the armeabi-v7a libraries to the armeabi APK.
Once we have our combined APK, we can sign using
jarsigner and align using
zipalign (jarsigner is deployed with the JDK and zipalign with the Android SDK - there are numerous resources online detailing how to use each, to I’ll refrain from going into any further detail here).
It would be quite a handy commodity to be able to share useful methods across gradle.build files. Well, fortunately, you can, though there’s a small extra step involved.
It’s not possible to refer directly to methods defined in other Gradle files. However, it is possible to share properties between Gradle files. The solution here is to store your method as a closure:
This stores the myMethodImplementation method as an extra property (a closure) named myMethod. Assuming this is in a file named common-methods.gradle that sits in the root of your project, you could now use this elsewhere:
This one is minor, but is easily overlooked. Gradle can sometimes be a little slow to start up due to the initialisation of various dependencies pulled from the JRE libraries (Gradle runs through JVM). A low-effort solution to help this is to enable the Gradle daemon - which will run as a background process and avoid the expensive initialisation process.
You can enable this by adding a single line to your gradle.properties file on your local development machine. This is usually found at ~/.gradle (if it doesn’t exist, you can go ahead and create it yourself).
This is a collection of stumbling blocks that have brought our Android Studio development efforts to its knees. I’ll add links to bug reports where I can.
Can’t specify platformVersion between ABIs
This is the bug listed in the ‘More Architectures, More Problems’ section above. The bug report is here and, as the above section details, you can workaround this (albeit through a set of fairly extensive steps). Still, it deserves its place in this section as its presence and consequent work around lay waste to some of the more useful Android Studio features, rendering them completely redudant. Well, that and because it feels somewhat cathartic to rant about it a little more.
Consequences: You can’t build 32-bit and 64-bit ABIs from a single build.gradle file. Workaround involves splitting your build files into two, producing large amounts of DRY-fail and having to manually create, sign and align fat APKs.
Code dependent on files included with -I will not be recompiled
Here’s the bug report for this one. It’s laughable. This was allegedly fixed back in February. The initial reporter commented two days after the fix to say that it’s still present and it’s been quiet ever since. I’ve added some simple reproduction steps and details to the bug report in an effort to help it along. Don’t hold your breath.
The crux of the issue is that if you include header paths in your C++ build via the -I compiler flag, any files dependent on headers within those paths will not be rebuilt should the header change. This seems like a pretty rudimentary compiler option to be bugged - the -I flag is possibly one of the more commonly used options when compiling C++ code. Check out my sample project exhibiting the problem behaviour to see what effect this has. Reproduction steps are in the README.md.
Consequences: Changes you make to your code won’t be reflected when you build. You’ll have to do a full rebuild in order to have them take effect.
ndkBuild in externalNativeBuild ignores Application.mk
Given the numerous issues above, it’s pretty evident that the Gradle experimental plugin isn’t sufficient enough the meet the needs of most NDK developers. Apparently, Google must realise this too. Why else would they opt to support external build systems in the latest Android Studio 2.2 preview release?
This felt like somewhat of a saving grace - after all, you’re probably already using ndk-build and its buddies Android.mk and Application.mk to build out your native projects. Being able to call into that build system from Gradle in Android Studio sounds great! Except it’s not.
Here’s the bug for this one. The issue here is that externalNativeBuild doesn’t abide by the settings specified in your Application.mk file. Oddly enough, it cares that one exists at all (I renamed Application.mk to something else to see what happens - sure enough, Gradle complains that it expects to see one in your jni directory), but it doesn’t care what’s in it. Instead, it looks like Studio attempts to go ahead and build with default settings. That means it’ll try to build all architectures using standard STL settings and there’s nothing you can do about it. I’ve tried specifying abiFilters in a bunch of different places within my Gradle build files to no avail.
It’s acknowledged in the technical docs that Application.mk isn’t supported yet. I can’t help but feel that this feature is a little oversold. Still, supporting one of the two critical files you need for your build ain’t bad, right?
I’ve noticed during this that the NDK platform it builds against is driven by the compileSdkVersion in your build.gradle as well. NDK platform versions and Java compile versions are orthogonal concepts. It’s perfectly valid to build against Android (Java) SDK 23 and have your native code compile against some much old version, for example. I’ve not bugged this because I’m assuming that this shortcoming will resolve itself once Application.mk support has been implemented.
Consequences: The settings in Application.mk will be ignored and your build will likely fail if you use anything other than the default settings. Your dreams of keeping your ndk-build process will be severely crushed in the process.
I wanted this to work. Really, I did. I don’t enjoy sparking up Eclipse whenever I need to make some changes to our Android code and, consequently, I don’t really enjoy Android development all that much. Possibly the most frustrating thing about this whole endeavour is that at times its seemed tantalisingly close to realising the possibility of using Studio as a primary native development path, only to fall short due to a few fundamental oversights.
The fact that this post exists at all is probably enough evidence in itself to suggest that Android Studio just isn’t ready for native development. If you’ve been working with native Android development at all in the last few years then this is not news to you. It seems like the carrot of happier Android native prospects to dangle merely inches in front of our faces for at least a little longer.
The future doesn’t look all that promising, either. The experimental Gradle plugin, used by all of the NDK examples, and the stable plugin used in the latest 2.2 preview build seem to be heading in different directions. While the former is focusing on Gradlifying the Android Studio build experience, the latter seems to be conceding that you really want to depend on some external system (at least in the immediate future). When the two might converge is anyone’s guess.
You could probably use some amalgamation of workarounds if you’re adamant that you need Android Studio in your life, but you’ll probably be setting yourself up for some sizeable development pains by straying from the beaten path. For the time being, it looks likes the only real option for Android developers making use of JNI is to keep on trucking with Eclipse and that’s what we’ll continue to do here at eeGeo.
Besides, having the ability to cancel running builds is overrated anyway.
The Android robot is reproduced or modified from work created and shared by Google and used according to terms described in the Creative Commons 3.0 Attribution License.