题外话
转眼间2019年也已经接近尾声了,回顾这一年也发生了很多事,换工作、搬家、赶项目、学习新技术等等很多事,忙碌的一年,博客也被放下了,真的是越来越懒了,每次都有一万个不写博客的理由,之前每年至少还要更新几篇文章,然而今年一篇文章也没写,趁着这个周末没事情,抓住2019年的小尾巴,把自己一直想写的文章写了,于是就有了今天这篇文章。
jni简介
JNI是Java Native Interface的缩写,它提供了若干的接口实现了Java和其他语言的通信(主要是c、c++)。从Java1.1开始,JNI标准成为java平台的一部分,它允许Java代码和其他语言写的代码进行交互。jni是Android中java和c++之间连接的桥梁,jni是jvm提供的一种与native方法对接的方式。
jni的注册方式分为静态注册和动态注册两种,之前有一篇文章写过jni相关的知识,那篇文章介绍的关于jni的知识就是静态注册的方式,今天主要写的是jni的动态注册。静态注册和动态注册, 两者优缺点如下:
静态注册
优点: 理解和使用方式简单, 属于傻瓜式操作, 使用相关工具按流程操作就行, 出错率低
缺点: 当需要更改类名,包名或者方法时, 需要按照之前方法重新生成头文件, 灵活性不高
动态注册
优点: 灵活性高, 更改类名,包名或方法时, 只需对更改模块进行少量修改, 效率高
缺点: 对新手来说稍微有点难理解, 同时会由于搞错签名, 方法, 导致注册失败
静态注册
开始jni动态注册之前,可以先来回顾一下静态注册的流程:
编写一个java类,在里面加载对应的so库并且通过native关键字定义需要调用的函数
在对应路径命令行下输入 javac xxx.java 生成xxx.class文件,然后在src目录下通过 javah xxx.class 生成 com_xxx_xxx.h 头文件
.将头文件拷贝到jni目录下(eclipse在src同级目录建立文件夹,Android studio 在java同级目录建立文件夹)
编写C/C++源代码 并把刚拷贝的头文件包含进去 ,复制头文件中函数的定义部分,并实现其中的你想要的功能,然后编写Android.mk Application.mk(ndk-build编译方式,cmake方式可以不写)
ndk-build编译方式在命令行中进入jni目录,输入ndk-build 即可生产对应so库,cmake方式build一下项目便会生成对应的so库,库文件会自动放在libs文件夹下 至此就可以运行程序了
动态注册
动态注册基本思想是在JNI_Onload()函数中通过JNI中提供的RegisterNatives()方法来将C/C++方法和java方法对应起来(注册), 我们在调用 System.loadLibrary的时候,会在C/C++文件中回调一个名为 JNI_OnLoad ()的函数,在这个函数中一般是做一些初始化相关操作, 我们可以在这个方法里面注册函数, 注册整体流程如下:
编写Java端的相关native方法
编写C/C++代码, 根据Java文件路径和方法签名确定JNINativeMethod,实现JNIEXPORT int JNICALL JNI_OnLoad(JavaVM *vm,void *reserved)方法
将Java 方法和 C/C++方法通过签名信息一一对应起来
通过JavaVM获取JNIEnv, JNIEnv主要用于获取Java类和调用一些JNI提供的方法
使用类名和对应起来的方法作为参数, 调用JNI提供的函数RegisterNatives()注册方法
相关实例代码如下:
定义jni工具类,里面定义native方法public class JniUtil {
// Used to load the 'native-lib' library on application startup.
static {
System.loadLibrary("native-lib");
}
/**
* A native method that is implemented by the 'native-lib' native library,
* which is packaged with this application.
*/
public native String stringFromJNI();
public native String dynamicMethod();
}
在对应的native-lib中实现定义的本地方法#include
#include
#define JNI_JAVA_CLASS "com/example/dynamicjnidemo/JniUtil"
//静态注册对应本地方法
extern "C"
JNIEXPORT jstring JNICALL
Java_com_example_dynamicjnidemo_JniUtil_stringFromJNI(JNIEnv *env, jobject thiz) {
std::string hello = "jniUtil,Hello from C++";
return env->NewStringUTF(hello.c_str());
}
//动态注册对应本地方法
extern "C"
JNIEXPORT jstring JNICALL
defineDynamicMethod(JNIEnv *env, jobject thiz) {
return env->NewStringUTF("dynamic register jni method from jni");
}
/*需要注册的函数列表,放在JNINativeMethod 类型的数组中,
以后如果需要增加函数,只需在这里添加就行了
参数:
1.java中用native关键字声明的函数名
2.签名(传进来参数类型和返回值类型的说明)
3.C/C++中对应函数的函数名(地址)
*/
static JNINativeMethod all_methods[] = {
// 多个方法依次都好分割写在这里
{"dynamicMethod", "()Ljava/lang/String;", (void *) defineDynamicMethod},
};
//必须实现的回调函数
JNIEXPORT int JNICALL JNI_OnLoad(JavaVM *vm, void *unused) {
JNIEnv *env;
// 获取JNIEnv
if (vm->GetEnv((void **) &env, JNI_VERSION_1_6) != JNI_OK) {
return JNI_ERR;
}
// 指定类路径,通过FindClass()方法找到对应类
jclass java_class = env->FindClass(JNI_JAVA_CLASS);
if (java_class == NULL) {
return JNI_ERR;
}
int method_count = sizeof(all_methods) / sizeof(all_methods[0]);
// 注册对应的本地方法 参数:java类 所要注册的函数数组 注册函数的个数
// registerNatives ->registerNativeMethods ->env->RegisterNatives
if (env->RegisterNatives(java_class, all_methods, method_count) < 0) {
return JNI_ERR;
}
// 返回jni 的版本
return JNI_VERSION_1_6;
}
在需要地方调用package com.example.dynamicjnidemo;
import androidx.appcompat.app.AppCompatActivity;
import android.os.Bundle;
import android.widget.TextView;
public class MainActivity extends AppCompatActivity {
private JniUtil jniUtil = new JniUtil();
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// Example of a call to a native method
TextView tv = findViewById(R.id.sample_text);
tv.setText(jniUtil.stringFromJNI());
TextView tvDynamic = findViewById(R.id.textViewDynamic);
tvDynamic.setText(jniUtil.dynamicMethod());
}
}
最终效果如下图:
上面的代码就能实现动态注册JNI了 以后要增加函数只需在java文件中声明native方法,在C/C++文件中实现,
并在getMethods数组添加一个元素并指明对应关系,通过ndk-build 生成so库就可以运行了。
Java方法签名
关于查询Java方法签名,可以使用javap -s命令查询对应的class文件(先编译出对应的class文件,再查询),如果方法签名错了,编译能通过,但运行时会报NoSuchMethod异常。动态注册中 JNINativeMethod 结构体中第二个参数需注意
括号内代表传入参数的签名符号,为空可以不写,括号外代表返回参数的签名符号,为空填写 V,对应关系入下表:
签名对应关系表
签名符号
C/C++
java
V
void
void
Z
jboolean
boolean
I
jint
int
J
jlong
long
D
jdouble
double
F
jfloat
float
B
jbyte
byte
C
jchar
char
S
jshort
short
[Z
jbooleanArray
boolean[]
[I
jintArray
int[]
[J
jlongArray
long[]
[D
jdoubleArray
double[]
[F
jfloatArray
float[]
[B
jbyteArray
byte[]
[C
jcharArray
char[]
[S
jshortArray
short[]
L完整包名加类名;
jobject
class
如果有内部类 则用 $ 来分隔 如:Landroid/os/xxx$xxx;
总结:
一般来说,静态注册书写简单,但是由于方法名的对应关系,所以耦合性强,如果包名发生变化,那么需要大量修改,而且初次运行的效率也不如动态注册,所以动态注册无疑是注册函数的更好方式, 唯一要注意的是注册函数时, 需要额外小心, 别把类名,函数名和签名写错了, 不然loadLibraries时会导致应用Crash,能力有限就先写这么多,后面再补充。