面试题预览
- 阐述你对JNI的理解⭐⭐⭐⭐⭐
- 使用JNI有什么优缺点⭐⭐⭐⭐⭐
- 什么是JNI?具体说说如何实现Java与C++的互调⭐⭐⭐⭐⭐
- 什么是NDK?为什么要使用NDK?⭐⭐
- JNI开发的一般步骤是?⭐⭐⭐⭐
- JNI函数的注册方法都有什么?⭐⭐⭐⭐
- 谈谈你对JNI静态注册和动态注册的区别。⭐⭐
1 概述
面试题:阐述你对JNI的理解⭐⭐⭐⭐⭐
JNI 是Java Native Interface的缩写,表示"Java本地调用"。通过JNI技术可以实现:
- Java调用C程序
- C程序调用Java代码
我们的android源码中有很多代码都是Jni的实现的。例如MediaScanner的实现。就是通过jni的技术让我们在java层扫描到媒体相关的资源的
面试题:使用JNI有什么优缺点?⭐⭐⭐⭐⭐
1.1 JNI的优缺点
1.1.1 JNI的优点
- 首先,Java语言提供的类库无法满足要求,且在数学运算,实时渲染的游戏上,音视频处理等方面上与C/C++相比效率稍低。
- 然后,Java语言无法直接操作硬件,C/C++代码不仅能操作硬件而且还能发挥硬件最佳性能。
- 接着,使用Java调用本地的C/C++代码所写的库,省去了重复开发的麻烦,并且可以利用很多开源的库提高程序效率。
通过对上面的几点的总结,可以归纳出一句话就是规避Java语言的弱点,然后利用C/C++的优点。
实际Android中的驱动都是C/C++开发的,通过JNI,Java可以调用C/C++实现的驱动,从而拓展Java虚拟机的能力。另外,在高效率的数学运算、游戏的实时渲染、音视频的编解码等方面,一般都是用C/C++开发的。
缺点部分:
1.1.2 JNI缺点
任何事情都是由两面性的,所以JNI也不例外,所以在决定使用 JNI之前,我想各位一定要了解JNI那些缺点,如果能接受和容纳那就可以放心大胆的使用了。
- 使用JNI细小的错误都能让这个JVM不稳定,并且这些错误很难再现和调试
- 使用JNI的应用失去了JAVA本身提供的不同平台的可移植性。
- JNI 框架不提供自动的垃圾回收机制,所以这部分代码要考虑内存的释放
面试题:具体说说如何实现Java与C++的互调⭐⭐⭐⭐⭐
1.2 Java调用C/C++的步骤:
- java声明native函数
- jni实现对应的c函数
- 编译生成so库
- java 加载so库,并调用native函数
1.3 C/C++调用Java的步骤:
- 从classpath路径下搜索ClassMethod这个类,并返回该类的Class对象。
- 获取类的默认构造方法ID。
- 查找实例方法的ID。
- 创建该类的实例。
- 调用对象的实例方法。
示例:
JNIEXPORT void JNICALL Java_com_study_jnilearn_AccessMethod_callJavaInstaceMethod
(JNIEnv *env, jclass cls)
{
jclass clazz = NULL;
jobject jobj = NULL;
jmethodID mid_construct = NULL;
jmethodID mid_instance = NULL;
jstring str_arg = NULL;
// 1、从classpath路径下搜索ClassMethod这个类,并返回该类的Class对象
clazz = (*env)->FindClass(env, "com/study/jnilearn/ClassMethod");
if (clazz == NULL) {
printf("找不到'com.study.jnilearn.ClassMethod'这个类");
return;
}
// 2、获取类的默认构造方法ID
mid_construct = (*env)->GetMethodID(env,clazz, "","()V");
if (mid_construct == NULL) {
printf("找不到默认的构造方法");
return;
}
// 3、查找实例方法的ID
mid_instance = (*env)->GetMethodID(env, clazz, "callInstanceMethod", "(Ljava/lang/String;I)V");
if (mid_instance == NULL) {
return;
}
// 4、创建该类的实例
jobj = (*env)->NewObject(env,clazz,mid_construct);
if (jobj == NULL) {
printf("在com.study.jnilearn.ClassMethod类中找不到callInstanceMethod方法");
return;
}
// 5、调用对象的实例方法
str_arg = (*env)->NewStringUTF(env,"我是实例方法");
(*env)->CallVoidMethod(env,jobj,mid_instance,str_arg,200);
// 删除局部引用
(*env)->DeleteLocalRef(env,clazz);
(*env)->DeleteLocalRef(env,jobj);
(*env)->DeleteLocalRef(env,str_arg);
}
2 JNI开发
2.1 JNI开发的一般步骤
面试题:JNI开发的一般步骤是?⭐⭐⭐⭐
- 编写声明了native方法的Java类
- 将Java源代码编译成class字节码文件
- 用javah -jni命令生成.h头文件(javah是jdk自带的一个命令,-jni参数表示将class中用native声明的函数生成jni规则的函数)
- 用本地代码实现.h头文件中的函数
- 将本地代码编译成动态库(windows:*.dll,linux/unix:*.so,mac os x:*.jnilib)
- 拷贝动态库至 java.library.path 本地库搜索目录下,并运行Java程序使用AndroidStudio创建JNI工程
上面的步骤是不是看起来有点复杂,但是在实际开发过程中不用担心。在Android Studio里集成了NDK工具集,能够快速的帮你完成上述步骤。
2.2 JNI与NDK
面试题:什么是NDK?⭐⭐
NDK全称是Native Development Kit,NDK提供了一系列的工具,帮助开发者快速开发C(或C++)的动态库,并能自动将so和java应用一起打包成apk。NDK集成了交叉编译器(交叉编译器需要UNIX或LINUX系统环境),并提供了相应的mk文件隔离CPU、平台、ABI等差异,开发人员只需要简单修改mk文件(指出“哪些文件需要编译”、“编译特性要求”等),就可以创建出so。
面试题:为什么使用NDK?⭐⭐
1.)代码的保护。由于apk的java层代码很容易被反编译,而C/C++库反汇难度较大。
2.)可以方便地使用现存的开源库。大部分现存的开源库都是用C/C++代码编写的。
3.)提高程序的执行效率。将要求高性能的应用逻辑使用C开发,从而提高应用程序的执行效率。
4.)便于移植。用C/C++写得库可以方便在其他的嵌入式平台上再次使用。
2.3 JNI的注册方法
2.3.1 静态方法
- 创建Java类,声明 native 方法
- javah 生成头文件 .h文件的作用
- 创建 C/C++ 文件,实现对应的native方法
如何连接 Java 层方法和 native 层方法的:
Java方法被调用时,JVM会生成对应的 native 方法名,例如 com.example.StrHelper.getStr() ,JVM会在JNI库中查找 Java_com_example_StrHelper_getStr 函数,如果找到了,就会保存一个该 JNI 函数的指针,直接调用该指针。如果没找到就会报错。
上代码:
以下源码可以通过gitlab获取:JniTest
- 准备工作,Android Studio 中安装好NDK、 CMAK、 LLDB 工具
- 使用Android Studio创建一个工程,在Java代码中声明一个native方法
![](https://i-blog.csdnimg.cn/blog_migrate/625ba97a8f7bc82219642047a43e353d.png)
- 在java中声明native方法
public class MainActivity extends AppCompatActivity {
/**
*使用静态代码块加载'native-lib'库,该库即为C/C++代码编译后的共享库,
* 加载后才能让java层调用C/C++的代码。
* 库名称由CMakeLists.txt文件中的add_library指定。通常此处名称是native-lib的话,
* 那么编译成功的共享库名称为libnative-lib.so。
*/
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(sayHello());//本地方法调用和java函数调用毫无二致
}
/**
* 本地'native-lib'库中实现了的本地方法,共享库会打包到本应用中
* 区别于普通java函数,在函数申明中多了个native字段,以及没有函数体
*/
public native String sayHello();
}
4) javac xxx.java 或者 build 生成class文件
- 将 .class 生成 .h文件,javah -cp -d <生成路径>-jni com.hanson.jnitest.MainActivity 。如果有问题
- 点击"View->Tool Windows->Terminal",即在Studio中进行终端命令行工具.执行如下命令生成c语言头文件。
- 这里需要注意的是要进入 \app\src\main的目录下执行javah命令,为的是生成的 .h 文件同样是在\app\src\main路径下,可以在Studio的工程结构中直接看到。
操作命令:
javah -encoding UTF-8 -classpath D:\WorkSpace\AndroidStudio\JniTest2\app\src\main\java -d D:\WorkSpace\AndroidStudio\JniTest2\app\src\main\jni -jni com.hanson.jnitest.MainActivity
然后就可以看到在-d指定的目录下生成了jni的头文件
具体操作图如下:
最后的生成结果:
/* DO NOT EDIT THIS FILE - it is machine generated */
#include
/* Header for class com_hanson_jnitest_MainActivity */
#ifndef _Included_com_hanson_jnitest_MainActivity
#define _Included_com_hanson_jnitest_MainActivity
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class: com_hanson_jnitest_MainActivity
* Method: sayHello
* Signature: ()Ljava/lang/String;
*/
JNIEXPORT jstring JNICALL Java_com_hanson_jnitest_MainActivity_sayHello
(JNIEnv *env, jobject);
#ifdef __cplusplus
}
#endif
#endif
6.最后就是实现native方法了,创建一个C++文件,最好保持名字和.h文件一致
#include
#include "com_hanson_jnitest_MainActivity.h"
#include
#include
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, "In C/C++:", __VA_ARGS__);
using namespace std;
extern "C"
JNIEXPORT jstring JNICALL Java_com_hanson_jnitest_MainActivity_sayHello(JNIEnv *env, jobject)
{
std::string str = "Hello from C++";
return env->NewStringUTF(str.c_str());
}
NI中C/C++代码对应于java中的函数,命令有一定的规则。
我们看一下本例中,java函数和C/C++函数的对应关系:
java部分:
类名:com.hanson.jnitest.MainActivity
方法名:public native String sysHello();
c/c++部分:
JNIEXPORT jstring JNICALL Java_com_hanson_jnitest_MainActivity_sysHello(JNIEnv *env, jobject /* this */)
JNIEXPORT : 可以当做JNI方法的函数申明关键字。
jstring : 函数返回值,jstring 对应的是java的String对象。
JNICALL : 可以认为是JNI访问的关键字,固定格式啦。
Java_com_hanson_jnitest_MainActivity_sysHelloI: 在C函数中的方法名格式为Java_{package_and_classname}_{function_name}(JNI_arguments) ,只需要将包名中的点换成下横线就行。
JNIEnv *env: JNI的环境引用,一个非常有用的变量,可以通过它调用所有JNI函数。
jobject /* this */:函数调用者的对象,相当于java层中的this.
7)再在添加CMakeLists.txt,并在其中加入
add_library(
native-lib
SHARED
com_hanson_jnitest_MainActivity.cpp)//这个.cpp文件
8) 然后在build.gradle文件中指定CMakeLists.txt
如下
// 指定使用NDK的版本
ndkVersion '20.0.5594570'
// 指定CMakeLists.txt
externalNativeBuild {
cmake {
path "CMakeLists.txt"
}
}
9) 点击编译就大功告成了,直接运行,可以调用native方法来获取string了
静态注册方法的弊端:
- 编写不方便,JNI方法名字遵循规则,很长
- 编写过程步骤太多,每个声明 native 方法的类都要生成一个 .h 头文件。
- 初次调用需要在JNI 层根据函数名查找建立对应关系,耗时
2.3.2 动态方法
- 创建Java类,声明native方法
- 创建对应 C++ 类,在该类中实现JNI_OnLoad方法,定义JNINativeMethod列表,以及Java native方法具体实现(此时,方法的名称可以是任意的)
通过System.LoadLibrary()加载so库的时候,JVM会调用JNI_OnLoad方法,而我们可以通过在该方法中调用JNIEnv->RegisterNatives()方法将我们的native方法声明注册到JNI中,那是如何将native方法与Java方法联系起来的呢,就是通过JNINativeMethod结构体将两者联系起来的。
上代码:
typedef struct {
const char* name;//这个是java层函数的名字
const char* signature;//这个是Java层函数的签名,其他两个很好理解,这个函数签名是个啥???下面会讲
void* fnPtr;//这个是native层函数的名字
} JNINativeMethod;//这就是结构体的主要内容,然后我们怎么写呢
TestJni.java
package com.cn.mydynamic;
public class TestJni {
static {
System.loadLibrary("main");
}
public native String sayHello();//定义了一个native方法
}
上面的JNINativeMethod结构体中的第二项,Java层函数签名,就是按照一定的规则,将java层函数的参数返回值进行转化,为啥整出个这玩意儿?因为java支持函数重载,仅凭函数名称是找不对对应函数的,所以就用参数和返回值结合函数名称来找。
规则如下:
当参数的类型是引用类型时,其格式是" L包名;",其中包名中的"." 换成"/"。
很容易写错,但是可以通过javap -s -p xxx.class直接生成转换好的签名,上述的TestJni转换后为
>javap -s -p TestJni.class
Compiled from "TestJni.java"
public class com.cn.mydynamic.TestJni {
public com.cn.mydynamic.TestJni();
descriptor: ()V
public native java.lang.String sayHello();
descriptor: ()Ljava/lang/String;
static {};
descriptor: ()V
}
有了签名有了Java 函数,有了native函数,就可以放进结构体里了
//建立Java层函数与native层函数的对应关系
static const JNINativeMethod getMethod[] = {
{"sayHello",
"()Ljava/lang/String;",
(void*)sya_hello
}
};
JNIEXPORT jstring JNICALL sya_hello
(JNIEnv *env, jobject job)
{
char* app_key = "不要回答!!!不要回答!!!";
return env->NewStringUTF(app_key);
}
接下来就要把对应关系注册上
#define NELEM(m) (sizeof(m) / sizeof((m)[0]))
jint JNI_OnLoad(JavaVM* vm, void* reserved) {
JNIEnv* env = NULL;
if (vm ->GetEnv((void**) &env, JNI_VERSION_1_4) != JNI_OK) {
return -1;
}
assert(env != NULL);
jclass clazz;
clazz = env->FindClass("com/cn/mydynamic/TestJni");
if (clazz == NULL) {
return -1;
}
if (env->RegisterNatives(clazz, getMethods, NELEM(getMethods)) < 0) {
return -1;
}
return JNI_VERSION_1_4;
};
运行结果
3 JNI 基础
上面做了点环境铺垫,接下来开始上正菜。
我们都知道,java的数据类型和C/C++的数据类型并不一致,典型的例子是:java中的String是一个引用数据类型,但在C语言中的String是以NULL结尾的字符串数组。所以协调数据类型,是JNI的重点内容。
JNI 定义了如下的JNI类型用于本地代码中,对应java的数据类型:
3.1 Java 基础数据类型:
下表是对应关系
JNI数据类型 | java数据类型 |
jint | int |
jbyte | byte |
jshort | short |
jlong | long |
jfloat | float |
jdouble | double |
jchar | char |
jboolean | boolean |
3.2 java 引用数据类型:
JNI数据类型 | java数据类型 |
jobject | |
jclass | |
jstring | |
jthrowable |
3.3 在java中,数组中的数据类型和JNI的数组类型对应定义:
JNI数据类型 | java数据类型 |
jintArray | int [] |
jbyteArray | byte [] |
jshortArray | short [] |
jlongArray | long [] |
jfloatArray | float [] |
jdoubleArray | double [] |
jcharArray | char [] |
jbooleanArray | boolean [] |
jobjectArray | Object [] |
3.4 本地程序调用基本顺序
- 使用JNI数据类型接收参数(该参数通过java程序调用传递)
- 对于JNI引用数据类型,将参数转换或者复制为本地类型。比如:jstring 转为 C-string, jintArray转为C’s int[]等等。基本数据类型,例如jint, jdouble可以直接使用而不需要转换。
- 使用本地数据类型执行程序。
- 创建一个JNI类型的对象,用作返回(return),将程序运行的结果复制到返回对象中。
- 函数返回(return)。
在JNI程序开发过程中,比较困难而极具挑战的是JNI引用类型(例如 jstring, jobject, jintArray, jobjectArray) 和C本地数据类型(例如C-string, int[] )之间的转换。幸好,JNI环境提供了大量的函数来处理这种转换。
4 Java & Native 程序之间参数传递
4.1 基本数据类型传递
java中的8种基本数据类型可以直接被传递和使用,因为这些类型都在jni.h中申明了:
/* Primitive types that match up with Java equivalents. */
typedef uint8_t jboolean; /* unsigned 8 bits */
typedef int8_t jbyte; /* signed 8 bits */
typedef uint16_t jchar; /* unsigned 16 bits */
typedef int16_t jshort; /* signed 16 bits */
typedef int32_t jint; /* signed 32 bits */
typedef int64_t jlong; /* signed 64 bits */
typedef float jfloat; /* 32-bit IEEE 754 */
typedef double jdouble; /* 64-bit IEEE 754 */
示例
Java 层:
public class MainActivity extends AppCompatActivity {
// 在程序开始时使用静态代码块加载'native-lib'库
static {
System.loadLibrary("native-lib");
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Log.i("In java", String.valueOf(average(3, 4)));
}
//基本数据类型在c/java之间的传递
public native double average(int arg1, int arg2);
C 层:
extern "C"
JNIEXPORT jdouble JNICALL
Java_com_dali_jnitest_MainActivity_average(JNIEnv *env, jobject instance, jint arg1,
jint arg2) {
jdouble result;//基本数据类型无需变化,在jni.h中已经设置了类型别名
/**
*想要使用该打印,请在C文件头增加下列代码:
*include
*define LOGI(...) __android_log_print(ANDROID_LOG_INFO, "In C/C++:", __VA_ARGS__);
*/
LOGI("arg1: %d, ar2: %d", arg1, arg2);
result = (arg1 + arg2) / 2;
return result;
}
运行程序:
com.dali.jnitest I/In C/C++: arg1: 3, ar2: 4
com.dali.jnitest I/In java: 3.0
4.2 字符串传递
示例
java层:
public class MainActivity extends AppCompatActivity {
// 在程序开始时使用静态代码块加载'native-lib'库
static {
System.loadLibrary("native-lib");
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Log.i("In java", testString("hello"));
}
//字符串在c/java之间的传递
public native String testString(String str);
c层:
extern "C"
JNIEXPORT jstring JNICALL
Java_com_dali_jnitest_MainActivity_testString(JNIEnv *env, jobject instance, jstring str_) {
//在C中的String是以NULL结尾的字符串数组,需要通过特定方法转换,但在C++中有对应的String,是否可以不 //转换?
const char *str = env->GetStringUTFChars(str_, 0);
LOGI("str: %s", str);
char* returnValue = "hehe ,wo lai le";
env->ReleaseStringUTFChars(str_, str);
return env->NewStringUTF(returnValue);
}
JNI 定义了jstring类型来代表java的String。C层函数的最后一个参数(JNI类型的jstring)是Java层的String传递到C层的引用。该程序的返回值同样也是jstring类型。
传递字符串远比基本数据类型复杂,因为Java层的String是一个对象(引用数据类型),然而C层中的string是一个以NULL结尾的char数组。所以,使用时需要在Java层的String(以JNI 的jstring表示)和C层的string(char*)之间转换。
JNI环境(通过参数JNIENV*调用)提供了这种转换的函数:
使用const char* GetStringUTFChars(JNIEnv*, jstring, jboolean*)将JNIstring(jstring)类型转换为C层的string(char*)。
使用 jstring NewStringUTF(JNIEnv*, char*)将C层的string(char*)转为JNIstring(jstring)类型。
C层函数的实现步骤为:
从JNI的jstring接收数据,并通过GetStringUTFChars()转为C层的string (char*)类型。
然后执行程序,显示接收到的参数数据,并返回另外一个字符串。
将C层的string (char*)类型通过NewStringUTF()函数转换为JNI的jstring类型并返回。
运行程序:
com.dali.jnitest I/In C/C++: str: hello
com.dali.jnitest I/In java: hehe ,wo lai le
4.2.1 JNI本地String函数
JNI支持Unicode(16字节字符串)和UTF-8(1-3字节编码)不同格式字符串之间的转换。UTF-8编码的字符串和C语言中的字符串一样是以NULL结尾的char数组,用于C/C++程序中。
这些JNI字符串(jstring)为:
/** UTF-8 String (encoded to 1-3 byte, backward compatible with 7-bit ASCII)
* 获取以NULL结尾的字符数组,也就是C-string
*/
// 返回表示UTF-8编码字符串的数组指针
const char * GetStringUTFChars(jstring string, jboolean *isCopy);
// 通知VM 本地代码不再需要UTF引用。
void ReleaseStringUTFChars(jstring string, const char *utf);
// 根据字符串数组,构造一个UTF-8编码的java String新对象
jstring NewStringUTF(const char *bytes);
// 返回UTF-8编码字符串的长度
jsize GetStringUTFLength(jstring string);
// 将从偏移量start开始的length长度的Unicode字符转换为UTF-8编码,并将结果放在给定的缓冲区buf中。
void GetStringUTFRegion(jstring str, jsize start, jsize length, char *buf);
// Unicode Strings (16-bit character)
// 返回指向Unicode字符数组的指针
const jchar * GetStringChars(jstring string, jboolean *isCopy);
// 通知VM本机代码不再需要访问字符。
void ReleaseStringChars(jstring string, const jchar *chars);
// 从Unicode字符数组构造一个新的java.lang.String对象。
jstring NewString(const jchar *unicodeChars, jsize length);
// 返回Java字符串的长度(Unicode字符数)。
jsize GetStringLength(jstring string);
// 将从偏移量=start开始的length长度的Unicode字符数复制到给定的缓冲区buf。
void GetStringRegion(jstring str, jsize start, jsize length, jchar *buf);
4.2.2 UTF-8 strißngs & C-strings
GetStringUTFChars()函数可用于从给定的Java的jstring创建新的C字符串(char *)。 如果无法分配内存,则该函数返回NULL。 检查NULL是一个好习惯。
第三个参数isCopy(of jboolean *),它是一个“in-out”参数,如果返回的字符串是原始java.lang.String实例的副本,则将设置为JNI_TRUE。 如果返回的字符串是指向原始String实例的直接指针,则它将设置为JNI_FALSE- 在这种情况下,本机代码不应修改返回的字符串的内容。 如果可能,JNI运行时将尝试返回直接指针; 否则,它返回一份副本。 尽管如此,我们很少对修改底层字符串感兴趣,并且经常传递NULL指针。
不使用使用GetStringUTFChars()返回的字符串时,需要来释放内存和引用以便可以对其进行垃圾回收时,始终调用ReleaseStringUTFChars()。
NewStringUTF()函数使用给定的C字符串创建一个新的JNI字符串(jstring)。
JDK 1.2引入了GetStringUTFRegion(),它将jstring(或从长度开始的一部分)复制到“预分配”的C的char数组中。 可以使用它们代替GetStringUTFChars()。 由于预先分配了C的数组,因此不需要isCopy。
JDK 1.2还引入了Get / ReleaseStringCritical()函数。 与GetStringUTFChars()类似,如果可能,它返回一个直接指针; 否则,它返回一份副本。 本机方法不应阻止(对于IO或其他)一对GetStringCritical()和ReleaseStringCritical()调用。
有关详细说明,请始终参阅“Java Native Interface Specification”@ http://docs.oracle.com/javase/7/docs/technotes/guides/jni/index.html。
4.3 基本数据类型数组传递
4.4 访问对象的变量和函数回调
4.5 创建对象和对象数组
4.6 本地和全局引用
4.3~4.6小节,详细可以参考下面一篇不错的博客:
Android JNI 详解_SuperDali的博客-CSDN博客
本文不再赘述。