This article mainly explains the basic syntax of JNI and the basic use of cross-compilation, through the study of this article will be able to get started under Android JNI project development.

JNI concept

From the PERSPECTIVE of the JVM, there are two types of code: “Java” and “native”. Native generally refers to C/C ++. To enable Java and Native to interact, Java designed the Java Native Interface (JNI). JNI allows Java code running inside a Java virtual machine (VM) to interoperate with applications and libraries written in other programming languages such as C++, C++, and assembly.

Although most of our software can be implemented in Java, there are some scenarios where native code is more appropriate, such as:

  • Code efficiency: Higher performance with native code
  • Cross-platform features: The standard Java class library does not support platform-dependent features required by an application or wants to implement a small portion of time-critical code in a lower-level language such as assembly language.

Native layer uses JNI mainly to do:

  • Create, examine, and update Java objects, including arrays and strings.
  • Call the Java method.
  • Load the class and get the class information.

Create an Android NDK project

Create a native c++ project using as

The file structure is as follows:

As you can see, a CPP folder has been generated with cmakelists. TXT and native-lib. CPP. CMakeLists will be discussed later. Here we will take a look at native-lib. CPP and Java code.

public class MainActivity extends AppCompatActivity {
    static {
        System.loadLibrary("native-lib"); }...public native String stringFromJNI(a);
}
Copy the code
#include <jni.h>
#include <string>

extern "C" JNIEXPORT jstring JNICALL
Java_com_wangzhen_jnitutorial_MainActivity_stringFromJNI(JNIEnv* env, jobject thiz) {
    std: :string hello = "Hello from C++";
    return env->NewStringUTF(hello.c_str());
}
Copy the code

You can see that MainActivity defines a native method, and then the compiler creates a corresponding method Java_com_wangzhen_jnitutorial_MainActivity_stringFromJNI in the CPP file. It’s named Java_packageName_methodName.

Let’s take a closer look at the code in CPP.

Native code interpretation

extern “C”

Use C code in C ++

JNIEXPORT

Macro definition: #define JNIEXPORT __attribute__ ((visibility (“default”))) in Linux/Unix/Mac OS /Android, Define __attribute__ ((visibility (“default”)))

GCC has a property called visibility, which says to enable this property:

  • When -fvisibility=hidden, functions in the dynamic library are hidden by default.
  • When -fvisibility=default, functions in the dynamic library are visible by default.

JNICALL

Macro definition, on Unix-like systems like Linux/Unix/Mac OS /Android, it is an empty macro definition: #define JNICALL, so it can be removed on Android as well. Quickly generate.h code

JNIEnv

  • The JNIEnv type actually represents the Java environment, and with this JNIEnv* pointer, you can manipulate java-side code:
    • Calling a Java function
    • Manipulating Java objects
  • JNIEnv is essentially a thread-specific structure that holds a number of Pointers to JNI functions:
struct _JNIEnv {
    /** * defines a number of function Pointers **/
    const struct JNINativeInterface* functions;

#if defined(__cplusplus)
    /// Pass the class name (the full name of the class, not the package name). To get jclass, but delimited by /
    jclass FindClass(const char* name)
    { return functions->FindClass(this, name); }... }Copy the code

The JNIEnv structure is shown below:

JavaVM

  • JavaVM: JavaVM represents a Java virtual machine at the JNI layer. There is only one JNI globally

  • JNIEnv: JavaVM representation in threads. Each thread has one. There may be many JNIEnVs in A JNI, and A JNIEnv is thread-dependent, i.e. thread B cannot use thread A’s JNIEnv

The structure of the JVM looks like this:

jobject thiz

This object refers to this instance of the native method. For example, we print thiz’s className in the following native function called by MainActivity:

#define  LOGE(...) __android_log_print(ANDROID_LOG_ERROR,"JNI",__VA_ARGS__);

extern "C" JNIEXPORT jstring JNICALL
Java_com_wangzhen_jnitutorial_MainActivity_stringFromJNI(JNIEnv *env, jobject thiz) {
    std: :string hello = "Hello from C++";
    // 1. Get thiz's class information
    jclass thisclazz = env->GetObjectClass(thiz);
    MethodID = methodID; // 2. GetClass methodID = methodID
    jmethodID mid_getClass = env->GetMethodID(thisclazz, "getClass"."()Ljava/lang/Class;");
    // 3. Execute the getClass method to obtain the Class object
    jobject clazz_instance = env->CallObjectMethod(thiz, mid_getClass);
    // 4. Get the Class instance
    jclass clazz = env->GetObjectClass(clazz_instance);
    // 5. According to class methodID
    jmethodID mid_getName = env->GetMethodID(clazz, "getName"."()Ljava/lang/String;");
    // 6. Call getName
    jstring name = static_cast<jstring>(env->CallObjectMethod(clazz_instance, mid_getName));
    LOGE("class name:%s", env->GetStringUTFChars(name, 0));

    return env->NewStringUTF(hello.c_str());
}
Copy the code

The print result is as follows:

JNI basis

The data type

Underlying data types

Java Type Native Type Description
boolean jboolean unsigned 8 bits
byte jbyte signed 8 bits
char jchar unsigned 16 bits
short jshort signed 16 bits
int jint signed 32 bits
long jlong signed 64 bits
float jfloat 32 bits
double jdouble 64 bits
void void N/A

Reference types

Here’s a nice ugly picture from the Oracle documentation:

Field and Method IDs

JNIEvn uses reflection in Java to manipulate Java objects, requiring the ids of field and method to operate on any property. These ids are pointer types:

struct _jfieldID;              /* opaque structure */ 
typedef struct _jfieldID *jfieldID;   /* field IDs */ 
 
struct _jmethodID;              /* opaque structure */ 
typedef struct _jmethodID *jmethodID; /* method IDs */ 
Copy the code

JNI manipulates Java objects

Operating jarray

Passing a Java int[] object into C++, how do you manipulate the array?

JNIEXPORT void JNICALL
Java_com_wangzhen_jnitutorial_MainActivity_setArray(JNIEnv *env, jobject thiz, jintArray array) {

    // 1. Get the array length
    jint len = env->GetArrayLength(array);
    LOGE("array.length:%d", len);

    jboolean isCopy;
    // 2. Get the array address
    // The second argument represents javaArray -> C/C ++ Array conversion mode:
    // 0: passes Pointers to Java arrays directly back to native code
    // 1: new memory is allocated and arrays are copied
    // Return value: array address (first element address)
    jint *firstElement = env->GetIntArrayElements(array, &isCopy);
    LOGE("is copy array:%d", isCopy);
    // 3.
    for (int i = 0; i < len; ++i) {
        LOGE("array[%i] = %i", i, *(firstElement + i));
    }
    // 4. Free the array after use
    // The first argument is jarray, and the second argument is GetIntArrayElements return value
    // The third parameter represents mode
    env->ReleaseIntArrayElements(array,firstElement,0);

    // create a Java array
    jintArray newArray = env->NewIntArray(3);
}
Copy the code
  • Mode = 0 flushes the Java array and frees the C/C ++ array
  • Mode = JNI_COMMIT (1) Flushs only Java arrays
  • Mode = JNI_ABORT (2) Releases only C/C ++ arrays

Operating jstring

extern "C"
JNIEXPORT void JNICALL
Java_com_wangzhen_jnitutorial_MainActivity_setString(JNIEnv *env, jobject thiz, jstring str) {
    // 1.jstring -> char*
    // Java characters are encoded in Unicode, while C /C++ characters are encoded in UTF. The second argument works the same way
    const char *c_str = env -> GetStringUTFChars(str,NULL);

    // 2. Exception handling
    if(c_str == NULL) {return;
    }

    // 3. Print as a char array
    jint len = env->GetStringLength(str);
    for (int i = 0; i < len; ++i) {
        LOGE("c_str: %c",*(c_str+i));
    }

    4 / / release
    env->ReleaseStringUTFChars(str,c_str);
}
Copy the code

After calling GetStringUTFChars, don’t forget the security check, because the JVM needs to allocate memory for the newly created string. If the JVM runs out of memory, the call fails, and GetStringUTFChars returns NULL. And throws an OutOfMemoryError. JNI exception handling is different from Java exception handling. If Java encounters an exception and does not catch it, the program will immediately stop running. A pending exception in JNI does not change the flow of the program, that is, the program will continue, and all subsequent operations on the string will be dangerous, so we need to skip the code with a return statement and end the current method immediately.

Operating jobject

  • C/C ++ operations on objects in Java use reflection in Java. The steps are:
  • Obtain class class
  • Get methodID/fieldID according to the member variable name
  • Call get/set methods to manipulate field, or CallObjectMethod to call method
The operating Field
  • Non-static member variables use GetXXXField, such as GetIntField, and for reference types, such as String, GetObjectField
  • For static member variables, use GetStaticXXXField, such as GetStaticIntField

In Java code, MainActivity has two member variables:

public class MainActivity extends AppCompatActivity {

    String testField = "test1";

    static int staticField = 1;
}
Copy the code
    // 1. Get the class
    jclass clazz = env->GetObjectClass(thiz);

    // 2. Get the member variable id
    jfieldID strFieldId = env->GetFieldID(clazz,"testField"."Ljava/lang/String;");
    // 3. Obtain the value by id
    jstring jstr = static_cast<jstring>(env->GetObjectField(thiz, strFieldId));
    const char* cStr = env->GetStringUTFChars(jstr,NULL);
    LOGE("Get MainActivity's String field: %s",cStr);

    // 4. Modify String
    jstring newValue = env->NewStringUTF("New Character creation");
    env-> SetObjectField(thiz,strFieldId,newValue);

    // 5. Release resources
    env->ReleaseStringUTFChars(jstr,cStr);
    env->DeleteLocalRef(newValue);
    env->DeleteLocalRef(clazz);
    
    // Get static variables
    jfieldID staticIntFieldId = env->GetStaticFieldID(clazz,"staticField"."I");
    jint staticJavaInt = env->GetStaticIntField(clazz,staticIntFieldId);
Copy the code

GetFieldID and GetStaticFieldID require three parameters:

  • jclass
  • filed name
  • Type signature: JNI uses the JVM’s type signature
Type Signature List
Type Signature Java Type
Z boolean
B byte
C char
S short
I int
J long
F float
D double
V void
L fully-qualified-class; fully-qualified-class
[type type[]
(arg-types) ret-type method type
  • To get an int, GetFieldID needs to pass in the signature I;

  • If it is a class, such as String, the signature is L+ the full class name; : Ljava. Lang. String;

  • If it is an int array, we write [I

  • If you want to get a method signature that the signature of the method is: (parameters) sign the return value, parameter if it is multiple, don’t need to add spacing between characters, such as: | | Java method JNI signature | | — – | — – | | void f (int n); |(I)V| |void f (String s,int n); |(Ljava/lang/String; I)V| |long f (int n, String s, int[] arr); |(ILjava/lang/String; [I)J|

Operation method

Field operation Method is very similar to field operation. First obtain MethodID, and then the corresponding CallXXXMethod method

Java layer return value Methods the family The local return type NativeType
void CallVoidMethod() (not)
Reference types CallObjectMethod( ) jobect
boolean CallBooleanMethod ( ) jboolean
byte CallByteMethod( ) jbyte
char CallCharMethod( ) jchar
short CallShortMethod( ) jshort
int CallIntMethod( ) jint
long CallLongMethod() jlong
float CallFloatMethod() jfloat
double CallDoubleMethod() jdouble

In Java, to get the className of the MainActivity, we say:

 this.getClass().getName()
Copy the code

Call the getName method of the Class object. Then call the getName method of the Class object.

extern "C" JNIEXPORT jstring JNICALL
Java_com_wangzhen_jnitutorial_MainActivity_stringFromJNI(JNIEnv *env, jobject thiz) {
    std: :string hello = "Hello from C++";
    // 1. Get thiz's class information
    jclass thisclazz = env->GetObjectClass(thiz);
    MethodID = methodID; // 2. GetClass methodID = methodID
    jmethodID mid_getClass = env->GetMethodID(thisclazz, "getClass"."()Ljava/lang/Class;");
    // 3. Execute the getClass method to obtain the Class object
    jobject clazz_instance = env->CallObjectMethod(thiz, mid_getClass);
    // 4. Get the Class instance
    jclass clazz = env->GetObjectClass(clazz_instance);
    // 5. According to class methodID
    jmethodID mid_getName = env->GetMethodID(clazz, "getName"."()Ljava/lang/String;");
    // 6. Call getName
    jstring name = static_cast<jstring>(env->CallObjectMethod(clazz_instance, mid_getName));
    LOGE("class name:%s", env->GetStringUTFChars(name, 0));
    
    // 7. Release resources
    env->DeleteLocalRef(thisclazz);
    env->DeleteLocalRef(clazz);
    env->DeleteLocalRef(clazz_instance);
    env->DeleteLocalRef(name);
    
    return env->NewStringUTF(hello.c_str());
}
Copy the code

Create an object

Start by defining a Java class:

public class Person {
    private int age;
    private String name;

    public Person(int age, String name){
        this.age = age;
        this.name = name;
    }

    public void print(a){
        Log.e("Person",name + age + "Old"); }}Copy the code

Then we create a Person in JNI and call its print method:

// 1. Get Class
    jclass pClazz = env->FindClass("com/wangzhen/jnitutorial/Person");
    // 2. Get constructor 
      
    jmethodID constructID = env->GetMethodID(pClazz,"<init>"."(ILjava/lang/String;) V");
    if(constructID == NULL) {return;
    }
    // create a Person object
    jstring name = env->NewStringUTF("alex");
    jobject person = env->NewObject(pClazz,constructID,1,name);

    jmethodID printId = env->GetMethodID(pClazz,"print"."()V");
    if(printId == NULL) {return;
    }
    env->CallVoidMethod(person,printId);

    // 4. Release resources
    env->DeleteLocalRef(name);
    env->DeleteLocalRef(pClazz);
    env->DeleteLocalRef(person);
Copy the code

JNI references

JNI is divided into three types of citation:

  • Local references are similar to Local variables in Java
  • A Global Reference, similar to a Global variable in Java
  • Weak Global References are similar to Weak references in Java

The code snippet above always ends with code that frees the resource. This is good c/ C ++ programming practice and can be done differently for different JNI references.

Local reference

create

JNI function returns all Java objects are local references, such as the above NewObject/FindClass/NewStringUTF etc are all local references.

The release of

  • Auto-free local references are valid during a method call and are automatically freed by the JVM when the method returns.
  • Hand release
Manual release scenario

Why do you need manual release when you have automatic release? Consider the main scenario:

  • Native methods access large Java objects to create local references to Java objects. The native method then performs additional calculations before returning to the caller. A local reference to a large Java object will prevent garbage collection of that object, even if it is no longer used for the rest of the computation.
  • Native methods create a large number of local references, but not all local references are used at the same time. Because the JVM needs a certain amount of space to keep track of local references, too many local references are created, which can cause the system to run out of memory. For example, native methods iterate over a large array of objects, retrieving elements as local references, and operating on one element at a time. After each iteration, the programmer no longer needs local references to array elements.

So we should get into the good habit of manually releasing local references.

Manual release mode
  • GetXXX must call ReleaseXXX.

After the GetStringUTFChars function is called to retrieve a string from within the JVM, a new block of memory is allocated within the JVM to store a copy of the source string for local code to access and modify. While memory is allocated, it is a good programming habit to free it as soon as it is used up. The JVM is notified that the memory is no longer used by calling ReleaseStringUTFChars.

  • For manually created JClasses, objects such as jobject are released using the DeleteLocalRef method

Global references

create

JNI allows programmers to create global references from local references:

 static jstring globalStr;
 if(globalStr == NULL){
   jstring str = env->NewStringUTF("C++");
   // Create a global variable from local variable STR
   globalStr = static_cast<jstring>(env->NewGlobalRef(str));
   
   // Local can be freed because local STR is no longer used because there is a global reference to use STR
    env->DeleteLocalRef(str);
    }
Copy the code

The release of

Global references remain valid until they are explicitly freed, and global reference calls can be manually removed by DeleteGlobalRef.

Weak global reference

Like global references, weak references can be used across methods and threads. Unlike global references, weak references do not prevent the GC from reclaiming objects inside the VM to which it points

When using a weak reference, you must first check whether the cached weak reference points to an active object or to an object that has already been GC

create

    static jclass globalClazz = NULL;
    // Returns true for weak references if the referenced object is reclaimed, false otherwise
    // Determine whether Java null objects are referenced for local and global references
    jboolean isEqual = env->IsSameObject(globalClazz, NULL);
    if (globalClazz == NULL || isEqual) {
        jclass clazz = env->GetObjectClass(instance);
        globalClazz = static_cast<jclass>(env->NewWeakGlobalRef(clazz));
        env->DeleteLocalRef(clazz);
    }
Copy the code

The release of

Delete using DeleteWeakGlobalRef

Thread related

Local variables can only be used in the current thread, while global references can be used across methods and threads until they are released manually.

Loading the Dynamic library

There are two ways to load dynamic libraries on Android:

  • System.load(String filename) // The absolute path
  • System Library path // Load from the system lib path

For example, the following code fails to find Hello in java.library.path

static{
    System.loadLibrary("Hello");
}
Copy the code

To print out java.library.path and copy Hello to the path:

public static void main(String[] args){
    System.out.println(System.getProperty("java.library.path"));
}
Copy the code

JNI_OnLoad

When the system.loadLibrary () function is called, the JNI_OnLoad function in so is internally looked for and called if it exists. JNI_OnLoad must return the version of JNI, such as JNI_VERSION_1_6 and JNI_VERSION_1_8.

Dynamic registration

JNI matches the corresponding Java methods in two ways:

  • Static registration: The Java_com_wangzhen_jnitutorial_MainActivity_stringFromJNI we used earlier to match Java methods was static registration
  • Dynamic registration: Dynamically matching Java methods to JNI methods in code

Statically registered names require package names, which are too long. You can use dynamic registrations to shorten method names.

For example, we have two native methods in Java:

public class MainActivity extends AppCompatActivity {
    public native void dynamicJavaFunc1(a);

    public native int dynamicJavaFunc2(int i);
}
Copy the code

In Native code, instead of using static registration, we use dynamic registration

void dynamicNativeFunc1(a){
    LOGE("DynamicJavaFunc1 called");
}
// If the method takes arguments, be preceded by JNIEnv *env, jobject thisz
jint dynamicNativeFunc2(JNIEnv *env, jobject thisz,jint i){
    LOGE(DynamicTest2 called with :%d,i);
    return 66;
}

// An array of methods that need to be dynamically registered
static const JNINativeMethod methods[] = {
        {"dynamicJavaFunc1"."()V", (void*)dynamicNativeFunc1},
        {"dynamicJavaFunc2"."(I)I", (int*)dynamicNativeFunc2},
};
// The class name that needs to dynamically register native methods
static const char *mClassName = "com/wangzhen/jnitutorial/MainActivity";


jint JNI_OnLoad(JavaVM* vm, void* reserved){
    JNIEnv* env = NULL;
    // 1. Get JNIEnv
    int result = vm->GetEnv(reinterpret_cast<void **>(&env), JNI_VERSION_1_6);
    // 2. Check whether the operation is successful
    if(result ! = JNI_OK){ LOGE("Failed to get env");
        return JNI_VERSION_1_6;
    }
    // 3. Registration method
    jclass classMainActivity = env->FindClass(mClassName);
    // sizeof(methods)/ sizeof(JNINativeMethod)
    result = env->RegisterNatives(classMainActivity,methods, 2);

    if(result ! = JNI_OK){ LOGE("Registration method failed")
        return JNI_VERSION_1_2;
    }

    return JNI_VERSION_1_6;
}
Copy the code

In this case, calling dynamicJavaFunc1 in MainActivity calls the dynamicNativeFunc1 method in native.

JNIEnv* is called in a native thread

JNIEnv* is thread specific. If you create A thread A in c++, can you use JNIEnv* directly in thread A? The answer is no. If you want to use JNIEnv* in native threads, you need to use the JVM’s AttachCurrentThread method to bind:


JavaVM *_vm;

jint JNI_OnLoad(JavaVM* vm, void* reserved){
    _vm = vm;
    return JNI_VERSION_1_6;
 }

void* threadTask(void* args){
    JNIEnv *env;
    jint result = _vm->AttachCurrentThread(&env,0);
    if(result ! = JNI_OK){return 0;
    }

    // ...

    // Don't forget to detach tasks after they finish executing
    _vm->DetachCurrentThread();
}

extern "C"
JNIEXPORT void JNICALL
Java_com_wangzhen_jnitutorial_MainActivity_nativeThreadTest(JNIEnv *env, jobject thiz) {
    pthread_t pid;
    pthread_create(&pid,0,threadTask,0);
}
Copy the code

cross-compilation

The process of compiling a secondary file on one platform that can be executed on another platform is called cross-compilation. Such as compiling libraries on MacOS that are available on Android. If you want to compile libraries that run on the Android platform, you need to use the NDK.

Two library files

Libraries on the Linux platform fall into two categories:

  • Static library: When a link is compiled, all the library code is added to the executable, so the resulting file is larger, but the library file is no longer needed at runtime. The suffix is “.a “in Linux.
  • Dynamic library: Library code is not added to the executable file at link compile time, but is loaded by the runtime link file at program execution time. With the suffix “. So “in Linux, GCC uses dynamic libraries by default when compiling.

Android Native Development Suite (NDK) : This set of tools enables you to use C and C++ code in Android applications. CMake: An external compilation tool that can be used with Gradle to compile native libraries. This component is not required if you only plan to use the NdK-build. LLDB: Debugger used by Android Studio to debug native code.

NDK

The Native Development Suite (NDK) is a set of tools that enable you to use C and C++ code in Android applications and provides numerous platform libraries. We can see the NDK directory structure in SDK/NdK-bundle. Here are three important members:

  • Ndk-build: This Shell script is the starting point of the Android NDK build system. It is usually executed in a project only with this command to compile the corresponding dynamic link library.
  • Platforms: This directory contains headers and libraries that support different Android target versions. NDK build systems will reference headers and libraries on each platform depending on the configuration.
  • Toolchains: This directory contains cross-compilers for different platforms currently supported by the NDK – ARM, X86, MIPS, ARM is the most commonly used. // todo ndk-depends.cmd

Why does the NDK offer multiple platforms? Different Android devices use different cpus, and different cpus support different instruction sets. Please refer to the official documentation for more details

Manually compile dynamic libraries using the NDK

Toolchains in the NDK directory for multiple platforms, The arm-linux-androideabi-gcc executable can be found in /arm-linux-androideabi-4.9/prebuilt/ Darwin x86_64/bin. NDK GCC can be used to compile dynamic libraries running on Android (ARM architecture) :

arm-linux-androideabi-gcc -fPIC -shared test.c -o libtest.so
Copy the code

-fpic: generates location-independent code. -shared: compilers the dynamic library. If the static library test.c is the C file to compile. -o: outputs the libtest.so file name

GCC is no longer available in the NDK. If you want to use it, you need to refer to the standalone toolchain. For example, $NDK/build/tools/ make_standalone_toolchain-py –arch arm — API 21 –install-dir/$yourDir can generate a separate toolchain for arm

$NDK represents the absolute path of the NDK and $yourDir represents the output file path

Makefile compilation occurs when manual compilation is cumbersome and error-prone when source files are numerous.

makefile

A makefile is “automated compilation” : the source files in a project are not counted, they are placed in several directories by type, function, and module. A Makefile defines a set of rules that specify which files to compile first, which files to post compile, how to link, and so on. Android uses the Android.mk file to configure makefiles. Here is the simplest android. mk file:

# The location of the source file. The my-dir macro returns the path to the current directory (including the directory of the Android.mk file itself). LOCAL_PATH := $(call my-dir) # Import other makefiles. The CLEAR_VARS variable points to a special GNU Makefile that clears many LOCAL_XXX variables for you. # The LOCAL_PATH variable include $(CLEAR_VARS) # specifies the library name, if the module name already begins with lib, The build system does not append the extra prefix lib; Instead, take the module name as is and add the.so extension. LOCAL_MODULE := hello # Contains a list of C and/or C++ source files to build into the module separated by Spaces LOCAL_SRC_FILES := hello. C # Build dynamic library include $(BUILD_SHARED_LIBRARY)Copy the code

Once we have configured the Android.mk file, how do we tell the compiler that this is our configuration file? Gradle file (app/build.gradle)

apply plugin: 'com.android.application'

android {
    compileSdkVersion 29

    defaultConfig {
      ...
        // The source file should be compiled into several CPU SOS
        externalNativeBuild{
            ndkBuild{
                abiFilters 'x86'.'armeabi-v7a'}}// Need to pack into apK several so
        ndk {
            abiFilters 'x86'.'armeabi-v7a'}}// Configure the native build script location
    externalNativeBuild{
        ndkBuild{
            path "src/main/jni/Android.mk"}}// Specify the NDK version
    ndkVersion "20.0.5594570". }dependencies {
    implementation fileTree(dir: "libs".include: ["*.jar"])... }Copy the code

Google recommends that developers cross-compile makefiles using cmake instead of makefiles. Makefiles are a bit different before and after Android 6.0 when introducing third-party pre-compiled SO. For example, manual system. loadLibrary third-party so is required before 6.0, but not after. There are many more configuration parameters for makefiles that are not covered here, but refer to the official documentation for more.

Up to 6.0, system. loadLibrary does not automatically load internal dependencies of so, so using MK requires version compatibility

cmake

CMake is a cross-platform build tool that describes installation (compilation) for all platforms in simple statements. The ability to output various makefiles or project files. Cmake does not directly build the final software, but rather produces scripts for other tools (such as makefiles) that are then used in the way the tool is built. Android Studio uses CMake to generate Ninja, a small, speed-focused build system. We don’t need to worry about Ninja scripts, we just need to know how to configure cmake.

CMakeLists.txt

Android Studio New project: include C/C ++ : cmakelists. TXT

|- app |– src |— main |—- cpp |—– CMakeLists.txt |—– native-lib.cpp

Take a look at cmakelists.txt:

Set the minimum supported version of cmakeCmake_minimum_required (VERSION 3.4.1 track)Create a library
add_library( # library name, such as native-lib.so is now generated
             native-lib

             Set SHARED library to STATIC library
             SHARED

             Set the relative path of the source file
             native-lib.cpp )
             
 Search and specify prebuilt libraries and store paths as variables.
 There are already some pre-built libraries in NDK (such as log), and the NDK libraries are already configured as part of the cmake search path
 You can write log in target_link_libraries instead
 find_library( Set the name of the path variable
              log-lib

              # specify the name of the NDK library to be located by CMake
              log )
              
 # specify the library that CMake should link to the target library. You can link to multiple libraries, such as build scripts, pre-built third-party libraries, or system libraries.
 target_link_libraries( # Specifies the target library.
                       native-lib
                       ${log-lib} )
 
           
Copy the code

Let’s look at gradle configuration again:

android {
    compileSdkVersion 29
    buildToolsVersion "29.0.1"
    defaultConfig {
        ...
        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"// Set the compiled version externalNativeBuild {cmake {abiFilters"armeabi-v7a"."x86"}}}... // Set the configuration file path externalNativeBuild {cmake {path"src/main/cpp/CMakeLists.txt"
            version "3.10.2"}}}Copy the code

So you can see two versions of so in the compilation product:

Add multiple source files

Let’s say we add an extra. H:

#ifndef JNITUTORIAL_EXTRA_H
#define JNITUTORIAL_EXTRA_H
const char * getString(a){
    return "string from extra";
}
#endif //JNITUTORIAL_EXTRA_H
Copy the code

Then use it in native-lib.cpp:

#include <jni.h>
#include <string>
#include <android/log.h>
#include "extra.h"
// __VA_ARGS__ represents... Variable parameter
#define  LOGE(...) __android_log_print(ANDROID_LOG_ERROR,"JNI",__VA_ARGS__);

extern "C" JNIEXPORT jstring JNICALL
Java_com_wangzhen_jnitutorial_MainActivity_stringFromJNI(JNIEnv *env, jobject thiz) {
// std::string hello = "Hello from new C++";
    std: :string hello = getString();
    return env->NewStringUTF(hello.c_str());
}
Copy the code

Now that the source file is written, modify cmakelists.txt:

Add_library (native-lib SHARED native-lib. CPP // add extra.h extra.h)# = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =
# Of course, if there are a lot of source files, and they may be in different folders, it will be very tedious to import each file as specified above. In this case, batch import can be used

# if there are too many files in the CPP folder, you can load them in batches.
file(GLOB SOURCE *.cpp *.h)

add_library(
        native-lib
        SHARED
        Import all SOURCE files under SOURCE
        ${SOURCE}
        )
Copy the code

Add a third-party dynamic library

So how do you add a third party dynamic library?

The location of the third-party library

Dynamic libraries must be placed in the SRC /main/jniLibs/xxabi directory to be packaged into APK. In this case, virtual machines are used, so x86 platforms are used. So we put a third party library libexternal.so under SRC /main/jniLibs/x86. Libexternal. So has only one hello.c, and only one method:

const char * getExternalString(a){
    return "string from external";
}
Copy the code
The location of the CMakeLists. TXT

Cmakelists. TXT has been replaced in the app directory at the same level as SRC to make it easier to find the library under jniLibs. So don’t forget to modify Gradle

externalNativeBuild {
        cmake {
            path "CMakeLists.txt"
            version "3.10.2"}}Copy the code
Configuration CMakeLists. TXT
Cmake_minimum_required (VERSION 3.4.1 track)# if there are too many files in the CPP folder, you can load them in batches.
file(GLOB SOURCE src/main/cpp/*.cpp src/main/cpp/*.h)

add_library(
        native-lib
        SHARED
        Import all SOURCE files under SOURCE
        ${SOURCE}
        )
set_target_properties(native-lib PROPERTIES LINKER_LANGUAGE CXX)

#add_library( # Sets the name of the library.
# native-lib
#
# # Sets the library as a shared library.
# SHARED
#
# # Provides a relative path to your source file(s).
# native-lib.cpp
# extra.h )

find_library(
              log-lib
              log )

# ================== bring in external so===================
message("ANDROID_ABI : ${ANDROID_ABI}")
message("CMAKE_SOURCE_DIR : ${CMAKE_SOURCE_DIR}")
message("PROJECT_SOURCE_DIR : ${PROJECT_SOURCE_DIR}")

# external represents the third party so-libexternal
A STATIC library is STATIC.
# IMPORTED: indicates that it is IMPORTED as a form (precompiled library)
add_library(external SHARED IMPORTED)

External: : IMPORTED_LOCATION: : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : :
# CMAKE_SOURCE_DIR: current path to cmakelists.txt (built-in with cmake tool)
# Android Cmake built-in ANDROID_ABI: the current CPU architecture that needs to be compiled
set_target_properties(external PROPERTIES IMPORTED_LOCATION ${CMAKE_SOURCE_DIR}/src/main/jniLibs/x86/libexternal.so)
#set_target_properties(external PROPERTIES LINKER_LANGUAGE CXX)

# = = = = = = = = = = = = = = = = = = bring in outside so end = = = = = = = = = = = = = = = = = = =

target_link_libraries( # Specifies the target library.
                       native-lib

                       # Links the target library to the log library
                       # included in the NDK.
                       ${log-lib}
                       # Link third-party so
                       external
        )

Copy the code
Use third-party libraries
#include <jni.h>
#include <string>
#include <android/log.h>
#include "extra.h"
// __VA_ARGS__ represents... Variable parameter
#define  LOGE(...) __android_log_print(ANDROID_LOG_ERROR,"JNI",__VA_ARGS__);

extern "C"{
 const char * getExternalString(a);
}

extern "C" JNIEXPORT jstring JNICALL
Java_com_wangzhen_jnitutorial_MainActivity_stringFromJNI(JNIEnv *env, jobject thiz) {
// std::string hello = "Hello from new C++";
// std::string hello = getString();
    // the third party library method is called
    std: :string hello = getExternalString();
    return env->NewStringUTF(hello.c_str());
}
Copy the code
Add CMake lookup path

In addition to the above method, we can also add a path to CMake to find so, which will be found in the path when target_link_libraries external.

#= = = = = = = = = = = = = = = = = = = = = the second way of introducing external so = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =

#Add a lookup path directly to cmake to find external

#CMAKE_C_FLAGS stands for c compilation and CMAKE_CXX_FLAGS stands for C ++
# setMethod defines a variable CMAKE_C_FLAGS ="${CMAKE_C_FLAGS} XXXX"
#-l: indicates the library search path libexternal.so
#set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -L${CMAKE_SOURCE_DIR}/src/main/jniLibs/${ANDROID_ABI} ")
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -L${CMAKE_SOURCE_DIR}/src/main/jniLibs/x86")
#= = = = = = = = = = = = = = = = = = = = = the introduction of external so second way end = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =
Copy the code

reference

Android JNI env parsing

Operating jarray

NDK official materials

The solution to arm-linux-Androideabi-gcc is not found in the NDK

Cmake practice