Android JNI Loading & Live/partial App Update
In today's mobility world, apps that replace desktop apps tend to be more complex, making them bigger than ever. Furthermore, the added support for 64 bits mobile platforms in the last months caused C/C++ based apps to become even larger. Unfortunately, for many developers, the size of an app is an issue since some of their target clients don't have wifi access and must rely only on a low speed, and often expensive (pay per data) mobile connection.
In this way application size remains a challenge for developers using C/C++ code, but Android provides the necessary tools to help lowering app size and even provide some interesting capabilities regarding the standard application update process.
C/C++ apps need to be loaded from a JNI library at run time.
Android JNI Loading
Android support 7 platforms (armeabi, armeabi-v7a, arm64-v8a, x86, x86_64, mips, mips64). This means that if your C/C++ library is 2MBytes in size, you will embed 14 Mbytes of library in your app, while only 2MBytes is really useful for the device which will run your app. This may seem OK for such a small library, but if you use a library like CrossWalk where the JNI for a single platform is more than 20MBytes, things may turn ugly very quickly.
Fortunately, you are not forced to embed your JNI library in your APK. You can download and then load them at run time.
First, we will look at how we can load JNI libraries on Android.
Usually, JNI files are loaded using a static code block in the java file containing your JNI functions declarations like:
static { System.loadLibrary("my-jni-lib"); }
Where “my-jni-lib” is the name of your JNI library. The corresponding filename on your file system is “libmy-jni-lib.so” in the folder corresponding to the target platform.
Note that the call to “loadLibrary” can be made statically or not, from any java file. However, by calling it statically from the class containing the JNI functions declaration, you insure the JNI library will be loaded before any call to one of its function will be triggered.
When using “loadLibrary” you don't have to deal with the device architecture (ABI): Android selected the best library when you installed your APK on the device.
Furthermore, Java, and Android allow to load JNI libraries using an absolute path to the JNI file . To do that, you can use:
System.load("/path/to/libmy-jni-lib.so");
Unlike the “loadLibrary”, you have to provide the full path to the library file, with its complete filename. You have to insure the library you try to load is compatible with your platform (compatible ABI).
Note that like for the method “loadLibrary”, this function call can be made statically or not, and from any java file. However, whatever you do, you will have to insure the JNI library is loaded before calling one of its functions.
Now, If we don't want to embed all the JNI platforms files in the APK, we can try to download the good one from the internet and then load it at runtime.
Android JNI Download and Storage
One of the easiest strategy regarding the download of a JNI file is to use a dedicated activity which will be the launcher activity. Its job is to verify if the JNI library is already available on the device and download it if required while informing the user about what is happening. Then this activity will start the real application activity which will load the JNI file and do what your app is expected to do.
Downloading the good library for your platform
Downloading the JNI file is very easy, and you can use whatever system your prefer. However, to download the correct library for the architecture you are using, you will have to identify your platform ABI. ABI stands for Application Binary Interface.
To get the string corresponding to a device ABI, you can use the constant:
android.os.Build.CPU_ABI;
Note that “CPU_ABI” is deprecated since API level 21 and for devices with API level 21 and above, Google recommends to use:
android.os.Build.SUPPORTED_ABIS;
Unlike “CPU_ABI” which provides a single ABI, “SUPPORTED_ABIS” provides a list of supported ABIs, providing you more choices. For example, a device with “armeabi-v7a” as “CPU_ABI” can usually support JNI library for “armeabi-v7a” and “armeabi”.
In any cases, the strings provided by “CPU_ABI” or “SUPPORTED_ABIS” correspond to the folder name where each JNI is copied when you build an android app. So, for example, you can copy these folder on a server and use the ABI information to look for the folder containing your JNI library.
Storing your library in a safe place
When you download your JNI file, you need to copy it in a safe place where it can't be altered by another app. The best place for that is your application private folder. You can create a folder called “lib” int it, and copy your JNI file in. To get the path to this folder and create it if required, you can use:
File path = context.getDir("lib", Context.MODE_PRIVATE)
Where “context” is the current context of your application or activity. The method returns a File object containing the path to the folder
Now you have the path to the folder, you can copy your library in it, and load it using the code described above.
You have to note that the file in your private folder is protected from being modified by other apps, but the user can still delete it using the “Clear Data” option available in your app properties from Android settings. If you want to prevent the user from deleting your JNI library, android provides the tools for that.
Protecting the downloaded JNI Library
In order to prevent the user from deleting your JNI library, you have to handle the “Clear Data” function yourself. To do that, you have to create an Activity which will delete all private files except your JNI library file. Declare it as usual in you manifest file.
In the manifest, in “application” element, add the following attribute:
android:manageSpaceActivity="com.example.myapp.MyClearAppDataActivity"
Where "com.example.myapp.MyClearAppDataActivity" is the path to your activity class.
When the user will click on “Clear Data” button, then your activity will be launched, and you will be able to delete the private files, while keeping your JNI library.
Insure JNI Library integrity
Since many users have root access to their devices and for additional security reasons, you may want to insure the version of the JNI Library file you are about load is exactly the one you want. You can implement various solutions depending on the level of security you expect. A very basic and simple solution is to validate the checksum of the JNI file when the app starts.
Update your C/C++ CODE, NOT your APK
Since you are able to download a library and execute it, you are now able to download a new version of this library and use it, without having to update the whole APK.
This is a very powerful tool, since it may allow you to control your updates, force them if required, and even proceed with very small size updates by sending only the difference between the new JNI library and the one already installed on the user device.
Update your APK, not your C/C++ CODE
The other advantage of such system is that you can update you APK without having to update the JNI your downloaded from internet, since when an application is updated, private data is not deleted. This may be very useful if you use a very big JNI library like CrossWalk, and need to update only your code.
Important considerations
All the concepts described in this document are very useful, and may allow to have a better control on how your apps are installed and updated.
If your apps are distributed and installed through your own channel you can do whatever you want. However, if you distribute your apps through Google Play, you need to consider that Google may not allow such practice in the future and prevent your app from being distributed. Indeed, Google may consider that such practice may be a security hazard for the final user, considering that the JNI code which is executed by the device is not checked by Google Play anti-malware system.