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