环境配置
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博客