Using Android NDK to Optimize Barcode Reading Performance

Previously, I shared an article demonstrating how to use Camera2 APIs and Dynamsoft Barcode Reader to build a simple Android barcode reader app. In that demo project, the barcode decoding part is implemented in Java, which apparently has room for improvement. If we can get the pointer to the native buffer of the camera frame, we can invoke native Barcode Reader APIs directly. This article shares how to write JNI code for Android barcode detection, as well as how to use Android NDK and CMake to build the C++ code.

Prerequisites

Android NDK and CMake

Open Android Studio and select Tools > SDK Manager > SDK Tools. Check NDK and CMake to install:

android studio ndk cmake

To learn NDK, you can visit https://github.com/googlesamples/android-ndk.

Dynamsoft Barcode Reader for Android

Download Dynamsoft Barcode Reader for Android.

Extract the DynamsoftBarcodeReaderAndroid.aar file from the package. To build a barcode reader app in Java, you just need to import the *.aar file as a module. Here I’m going to show you how to invoke native APIs, so open the aar file with a file archiver, such as 7-Zip. Extract a platform-compatible shared library from DynamsoftBarcodeReaderAndroid.aar\jni\<ARCH>\ libDynamsoftBarcodeReaderAndroid.so.

dynamsoft barcode armeabi

The Simple Barcode Reader Demo

Get the source code from GitHub:

git clone https://github.com/yushulx/android-camera2-barcode

Android Barcode Decoding Using JNI

Import the project into Android Studio. It is time to do the optimization.

Here are three native methods:

private native ArrayList<SimpleResult> readBarcode(long hBarcode, ByteBuffer byteBuffer, int width, int height, int stride);
private native long createBarcodeReader(String license);
private native void destroyBarcodeReader(long ndkBarcodeReader);

What I’m going to do is to instantiate the barcode reader object in C++ and save its memory address in Java.

Android Camera2 APIs provide an ImageReader class to acquire preview images from the camera. The returned data type is ByteBuffer rather than byte[]. A byte buffer is allocated from native code via JNI. When using the buffer for barcode detection in Java, we have to copy it into a byte array:

byte[] bytes = new byte[buffer.remaining()];
buffer.get(bytes);

In contrast, there’s no extra memory copy step in C++:

unsigned char * buffer = (unsigned char*)env->GetDirectBufferAddress(byteBuffer);

The underlying technology of Dynamsoft Barcode Reader for Android is also based on JNI. When calling the barcode decoding method in Java, it will first copy the Java byte array into a native buffer in order to invoke the C++ methods. Therefore, the optimization I can do is to reduce the image memory copy twice.

Create a src/main/cpp/android_main.cpp file:

#include <jni.h>
#include <cstring>
#include "DynamsoftBarcodeReader.h"
#include <android/log.h>

#define LOG_TAG "BarcodeReader"
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__)
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__)
/**
 * BarcodeReader object:
 *
 */
void *pBarcodeReader = nullptr;

extern "C" JNIEXPORT jobject JNICALL
Java_com_example_android_camera2basic_Camera2BasicFragment_readBarcode(JNIEnv *env, jobject instance, jlong hBarcode,
                                                                       jobject byteBuffer, jint width, jint height, jint stride) {

    // ArrayList
    jclass classArray = env->FindClass("java/util/ArrayList");
    if (classArray == NULL) return NULL;
    jmethodID midArrayInit =  env->GetMethodID(classArray, "<init>", "()V");
    if (midArrayInit == NULL) return NULL;
    jobject objArr = env->NewObject(classArray, midArrayInit);
    if (objArr == NULL) return NULL;
    jmethodID midAdd = env->GetMethodID(classArray, "add", "(Ljava/lang/Object;)Z");
    if (midAdd == NULL) return NULL;

    // SimpleResult
    jclass cls = env->FindClass("com/example/android/camera2basic/Camera2BasicFragment$SimpleResult");
    if (NULL == cls) return NULL;
    jmethodID midInit = env->GetMethodID(cls, "<init>", "(Ljava/lang/String;Ljava/lang/String;)V");
    if (NULL == midInit) return NULL;


    unsigned char * buffer = (unsigned char*)env->GetDirectBufferAddress(byteBuffer);

    int ret = DBR_DecodeBuffer((void *)hBarcode, buffer, width, height, stride, IPF_NV21, "");

    if (ret) {
        LOGE("Detection error: %s", DBR_GetErrorString(ret));
//        return NULL;
    }

    STextResultArray *pResults = NULL;
    DBR_GetAllTextResults((void *)hBarcode, &pResults);
    if (pResults)
    {
        int count = pResults->nResultsCount;

        for (int i = 0; i < count; i++)
        {
            jobject newObj = env->NewObject(cls, midInit, env->NewStringUTF(pResults->ppResults[i]->pszBarcodeFormatString), env->NewStringUTF(pResults->ppResults[i]->pszBarcodeText));
            env->CallBooleanMethod(objArr, midAdd, newObj);
        }

        // release memory of barcode results
        DBR_FreeTextResults(&pResults);
    }
    return objArr;

}

extern "C" JNIEXPORT jlong JNICALL
Java_com_example_android_camera2basic_Camera2BasicFragment_createBarcodeReader(JNIEnv *env, jobject instance, jstring license) {
    if (!pBarcodeReader) {
        // Instantiate barcode reader object.
        pBarcodeReader = DBR_CreateInstance();

        // Initialize the license key.
        const char *nativeString = env->GetStringUTFChars(license, 0);
        DBR_InitLicense(pBarcodeReader, nativeString);
        env->ReleaseStringUTFChars(license, nativeString);
    }

    return (jlong)(pBarcodeReader);

}

extern "C" JNIEXPORT void JNICALL
Java_com_example_android_camera2basic_Camera2BasicFragment_destroyBarcodeReader(JNIEnv *env, jobject instance, jlong hBarcode) {

    if (hBarcode) {
        DBR_DestroyInstance((void *)hBarcode);
    }

}

Where is the DynamsoftBarcodeReader.h file? The header file does not exist in the SDK package for Android.  But don’t worry. Since Dynamsoft Barcode Reader is cross-platform, it is easy to get the header file from other editions.

We can compare the code change.

Reading barcodes from the Java layer:

ByteBuffer buffer = image.getPlanes()[0].getBuffer();
int nRowStride = image.getPlanes()[0].getRowStride();
int nPixelStride = image.getPlanes()[0].getPixelStride();
byte[] bytes = new byte[buffer.remaining()];
buffer.get(bytes);
TextResult[] results = mBarcodeReader.decodeBuffer(bytes, mImageReader.getWidth(), mImageReader.getHeight(), nRowStride * nPixelStride, EnumImagePixelFormat.IPF_NV21, "");

Reading barcodes from the JNI layer:

ByteBuffer buffer = image.getPlanes()[0].getBuffer();
int nRowStride = image.getPlanes()[0].getRowStride();
int nPixelStride = image.getPlanes()[0].getPixelStride();
ArrayList<SimpleResult> results = readBarcode(hBarcode, buffer, mImageReader.getWidth(), mImageReader.getHeight(), nRowStride * nPixelStride);

Here is the final look of the project structure:

camera project structure

Building C++ Code with Android NDK and CMake

Create a CMakeLists.txt file under src/main/cpp:

cmake_minimum_required(VERSION 3.4.1)

set(CMAKE_VERBOSE_MAKEFILE on)
set(CMAKE_ANDROID_ARCH_ABI armeabi-v7a)

link_directories("${CMAKE_CURRENT_SOURCE_DIR}")

add_library(dynamsoft_barcode SHARED
    ${CMAKE_CURRENT_SOURCE_DIR}/android_main.cpp)

# add include path
target_include_directories(dynamsoft_barcode PRIVATE ${COMMON_SOURCE_DIR})

# add lib dependencies
target_link_libraries(dynamsoft_barcode dl android log m DynamsoftBarcodeReaderAndroid)

Note: do not move link_directories down below add_library. If you do so, the build will fail:

Android NDK CMake Error:
error: cannot find -lDynamsoftBarcodeReaderAndroid

android ndk cmake error

In the build.gradle file, add the following script:

defaultConfig {
    minSdkVersion 21
    targetSdkVersion 27
    ndk {
        abiFilters 'armeabi-v7a'
    }
    externalNativeBuild {
        cmake {
            arguments '-DANDROID_STL=c++_static', '-DANDROID_ABI=armeabi-v7a'
        }
    }
}

externalNativeBuild {
    cmake {
        version '3.10.2'
        path 'src/main/cpp/CMakeLists.txt'
    }
}

By default, the build will generate shared libraries including arm64-v8a, armeabi-v7a, x86 and x86_64. To only generate the matched share library, the ABI filter is required.

Now we can successfully build and run the barcode reader app:

android ndk barcode

Source Code

https://github.com/yushulx/android-camera2-barcode-ndk