一看就会的jni,不会你来打我!

环境配置

Android Studio,这个不多说了。

简单说一下NDK的下载和环境变量,方便在Terminal里使用命令(mac版)。

下载

1.可以通过Android Studio内置的Settings-Android SDK-SDK Tools安装NDK,下载目录为

/Users/mac-xxx(Username)/Library/Android/sdk/ndk/(NDK Version Number)

2.也可通过官网下载NDK 下载  |  Android NDK  |  Android Developers,选择一个安装目录,方便后续配置环境变量。

环境变量

1.如果是mac,我们要配置到具体的NDK版本目录,方便使用ndk-build打包so库

打开终端,输入open -e .bash_profile打开配置文件

export ANDROID_NDK=/Users/mac-xxx(Username)/Library/Android/sdk/ndk/(NDK Version Number)
export PATH=$PATH:$ANDROID_NDK

执行source .bash_profile使环境变量生效

2.如果是win,我们右键

"我的电脑"—>"属性"—>"高级系统设置"—>"环境变量"—>"系统变量"—>"新建"

%ANDROID_HOME%是我们安卓SDK的默认安装目录

将我们的NDK目录放进去,保存。

打开终端,输入ndk-build,如果有以下输出,则OK。

Android NDK: Could not find application project directory !    Android NDK: Please define the NDK_PROJECT_PATH variable to point to it.    /Users/mac-xxx/Library/Android/sdk/ndk/26.1.10909125/build/core/build-local.mk:151: *** Android NDK: Aborting  . Stop.

JNI编写

1.java端本地代码接口,这里以初始化方法,int,String类型参数为例,列举了三个native接口方法

package com.monke.simplejnidemo;

public class SimpleJniUtils {
    //初始化操作
public static native void init();

    //翻倍一个数字,并且返回
public static native  int doubleData(int data);

    //输入一个字符串,并且返回一个字符串
public static native String testStr(String str);

}

2.生成.h头文件

使用Terminal 将目录先定位到java目录,执行

javah -jni com.monke.simplejnidemo.SimpleJniUtils

确保jdk环境变量已经配置OK。然后在java目录下会生成以包名路径开始的C语言头文件com_monke_simplejnidemo_SimpleJniUtils.h

/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class com_monke_simplejnidemo_SimpleJniUtils */

#ifndef _Included_com_monke_simplejnidemo_SimpleJniUtils
#define _Included_com_monke_simplejnidemo_SimpleJniUtils
#ifdef __cplusplus
extern "C" {
#endif
/*
 * Class:     com_monke_simplejnidemo_SimpleJniUtils
 * Method:    init
 * Signature: ()V
 */
JNIEXPORT void JNICALL Java_com_monke_simplejnidemo_SimpleJniUtils_init(JNIEnv *, jclass);

/*
 * Class:     com_monke_simplejnidemo_SimpleJniUtils
 * Method:    doubleData
 * Signature: (I)I
 */
JNIEXPORT jint JNICALL Java_com_monke_simplejnidemo_SimpleJniUtils_doubleData(JNIEnv *, jclass, jint);

/*
 * Class:     com_monke_simplejnidemo_SimpleJniUtils
 * Method:    testStr
 * Signature: (Ljava/lang/String;)Ljava/lang/String;
 */
JNIEXPORT jstring JNICALL Java_com_monke_simplejnidemo_SimpleJniUtils_testStr(JNIEnv *, jclass, jstring);

#ifdef __cplusplus
}
#endif
#endif

3.在main目录下,新建jni目录,将第二步生成的.h文件复制进来,并且创建simplejniutils.c文件,编码如下 

#include <com_monke_simplejnidemo_SimpleJniUtils.h> //引入.h头文件

#include <android/log.h>


JNIEXPORT void JNICALL Java_com_monke_simplejnidemo_SimpleJniUtils_init  //从.h文件里复制方法名过来,补起参数和方法体
(JNIEnv *env, jclass j){
    //方便定位函数调用,这里引用Android的log作为输出
     __android_log_print(ANDROID_LOG_INFO, "MainActivity", "Java_com_monke_simplejnidemo_SimpleJniUtils_init");
}


JNIEXPORT jint Java_com_monke_simplejnidemo_SimpleJniUtils_doubleData(JNIEnv *env, jclass j, jint data){
    __android_log_print(ANDROID_LOG_INFO,"MainActivity","Java_com_monke_simplejnidemo_SimpleJniUtils_doubleData value is %d",data);
    return data*2;
}


JNIEXPORT jstring Java_com_monke_simplejnidemo_SimpleJniUtils_testStr(JNIEnv *env, jclass j, jstring jstr){
    //方便打印结果,转char类型const char *str = (*env)->GetStringUTFChars(env, jstr, 0);
    __android_log_print(ANDROID_LOG_INFO,"MainActivity","Java_com_monke_simplejnidemo_SimpleJniUtils_testStr value is %s",str);
    return jstr;
}

4.新建Android.mk文件,主要作用是编译的c文件和生成的so库配置

LOCAL_PATH := $(call my-dir)

include $(CLEAR_VARS)

# 打印安卓log日志库
LOCAL_LDLIBS := -llog
# 生成的so库名
LOCAL_MODULE := SimpleJni
# 要编译的源文件,如果有多个,以空格隔开
LOCAL_SRC_FILES =: simplejniutils.c
include $(BUILD_SHARED_LIBRARY)

5.新建Application.mk文件,主要作用是配置生成so库的cpu目录

APP_ABI := all

# or 具体某些cpu架构

# 常用的安卓cpu架构目录
# armeabiv-v7a: 第7代及以上的 32位ARM 处理器
# arm64-v8a: 第8代、64位ARM处理器
# armeabi: 第5代、第6代的32位ARM处理器,早期的手机在使用,现在基本很少了。
# x86: Intel 32位处理器,在平板、模拟器用得比较多。
# x86_64: Intel 64位处理器,在平板、模拟器用得比较多

# APP_ABI := armeabi-v7a arm64-v8a x86 x86_64

6.进入项目main目录,执行ndk-build,生成so库

自测用例

在上一步,我们已经可以成功生成so库了,下面在java项目中,如何使用呢?

第一步:回到SimpleJniUtils.java类中,补充so库的调用

public class SimpleJniUtils {
    ……
    static {
        System.loadLibrary("SimpleJni");
    }
    ……
}

第二步:在app/build.gradle下,加入so库路径,否则运行会找不到so库

android {
……
sourceSets{
    main{
            jni.srcDirs=[] //不使用gradle编译本地c/c++代码
            jniLibs.srcDirs = ['libs','src/main/libs']//加载so库 lib是第三方so src/main/libs是准备生成的so库位置
    }
}
……
}

java.lang.UnsatisfiedLinkError: dalvik.system.PathClassLoader…… couldn't find "libSimpleJni.so"

第三步:调用native方法,验证结果

可以看到,jni的执行和java层的结果打印都是ok的。

第三方so库融合与调用

基于以上步骤,我们已经生成了一个so库,并且调用验证是没问题的。那么如果是一个第三方的so库,我们应该如何调用呢?或者基于c上述流程是闭环的,如果是c++的库,又该如何处理?从这个思考出发,下面进行so库的融合。

新建一个Android项目,在main目录下,新建jni目录,再新建simplejinlib目录,把上面我们生成的so库以及so库的.h头文件放进来,目录结构如下:

在simplejinlib目录下,新建Android.mk文件,内容如下:

LOCAL_PATH:= $(call my-dir)
include $(CLEAR_VARS)
# so库的名字
LOCAL_MODULE := SimpleJni
# so库的路径
LOCAL_SRC_FILES := $(TARGET_ARCH_ABI)/libSimpleJni.so
LOCAL_EXPORT_C_INCLUDES := $(LOCAL_PATH)
include $(PREBUILT_SHARED_LIBRARY)

在app的build.gradle目录下,加入以下代码

android {
    
     # ……
    sourceSets{
        main{
            jni.srcDirs=[]     //不使用gradle编译本地c/c++代码
            jniLibs.srcDirs = ['libs','src/main/libs']    //加载so库 lib是第三方so   src/main/libs 是准备生成的so库位置
        }
    }
    # ……
}

基于以上,我们已经把第三方so库导入成功了,接下来需要完善java层native接口,调用so库内容即可。

第一步:编写java(这里以调用so库的翻倍函数为例)

package com.monke.sotest;

public class Utils {
    public static native  int useSoDoubleData(int data)
}

第二步:导出jni所需的.h头文件

使用Terminal 将目录定位到java目录,执行

javah -jni com.monke.sotest.Utils

 将生成的com_monke_sotest_utils.h文件,放入jni目录下,并#include第三方so库的.h头文件,内容如下: 

/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
#include "simplejinlib/com_monke_simplejnidemo_SimpleJniUtils.h"
/* Header for class com_monke_sotest_Utils */

#ifndef _Included_com_monke_sotest_Utils
#define _Included_com_monke_sotest_Utils
#ifdef __cplusplus
extern "C" {
#endif
/*
 * Class:     com_monke_sotest_Utils
 * Method:    useSoDoubleData
 * Signature: (I)I
 */
JNIEXPORT jint JNICALL Java_com_monke_sotest_Utils_useSoDoubleData(JNIEnv *, jclass, jint);

#ifdef __cplusplus
}
#endif
#endif

新建externdemojin.cpp(这是一个c++文件),在这里调用第三方so库的函数,内容如下:

#include <com_monke_sotest_Utils.h>

JNIEXPORT jint Java_com_monke_sotest_Utils_useSoDoubleData(JNIEnv *env, jclass j, jint data){
    //调用第三方so库的翻倍函数,注意类名路径和.h保持一致
return Java_com_monke_simplejnidemo_SimpleJniUtils_doubleData(env,j,data);
}

新建Android.mk,声明ndk要编译的c/c++文件,引用的第三方so库名称,导出的so库名称,日志等

LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
# LOCAL_SHARED_LIBRARIES 引用的第三方so库的名字 如果有多个用 \ 分割
LOCAL_SHARED_LIBRARIES :=SimpleJni
LOCAL_MODULE    := ExternJni
# LOCAL_SRC_FILES .c源文件
LOCAL_SRC_FILES := externdemojin.cpp

LOCAL_LDLIBS += -llog
include $(BUILD_SHARED_LIBRARY)
include $(LOCAL_PATH)/simplejinlib/Android.mk

新建Application.mk文件,作用是so库的架构支持

#APP_ABI := all
APP_ABI := armeabi-v7a arm64-v8a x86 x86_64

这样,我们融合so库的准备工作就做好了,再看一下现在的项目目录

 接着,定位main目录,执行ndk-build,结果如下:

 在这里,也生成了两个so库,一个是原第三方的libSimpleJni.so,一个是新生成的libExternJni.so

最后,我们在java层,静态导入so库,测试验证,发现,融合三方so库,c++调用c层也是没问题的。

package com.monke.sotest;

public class Utils {

    static {
        System.loadLibrary("ExternJni");
    }
    public static native  int useSoDoubleData(int data);
}

总结

基于以上步骤,我们实现了java到jni-到c层的调用,再扩展到java到jni到c++再到c的so库的调用。其中,在jni层,我们引用了安卓的log库,输出日志,方便定位问题。如果是c++层,在函数传参时,需要类型转换,也很简单。

jni中的jstring转char

const char* str = env->GetStringUTFChars(jstr, NULL);

其中,jstr为需要转换的jstring类型变量,env为JNIEnv指针。需要注意的是,GetStringUTFChars()函数返回的char*类型指针需要在使用完后调用ReleaseStringUTFChars()函数释放,以避免内存泄漏。具体使用方法如下:

env->ReleaseStringUTFChars(jstr, str);

常见问题

1.Android Studio NDK配置目录为灰色, 且无法选择目录

在local.properties文件里配置,重启IDE即可。

ndk.dir=/Users/mac-xxx(Username)/Library/Android/sdk/ndk/ndk/(NDK Version Number)

2.执行ndk-build报

zsh: command not found: ndk-build

在终端,执行open -e .zshrc 配置ndk环境变量,再运行source ~/.zshrc 使其生效

3.error: expected identifier or '('extern "C" ……之类的错误

因为 extern “C” 是声明给C++用的,如果在安卓里使用C文件,不需要这个声明,去掉即可

4.jni/simplejniutils.c:5:11: error: redefinition of 'jint' as different kind of symbol

JNIEXPORT jint JNICAll Java_com_monke_simplejnidemo_SimpleJniUtils_doubleData

这个错误去掉 JNICAll

工具类

一、在jni中c++层进行log的打印

1、在需要使用log的cpp文件中加入

#include <android/log.h>


2、在需要打印的地方直接调用

%d是数字,%s是string或者char

__android_log_print(ANDROID_LOG_INFO,"test","value is %d\n",a);

二、jni中的jstring转char

在JNI开发中,可以使用GetStringUTFChars()函数将jstring类型转换为char*类型。具体使用方法如下:

const char* str = env->GetStringUTFChars(jstr, NULL);

其中,jstr为需要转换的jstring类型变量,env为JNIEnv指针。需要注意的是,GetStringUTFChars()函数返回的char*类型指针需要在使用完后调用ReleaseStringUTFChars()函数释放,以避免内存泄漏。具体使用方法如下:

env->ReleaseStringUTFChars(jstr, str);

参考

JNI开发(一) 简单的C代码打包成SO库以及项目如何调用SO库_c打包成so-CSDN博客

JNI开发(二) 在JNI开发中调用第三方so库_jni调用第三方so库-CSDN博客 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
JNIJava Native Interface)是Java平台的一个组成部分,它允许Java代码与其他语言写的代码进行交互。当Java应用程序需要调用本地代码(如C或C++)时,就使用JNI。 在JNI中,操作`BufferedImage`对象主要涉及到以下几个步骤: 1. **创建本地方法声明**:首先,你需要声明一个本地方法,该方法接受一个`BufferedImage`对象作为参数。这个方法通常用于加载图像或进行图像处理。 ```java public native void processImage(BufferedImage image); ``` 2. **生成JNI头文件**:使用`javah`工具从你的Java类生成JNI头文件。这个工具生成一个包含本地方法声明的头文件。 ```bash javah -jni com.yourpackage.YourClass ``` 3. **实现本地方法**:使用C或C++实现这个本地方法。在这个方法中,你可以访问`BufferedImage`对象并进行处理。需要注意的是,在JNI中,所有的数据都是通过指针传递的,所以你需要正确地管理这些指针。 ```c JNIEXPORT void JNICALL Java_com_yourpackage_YourClass_processImage(JNIEnv *env, jobject obj, jobject image) { // 获取BufferedImage对象的指针 jobject realImage = (*env)->GetObjectField(env, image, (jfieldID) 0); BufferedImage *bufferedImage = (BufferedImage *) (*env)->GetIntField(env, realImage, (jfieldID) 0); // 在这里你可以对bufferedImage进行操作... } ``` 4. **编译并链接你的本地代码**:使用你的构建工具(如gcc或g++)编译你的本地代码,并链接到你的Java应用程序。 5. **在Java代码中调用本地方法**:最后,你可以在Java代码中调用这个本地方法,并传递一个`BufferedImage`对象作为参数。 请注意,JNI操作通常需要一些底层知识,包括内存管理、指针操作等。同时,你也需要注意Java的安全模型,以确保你的代码不会对系统造成危害。如果你不熟悉这些概念,我建议你寻求一些更详细的教程或资源。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

sunbofiy23

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值