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");
}
// 本地方法1 静态注册。
public static native String dataFromNative(int param);
// 本地方法2 动态注册。
public static native String nativeMethod1(int param);
/// 本地方法3 动态注册。
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 <play.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 {
...
// 供Native调用的方法。
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.youkuaiyun.com/carson_ho/article/details/73250163
https://blog.youkuaiyun.com/q610098308/article/details/79395232