Android JNI及NDK入门教程
为什么要使用JNI
- 一、native语言性能更优。
- 二、在Java诞生前很多库都是native语言写的,没必要再用Java实现一遍。
- 三、安全性更好:不容易被反编译。
JNI是什么
- 全称Java Native Interface。字面上理解就是Java和本地语言的接口。定义了Java和native语言之间互相通信的一套规范。(这是Java定义的,和安卓无关。)
- NDK是将native语言编译成特定平台的可执行文件的一套开发组件。(将C/C++编译成so库,由Android定义的,和Java无关。)
所以jni和ndk是两个东西,ndk生成可执行so文件,jni通过加载so供java调用。如果有so文件,可以直接使用。
3、Native项目的创建
3.1、创建Native项目
cmake脚本文件定义编译和链接方式。
3.2、Native项目结构
之前的AS版本需要在local.properties文件指定ndk路径:ndk.dir=xxx。新版AS会自动合适的ndk版本,如果本地没有会推荐一个版本让你下载。
3.3、在Java中声明本地方法。
- 一般我们有一个专门的访问native方法的Java类。比如本项目的NativeBridge.java。
public class NativeBridge {
static {
System.loadLibrary("native-lib");
}
public static native String dataFromNative(int param);
public static native String nativeMethod1(int param);
public static native String nativeMethod2(int param);
}
声明后需要在jni层进行注册和实现。注册native方法分为静态注册和动态注册。
4、Native方法的注册和实现。
4.1、Native方法的静态注册和动态注册的对比。
- 静态注册写起来简单,可以AS快捷键一键生成。但运行效率低(第一次调用native方法时需要搜索一遍jni层的native方法,建立对应关系。)
- 动态注册运行效率高(因为方法的映射关系我们已建立好)、写起来相对麻烦一点点。
4.2、静态注册
extern "C" JNIEXPORT jstring JNICALL
Java_com_hongenit_jnindkdemo_NativeBridge_dataFromNative(JNIEnv *env, jclass clazz, jint param) {
std::string hello = "静态方法返回的Native字符串";
return env->NewStringUTF(hello.c_str());
}
4.3、动态注册
-
准备好要注册的方法的JNINativeMethod结构体数组。 // JNI本地方法的数组。
static JNINativeMethod gMethods[] = {
{"nativeMethod1", "(I)Ljava/lang/String;", (void*)nativeMethod1},
{"nativeMethod2", "(I)Ljava/lang/String;", (void*)nativeMethod1},
};
JNINativeMethod结构体的
java数据类型 | native 类型 | 域描述符 |
---|
基本数据类型 | | | boolean | jboolean | Z | byte | jbyte | B | char | jchar | C | short | jshort | S | int | jint | I | long | jlong | J | float | jfloat | F | double | jdouble | D | void | void | V | 对象引用类型 | | 以”L”开头,以”;”结尾,用”/” 隔开的包及类名,如果内部类则使用$连接内部类; | Surface | jobject | Landroid/view/Surface; | String | jstring | Ljava/lang/String; | 数组数据类型 | | 对应的基本数据类型前面加个中括号[ | int[] | jintArray | [I | float[] | jfloatArray | [f | Surface[] | jobjectArray | [Landroid/view/Surface; | | | |
-
覆写JNI_OnLoad方法。 extern "C" jint JNI_OnLoad(JavaVM* vm, void* reserved)
{
JNIEnv* env = NULL;
jint result = -1;
if (vm->GetEnv((void**) &env, JNI_VERSION_1_6) != JNI_OK) {
return result;
}
// 动态注册native方法。
register_gmethods(vm, env);
return JNI_VERSION_1_6;
}
-
在JNI_OnLoad中调用RegisterNatives方法注册native方法。 void register_gmethods(JavaVM *pVm, JNIEnv *pEnv) {
static const char* const jclassName = "com/hongenit/jnindkdemo/NativeBridge";
jclass jclazz = pEnv->FindClass(jclassName);
pEnv->RegisterNatives(jclazz,gMethods,int(sizeof(gMethods)/sizeof(gMethods[0])));
}
生成的so在build目录下。
4.4、Native方法的实现。
5、cmake编译脚本
# Sets the minimum version of CMake required to build the native library.
cmake_minimum_required(VERSION 3.10.2)
project("jnindkdemo")
# 命名并创建一个动态库(与静态库的区别是不被编译进目标内部),并指定其源码文件。可以定义多个库,CMake会为你编译好。gradle会自动将so库打包到apk中。
add_library(native-lib SHARED native-lib.cpp)
# 从NDK的系统库中搜索某个库并取个别名,通过这个别名可以定位到这个系统库的路径。
find_library(log-lib log)
# 指定哪些库需要Cmake链接到你的目标库。可以链接多个库,比如你在脚本中定义了的库、预编译好的三方库、系统库。
target_link_libraries(
native-lib
play gnustl_shared
${log-lib}
)
5.1、使用三方库
- 假设需要用三方so库,在链接到目标库之前先添加定义的库。
#---------------------------三方so库----------------
#添加头文件路径(相对于本文件路径)
include_directories(include)
#设置so库所在路径的变量
set(SO_PATH ${CMAKE_CURRENT_SOURCE_DIR}/libs/${ANDROID_ABI}) #CMAKE_CURRENT_SOURCE_DIR变量为当前cmake文件所在的目录
add_library(gnustl_shared SHARED IMPORTED)
set_target_properties(gnustl_shared PROPERTIES IMPORTED_LOCATION ${SO_PATH}/libgnustl_shared.so)
add_library(play SHARED IMPORTED)
set_target_properties(play PROPERTIES IMPORTED_LOCATION ${SO_PATH}/libplay.so)
如果三方库A(play)需要依赖另一个三方库B,也必须将B(gnustl_shared)链接到目标库。否则会报错如下:
java.lang.UnsatisfiedLinkError: dlopen failed: library "libgnustl_shared.so" not found: needed by /data/app//lib/arm64/libplay.so in namespace。。。
- 如下表示头文件在include目录下,所以应将so库的头文件放入include目录,然后可以在目标程序中引入头文件并调用方法。
#添加头文件路径(相对于本文件路径)
include_directories(include)
eg. 调用该so库的获取版本号方法
#include <dhplay.h>
...
jstring nativeMethod1(JNIEnv *env, jclass clazz, jint param) {
...
unsigned version = PLAY_GetSdkVersion();
...
}
6、调用Native方法
一般在一个专门访问native方法的类中,在类的静态代码块中用System.loadLibrary加载so库。调用这些声明在Java层Native方法即执行Native对应的实现。
public class NativeBridge {
static {
System.loadLibrary("native-lib");
}
// 静态注册本地方法。
public static native String dataFromNative(int param);
// 动态注册本地方法1 。
public static native String nativeMethod1(int param);
/// 动态注册本地方法2 。
public static native String nativeMethod2(int param);
}
7、Native调用Java方法。
7.1、定义被调用的Java方法。
public class NativeBridge {
...
public void printResult(String result) {
System.out.println(" print result = " + result);
}
}
7.2、Native调用流程。
-
获取Java方法所在类的字节码 static const char *const jclassName = "com/hongenit/jnindkdemo/NativeBridge";
// 获取NativeBridge的字节码对象
jclass jclazz = env->FindClass(jclassName);
还可以通过以下两个方法获取 // 通过对象实例来获取jclass,相当于Java中的getClass()函数
jclass GetObjectClass(jobject obj):
// 通过jclass可以获取其父类的jclass对象
jclass getSuperClass(jclass obj):
-
获取对象 构造方法的methodId固定为“” ,根据签名不同调用不同的构造方法。 jobject obj= env->NewObject(jclazz,env->GetMethodID(jclazz, "<init>", "()V"));
-
调用方法 // 获取要调用的Java方法的methodID
jmethodID methodId = env->GetMethodID(jclazz, "printResult", "(Ljava/lang/String;)V");
// 调用Java方法。
env->CallVoidMethod(obj,methodId,env->NewStringUTF(("call from native " + std::to_string(param)).c_str()));
void nativeMethod2(JNIEnv *env, jclass clazz, jint param) {
static const char *const jclassName = "com/hongenit/jnindkdemo/NativeBridge";
// 获取NativeBridge的字节码对象
jclass jclazz = env->FindClass(jclassName);
// 获取NativeBridge对象
jobject obj= env->NewObject(jclazz,env->GetMethodID(jclazz, "<init>", "()V"));
// 获取要调用的Java方法的methodID
jmethodID methodId = env->GetMethodID(jclazz, "printResult", "(Ljava/lang/String;)V");
// 调用Java方法。
env->CallVoidMethod(obj,methodId,env->NewStringUTF(("call from native " + std::to_string(param)).c_str()));
}
注意事项
1、全局引用对象的内存泄露问题
参考文献
https://www.jianshu.com/p/d8be99605c65
https://blog.csdn.net/carson_ho/article/details/73250163
https://blog.csdn.net/q610098308/article/details/79395232
|