安卓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 文件,而您可将静态库关联到其他库。

配置开发环境

  1. 下载NDK
    File>Settings>Android SDK>SDK Tools>勾选需要的版本号>apply>OK
  2. 配置项目NDK
    在这里插入图片描述
    如果NDK location无法编辑输入,可以在local.properties中新增ndk.dir进行设置:
    sdk.dir=D\:\\win10_program\\develop\\Android\\AndroidSDK
    ndk.dir=D\:\\win10_program\\develop\\Android\\AndroidSDK\\ndk\\26.1.10909125  // 已经过时的用法,可以直接删除此行配置,具体参考下面的操作。
    
    较新的项目,直接在app\build.gradle中直接配置ndk版本号即可。
    android {
        namespace 'com.xxx.xxx'
        compileSdk 33
        ndkVersion "25.2.9519653"
        ...
    }
    
    命令查看ndk版本号:ndk-build --version

JNI实践

这里使用官方例子介绍。

配置CMake

配置CMake的目的是:告诉CMake改如何从源码编译生成目标库。

# 需要生成的目标库native-lib
# 也可以使用add_executable()生成可执行文件
add_library( # 指定要生成的目标库名称为native-lib
             native-lib
             # 将native-lib库设置默认为SHARED(静态库/共享库.so)STATIC(静态库.a)
             SHARED
             # 生成native-lib库所需源码的相对路径列表。包含.cpp和.h
             src/main/cpp/native-lib.cpp
             NativeImpl.cpp)   # NativeImpl.cpp在后面代码分离部分实现
             
# 指定源文件关联的头文件(适用于头文件和源文件分离的情况,但是也可以不写,因为.cpp中已经include了)           
include_directories(src/main/cpp/include/)

# 在已有库中查找需要的库,并将它的路径存储在变量xxx-lib中。类似用法的函数
# find_file()find_path()find_program()find_package()
find_library( # 自定义变量的名称xxx-lib
              xxx-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_librarytarget_link_libraries函数。

JNI编码

  1. 在Java侧声明调用方法。如stringFromJNI
    // 应用启动时,调用此函数会加载原生共享文件sodemo.so
    static {
        System.loadLibrary("sodemo");  //官方推荐使用:ReLinker.loadLibrary
    }
    /**
     * 声明此方法在原生端(共享文件sodemo.so)中实现,它与该应用程序打包在一起。
     */
    public native String stringFromJNI();
  1. 在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 33

    defaultConfig {
        applicationId "com.xxx.sodemo"
        minSdk 31
        targetSdk 33
        versionCode 1
        versionName "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
...

命令编译

编译LOG

1、Android.mk打印log

JNI_PATH := $(JNIINTERFACE_PATH)/my_jni
$(warning "Android.mk Log JNI_PATH == $(JNI_PATH)")

2.cmake中打印log

message(STATUS "OpenCV library status:")

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_librarytarget_link_libraries
参考配置CMake

# 生成libtest-1.so
add_library(
		test-1
		SHARED
        native-lib.cpp)
target_link_libraries(
		test-2 
        android)
...

# 生成libtest-2.so
add_library(
		test-2 
		SHARED
        native-lib.cpp)
target_link_libraries(
		test-2
        android)

JNI调试

在这里插入图片描述
在Debug模式下,有时候会出现这个Permission denied的提示。
解决方法:退出App重新debug运行。如果退出无法解决此问题,重新USB连接即可。

与君共勉人生自当扶摇上,揽星衔月逐日光。你只管去劈浪,与众生争锋芒,你举步是八万里宽广,你眼望是千江拍白浪,你平生这一趟,要让旁人想都不敢想!

  • 28
    点赞
  • 28
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值