第14章 JNI和NDK编程
JNI(Java Native Interface:java本地接口)是为了方便Java与C,C++等本地代码之间进行交互,Java的跨平台特性导致了本地交互的能力不够强大,故而提供JNI用于和本地交互。
通过NDK可以在android中更加方便的通过jni来访问本地代码。比如c/c++,ndk还提供了交叉编译器,简单修改mk文件即可生成特定于CPU的动态库。优势如下:1.提高了代码的安全性;2.很方便使用目前已有的C/C++开源库;3.便于平台间的移植;4.提高程序在某些特定情形下的执行效率,但不能明显提升。JNI和NDK开发所用到的动态库是以.so为后缀的文件。
(1)JNI的开发流程
步骤一:Java中声明Native方法;
public class JniTest {
static {
System.loadLibrary("jni-test");
}
public static void main(String args[]){
JniTest jniTest = new JniTest();
System.out.println(jniTest.get());
jniTest.set("hello world");
}
public native String get();
public native void set(String str);
}
步骤二:编译Java源文件得到class文件,通过javah导出JNI头文件
Javac JniTest.java
//JDK8以下
Javah JniTest.java
//JDK10
Java -h . JniTest.java
此时会产生头文件:
#include <jni.h>
/* Header for class com_nwu_hzk_myapplication_JniTest */
#ifndef _Included_com_nwu_hzk_myapplication_JniTest
#define _Included_com_nwu_hzk_myapplication_JniTest
#ifdef __cplusplus
//内部采用C语言的命名风格来编译
extern "C" {
#endif
JNIEXPORT jstring JNICALL Java_com_nwu_hzk_myapplication_JniTest_get
(JNIEnv *, jobject);
//函数名的格式为Java_包名_类名_方法名,比如jniTest中的set方法,
/*这里只需要知道Java的String对应的JNI的jstring,
JNIEXPORT,JNICALL,JNIEnv和jobject都是JNI标准定义的类型或者宏,它们的含义:
1.JNIEnv*:表示一个指向JNI环境的指针,可以通过他来访问JNI提供的接口方法
2.jobjct:表示Java对象的this
3.JNIEXPORT,JNICALL:他们是JNI所定义的宏,可以在jni.h这个头文件中查看
*/
JNIEXPORT void JNICALL Java_com_nwu_hzk_myapplication_JniTest_set
(JNIEnv *, jobject, jstring);
#ifdef __cplusplus
}
#endif
#endif
步骤三:实现JNI方法:
#include "com_nwu_hzk_myapplication_JniTest.h"
#include "stdio.h"
JNIEXPORT jstring JNICALL Java_com_nwu_hzk_myapplication_JniTest_get
(JNIEnv *env, jobject thiz){
printf("invoke get from C\n");
return (*env)->NewStringUTF(env,"Hello from jni!");
}
JNIEXPORT void JNICALL Java_com_nwu_hzk_myapplication_JniTest_set
(JNIEnv *env, jobject thiz, jstring string){
printf("invoke set from C\n");
char *str = (char*)(*env)->GetStringUTFChars(env,string,NULL);
printf("%s\n",str);
(*env)->ReleaseStringUTFChars(env,string,str);
}
步骤四:编译So库并在Java中调用,So库编译需要gcc,其指令如下所示:gcc -shared -I /usr/lib/jvm/java-7-openjdk-amd64/include -fPIC test.c -o libjni-test.so
Java程序中调用so库,通过Java指令来执行Java程序,指明so库的路径:Java-Djava.library.path = JniTest。
(2)NDK的开发流程
NDK开发是基于JNI的,包括以下步骤:
步骤一:下载并配置NDK,注意下载r20可能会报确保错误,尽量选择低版本的譬如:NDK Version 16,NDK Version 17等。配置进环境变量。
步骤二:声明所需要的native方法,声明调用的so文件。
public class MainActivity extends AppCompatActivity {
@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());
}
/**
* A native method that is implemented by the 'native-lib' native library,
* which is packaged with this application.
*/
public native String stringFromJNI();
// Used to load the 'native-lib' library on application startup.
static {
System.loadLibrary("native-lib");
}
}
步骤三:编写CPP文件,实现项目中声明的Native类型的stringFronJNI方法。
#include <jni.h>
#include <string>
extern "C"
JNIEXPORT jstring JNICALL
Java_com_example_hzk_myapplication_MainActivity_stringFromJNI(
JNIEnv* env,
jobject /* this */) {
std::string hello = "Hello from C++";
return env->NewStringUTF(hello.c_str());
}
步骤四:编译CMakeLists文件,里面声明如何编译。
cmake_minimum_required(VERSION 3.4.1)
add_library( # Sets the name of the library.
native-lib
# Sets the library as a shared library.
SHARED
# Provides a relative path to your source file(s).
src/main/cpp/native-lib.cpp )
find_library( # Sets the name of the path variable.
log-lib
# Specifies the name of the NDK library that
# you want CMake to locate.
log )
target_link_libraries( # Specifies the target library.
native-lib
# Links the target library to the log library
# included in the NDK.
${log-lib} )
步骤五:build.gradle中包含CMakeList.txt这一文件,确保使用cmake对其进行编译。
apply plugin: 'com.android.application'
android {
compileSdkVersion 25
buildToolsVersion "29.0.0"
....
externalNativeBuild {
cmake {
path "CMakeLists.txt"
}
}
}
dependencies {
.....
}
以上步骤都是由ndk自动生成的,自己按照自己的需要可以进行更改。
(3)JNI的数据类型和类型签名
JNI的数据类型主要有两种:基本类型和引用类型。基本类型为jboolean、jchar、jint等,它们和java中的类型一一对应。JNI中的引用类型包括类、对象和数组。
JNI的签名(或者称为Dalviik字节码)表示了一个特定的Java类型,可以类和方法,也可以是数据类型。类的签名比较简单,它使用了L+类名+包名+;的方式,将其中的.替换为/即可。譬如java.lang.String,应当改为Ljava/lang/String;
基本数据类型的签名采用一些列大写字母来表示。
举例来说:多维数组Int[][],可以写成[[I;boolean fun1(int a,String b,int []c);它的签名是(ILjava/lang/String;[I)Z;void fun1(int i);它的签名是(I)V。
(4)JNI调用Java方法的流程
JNI调用Java方法的流程是:先通过类名找到类,在通过方法名找到方法id,最后可以调用这个方法。如果是非静态对象方法,则需要构造出类的对象指后才能调用它,下面例子演示了如何在JNI中调用Java的静态方法。
步骤一:Java中定义静态方法供JNI调用。
public class MainActivity extends AppCompatActivity {
private static String TAG = "MainActivity";
@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 native String stringFromJNI();
// Used to load the 'native-lib' library on application startup.
static {
System.loadLibrary("native-lib");
}
public static void methodCalledByJNI(String msgFromJni) {
Log.d(TAG, "methodCalledByJNI: "+msgFromJni);
}
}
步骤二:在cpp中首先根据类名com/example/hzk/myapplication/MainActivity找到类,再根据类名、方法名、方法签名找到方法id,其中(Ljava/lang/String;)V为方法签名,最后通过env->CallStaticVoidMethod完成最终的调用过程。
#include <jni.h>
#include <string>
extern "C"
JNIEXPORT jstring JNICALL
Java_com_example_hzk_myapplication_MainActivity_stringFromJNI(
JNIEnv* env,
jobject /* this */) {
std::string hello = "Hello from C++";
jclass clazz = env->FindClass("com/example/hzk/myapplication/MainActivity");
if(clazz == NULL){
printf("find the mainActivity error!");
return NULL;
}
jmethodID id = env->GetStaticMethodID(clazz,"methodCalledByJNI","(Ljava/lang/String;)V");
if(id == NULL){
printf("find the methodCalledByJni error!");
}
jstring msg = env->NewStringUTF("msg from native stringFromJni.cpp");
env->CallStaticVoidMethod(clazz,id,msg);
return env->NewStringUTF(hello.c_str());
}
最终的结果如下: