MobileAndroidLearn the Android NDK Fundamentals

Learn the Android NDK Fundamentals

Developer.com content and product recommendations are editorially independent. We may make money when you click on links to our partners. Learn More.

In addition to the Android Software Development Kit (SDK), NDK is Android’s Native Development Kit that provides the option for the support of integrating native coding development from other computer languages such as C, C++, Kotlin, and so on. However, it is certainly not for everyone, considering the knowledge and complexity involved. Usually, developers who consider fine-tuning the device performance or implementing apps with computationally-intensive operations will need to look into this option. It also comes in handy for those who simply want to incorporate some existing libraries done in other computer languages other than Android’s own without re-inventing the wheel.

In this tutorial, we try to cover the fundamental steps required in NDK development and point out the key resources to further explore the advanced aspects of the toolsets. First, we provide the info where to download the NDK libraries, how to install them, and what components to verify before you start. Next, we try a hello-world build just to make sure everything is set up correctly. Finally, we walk through a more complicated example to illustrate the NDK basics.

Downloading and Installing NDK-related Tools

Because Android is only fully supported in the Android Studio IDE, I would recommend you go ahead and get its latest version (3.0.1 as of now). The NDK download is also available, so you can download the package and do it manually after choosing the correct platform. Then, you’ll need to download your preferred native compiler; for example, the freely available CMake. Perhaps you want a debugger as well. When all these tools are installed, the most important thing you want to check is whether you have the NDK default directory pointing to the downloaded package, as in Figure 1. To open this dialog, you can right-click the project and select “Open Module Settings.” It is under the “SDK Location” tab on the left-hand side panel.

Luckily, you can simply avoid adventuring through all the trouble of setting up the preferred tools by using the defaults provided to you in Android Studio. We are going to take advantage of this convenient offer and focus on how we establish the connection between Android and native code. In Figure 2, it is the SDK Manager started up by Android Studio when you go to its menu under “Tools -> Android.” Switch to “SDK Tools” and you should see the recommended tools highlighted in Figures 1 and 2. Press the “Apply” button and you are all set without the need to worry about the options and installations.

Android NDK Location
Figure 1: Android NDK Location

NDK Tools by Android Studio
Figure 2: NDK Tools by Android Studio

Setting Up for a Hello-world Build

Now, we have all the basic NDK tools ready for native code development in Android. The first thing we can do is to check if it even compiles with the default before we start thinking too far ahead on those complicated projects. In Figure 3, we start a new project just like other Android ones except for checking the “Include C++ support” option this time, assuming our native code will be in C or C++. Wait a little bit when Android Studio is preparing all the files for you; then, you should see something similar to Figure 4. To help us understand the differences between NDK and non-NDK projects, there are four items we want to look more into:

  • CMakeLists.txt: This file basically lets you create and name the libraries. It has the relative paths to the source code. This is used by the compiler CMake.
  • build.gradle: This is a project management file and its externalNativeBuild property links to the above file CMakeLists.txt.
  • native-lib.cpp: This is your C file in its JNI-style format. Because Android is based on Java, it adopts the JNI (Java Native Interface) to establish communication between Java and C++ components. To get the complete list of JNI specifications, you can check out the link in the references.
  • MainActivity.java: Inside this automatically generated main activity, you can see it includes the default named NDK library native-libas follows:
    static {
       System.loadLibrary("native-lib");
    }
    

Android devices are manufactured by many equipment makers and thus have various hardware architectures. To interact with different hardware systems more efficiently, Application Binary Interface (ABI) defines your app’s machine code for this purpose and different ABIs correspond to different architectures. The NDK includes ABI support for ARM, AArch64, x86, and x86-64. Now, if you press the “Run” button, your main Android files as well as those NDK ones will be compiled and integrated into the final app build. The result is shown in Figure 5. In a very extreme case for people interested in developing an entirely native activity, you should check out the info on NativeActivity. Your application can simply be declared as native in the AndroidManifest.xml file.

Also, if you are like me, adopting NDK at an earlier stage using ndk-build, it is also supported by Android Studio. Many other existing projects from the past also use that build toolkit, but I’ll leave it to readers to explore in more details.

Android Project with NDK Support
Figure 3: Android Project with NDK Support

NDK Default Hello-world App
Figure 4: NDK Default Hello-world App

Result of Default App
Figure 5: Result of Default App

Working with a More Useful Example

The previous section was used to prove our installation and compilation has been set up correctly. We are now ready to work on something more interesting by calling an NDK function with an array. The array contains the pixel info from a bitmap image. Once the pixel array is updated from the NDK call, we then render the result bitmap in Android’s calling activity. In Listing 1, we declare an NDK function called ndkEmboss for the emboss effect and add a keyword native, required by NDK. Then, a test bitmap is loaded and its pixel info is extracted into an array called pix. After calling the NDK function, we render the pixel array in our new bitmap object out. Lastly, we updated the display with the new bitmap resource on the device screen.

public class MyNDKActivity extends Activity {
   // Load the native library 'native-lib' on app startup
   static {
      System.loadLibrary("native-lib");
   }

   // Native methods are listed here but implemented in C++ files
   public static native void ndkEmboss(int[] data, int width,
      int height);

   @Override
   protected void onCreate(Bundle savedInstanceState) {
      super.onCreate(savedInstanceState);

      // Get resource from the layout
      setContentView(R.layout.activity_my_ndk);
      ImageView iv = (ImageView)findViewById(R.id.iv);

      // Load the bitmap into an array
      Bitmap b = BitmapFactory.decodeResource(getResources(),
         R.drawable.bird);
      int[] pix = new int[b.getWidth() * b.getHeight()];
      b.getPixels(pix, 0, b.getWidth(), 0, 0, b.getWidth(),
         b.getHeight());

      // Call the native method 'ndkEmboss'
      ndkEmboss(pix, b.getWidth(), b.getHeight());

      // Create a new bitmap with the result array
      Bitmap out = Bitmap.createBitmap(b.getWidth(), b.getHeight(),
         Bitmap.Config.ARGB_8888);
      out.setPixels(pix, 0, b.getWidth(), 0, 0, b.getWidth(),
         b.getHeight());
      iv.setImageBitmap(out);

      // Clean up
      pix = null;
      b.recycle();
   }
}

Listing 1: Android Activity Calling NDK Function

Listing 2 is the implementation for the NDK function ndkEmboss. As you can see in the code, the JNI function name Java_com_chunyenliu_myndk_MyNDKActivity_ndkEmboss contains the names of the app package as well as the activity. The first two arguments, JNIEnv and jobject, are required by JNI, so you should start matching the argument list after those. The mapping between Android’s int and JNI’s jint or Android’s integer array and JNI’s jintArray can be located in JNI’s complete list of specifications. There are simply too many to list here. If you are interested, the emboss effect basically computes the inner product values of the two vectors of a virtual light source and a pixel source normal. And, the final values are scaled to fall within the range of 0 and 255, the 8-bit value for a color channel in this case. All channels are supplied with the same values, so the result looks like a grayscale image, as in Figure 6.

JNIEXPORT void JNICALL Java_com_chunyenliu_myndk_MyNDKActivity
      _ndkEmboss(
   JNIEnv* env, jobject thiz, jintArray data, jint width,
      jint height) {
   int i, index, pp, rr, gg, bb;
   int* tem;
   int x, y;
   double Lx, Ly, Lz, NzLz, az, elev;
   int Nx, Ny, Nz;
   int yl, yr, xl, xr, v;
   int i1, i2, i3, i4, i5, i6, i7, i8;

   // Get input data
   jint *buffer = env->GetIntArrayElements(data, NULL);
   env->ReleaseIntArrayElements(data, buffer, JNI_ABORT);
   tem = (int*)malloc(sizeof(int) * width * height);

   // Populate into the output array as greyscale
   for (i = 0; i < width * height; i++) {
      pp = buffer[i];
      rr = ((pp >> 16) & 0xff);
      gg = ((pp >> 8) & 0xff);
      bb = (pp & 0xff);
      pp = (int)(rr * 0.3 + gg * 0.59 + bb * 0.11);
      tem[i] = pp;
   }

   // Prepare the light source
   az = 30 * 3.14159 / 180;
   elev = 30 * 3.14159 / 180;
   Lx = cos(az) * cos(elev) * 255;
   Ly = sin(az) * cos(elev) * 255;
   Lz = sin(elev) * 255;
   Nz = (6 * 255) / 3;
   NzLz = Nz * Lz;

   // Apply the light source
   for (y = 0; y <= height - 1; y++) {
      for (x = 0; x <= width - 1; x++) {
         index = y * width + x;
         yl = (y - 1 + height) % height;
         xl = (x - 1 + width) % width;
         yr = (y + 1 + height) % height;
         xr = (x + 1 + width) % width;
         i1 = yl * width + xl;
         i2 = yr * width + xr;
         i3 = yr * width + xl;
         i4 = yl * width + xr;
         i5 = y * width + xl;
         i6 = y * width + xr;
         i7 = yr * width + x;
         i8 = yl * width + x;
         Nx = tem[i1] + tem[i5] + tem[i3] - tem[i4] - tem[i6]
            - tem[i2];
         Ny = tem[i3] + tem[i7] + tem[i2] - tem[i1] - tem[i8]
            - tem[i4];
         v = (int)(Nx * Lx + Ny * Ly + NzLz);
         if (Nx == 0 && Ny == 0)
            v = (int)Lz;
         else if (v < 0)
            v = 0;
         else
            v = (int)(v / sqrt(Nx * Nx + Ny * Ny + Nz * Nz));
         v = (v > 255) ? 255 : ((v < 0) ? 0 : v);
         buffer[index] = 0xff000000 | (v << 16) | (v << 8) | v;
      }
   }

   free(tem);

   // Update output array
   env->SetIntArrayRegion(data, 0, width * height, buffer);
}

Listing 2: NDK Function Implementing the Emboss Effect

Result after the NDK Emboss Effect
Figure 6: Result after the NDK Emboss Effect

Conclusion

Once a while, you, as a developer think about how to make your Android apps perform better and run smoother. Once a while, you also wonder how you can add some nicely done libraries in other languages to improve your Android app functionalities to attract more users. NDK is what you need to get yourself started to realize those thoughts. It might be intimidating at first, but once you get more familiar with NDK, it is definitely not rocket science.

We provided the basic setup process for the NDK development in this tutorial. Furthermore, we illustrated the concept by a simple hello-world example as well as a slightly more practical one. Whether you decide to create the native projects on your own or try to incorporate third-party native libraries, the best way will be to give it a try and figure out some tool discrepancies. Sources codes for the tutorial examples are available for download in the references section.

References

About the Author

Author Chunyen Liu has been a software veteran in Taiwan and the United States. He is a published author of 40+ articles and 100+ tiny apps, a software patentee, technical reviewer, and programming contest winner by ACM/IBM/SUN. He holds advanced degrees in Computer Science with 20+ graduate-level classes. On the non-technical side, he is enthusiastic about the Olympic sport of table tennis, being a USA certified umpire, certified coach, certified referee, and categorized event winner at State Games and the US Open.

Get the Free Newsletter!

Subscribe to Developer Insider for top news, trends & analysis

Latest Posts

Related Stories