android JNI开发基础

JNI的基础开发知识

参考文章

初识JNI/NDK

NDK(Native Development Kit)是一个允许开发者使用C和C++编写Android应用程序的工具集。它提供了一系列的工具和库,可以帮助开发者将高性能的原生代码集成到Android应用中

NDK的主要目标是提供一种方式,让开发者能够在需要更高性能或更底层控制的情况下使用C和C++编写部分应用程序,而不仅仅依赖于Java。

JNI(Java Native Interface)是一种编程框架,用于在Java代码和原生代码(如C和C++)之间进行交互。通过JNI,开发者可以在Java代码中调用原生代码的函数,并且可以将Java对象传递给原生代码进行处理。

JNI 开发大致框架

android studio创建 Native项目

创建好了Natvie项目后,需要点开Tools→SDK Manager下载 NDK和CMake

image.png

静态注册Native方法

Java层注册/调用

在任意类的中,要想调用JNI .so层的native方法需要存在如下定义

1
2
3
4
//导入.so中的代码
static {
        System.loadLibrary("learnjni");
}

并且存在相应的Native函数声明

1
2
3
public native String stringFromJNI();
public native String stringFromJAVA();
public native String stringFromC();

然后就能在该class中调用这些Native函数方法

c/c++层定义

上面导入的libc库文件learnjni,是在cpp/CMakeLists中定义的

image.png

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
# For more information about using CMake with Android Studio, read the
# documentation: https://d.android.com/studio/projects/add-native-code.html.
# For more examples on how to use CMake, see https://github.com/android/ndk-samples.

# Sets the minimum CMake version required for this project.
cmake_minimum_required(VERSION 3.22.1)

# Declares the project name. The project name can be accessed via ${ PROJECT_NAME},
# Since this is the top level CMakeLists.txt, the project name is also accessible
# with ${CMAKE_PROJECT_NAME} (both CMake variables are in-sync within the top level
# build script scope).
project("learnjni")

# Creates and names a library, sets it as either STATIC
# or SHARED, and provides the relative paths to its source code.
# You can define multiple libraries, and CMake builds them for you.
# Gradle automatically packages shared libraries with your APK.
#
# In this top level CMakeLists.txt, ${CMAKE_PROJECT_NAME} is used to define
# the target library name; in the sub-module's CMakeLists.txt, ${PROJECT_NAME}
# is preferred for the same purpose.
#
# In order to load a library into your app from Java/Kotlin, you must call
# System.loadLibrary() and pass the name of the library defined here;
# for GameActivity/NativeActivity derived applications, the same library name must be
# used in the AndroidManifest.xml file.

add_library(${CMAKE_PROJECT_NAME} SHARED
        # List C/C++ source files with relative paths to this CMakeLists.txt.
        native-lib.cpp)

# Specifies libraries CMake should link to your target library. You
# can link libraries from various origins, such as libraries defined in this
# build script, prebuilt third-party libraries, or Android system libraries.
target_link_libraries(${CMAKE_PROJECT_NAME}
        # List libraries link to the target library
        android
        log)

其中的add_library用于添加生产库的源代码文件,比如这里的native-lib.cpp,然后再来看看native-lib.cpp中的c语言如何Java进行交互

初始化生成的代码如下

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
#include <jni.h>
#include <string>

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

首先来看函数函数前一行声明

  • extern “C"表示下面的函数通过c语言进行编译
  • JNIEXPORT表示函数是JNI中的导出函数,能过在JAVA代码中调用
  • JNICALL修饰符告诉编译器函数调用约定为JNI的规范
  • jstring是该函数的返回类型,J开头代表说JAVA中的类型

再来看看函数命名,其规范如下

1
Java_包名_类名_方法名
  • 如果其中的名字包含_下划线,通过1_ 来区分。

再来看看函数传递的参数,存在两个参数JNIEnv和jobject,其中JNIEnv定义如下

1
2
3
4
5
6
7
#if defined(__cplusplus)
typedef _JNIEnv JNIEnv;
typedef _JavaVM JavaVM;
#else
typedef const struct JNINativeInterface* JNIEnv;
typedef const struct JNIInvokeInterface* JavaVM;
#endif

这里区分C和Cpp,先来看Cpp,其JNIEnv就是_JNIEnv的引用,_JNIEnv是一个结构体,包含了很多的函数指针,我们在c语言中处理Java中的类,对象,字段的时候需要用到这些方法。

image.png

由于c语言中环境和JAVA的环境完全不同,JAVA中存在类,类对象这些概念,因此存在如下定义

  • jclass 表示对Java中的某个类引用
  • jobject 表示对Java中某个类对象的引用
  • jstring/jint这里表示Java中的string/int类型

假如要在native层修改Java层类中的一个普通字段String,那么其获取思路如下

  1. 普通字段属于类中的字段,首先需要获取到Java中的类,也就是jclass,存在两个方法

    1
    2
    
    jclass GetObjectClass(jobject obj)
    jclass FindClass(const char* name)
    

    对应的调用例子

    1
    2
    
    jclass mainclass = env->GetObjectClass(thiz);
    jclass mainclass = env->FindClass("com/example/learnjni/MainActivity");
    
    1. 通过类的对象获取到相应类,这里的thiz就是函数定义的第二个参数jobject(类的对象),
    2. 直接通过包名/类名也可以获取到相应类
  2. 获取到Java中的类之后,就能过定位到类中相应字段

1
jfieldID GetFieldID(jclass clazz, const char* name, const char* sig)

对应调用例子

1
jfieldID strId = env->GetFieldID(mainclass,"str","Ljava/lang/String;");
  • clazz表示字段处于到方法
  • name表示字段名称
  • sig表示字段的类型,也就是JNI类型

image.png

sig中最特殊的就是对象类型,其命名方式为L包名/对象名,这里以Java中的String为例子,JNI类型如下

1
2
JNI类型 Ljava/lang/String
Java类型 java.lang.String 
  1. 获取到类中的字段后,就能修改其字段的值
1
2
3
4
void SetObjectField(jobject obj, jfieldID fieldID, jobject value)

jstring modify = env->NewStringUTF("Hellow Java ,这是修改后的普通字段");
env->SetObjectField(thiz,strId,modify);

要在c语言中修改的Java中的字符串,因此value需要jstring类型,通过NewStringUTF方法进行转换即可。

以上就是获取java中的类,对象,以及类中的字段,如果要修改的字段为静态的,和普通字段类似

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
extern "C" JNIEXPORT jstring JNICALL
Java_com_example_learnjni_MainActivity_staticFromC(JNIEnv * env,jobject thiz){
    //modfty static string from Java
//    jclass mainclass = env->FindClass("com/example/learnjni/MainActivity");
    jclass mainclass = env->GetObjectClass(thiz);
    jfieldID strId = env->GetStaticFieldID(mainclass,"static_str","Ljava/lang/String;");
    jstring modify = env->NewStringUTF("Hellow Java ,这是修改后的静态字段");
    env->SetStaticObjectField(mainclass,strId,modify);
    return nullptr;
}

唯一的区别在于静态static字段属于整个class类,而普通字段属于类的实例对象,因此在调用

setxxxObject的时候,static字段传入的类,普通字段传入的类的对象thiz。


在来看看c语言的情况

1
2
typedef const struct JNINativeInterface* JNIEnv;
typedef const struct JNIInvokeInterface* JavaVM;

由于c语言没类机制,因此JNIEnv是一个指针,指向JNINativeInterface的结构题,该结构体和cpp中的_JNIEnv长得很像

image.png

image.png

其实本质上_JNIEnv就是JNINativeInterface类的封装,你会发现_JNIEnv中的函数,其实最终都调用JNINativeInterface类中的函数(Fuctions→xxx)。

image.png

而传入的参数为JNIEnv * evn,因此evn是一个二级指针,要想获取到JNINativeInterface结构体中的函数需要先解引用得到JNINativeInterface *的指针,c语言调用JNI的方法如下

1
(*evn)->function(xxx);

理解了上面类,对象获取的方法,那么再来看看下面的代码就十分容易理解了

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
extern "C" JNIEXPORT jstring JNICALL
Java_com_example_learnjni_MainActivity_stringFromMethod(JNIEnv * env,jobject thiz){
    jclass mainclass = env->GetObjectClass(thiz);
    jmethodID voidMethod = env->GetMethodID(mainclass,"str_method", "()V");
    env->CallVoidMethod(thiz,voidMethod);
    return nullptr;
}

extern "C" JNIEXPORT jstring JNICALL
Java_com_example_learnjni_MainActivity_staticFromMethod(JNIEnv * env,jobject thiz){
    jclass mainclass = env->GetObjectClass(thiz);
    jmethodID voidMethod = env->GetStaticMethodID(mainclass,"static_method", "()V");
    env->CallStaticVoidMethod(mainclass,voidMethod);
    return nullptr;
}

上面代码就是在cpp中调用了Java中的方法,这里需要注意获取方法ID的时候提供sig的目的在于区分重载函数

动态注册

除了让代码静态注册到.so库文件中,JNI还提供了动态注册native层代码方法,其实现方法就是在加载libc.so到库文件时,自动执行JNI_Onload函数,在该函数中进行代码的动态注册,具体实现思路如下

  1. java层和静态注册相同,直接声明该方法即可,比如这里的intNum
1
public native int intNum(int num);
  1. 再cpp/c层不再更具JNI函数命名规则进行命名,而是直接以原名字编写
1
2
3
jint intNum(JNIEnv* env, jobject thiz,jint num){
    return  num + 123;
}
  1. 再定义JNI_OnLoad将intNum函数注册即可(注意JNI_OnLoad中的L是大写)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
extern "C" JNIEXPORT jint JNICALL
JNI_OnLoad(JavaVM * vm , void * reserved){
    JNIEnv * evn = nullptr;
    jint result = vm->GetEnv((void **)&evn,JNI_VERSION_1_6);
    if(result != JNI_OK){
        return -1;
    }
    jclass mainclass = evn->FindClass("com/example/learnjni/MainActivity");
    JNINativeMethod methods[] = {
            {"intNum","(I)I",(void *)intNum},
    };
    evn->RegisterNatives(mainclass,methods,1);
    return JNI_VERSION_1_6;
}

其注册的思路如下

  1. 注册需要用到evn->RegisterNatives,因此先通过vm→GetEnv获取JNIEnv,并判断是否获取成功

  2. RegisterNatives函数需要包含JNINativeMethod结构题的数组参数,其定义如下

    1
    2
    3
    4
    5
    
    typedef struct {
        const char* name; //函数名称
        const char* signature; // 函数签名 其参数类型和返回值类型
        void*       fnPtr; //函数指针,指向第一步定义的本地函数
    } JNINativeMethod;
    

    因此先注册methods数组当中RegisterNatives

  3. 然后获取到注册的Java类jclass,最后调用evn->RegisterNatives注册methods数组中的函数,第三个参数1代表methods数组中方法个数。

  4. 最后JNI_Onload函数需要返回本次注册函数的JNI版本

这里再记录一下安卓apk加载动态库.so文件的执行函数过程如下

1
.init -> .init_array -> JNI_Onload

code

最后附上代码

  • native-lib.cpp

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    
    
    #include <jni.h>
    #include <string>
    
    extern "C" JNIEXPORT jstring JNICALL
    Java_com_example_learnjni_MainActivity_stringFromJNI(
            JNIEnv* env,
            jobject /* this */ ) {
        std::string hello = "Hello from C++";
        return env->NewStringUTF(hello.c_str());
    }
    
    extern "C" JNIEXPORT jstring JNICALL
    Java_com_example_learnjni_MainActivity_stringFromJAVA(JNIEnv * env,jobject thiz){
        //modfty string from Java
        jclass mainclass = env->FindClass("com/example/learnjni/MainActivity");
        jfieldID strId = env->GetFieldID(mainclass,"str","Ljava/lang/String;");
        jstring modify = env->NewStringUTF("Hellow Java ,这是修改后的普通字段");
        env->SetObjectField(thiz,strId,modify);
        return nullptr;
    }
    
    extern "C" JNIEXPORT jstring JNICALL
    Java_com_example_learnjni_MainActivity_staticFromC(JNIEnv * env,jobject thiz){
        //modfty static string from Java
    //    jclass mainclass = env->FindClass("com/example/learnjni/MainActivity");
        jclass mainclass = env->GetObjectClass(thiz);
        jfieldID strId = env->GetStaticFieldID(mainclass,"static_str","Ljava/lang/String;");
        jstring modify = env->NewStringUTF("Hellow Java ,这是修改后的静态字段");
        env->SetStaticObjectField(mainclass,strId,modify);
        return nullptr;
    }
    
    extern "C" JNIEXPORT jstring JNICALL
    Java_com_example_learnjni_MainActivity_stringFromMethod(JNIEnv * env,jobject thiz){
        jclass mainclass = env->GetObjectClass(thiz);
        jmethodID voidMethod = env->GetMethodID(mainclass,"str_method", "()V");
        env->CallVoidMethod(thiz,voidMethod);
        return nullptr;
    }
    
    extern "C" JNIEXPORT jstring JNICALL
    Java_com_example_learnjni_MainActivity_staticFromMethod(JNIEnv * env,jobject thiz){
        jclass mainclass = env->GetObjectClass(thiz);
        jmethodID voidMethod = env->GetStaticMethodID(mainclass,"static_method", "()V");
        env->CallStaticVoidMethod(mainclass,voidMethod);
        return nullptr;
    }
    
    jint intNum(JNIEnv* env, jobject thiz, jint num) {
        return num + 123;
    }
    
    extern "C" JNIEXPORT jint JNICALL
    JNI_OnLoad(JavaVM * vm , void * reserved){
        JNIEnv * evn = nullptr;
        jint result = vm->GetEnv((void **)&evn,JNI_VERSION_1_4);
        if(result != JNI_OK){
            return -1;
        }
        jclass mainclass = evn->FindClass("com/example/learnjni/MainActivity");
        JNINativeMethod methods[] = {
                {"intNum","(I)I",(void *)intNum},
        };
        evn->RegisterNatives(mainclass,methods,1);
        return JNI_VERSION_1_4;
    }
    
  • MainAvtivity

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    
    package com.example.learnjni;
    
    import androidx.appcompat.app.AppCompatActivity;
    
    import android.os.Bundle;
    import android.util.Log;
    import android.widget.TextView;
    import android.widget.Toast;
    import com.example.learnjni.databinding.ActivityMainBinding;
    
    public class MainActivity extends AppCompatActivity {
        public native int intNum(int num);
        public String str = "Helloc Java 我是普通字段";
        public static AppCompatActivity your_this;
        public static String static_str = "Helloc Java 我是静态字段";
    
        // Used to load the 'learnjni' library on application startup.
        static {
            System.loadLibrary("learnjni");
        }
        public void str_method() {
            Toast.makeText(this, "普通方法", Toast.LENGTH_LONG).show();
        }
        public static void static_method() {
            Toast.makeText(your_this, "静态方法", Toast.LENGTH_LONG).show();
        }
    
        private ActivityMainBinding binding;
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            your_this = this;
            binding = ActivityMainBinding.inflate(getLayoutInflater());
            setContentView(binding.getRoot());
    
            // Example of a call to a native method
            TextView tv = binding.sampleText;
            tv.setText(stringFromJNI());
            String ret = String.valueOf(intNum(123));
            tv.setText(ret);
        }
    
        /**
         * A native method that is implemented by the 'learnjni' native library,
         * which is packaged with this application.
         */
        public native String stringFromJNI();
        public native String stringFromJAVA();
        public native String staticFromC();
    
        public native String stringFromMethod();
        public native String staticFromMethod();
    }
    
Licensed under CC BY-NC-SA 4.0
comments powered by Disqus