PlayPacks is our newest app for Android and iOS, and one of our most sophisticated. It combines augmented reality technology with multiple sample-accurate audio stream playback, all connected to a web sharing backend. Making this work smoothly across both platforms proved to be a real technical challenge. In this post I'll share some of the techniques we used to get it running across two very different operating systems.
The porting problem
Today there are just two mobile platforms that are relevant to consumers – Android and iOS. Reaching the widest possible audience for your app means releasing on both. Unfortunately this usually requires creating and maintaining two codebases, an undertaking requiring as much work as writing two separate apps.
On the surface Android and iOS development are radically different. Android uses the Java platform as a base, and provides an application framework on top of it. iOS apps are written in Objective-C and built with the Cocoa framework, a library with a history extending back to the 80's. Between different vendors, languages, and foundation libraries the opportunities for code sharing seem limited.
A native strategy
We have to dig deeper at the operating system level to find the similarities. Beneath its Java layers Android is built on a Linux kernel. And iOS is based on Darwin, Apple's BSD derivative. Both share a lineage with Unix, the great-grandaddy of modern operating systems. Although their paths diverged a long time ago, there remains a common core set of functionality across each. This is exposed through lower level C and C++ code in their respective environments.
To be clear, this isn't enough to write an entire app. The common native libraries provide interfaces to low-level plumbing, including parts of the POSIX standard (threading, I/O, system utilities), C and C++ standard libraries, and the OpenGL accelerated graphics interface. Standard platform features (e.g. user interface controls, app lifecycle management, AirPlay) must still be written within the platform framework (Cocoa, Android SDK) and interfaced with glue code.
The payoff for living with these limitations is true code sharing. By writing the core features of the app in native code and using portable native libraries, we can reuse the exact same codebase across both platforms. Within this core set, features are written once, bugs are fixed once, and the changes propagate to both platforms.
Xcode provides direct support for C and C++ code natively. On Android, the Native Development Kit (NDK) provides toolchains to build shared libraries from C or C++. The libraries are packaged with the Java parts of the app and interoperate through the Java Native Interface (JNI), a way to call native code from Java and vice versa.
Porting in practice
With PlayPacks we knew we were going to produce Android and iOS versions in quick succession. This informed us to design the app upfront with portability in mind. Our technique consists of two golden rules:
- Write as much portable code as possible using native (C++) code
- Strictly adhere to an identical structure for platform specific code
Rule #1 – write and use portable native code
The unique technologies built into PlayPacks are the augmented reality and synchronized audio features. We built these on Vuforia and libpd respectively. These are two cross-platform native libraries built with platform glue code. This is, not coincidentally, exactly the same technique we employ for portability and made it easier to integrate them into our source build tree.
The core parts of the app are written almost completely in native code. This includes code to draw and animate the Mixer and AR views, play back audio, record and save audio sequences, store user configurations and interact with the augmented reality markers.
From the brief we knew we required custom rendering and animation in each screen that required complete control of the drawing surface – tasks that OpenGLES is particularly well suited to. On each platform these OpenGLES views are embedded into customized platform-specific views that integrate with the native UI hierarchy.
Similarly, sound is generated as a final mixed floating point stream and passed to platform specific interfaces to the audio hardware. I'll go into more detail about the media playback details in a separate post.
Rule #2 – use a shared structure for platform specific code
As outlined above, accessing standard features still requires us to write a lot of platform specific code. In PlayPacks, all the menu controls, video playback, network access and interstitial screens are written against their respective platform libraries – Java/Android SDK on Android and Objective-C/Cocoa on iOS. Obviously reusing the source code in these environments is impossible.
Instead we do the next best thing – reusing the code structure. By exploiting the similarities between Java/Objective-C and Cocoa/Android SDK, the shape of our platform specific code is identical on each platform. Both languages are high level and object oriented. And we can map common concepts between frameworks – see the table below. In this way, we created every Objective-C class with a functionally equivalent Java class containing the same methods and calling patterns.
Mapping concepts between Cocoa and Android
|View controller containment||Fragment|
This isn't quite as low maintenance as reusable source across platforms, but a lot nicer than decoupled codebases. We had the iOS version mostly completed before commencing work on Android, and following this rigid structure made the porting process very fast. Using the iOS code for reference, there were no decisions to be made about structure – instead classes were converted method by method, line by line. Any differences precipitated by the platform library were encapsulated as best as possible in helper classes.
The shared structure also helps with synchronizing changes between the two. When a change is made in one codebase it is easy to find and amend the equivalent code in the other, usually by looking up the same class and method name.
Before jumping into native code, its worth considering some of the pros and cons:
- Low level access to system facilities, including OpenGLES
- Runs across both platforms
- Modern language features (C++11 support)
- Works across platforms
- Higher upfront development cost
- Bigger, longer build times – must be built and packaged for each supported architecture
- iOS – armv7, armv7s, arm64
- Android – armv7, armv7eabi, mips, x86
- Harder to debug on Android – stack traces will be your friend
We've been playing with these technologies for a while now – a good chunk of the rendering code was cribbed directly from a long-term internal prototype. PlayPacks is the first app we've released where we've been able to demonstrate the feasibility of this approach for rapidly deploying to iOS and Android while keeping feature parity. Code sharing with native code enables us to target both platforms without compromising performance along the way. We hope by evolving this technique we can improve our time to market while treating both platforms as first class citizens.