JNI基础知识
- JNI简介
- NDK
- 配置开发环境
- JNI实践
- 配置CMake
- JNI编码
- JNI注册
- 1.静态注册
- 2.动态注册
- 编译方式
- CMakeLists编译
- Makefile编译
- 命令编译
- JNI和C/C++代码分离
- Java调用C/C++
- 查看so中包含的方法
- C/C++调用Java
- 打印C/C++的log
- 生成多个共享库so
- JNI调试
本文整理了JNI技术基础知识
JNI简介
JNI 是java原生接口(Java Native Interface),它定义了 Android 从受管理代码(使用 Java 或 Kotlin 编程语言编写)编译的字节码与原生代码(使用 C/C++ 编写)进行交互的方法,也就是安卓通过JNI技术提供Java调用C/C++或者C/C++调用Java的能力。JNI 不依赖于供应商,支持从动态共享库加载代码,虽然有时较为繁琐,但效率较高。
NDK
Android NDK(Native Development Kit),原生开发工具包,它是一组能将C或C++(“原生代码”)嵌入到Android 应用中的工具。可以帮助开发者快速开发C/C+的动态库,自动将so和java应用一起打包成apk。
NDK集成了一些交叉编译器,并提供了相应的mk文件,用于隔离CPU、平台、ABI等差异,开发人员通过配置mk文件(指出“哪些文件需要编译”、“编译特性要求”等),就可以生成自己的so库。
原生共享库:NDK 从 C/C++ 源代码构建这些库或 .so 文件。so是shared object的缩写。
原生静态库:NDK 也可构建静态库或 .a 文件,而您可将静态库关联到其他库。
配置开发环境
- 下载NDK
File>Settings>Android SDK>SDK Tools>勾选需要的版本号>apply>OK - 配置项目NDK
如果NDK location无法编辑输入,可以在local.properties中新增ndk.dir
进行设置:
较新的项目,直接在app\build.gradle中直接配置ndk版本号即可。sdk.dir=D\:\\win10_program\\develop\\Android\\AndroidSDK ndk.dir=D\:\\win10_program\\develop\\Android\\AndroidSDK\\ndk\\26.1.10909125 // 已经过时的用法,可以直接删除此行配置,具体参考下面的操作。
命令查看ndk版本号:android {namespace 'com.xxx.xxx'compileSdk 33ndkVersion "25.2.9519653"... }
ndk-build --version
JNI实践
这里使用官方例子介绍。
配置CMake
配置CMake的目的是:告诉CMake改如何从源码编译生成目标库。
# 需要生成的目标库native-lib
# 也可以使用add_executable()生成可执行文件
add_library( # 指定要生成的目标库名称为native-libnative-lib# 将native-lib库设置默认为SHARED(原生共享库.so)或STATIC(原生静态库.a)SHARED# 生成native-lib库所需源码的相对路径列表。包含.cpp和.hsrc/main/cpp/native-lib.cppNativeImpl.cpp) # NativeImpl.cpp在后面代码分离部分实现# 指定源文件关联的头文件(适用于头文件和源文件分离的情况,但是也可以不写,因为.cpp中已经include了)
include_directories(src/main/cpp/include/)# 在已有库中查找需要的库,并将它的路径存储在变量xxx-lib中。类似用法的函数
# find_file()、find_path()、find_program()、find_package()
find_library( # 自定义变量的名称xxx-libxxx-lib#在ndk开发包中查找需要的libyyy.so,存储到xxx-lib变量中yyy ) # 将依赖的库文件链接到此目标库上
target_link_libraries(# 指定目标库native-lib# 将下面的库列表全部连接到目标库上${xxx-lib} # 获取find_library找到的yyy库android # 获取android库log) # 获取log库
注意:
1、如果对库文件有修改变动,请务必在Gradle之前清理一下项目 Build > Clean Project
。
2、如果需要生成多个共享库,可以在CMakeLists.txt文件中增加多个成对的add_library
和target_link_libraries
函数。
JNI编码
- 在Java侧声明调用方法。如
stringFromJNI
// 应用启动时,调用此函数会加载原生共享文件sodemo.sostatic {System.loadLibrary("sodemo"); //官方推荐使用:ReLinker.loadLibrary}/*** 声明此方法在原生端(共享文件sodemo.so)中实现,它与该应用程序打包在一起。*/public native String stringFromJNI();
- 在C侧实现具体方法
Java_com_wingtech_sodemo_JNIUtils_stringFromJNI
#include <jni.h>
#include <string>extern "C" JNIEXPORT jstring JNICALL
Java_com_wingtech_sodemo_JNIUtils_stringFromJNI(JNIEnv *env, jobject thiz) {std::string hello = "Hello from C++";return env->NewStringUTF(hello.c_str());
}
方法名称:Java_包名_类名_方法名
方法参数:JNIEnv* 是指向虚拟机环境的指针。jobject 是指向从 Java 端传递的隐式 this 对象的指针。
重要:C/C++和Java通过此方法名称建立了一对一映射关系。
JNI注册
1.静态注册
如果只有一个类具有原生方法,建议使用静态注册。使用标准 System.loadLibrary 从共享库加载原生代码。
从静态类初始化程序中调用 System.loadLibrary(或 ReLinker.loadLibrary)。具体静态注册同前面JNI编码部分所述。
2.动态注册
如果有多个类有原生方法,可以使用RegisterNatives
注册,也可以让运行时使用dlsym
动态查找它们。可以从 Application进行调用,这样始终加载该库,而且总是会提前加载。当执行到System.loadLibrary()
函数时,会回调JNI组件中的JNI_OnLoad()
函数;当释放该组件时会回调JNI_OnUnload()
函数。
JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void* reserved) {JNIEnv* env;// 通过调用了GetEnv函数获取JNIEnv结构体指针env(JNI环境变量),JNIEnv结构体是指向一个函数表的,// 该函数表又指向了一些列对应的JNI函数。所以可以通过env和java交互,如GetObjectClass,CallVoidMethod等if (vm->GetEnv(reinterpret_cast<void**>(&env), JNI_VERSION_1_6) != JNI_OK) {return JNI_ERR;}// Find your class. JNI_OnLoad is called from the correct class loader context for this to work.jclass c = env->FindClass("com/example/app/package/MyClass");if (c == nullptr) return JNI_ERR;// 将所有方法装进数组中。这里数组中每个元素是结构体JNINativeMethod。// typedef struct {// const char* name;//Java层native方法的名字// const char* signature;//Java层native方法的描述符// void* fnPtr;//对应JNI函数的指针// } JNINativeMethod;static const JNINativeMethod methods[] = {{"nativeFoo", "()V", reinterpret_cast<void*>(nativeFoo)},{"nativeBar", "(Ljava/lang/String;I)Z", reinterpret_cast<void*>(nativeBar)},};// 使用RegisterNatives注册所有原生方法int rc = env->RegisterNatives(c, methods, sizeof(methods)/sizeof(JNINativeMethod));if (rc != JNI_OK) return rc;return JNI_VERSION_1_6;
}
jni函数的指针
void regist(JNIEnv *env, jobject thiz, jobject jCallback) {
LOGD(“–动态注册调用成功–>”);
jstring pJstring = env->NewStringUTF(“动态注册调用成功”);
jclass pJclass = env->GetObjectClass(thiz);
jmethodID id = env->GetMethodID(pJclass, “beInjectedDebug”, “(Ljava/lang/String;)V”);
//执行函数
env->CallVoidMethod(thiz,id,pJstring);
}
编译方式
一般有两种编译方式:
- 1、CMakeLists编译
- 2、Makefile编译
- 3、命令编译
CMakeLists编译
1、CMakeLists配置
具体配置如前面配置CMake的介绍,这里使用cpp目录下的CMakeLists.txt、native-lib.cpp文件生成.so库。
2、gradle配置
在app\build.gradle中设置库文件适配的CPU架构类型和CMakeLists.txt 文件路径。
android {namespace 'com.xxx.sodemo'compileSdk 33defaultConfig {applicationId "com.xxx.sodemo"minSdk 31targetSdk 33versionCode 1versionName "1.0"testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"ndk {// 生成so库类型abiFilters 'armeabi-v7a', 'arm64-v8a','x86','x86_64'}}externalNativeBuild {cmake {// 设置CMakeLists.txt文件路径path file('src/main/cpp/CMakeLists.txt')version '3.22.1'}}...}
3、编译库文件
点击Make按钮或Build->Make Project,运行结束后,会在 根目录/app/build/intermediates/cmake/debug/obj
路径下生成对应平台的.so库文件。
Makefile编译
使用MK文件编译,不需要编辑CMakeLists.txt,也不需要在build.gradle中配置,只要在Android.mk和Application.mk文件中配置好即可。一般在C/C++同目录下创建mk文件。
1、编写Android.mk文件
#设置当前编译路径为当前文件夹路径
LOCAL_PATH :=$(call my-dir)#清空编译环境的变量(由其他模块设置过的变量)
include $(CLEAR_VARS)
LOCAL_LDLIBS := -lm -llog#指定生成模块的名称(库引用名称),编译时会自动添加lib前缀
LOCAL_MODULE :=JNITest123#需要编译的源文件。如果存在多个.cpp文件时使用"\"隔开
LOCAL_SRC_FILES :=native-lib.cpp \NativeImpl.cpp # NativeImpl.cpp在后面代码分离部分实现#生成动态库
include $(BUILD_SHARED_LIBRARY)
2、编写Application.mk文件
#模块名字,与Android.mk中保持一致
APP_MODULES := JNITest123#支持平台,这里支持所有平台
APP_ABI := all
APP_ALLOW_MISSING_DEPS=true
3、编译库文件
方式一:
1)检查编译环境
打开cmd窗口,运行ndk-build --version
,如下输出,说明ndk配置正确。
2)在cmd中进入C/C++文件所在目录下,执行ndk-build
命令编译。
NDK_PROJECT_PATH=. # 当前项目
APP_PLATFORM=android-16 # 有默认值,可以不设置
APP_BUILD_SCRIPT=./Android.mk # 当前目录下的Android.mk文件。注意:这里根据实际情况修改路径
NDK_APPLICATION_MK=./Application.mk # 当前目录下的Application.mk文件。注意:这里根据实际情况修改路径
NDK_LOG=1 # 打印编译日志
整理成一行命令执行:
ndk-build NDK_PROJECT_PATH=. APP_BUILD_SCRIPT=./Android.mk NDK_APPLICATION_MK=./Application.mk NDK_LOG=1
3)执行结果:
说明:在哪个目录下执行ndk-build
命令编译,就在此目录下生成库文件。
E:\work\Test\Andriod\SoDemo\app\src\main>ndk-build NDK_PROJECT_PATH=. APP_BUILD_SCRIPT=./cpp/Android.mk NDK_APPLICATION_MK=./cpp/Application.mk APP_PLATFORM=android-16
Android NDK: WARNING: APP_PLATFORM android-16 is higher than android:minSdkVersion 1 in ./AndroidManifest.xml. NDK binaries will *not* be compatible with devices older than android-16. See https://android.googlesource.com/platform/ndk/+/master/docs/user/common_problems.md for more information.
[arm64-v8a] Compile++ : JNITest123 <= native-lib.cpp
[arm64-v8a] SharedLibrary : libJNITest123.so
[arm64-v8a] Install : libJNITest123.so => libs/arm64-v8a/libJNITest123.so
[x86_64] Compile++ : JNITest123 <= native-lib.cpp
[x86_64] SharedLibrary : libJNITest123.so
[x86_64] Install : libJNITest123.so => libs/x86_64/libJNITest123.so
[armeabi-v7a] Compile++ thumb: JNITest123 <= native-lib.cpp
[armeabi-v7a] SharedLibrary : libJNITest123.so
[armeabi-v7a] Install : libJNITest123.so => libs/armeabi-v7a/libJNITest123.so
[x86] Compile++ : JNITest123 <= native-lib.cpp
[x86] SharedLibrary : libJNITest123.so
[x86] Install : libJNITest123.so => libs/x86/libJNITest123.so
方式二:在Android Studio中,打开终端Terminal,cd进入C/C++文件所在目录的父目录下,执行ndk-build.cmd
即可。只要代码没有问题,一般可以在同级目录下生成文件。
PS E:\work\Test\Andriod\SoDemo\app\src\main> D:\win10_program\develop\Android\AndroidSDK\ndk\26.1.10909125\ndk-build.cmd
Android NDK: APP_PLATFORM not set. Defaulting to minimum supported version android-21.
[arm64-v8a] SharedLibrary : libJNITest123.so
[arm64-v8a] Install : libJNITest123.so => libs/arm64-v8a/libJNITest123.so
[x86_64] Compile++ : JNITest123 <= native-lib.cpp
[x86_64] SharedLibrary : libJNITest123.so
[x86_64] Install : libJNITest123.so => libs/x86_64/libJNITest123.so
[armeabi-v7a] Compile++ thumb: JNITest123 <= native-lib.cpp
[armeabi-v7a] SharedLibrary : libJNITest123.so
[armeabi-v7a] Install : libJNITest123.so => libs/armeabi-v7a/libJNITest123.so
[x86] Compile++ : JNITest123 <= native-lib.cpp
[x86] SharedLibrary : libJNITest123.so
[x86] Install : libJNITest123.so => libs/x86/libJNITest123.so
常见问题:
E:\work\Test\Andriod\SoDemo\app\src\main> D:\win10_program\develop\Android\AndroidSDK\ndk\21.0.6113669\ndk-build.cmd
Android NDK: APP_PLATFORM not set. Defaulting to minimum supported version android-16.
Android NDK: Your APP_BUILD_SCRIPT points to an unknown file:
D:\win10_program\develop\Android\AndroidSDK\ndk\21.0.6113669/jni/Android.mk // 这里是路径问题jni
D:/win10_program/develop/Android/AndroidSDK/ndk/21.0.6113669/build//../build/core/add-application.mk:88: *** Android NDK: Aborting... . Stop.
因为这里的mk文件实际在src\main\cpp中,而NDK编译环境默认在jni目录下找mk文件,所以报错无法找到。这里可以将文件名称cpp修改为默认路径jni,也可以在ndk-build命令里指定mk的路径,具体修改如下。
ndk-build.cmd APP_BUILD_SCRIPT=./cpp/Android.mk NDK_APPLICATION_MK=./cpp/Application.mk
PS E:\work\Test\Andriod\SoDemo\app\src\main> D:\win10_program\develop\Android\AndroidSDK\ndk\26.1.10909125\ndk-build.cmd APP_BUILD_SCRIPT=./cpp/Android.mk NDK_APPLICATION_MK=./cpp/Application.mk
Android NDK: APP_PLATFORM not set. Defaulting to minimum supported version android-21.
[arm64-v8a] Compile++ : JNITest123 <= native-lib.cpp
[arm64-v8a] SharedLibrary : libJNITest123.so
...
命令编译
JNI和C/C++代码分离
分离设计目的是希望在JNI文件中出现少量的C++代码。
1、编写.cpp文件
这里以NativeImpl.cpp为例
#include <jni.h>
#include "NativeImpl.h"NativeImpl::NativeImpl() {}NativeImpl::~NativeImpl() {}int NativeImpl::Clear_Zero() {LOGD("打印C++ LOGD");LOGE("打印C++ LOGE");LOGI("打印C++ LOGI");LOGW("打印C++ LOGW");return 0;
}
2、编写.h文件
这里以NativeImpl.h为例
#ifndef SODEMO_NATIVEIMPL_H
#define SODEMO_NATIVEIMPL_H
#include "LOG.h"#include <vector>
class NativeImpl {public:NativeImpl();virtual ~NativeImpl();virtual int Clear_Zero();
};#endif //SODEMO_NATIVEIMPL_H
3、在JNI文件中调用C++方法。
#include <jni.h>
#include "NativeImpl.h"NativeImpl nativeImpl;
NativeImpl* getNativeImpl(){return &nativeImpl;
}extern "C"
JNIEXPORT jint JNICALL
Java_com_mytest_sodemo_JNIUtils_clrNumber(JNIEnv *env, jclass clazz) {// TODO: implement clrNumber()int zero = getNativeImpl()->Clear_Zero();return zero;
}
注意:代码分离后,需要将纯C++的源码添加到编译环境中,也就是在CMakeLists.txt的add_library方法中添加NativeImpl.cpp,或者在Android.mk的LOCAL_SRC_FILES中添加NativeImpl.cpp。具体参考上面的编译方式。
Java调用C/C++
Java调用C或C++程序,前提是给定了C或C++的动态库dll(Windows)或so(Linux)文件和函数头文件说明,这里介绍如何正确调用第三方so。
Java层调用C++函数主要通过建立的映射关系,这里jni函数调用java层的函数就要通过JNIEnv。
1、将第三方提供的so文件全部放进app\libs目录下,然后在app\build.gradle的sourceSets中配置libs,这样就会在打包时,自动把libs下的文件副本迁移到apk的lib目录下。
当然,这里的路径可以自定义,只要Gradle在打包时能通过你配置的路径,找到so的存放位置即可。
android {defaultConfig {...}sourceSets {main {jniLibs.srcDirs = ['libs'] // 打包时会把app\libs下的共享库.so的副本迁移到apk的lib目录下。// jniLibs.srcDirs = ['libs/test'] // 也可以在app\libs下新建各个公司或模块提供的库目录。// jniLibs.srcDirs = ['src/mylibs'] // 也可以自定义路径so的存储路径,只要能找到就行。}}...
}
2、根据已知的Java_xx包名_yy类名_方法名
格式(也可以通过nm命令获取so库的方法),在自己的项目中app\src\main\java目录下新建xx包名
,然后再创建一个和so中的yy类名
相同的类,这里要确保包名、类名、方法名、库名(不带lib前缀)四者一致,最后在自己的项目中直接调用yy类
中的方法即可。
extern "C" JNIEXPORT jstring JNICALL
Java_com_mytest_sodemo_JNIUtils_stringFromJNI(JNIEnv *env, jobject) {std::string hello = "Hello from C++";return env->NewStringUTF(hello.c_str());
}
注意:这一步非常重要,通过如此设计的方法名,建立了java和C之间的映射关系,所以在其他应用中使用时,也需要建立这种映射关系,否则报错UnsatisfiedLinkError。
//todo 缺图
3、清理和检查
清理后重新打包:Build>Clean Project>等一会儿>Make Project或者Build APK(s)>等一会儿。
检查so是否打入包内:Build>Analyze APK>OK>打开目标APK的lib目录。
4、常见异常
如果检查没有发现错误,编译运行后还是出现UnsatisfiedLinkError异常,多半是因为apk中的so没有打入包内,请按照第3步处理.
查看so中包含的方法
需要使用nm工具,一般在sdk\ndk\xx版本\toolchains\x86-4.9\prebuilt\windows-x86_64\i686-linux-android\bin
nm -D "so文件路径"
C/C++调用Java
打印C/C++的log
在CPP目录下新建head文件LOG.h
#ifndef SODEMO_LOG_H
#define SODEMO_LOG_H#include <android/log.h>#define TAG "haitao"
#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, TAG, __VA_ARGS__);
#define LOGW(...) __android_log_print(ANDROID_LOG_WARN, TAG, __VA_ARGS__);
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, TAG, __VA_ARGS__);
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, TAG, __VA_ARGS__);#endif //SODEMO_LOG_H
在需要使用的文件中#include "LOG.h"
即可。
生成多个共享库so
如果需要生成多个共享库,可以在CMakeLists.txt再增加add_library
和target_link_libraries
参考配置CMake
# 生成libtest-1.so
add_library(test-1SHAREDnative-lib.cpp)
target_link_libraries(test-2 android)
...# 生成libtest-2.so
add_library(test-2 SHAREDnative-lib.cpp)
target_link_libraries(test-2android)
JNI调试
在Debug模式下,有时候会出现这个Permission denied的提示。
解决方法:退出App重新debug运行。如果退出无法解决此问题,重新USB连接即可。
与君共勉:人生自当扶摇上,揽星衔月逐日光。你只管去劈浪,与众生争锋芒,你举步是八万里宽广,你眼望是千江拍白浪,你平生这一趟,要让旁人想都不敢想!