zl程序教程

您现在的位置是:首页 >  后端

当前栏目

JNI开发探索之旅

开发 探索之旅 JNI
2023-06-13 09:16:59 时间

jni开发探索之旅

由于工作上的需求需要使用java和c++互调实现功能,所以要对jni进行深入研究,故此入坑。对安卓也比较感兴趣,大学里还做过几个APP,现在已经很久没有写界面布局这方面的了...

JNI是什么

JNI全程Java Native Interface,意为Java本地调用,它允许Java代码和其他语言写的代码进行交互,简单的说,一种在Java虚拟机控制下执行代码的标准机制。 可以用它实现java和c语言互调。对于初学者来讲,很容易吧jni和ndk的概念搞混淆(当然也可能只有博主一个人o(╯□╰)o),那jni和ndk的区别到底是什么?

NDK是什么

Android NDK(Native Development Kit )是一套工具集合,允许你用像C/C++语言那样实现应用程序的一部分。 简单的说,NDK其实多了一个把.so和.apk打包的工具,而JNI开发并没有打包,只是把.so文件放到文件系统的特定位置。可以将NDK看做是Google提供的一个打包工具,方便开发者使用,有了这个工具,我们只需要关注代码的具体实现,而不需要关注如何编译动态链接库。

上手之前

先看看jni中的数据类型:

函数操作(只列出了一些常用的):

函数

Java数据类型

本地类型

函数说明

GetBooleanArrayElements

boolean

jboolean

需要调用ReleaseBooleanArrayElements 释放

GetByteArrayElements

byte

jbyte

需要调用ReleaseByteArrayElements 释放

GetCharArrayElements

char

jchar

需要调用ReleaseShortArrayElements 释

GetObjectArrayElement

自定义对象

jobject

SetObjectArrayElement

自定义对象

jobject

New<Type>Array

创建一个指定长度的原始数据类型的数组

NewStringUTF

jstring类型的方法转换

DeleteLocalRef

删除 localRef所指向的局部引用

DeleteGlobalRef

删除 globalRef 所指向的全局引用

GetMethodID

返回类或接口实例(非静态)方法的方法 ID。方法可在某个 clazz 的超类中定义,也可从 clazz 继承。该方法由其名称和签名决定。 GetMethodID() 可使未初始化的类初始化。要获得构造函数的方法 ID,应将<init> 作为方法名,同时将void (V) 作为返回类型。

GetStaticMethodID

调用静态方法

CallVoidMethod

调用实例方法

Call<type>Method

天才第一步,Hello World来一个

首先得有ndk的环境,环境配置很简单,博主就不在这里演示了。直接新建一个工程,勾选上c++支持:

然后看看Android Studio给我们生成了什么:

#####初识cmake

  1. cmake是什么:脱离 Android 开发来看,c/c++ 的编译文件在不同平台是不一样的。Unix 下会使用 makefile 文件编译,Windows 下会使用 project 文件编译。而 CMake 则是一个跨平台的编译工具,它并不会直接编译出对象,而是根据自定义的语言规则(CMakeLists.txt)生成 对应 makefileproject 文件,然后再调用底层的编译。
  2. 和ndk的区别:在 Android Studio 2.2 之后你有2种选择来编译你写的 c/c++ 代码。一个是 ndk-build + Android.mk + Application.mk 组合,另一个是 CMake + CMakeLists.txt 组合。这2个组合与Android代码和c/c++代码无关,只是不同的构建脚本和构建命令。说白了,cmake就是ndk的替代者。

本文使用的是后者即cmake构建,这也是google官方主推的。

cmake工程和普通的工程相比就多了这三个地方,一个是CMakeLists.txt文件,文件内容如下:

cmake_minimum_required(VERSION 3.4.1)
add_library( # 生成的so库名称,此处生成的so文件名称是libnative-lib.so
             native-lib
             # SHARED是动态库,会被动态链接,在运行时被加载
             # STATIC:静态库,是目标文件的归档文件,在链接其它目标的时候使用
             # MODULE:模块库,是不会被链接到其它目标中的插件
             SHARED
             # 资源路径是相对路径,相对于本CMakeLists.txt所在目录
             src/main/cpp/native-lib.cpp )
# 从系统查找依赖库
find_library( # android系统每个类型的库会存放一个特定的位置,而log库存放在log-lib中
              log-lib
              # android系统在c环境下打log到logcat的库
              log )
# 配置库的链接(依赖关系)
target_link_libraries( # 目标库
                       native-lib
                       # 依赖于
                       ${log-lib} )

注释写的很明确了,对于初学者,只需要注意的两个地方是,第一处和第三处的名字必须是相同的,第二处只要你在cpp文件夹下新建了.cpp文件,都需要在这里申明一下,是不是有点像清单文件的感觉。

关于cmake的具体使用,网上有很多教程,博主就不多说了。

cpp文件分析

然后就是.cpp文件里的内容了:

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

一个一个分析。

  1. 首先前两句是头文件,没什么好说的。
  2. extern "C"主要作用就是为了能够正确实现C++代码调用其他C语言代码 ,也就是兼容c语言。
  3. JNIEXPORT 在Jni编程中所有本地语言实现Jni接口的方法前面都有一个"JNIEXPORT",这个可以看做是Jni的一个标志,表示此函数是被jni调用的
  4. jstring 返回值类型是string类型的
  5. JNICALL 这个可以理解为Jni 和Call两个部分,和起来的意思就是 Jni调用XXX(后面的XXX就是JAVA的方法名)
  6. Javacom_ndk_lingxiao_ndkproject_MainActivity_stringFromJNI,别看这玩意儿这么长,他就是吓唬你的,我相信人有所长,你一定比他长,不要被吓到[]~( ̄▽ ̄)~*。固定写法Java+类名全路径+方法名,只是把类名的“.”替换为了下划线""。很简单的有木有。
  7. JNIEnv * env:这个env可以看做是Jni接口本身的一个对象,jni.h头文件中存在着大量被封装好的函数,这些函数也是Jni编程中经常被使用到的,要想调用这些函数就需要使用JNIEnv这个对象。例如:env->GetObjectClass()。
  8. jobject obj 有两种情况,一种是可以看做Java类的一个实例化对象 ,如Hello hello = new Hello(),hello.method(),这时候的obj 就是hello。哎,一不小心又new了一个对象出来。一种是可以看做是java类的本身 ,如果method是静态方法,它不是属于一个对象的,而是属于一个类的 ,这时候就代表Hello.class。
  9. std::string hello = "Hello from C++" 相当于stirng str = "Hello from C++",但是c++的字符串和java的字符串不一样,所以需要转换一下再返回,所以通过env对象调用方法转换为java能识别的env->NewStringUTF(hello.c_str())

cpp文件也讲完了,现在看看MainActivity里的代码:

public class MainActivity extends AppCompatActivity {
    static {
        System.loadLibrary("native-lib");
    }
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        // Example of a call to a native method
        TextView tv = (TextView) findViewById(R.id.sample_text);
        tv.setText(stringFromJNI());
    }
    public static native String stringFromJNI();
}

只需要将一下那个静态代码块,loadLibrary的时候,本来生成的.so文件为libnative-lib.so但是这里没有加是android studio会自动给我们加上去,如果这里再加上就会重复,所以只需要填写和CMakeLists.txt里的命名相同就行了。

c语言里打印Log

首先在module级的build.gradle里加入:

defaultConfig {
		ndk{
             ldLibs "gomp"
           }
       }

然后在cpp中加入如下的宏定义:

#include <android/log.h>
#define LOG_TAG "NATIVE_LIB"

#define DEBUG
#define ANDROID_PLATFORM

#ifdef DEBUG
#ifdef ANDROID_PLATFORM
#define LOGD(...) ((void)__android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, __VA_ARGS__))
#define LOGI(...) ((void)__android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__))
#define LOGW(...) ((void)__android_log_print(ANDROID_LOG_WARN, LOG_TAG, __VA_ARGS__))
#define LOGE(...) ((void)__android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__))
#else
#define LOGD(fmt, ...) printf(fmt"\n", ##__VA_ARGS__)
#define LOGI(fmt, ...) printf(fmt"\n", ##__VA_ARGS__)
#define LOGW(fmt, ...) printf(fmt"\n", ##__VA_ARGS__)
#define LOGE(fmt, ...) printf(fmt"\n", ##__VA_ARGS__)
#endif
#else
#define LOGD(...)
#define LOGI(...)
#define LOGW(...)
#define LOGE(...)
#endif

搞定,这个是固定写法,没什么好说的。

java调用C++方法

这个比较简单,这里就随便提一下,首先我新建了一个Hello类,写了两个方法,android studio会提示是否生成方法:

生成方法之后我只加了两句打印:

extern "C"
JNIEXPORT void JNICALL
Java_com_ndk_lingxiao_ndkproject_Hello_callStaticMethod(JNIEnv *env, jclass type, jint i) {
    LOGD("im from static moethod C++ , value is : %d",i);
}

extern "C"
JNIEXPORT void JNICALL
Java_com_ndk_lingxiao_ndkproject_Hello_callInstanceMethod
        (JNIEnv *, jobject, jint i){
    LOGD("im from instance moethod C++ , value is : %d",i);
}

然后在相应的地方调用一下,我是在MainActivity中调用的:

然后看一下后面的重点,c++中调用java层的方法和修改java层的属性。

方法签名

在学习c++调用java方法时需要了解的是方法签名,关于方法签名,我觉得只要关注这两个地方就行了:

  1. 什么是方法签名:方法签名由方法名称和一个参数列表(方法的参数的顺序和类型)组成。
  2. 为什么要用方法签名:c语言中没有方法重载这个概念,如果java中有两个方法:long test(int n, String str, int[] arr) ,long test(String str) 。那么没有方法签名来标注一下,编译器不就懵逼了嘛(ノ`Д)ノ。

下面有请方法签名规则表开始表演:

Java类型

签名类型

boolean

Z

byte

B

char

C

long

J

float

F

double

D

short

S

int

I

L全限定类名

数组

[全限定类名

上述中类的签名规则是:”L+全限定类名+;”三部分组成,其中全限定类名以”/”分隔,而不是用”.”或”_”分隔。

比如刚刚说的那两个方法:

  1. long test(String str) :方法签名为(Ljava/lang/String;)J ,括号里的内容代表string括号后面是返回值类型签名,J代表long型。
  2. long test(int n, String str, int[] arr) :其方法签名为(ILjava/lang/String;[I)J括号里的内容分成三部分,之间没有空格,即”I”,”Ljava/lang/String;”和”[I”,分别代表int,String,int[]

有迷妹私信我了:这么复杂的吗?有没有简单快捷的方法,每次都这么麻烦,太浪费时间了吧!我的时间很宝贵的嘤嘤嘤,要是没有我砍死你

30米的大刀

很大方的(迫不得已)交出偷懒方法:

javap -s 类的.class路径

可以说是很直观了(逃),博主用的as3.1,所以这个目录在工程,目录\module目录\build\intermediates\classes\debug下面。得到方法签名之后,就可以开始下面的操作了

C++调用Java静态方法

在java中写了一个这样的方法:

public static void staticMethod(String data){
        logMessage(data);
    }
public static void logMessage(String data){
        Log.d("hello", data);
    }

我希望在cpp中调用staticMethod方法,该怎么做呢?先贴代码:

extern "C"
JNIEXPORT void JNICALL
Java_com_ndk_lingxiao_ndkproject_Hello_callJavaStaticMethod(JNIEnv *env, jclass type) {
    jclass clazz = NULL;
    jmethodID method_id = NULL;
    jstring str_log = NULL;

    clazz = env->FindClass("com/ndk/lingxiao/ndkproject/Hello");
    if (clazz == NULL){
        LOGD("没有发现该类");
        return;
    }
    method_id = env->GetStaticMethodID(clazz,"staticMethod","(Ljava/lang/String;)V");
    if (method_id == NULL){
        LOGD("没有发现该方法名");
        return;
    }
    str_log = env->NewStringUTF("c++ 调用java的静态方法");
    env->CallStaticVoidMethod(clazz,method_id,str_log);

    env->DeleteLocalRef(clazz);
    env->DeleteLocalRef(str_log);
    return ;
}

这里如果对jvm虚拟机比较了解的同学可能会更容易理解,博主正在了解中,所以假装解释一波,只是按照我自己的理解,来解释,可能后面会改动(~ ̄▽ ̄)~ 。

首先定义了三个变量,然后使用env调用封装好的方法FindClass,传入类名全路径,在jvm中如果有加载这个类,那么就会返回我们的这个类。

接着是获取方法的id,使用env调用GetStaticMethodID,第一个参数是方法所在的类,第二个是方法名,第三个是方法签名。

然后使用env调用CallStaticVoidMethod,传入类和方法和参数,完成对java层方法的调用。

最后不要忘记删除引用,不然会发生内存泄漏。

C++调用Java实例方法

和静态方法的区别就两个地方,一个是GetStaticMethodID,一个是CallStaticVoidMethod:

extern "C"
JNIEXPORT void JNICALL
Java_com_ndk_lingxiao_ndkproject_Hello_callJavaInstanceMethod(JNIEnv *env, jobject instance) {
    jclass clazz = NULL;
    jmethodID method_id = NULL;
    jstring str_log = NULL;

    clazz = env->FindClass("com/ndk/lingxiao/ndkproject/Hello");
    if (clazz == NULL){
        LOGD("没有发现该类");
        return;
    }
    method_id = env->GetMethodID(clazz,"instanceMethod","(Ljava/lang/String;)V");
    if (method_id == NULL){
        LOGD("没有发现该方法名");
        return;
    }
    str_log = env->NewStringUTF("c++ 调用java的实例方法");
    env->CallVoidMethod(instance,method_id,str_log); //clazz 改为instance

    env->DeleteLocalRef(clazz);
    env->DeleteLocalRef(str_log);
    return ;
}
C++调用Java变量

首先在java类中定义一个变量:

public String name = "im is java";

然后贴上jni代码,主要方法是GetFieldID,第一个参数传入变量所在类,第二个参数是变量名,第三个参数是签名类型:

extern "C"
JNIEXPORT void JNICALL
Java_com_ndk_lingxiao_ndkproject_Hello_changeField(JNIEnv *env, jobject instance) {
    jclass clazz = env->GetObjectClass(instance);
    if (clazz == NULL){
        return;
    }
    jfieldID jfieldID = env->GetFieldID(clazz,"name","Ljava/lang/String;");
    if (jfieldID == NULL){
        return;
    }
    jstring obj_str = (jstring) env->GetObjectField(instance,jfieldID);
    if (obj_str == NULL){
        return;
    }
    char* c_str = (char*) env->GetStringUTFChars(obj_str,JNI_FALSE);

    const char new_char[40] = "changed from c";
    //复制new_char的内容到c_str
    strcpy(c_str,new_char);

    jstring new_str = env->NewStringUTF(c_str);
    LOGD("%s",new_char);
    env->SetObjectField(instance,jfieldID,new_str);

    env->DeleteLocalRef(clazz);
    env->DeleteLocalRef(obj_str);
    env->DeleteLocalRef(new_str);
    return;
}
C++调用Java静态变量

同理,静态变量也没啥好讲的了,这里就贴一下代码:

extern "C"
JNIEXPORT void JNICALL
Java_com_ndk_lingxiao_ndkproject_Hello_changeStaticField(JNIEnv *env, jclass type) {
    jclass clazz = env->FindClass("com/ndk/lingxiao/ndkproject/Hello");
    if (clazz == NULL){
        return;
    }
    jfieldID jfieldID = env->GetStaticFieldID(clazz,"age","I");
    if (jfieldID == NULL){
        return;
    }
    int age = env->GetStaticIntField(clazz,jfieldID);
    LOGD("%d",age);
    jint change_int = 12;
    env->SetStaticIntField(clazz,jfieldID,change_int);

    env->DeleteLocalRef(clazz);
}

学习JNI,个人建议是在平常的工作中能用到的才去深入学习,因为这个东西只有实践才有意义。关于如何在native中排查错误,可以使用ndk-stack工具,使用方法贼简单,一个命令行的事儿,这里就不说了。

本文demo的github地址:NdkDemo

参考链接:

JNI实战全面解析

Android NDK开发扫盲及最新CMake的编译使用(