JNI 与 NDK 入门
JNI
概念
JNI是Java Native Interface
的简写,它可以使Java与其他语言(如C、C++)进行交互。
它是Java调用Native语言的一种特性,属于Java语言的范畴,与Android无关。
为何需要JNI
- Java的源文件非常容易被反编译,而通过Native语言生成的.so库文件则不容易被反编译。
- 有时我们使用Java时需要使用到一些库来实现功能,但这些库仅仅提供了一些Native语言的接口。
- 使用Native语言编写的代码运行效率高,尤其体现在音频视频图片的处理等需要大量复杂运算的操作上。充分利用了硬件的性能。
由于上述原因,此时我们就需要让Java与Native语言交互。而由于Java的特点,与Native语言的交互能力很弱。因此在此时,我们就需要用到JNI特性增强Java与Native方法的交互能力。
实现的步骤
- 在Java中声明Native方法(需要调用的本地方法)
- 通过 javac 编译 Java源文件( 生成.class文件)
- 通过 javah 命令生成JNI头文件(生成.h文件)
- 通过Native语言实现在Java源码中声明的Native方法
- 编译成.so库文件
- 通过
Java
命令执行Java
程序,最终实现Java
调用本地代码(借助so库文件)
NDK
概念
Native是Native Development Kit
的简写,是Android的开发工具包,属于Android,与Java无关系。
它可以快速开发C/C++的动态库,自动将.so和应用一起打包为APK。因此我们可以通过NDK来在Android开发中通过JNI与Native方法交互。
使用方式
- 配置 Android NDK环境(在SDK Manager中下载NDK、CMake、LLDB)
- 创建 Android 项目,与 NDK进行关联(创建项目时选择C++ support)
- 在 Android 项目中声明所需要调用的 Native方法
- 用Native语言实现在Android中声明的Native方法
- 通过 ndk-bulid 命令编译产生.so库文件
将Android项目与NDK关联
配置NDK路径
在local.properties中
加入如下一行即可
ndk.dir=
添加配置
在Gradle的 gradle.properties
中加入如下一行,目的是对旧版本的NDK支持
android.useDeprecatedNdk=true
添加ndk节点
在build.gradle中的defaultConfig
和android
中加入如下的externalNativeBuild
节点
apply plugin: 'com.android.application'
android {
compileSdkVersion 27
defaultConfig {
applicationId "com.n0texpecterr0r.ndkdemo"
minSdkVersion 19
targetSdkVersion 27
versionCode 1
versionName "1.0"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
externalNativeBuild {
cmake {
cppFlags ""
}
}
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
externalNativeBuild {
cmake {
path "CMakeLists.txt"
}
}
}
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation 'com.android.support:appcompat-v7:27.1.1'
implementation 'com.android.support.constraint:constraint-layout:1.1.3'
testImplementation 'junit:junit:4.12'
androidTestImplementation 'com.android.support.test1.0.2'
androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'
}
开发Native代码
在Java文件中声明native方法
我们首先需要在Java代码的类中通过static块来加载我们的Native库。可以通过如下代码,其中loadLibrary的参数是在CMakeList.txt中定义的Native库的名称
static {
System.loadLibrary("native-lib");
}
之后,我们便可以在这个类中声明Native方法
public native String getStringFromJNI();
创建CMakeList.txt
我们还需要在src中创建一个CMakeList.txt文件,这个文件约束了Native语言源文件的编译规则。比如下面
cmake_minimum_required(VERSION 3.4.1)
add_library(native-lib SHARED src/main/cpp/native-lib.cpp)
find_library(log-lib log)
target_link_libraries(native-lib ${log-lib})
add_library
方法中定义了一个so库,它的名称是native-lib,也就是我们在Java文件中用到的字符串,而后面则跟着这个库对应的Native文件的路径
find_library
则是定义了一个路径变量,经过了这个方法,log-lib这个变量中的值就是Android中log库的路径
target_link_libraries
则是将native-lib这个库和log库连接了起来,这样我们就能在native-lib中使用log库的方法。
创建Native方法文件
在前面的CMake文件中可以看到,我们把文件放在了src/main/cpp/,因此我们创建cpp这个目录,在里面创建C++源文件native-lib.cpp。
然后, 我们便可以开始编写如下的代码:
#include
#include
extern "C"{
JNIEXPORT jstring JNICALL
Java_com_n0texpecterr0r_ndkdemo_MainActivity_getStringFromJNI(
JNIEnv* env,
jobject) {
std::string hello = "IG牛逼";
return env->NewStringUTF(hello.c_str());
}
}
此处我们使用的是C++语言,让我们来看看具体的代码。
首先我们引入了jni需要的jni.h,这个头文件中声明了各个jni需要用到的函数。同时我们引入了C++中的string.h。
然后我们看到extern "C"。为了了解这里为什么使用了extern "C",我们首先需要知道下面的知识:
在C中,编译时的函数签名仅仅是包含了函数的名称,因此不同参数的函数都是同样的签名。这也就是为什么C不支持重载。
而C++为了支持重载,在编译的时候函数的签名除了包含函数的名称,还携带了函数的参数及返回类型等等。
试想此时我们有个C的函数库要给C++调用,会因为签名的不同而找不到对应的函数。因此,我们需要使用extern "C"
来告诉编译器使用编译C的方式来连接。
接下来我们看看JNIEXPORT和JNICALL关键字,这两个关键字是两个宏定义,他主要的作用就是说明该函数为JNI函数。
而jstring则对应了Java中的String类,JNI中有很多类似jstring的类来对应Java中的类,下面是Java中的类与JNI类型的对照表
我们继续看到函数名Java_com_n0texpecterr0r_ndkdemo_MainActivity_getStringFromJNI
。其实函数名中的_相当于Java中的 . 也就是这个函数名代表了java.com.n0texpecterr0r.ndkdemo.MainActivity.java
中的getStringFromJNI
方法,也就是我们之前定义的native方法。
格式大概如下:
Java_包名_类名_需要调用的方法名
其中,Java必须大写,包名里的.
要改成_
,_
要改成_1
接下来我们看到这个函数的两个参数:
- JNIEnv* env:代表了JVM的环境,Native方法可以通过这个指针来调用Java代码
- jobject obj:它就相当于定义了这个JNI方法的类 (MainActivity) 的this引用
然后可以看到后面我们创建了一个string hello,之后通过env->NewStringUTF(hello.c_str())
方法创建了一个jstring类型的变量并返回。
在Java代码中调用native方法
接着,我们便可以在MainActivty中像调用Java方法一样调用这个native方法
TextView tv = findViewById(R.id.sample_text);
tv.setText(getStringFromJNI());
我们尝试运行,可以看到,我们成功用C++构建了一个字符串并返回给Java调用:
CMake
我们在NDK开发中使用CMake的语法来编写简单的代码描述编译的过程,由于这篇文章是讲NDK的,所以关于CMake的语法就不再赘述了。。。如果想要了解CMake语法可以学习这本书《CMake Practice》
JNI与Java代码交互
方法签名
概念
在我们JNI层调用一个方法时,需要传递一个参数——方法签名。
为什么要使用方法签名呢?因为在Java中的方法是可以重载的,两个方法可能名称相同而参数不同。为了区分调用的方法,就引入了方法签名的概念。
签名规则
对于基本类型的参数,每个类型对应了一个不同的字母:
- boolean Z
- byte B
- char C
- short S
- int I
- long J
- float F
- double D
- void V
对于类,则使用 L+类名 的方式,其中(.)用(/)代替,最后加上分号
比如 java.lang.String就是 Ljava/lang/String;
对于数组,则在前面加 [ ,然后加类型的签名,几维数组就加几个。
比如 int[]
对应的就是[I
, boolean[][]
对应的则是[[Z
,而java.lang.String[]
就是[Ljava/lang/String;
打印方法签名
我们可以通过 javap -s
命令来打印方法的签名。
例子
比如下面的方法
public native String getMessage();
public native String getMessage(String id,long i);
对应的方法签名分别为:
()Ljava/lang/String;
(Ljava/long/String;J)Ljava/lang/String;
可以看到,前面括号中表示的是方法的参数列表,后面表示的则是返回值。