JNI开发探索之旅
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
- cmake是什么:脱离 Android 开发来看,c/c++ 的编译文件在不同平台是不一样的。Unix 下会使用
makefile
文件编译,Windows 下会使用project
文件编译。而CMake
则是一个跨平台的编译工具,它并不会直接编译出对象,而是根据自定义的语言规则(CMakeLists.txt
)生成 对应makefile
或project
文件,然后再调用底层的编译。 - 和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());
}
一个一个分析。
- 首先前两句是头文件,没什么好说的。
- extern "C"主要作用就是为了能够正确实现C++代码调用其他C语言代码 ,也就是兼容c语言。
- JNIEXPORT 在Jni编程中所有本地语言实现Jni接口的方法前面都有一个"JNIEXPORT",这个可以看做是Jni的一个标志,表示此函数是被jni调用的
- jstring 返回值类型是string类型的
- JNICALL 这个可以理解为Jni 和Call两个部分,和起来的意思就是 Jni调用XXX(后面的XXX就是JAVA的方法名)
- Javacom_ndk_lingxiao_ndkproject_MainActivity_stringFromJNI,别看这玩意儿这么长,他就是吓唬你的,我相信人有所长,你一定比他长,不要被吓到[]~( ̄▽ ̄)~*。固定写法Java+类名全路径+方法名,只是把类名的“.”替换为了下划线""。很简单的有木有。
- JNIEnv * env:这个env可以看做是Jni接口本身的一个对象,jni.h头文件中存在着大量被封装好的函数,这些函数也是Jni编程中经常被使用到的,要想调用这些函数就需要使用JNIEnv这个对象。例如:env->GetObjectClass()。
- jobject obj 有两种情况,一种是可以看做Java类的一个实例化对象 ,如Hello hello = new Hello(),hello.method(),这时候的obj 就是hello。哎,一不小心又new了一个对象出来。一种是可以看做是java类的本身 ,如果method是静态方法,它不是属于一个对象的,而是属于一个类的 ,这时候就代表Hello.class。
- 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方法时需要了解的是方法签名,关于方法签名,我觉得只要关注这两个地方就行了:
- 什么是方法签名:方法签名由方法名称和一个参数列表(方法的参数的顺序和类型)组成。
- 为什么要用方法签名: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+全限定类名+;”三部分组成,其中全限定类名以”/”分隔,而不是用”.”或”_”分隔。
比如刚刚说的那两个方法:
- long test(String str) :方法签名为(Ljava/lang/String;)J ,括号里的内容代表string括号后面是返回值类型签名,J代表long型。
- long test(int n, String str, int[] arr) :其方法签名为(ILjava/lang/String;[I)J括号里的内容分成三部分,之间没有空格,即”I”,”Ljava/lang/String;”和”[I”,分别代表int,String,int[]
有迷妹私信我了:这么复杂的吗?有没有简单快捷的方法,每次都这么麻烦,太浪费时间了吧!我的时间很宝贵的嘤嘤嘤,要是没有我砍死你
很大方的(迫不得已)交出偷懒方法:
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的编译使用(