JNI开发流程
- 编写Java接口(native方法)
- (选中需要生成C代码的Java文件)生成.c接口文件(使用javah命令生成)
- 编写Android.mk文件
- 编写Application.mk文件
- 实现.c接口文件
- ndk-build编译
- so文件在src/main/libs目录下
- Java中使用System.loadLibrary(“Android.mk中LOCAL_MODULE的值”)加载动态链接库
Android Studio 配置External Tools实现快速生成.so文件
- External Tools 1: 根据Java文件生成.c接口文件
Name: javah
Description: JDK-javah tool
Program: javah
Arguments: -v -jni -d $ModuleFileDir$/src/main/jni $FileClass$
Working directory: $SourcepathEntry$
- External Tools 2: ndk-build生成动态链接库.so
Name: ndk build
Description: ndk build
Program: D:\android-ndk\android-ndk-r20b\ndk-build.cmd (ndk目录下的ndk-build.cmd文件地址)
Arguments:
Working directory: $ModuleFileDir$\src\main\
JNI接口与接口实现示例解释
接口:
#include <jni.h>
JNIEXPORT jstring JNICALL Java_com_example_myapplication_JNITest_getJniTestString
(JNIEnv *, jclass);
接口实现:
#include "jni.h"
#include "com_example_myapplication_JNITest.h"
JNIEXPORT jstring JNICALL Java_com_example_myapplication_JNITest_getJniTestString
(JNIEnv * env, jclass object) {
return (*env)->NewStringUTF(env,"测试 jni");
}
- 接口命名规则: JNIEXPORT 返回值类型 JNICALL Java_包名_类名_方法名(JNIEnv *, jclass, 参数)
- JNIEXPORT : 固定格式
- jstring : 返回值为String类型
- JNICALL : 固定格式
- JNIEnv * :
- 针对C : JNIEnv 为struct JNINativeInterface的一级指针, JNIEnv *为struct JNINativeInterface的二级指针, 通过该指针能调用大量操作java类型与c类型互转的函数(细节见头文件: jni.h), 调用时用 (*env)->方法
- 针对C++ : JNIEnv为结构体struct JNINativeInterface的别名, JNIEnv *为struct JNINativeInterface的一级指针, 故在调用时, 直接使用 env->方法; (注意: C++函数应该需要先声明再使用,因此需要先把javap生成的接口文件include一下)
- jclass/jobject:
- 若Java中的方法为静态方法则为jclass,表示字节码对象
- 若Java中的方法为非静态方法则为jobject,表示对象实例
- JNINativeInterface : 该结构体位于jni.h头文件中, 该结构体中定义了大量函数指针, 这些函数在JNI开发中常用
- 调用方式: (*env) -> 函数
- 详细变量细节定义可见: jni.h
- 注意点:
- 如果Java方法名中存在下划线(_), 那么JNI的C接口函数命名就应该用(_1)来代替 _ (使用javah命令可以避免这些问题, 让程序自动生成接口文件)
Android.mk文件介绍
- 作用: 告诉ndk-build资源文件所在的位置
#找到jni目录
LOCAL_PATH := $(call my-dir)
#清除上一次编译的环境信息
include $(CLEAR_VARS)
#编译生成的文件的类库叫什么名字(JNITest生成出来的类库名为:libJNITest.so)
LOCAL_MODULE := JNITest
#要编译的c源文件, 多个以空格隔开
LOCAL_SRC_FILES := JNITest.c
#指定要加载的链接库(如下为加载logcat支持库)
#LOCAL_LDLIBS += -llog
#指定要生成的为动态链接库(.so文件)
include $(BUILD_SHARED_LIBRARY)
Application.mk文件介绍
- 作用: 指定编译哪些架构的动态链接库(如: arm, x86等)
#编译指定CPU架构的动态链接库(全部用: "ALL", 单个: "armeabi", "armeabi-v7a", "x86", "x86_64"等)
#arm架构为Linux平台, x86为Intel平台
#多个平台可以用空格隔开
APP_ABI := ALL
#指定编译最小SDK版本
APP_PLATFORM := android-23
实用技巧
- Android Studio NDK开发, 设置代码提示补全功能
- file -> Project Structure -> SDK Location -> Android NDK location 设置本地NDK路径
- 鼠标右击项目
- 选中Link C++ Project with Gradle
- Build System 选中 ndk-build
- Project Path 填入 Android.mk的文件路径
- 点击Apply
- Android Studio 中C代码字体颜色不好看,可以进入 file -> setting -> Editor -> Color Scheme -> C/C++ 进行设置
- JNI中, 在C代码中打日志方法(使用logcat)
- 包含头文件: #include <android/log.h>
- 配置Android.mk: 在Android.mk中添加: LOCAL_LDLIBS := -llog
- 定义LOGI,LOGE等宏
#include <android/log.h> #define LOG_TAG "System.out" #define LOGI(...) __android_log_print(ANDROID_LOG_INFO,LOG_TAG,__VA_ARGS__) #define LOGE(...) __android_log_print(ANDROID_LOG_ERROR,LOG_TAG,__VA_ARGS__)
- 使用方法跟printf类似: LOGI("%d", i)
- Android Studio 3.x以上查看logcat方法
- 进入AndroidSDK/tools目录
- 运行monitor.bat文件 (查不到文件可以使用Listary或everthing搜索)
NDK的DEBUG调试方法
- 预配置说明:
- build.gradle中的android节点下添加配置:
android { ... externalNativeBuild { ndkBuild { path file('src/main/jni/Android.mk') } } }
- src\main目录下不能有jniLibs目录
- build.gradle中的android节点下添加配置:
- 方法1: 直接点击DEBUG按钮(但有时C代码有一些隐式声明, 导致直接DEBUG时编译不通过, 但是使用ndk-build进行手动编译却能通过, 那么此时就采用方法2)
- 方法2: 使用ndk-build后, 将src\main\obj\local下的文件拷贝到build\intermediates\ndkBuild\debug\obj\local目录下, 拷贝成功后, 将后者下面的每个架构文件夹下的objs文件夹改为objs-debug即可;
- 文件拷问bat脚本
@echo off set SOURCES_PATH=项目路径\src\main\obj\local set TARGET_PATH=项目路径\build\intermediates\ndkBuild\debug\obj\local echo 正在删除%TARGET_PATH% rmdir /s /q %TARGET_PATH%\ echo 正在从%SOURCES_PATH%拷贝到%TARGET_PATH% xcopy %SOURCES_PATH% %TARGET_PATH%\ /s ren %TARGET_PATH%\arm64-v8a\objs objs-debug ren %TARGET_PATH%\x86\objs objs-debug echo OK pause
示例
1. Java向C传递int类型数据
- 需求:传递两个int类型的数据给C, C进行相加后返回
Java代码
public class JNI {
static {
System.loadLibrary("JNITest");
}
public static native int add(int a, int b);
}
C代码
#include <jni.h>
JNIEXPORT jint JNICALL Java_com_cnostar_myapplication_JNI_add
(JNIEnv *env, jclass clazz, jint a, jint b) {
return a + b;
}
2. Java向C传递String类型数据
- 需求:Java传递一个String数据到C, C做一些加工后返回
- 流程:
- 调用工具方法把 java中的string 类型 转换成 C 语言中的 char*
- 调用strlen 可获取 cstr 字符串的长度
将JavaString转换成char*的工具类
/**
* 把一个jstring转换成一个c语言的char* 类型.
*/
char* _JString2CStr(JNIEnv* env, jstring jstr) {
// 定义一个char指针变量
char* rtn = NULL;
// 获取String类型字节码对象
jclass clsstring = (*env)->FindClass(env, "java/lang/String");
// 将"GB2312"字符编码字符串转换为jstring类型
jstring strencode = (*env)->NewStringUTF(env,"GB2312");
// 获取getBytes方法
jmethodID mid = (*env)->GetMethodID(env, clsstring, "getBytes", "(Ljava/lang/String;)[B");
// 执行String的getByte("GB2312");
jbyteArray barr = (jbyteArray)(*env)->CallObjectMethod(env, jstr, mid, strencode);
// 获取数组长度
jsize alen = (*env)->GetArrayLength(env, barr);
// 获取数组首地址
jbyte* ba = (*env)->GetByteArrayElements(env, barr, JNI_FALSE);
// alen>0表示数组有内容
if(alen > 0) {
// 在堆内存中申请alen+1大小的空间, +1是因为字符串数组有结束符"\0"
rtn = (char*)malloc(alen+1); //"\0"
// 将ba拷贝到rtn
memcpy(rtn, ba, alen);
// 将结束符给加上
rtn[alen]=0;
}
// 释放
(*env)->ReleaseByteArrayElements(env, barr, ba,0);
return rtn;
}
Java代码
public class JNI {
static {
System.loadLibrary("JNITest");
}
public static native String stringToC(String str);
}
C代码
#include <jni.h>
#include <string.h>
#include <malloc.h>
JNIEXPORT jstring JNICALL
Java_com_cnostar_myapplication_JNI_stringToC(JNIEnv *env, jclass clazz, jstring str) {
char *cstr = _JString2CStr(env, str);
char *append = "say ";
char *newstr = (char *) malloc(strlen(cstr) + strlen(append));
sprintf(newstr, "%s %s", append, cstr);
return (*env)->NewStringUTF(env, newstr);
}
/**
* 把一个jstring转换成一个c语言的char* 类型.
*/
char* _JString2CStr(JNIEnv* env, jstring jstr) {
char* rtn = NULL;
jclass clsstring = (*env)->FindClass(env, "java/lang/String");
jstring strencode = (*env)->NewStringUTF(env,"GB2312");
jmethodID mid = (*env)->GetMethodID(env, clsstring, "getBytes", "(Ljava/lang/String;)[B");
jbyteArray barr = (jbyteArray)(*env)->CallObjectMethod(env, jstr, mid, strencode); // String .getByte("GB2312");
jsize alen = (*env)->GetArrayLength(env, barr);
jbyte* ba = (*env)->GetByteArrayElements(env, barr, JNI_FALSE);
if(alen > 0) {
rtn = (char*)malloc(alen+1); //"\0"
memcpy(rtn, ba, alen);
rtn[alen]=0;
}
(*env)->ReleaseByteArrayElements(env, barr, ba, 0);
return rtn;
}
3. Java代码传递一个int[]到C
- 获取数组首地址: 以int[]数组为例(其他的类型方法名规则类似): jint *arr_pointer = (*env)->GetIntArrayElements(env, int_arr, NULL);
- 参数1: JNIEnv*
- 参数2: 要取首地址的数组
- 参数3: 可以定义一个jboolean*指针, 该方法执行会赋值jboolean*,告诉开发者系统是否生成了一份数组拷贝,不想传可以为NULL
- 获取数组长度: jsize len = (*env)->GetArrayLength(env, int_arr);
- 参数1: JNIEnv*
- 参数2: 要获取长度的数组
- 将int*数组数据拷贝到jintArray = (*env)->ReleaseIntArrayElements(env, int_arr, arr_pointer, 0);
- 参数1: JNIEnv*
- 参数2: 目标数组
- 参数3: 源指针
- 参数4:
Java代码
public class JNI {
static {
System.loadLibrary("JNITest");
}
public static native int[] intArrToC(int[] intArr);
}
C代码
#include <jni.h>
#include <android/log.h>
// 日志宏定义,可在logcat中输出
#define LOG_TAG "System.out"
#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, __VA_ARGS__)
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__)
JNIEXPORT jintArray JNICALL
Java_com_cnostar_myapplication_JNI_intArrToC(JNIEnv *env, jclass clazz, jintArray int_arr) {
jsize len = (*env)->GetArrayLength(env, int_arr);
jint *arr_pointer = (*env)->GetIntArrayElements(env, int_arr, NULL);
for (int i = 0; i < len; ++i) {
*(arr_pointer + i) *= 10;
LOGI("%d", *(arr_pointer + i));
}
(*env)->ReleaseIntArrayElements(env, int_arr, arr_pointer, 0);
return int_arr;
}
4. C回调Java的方法
- 关键流程: 在JNI/C中使用反射获取到Java的方法, 然后调用
- 步骤:
- 反射获取字节码对象
- 方法: FindClass(env, “目标Java类的全路径”)
- 返回值: jclass
- 获取Method方法对象
- 获取普通方法: GetMethodID(evn, 第一步获取的字节码对象jclass, “方法名”, “参数签名,如(II)I”)
- 获取静态方法: GetStaticMethodID(evn, 第一步获取的字节码对象jclass, “方法名”, “参数签名,如(II)I”)
- 返回值: jmethodID
- 参数签名: 因为Java中的方法会有重载的情况, 所以需要传入签名来找到对应的方法, 如(II)I: 表示两个形参为int类型,返回值为int类型的方法;
- 获取参数签名的方法:
- 如果JNI接口是采用javah命令生成的, 那么方法上面的注释会标注签名,如: Signature: (Ljava/lang/String;)Ljava/lang/String;
- 也可以自行进入class文件所在目录, 然后打开cmd使用命令 javap -s class文件的全限定名 来查看
- 通过字节码对象创建实例(若对象实例已存在, 则直接使用, 无需创建)
- 方法: AllocObject(env, jclass)
- 返回值: jobject
- 通过方法对象执行方法
- 方法
- Call返回值类型Method(env, 调用方法的对象, jmethodID, …参数) 如: CallBooleanMethod
- Call返回值类型MethodV(env, 调用方法的对象, jmethodID, 不知道意思) 如: CallBooleanMethodV
- Call返回值类型MethodA(env, 调用方法的对象, jmethodID, 不知道意思) 如: CallBooleanMethodA
- 如果为静态方法, 那么方法名为 CallStatic返回值类型Method
- 方法
- 反射获取字节码对象
Java代码
public class JNI {
static {
System.loadLibrary("JNITest");
}
public native void requestC();
public void callBack() {
System.out.println("回调成功!");
}
}
C代码
#include <jni.h>
JNIEXPORT void JNICALL
Java_com_cnostar_myapplication_JNI_requestC(JNIEnv *env, jobject thiz) {
// 1. 获取字节码对象
jclass clz = (*env)->FindClass(env, "com/xxx/myapplication/JNI");
// 2. 获取方法
jmethodID methodID = (*env)->GetMethodID(env, clz, "callBack", "()V");
// 3. 执行方法
(*env)->CallVoidMethod(env, thiz, methodID);
}
5. C回调Java静态方法,传入String,返回String
Java代码
public class JNI {
static {
System.loadLibrary("JNITest");
}
public static native String requestC();
public static String callBack(String a) {
return "back: " + a;
}
}
C代码
#include <jni.h>
#include <string.h>
#include <malloc.h>
#include <android/log.h>
#define LOG_TAG "System.out"
#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, __VA_ARGS__)
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__)
JNIEXPORT jstring JNICALL
Java_com_cnostar_myapplication_JNI_requestC(JNIEnv *env, jclass clazz) {
// 获取静态方法
jmethodID methodID = (*env)->GetStaticMethodID(env, clazz, "callBack","(Ljava/lang/String;)Ljava/lang/String;");
// 将hello字符串转换为jstring类型
jstring param = (*env)->NewStringUTF(env, "hello");
// 调用方法,并接收返回值
jstring result = (*env)->CallStaticObjectMethod(env, clazz, methodID, param);
// 将jstring转换为char*, 为了打印日志看看
char *result_pointer = _JString2CStr(env, result);
// 打印日志
LOGI("===>>>%s", result_pointer);
// 将char*转换为jstring并返回给java
return (*env)->NewStringUTF(env, result_pointer);
}
/**
* 把一个jstring转换成一个c语言的char* 类型.
*/
char* _JString2CStr(JNIEnv* env, jstring jstr) {
char* rtn = NULL;
jclass clsstring = (*env)->FindClass(env, "java/lang/String");
jstring strencode = (*env)->NewStringUTF(env,"GB2312");
jmethodID mid = (*env)->GetMethodID(env, clsstring, "getBytes", "(Ljava/lang/String;)[B");
jbyteArray barr = (jbyteArray)(*env)->CallObjectMethod(env, jstr, mid, strencode); // String .getByte("GB2312");
jsize alen = (*env)->GetArrayLength(env, barr);
jbyte* ba = (*env)->GetByteArrayElements(env, barr, JNI_FALSE);
if(alen > 0) {
rtn = (char*)malloc(alen+1); //"\0"
memcpy(rtn, ba, alen);
rtn[alen]=0;
}
(*env)->ReleaseByteArrayElements(env, barr, ba,0);
return rtn;
}
6. C回调Java弹出Toast
- 关键点: 弹Toast需要用到Context, 那么就在Java的JNI定义类时,传入Context即可
java代码
public class JNI {
static {
System.loadLibrary("JNITest");
}
private Context mContext;
public JNI(Context mContext) {
this.mContext = mContext;
}
public native void requestC();
public void callBack(String a) {
Toast.makeText(mContext, "=>" + a, Toast.LENGTH_LONG).show();
}
}
C代码
#include <jni.h>
JNIEXPORT void JNICALL
Java_com_cnostar_myapplication_JNI_requestC(JNIEnv *env, jobject thiz) {
jclass clazz = (*env)->FindClass(env, "com/cnostar/myapplication/JNI");
// 获取方法
jmethodID methodID = (*env)->GetMethodID(env, clazz, "callBack","(Ljava/lang/String;)V");
jstring param = (*env)->NewStringUTF(env, "hello");
// 执行方法
(*env)->CallVoidMethod(env, thiz, methodID, param);
}