Preface
image-20210417234903849.png在用安卓处理音视频开发时,往往需要我们调用已有的成熟的用C/C++语言编写的库,比如FFmpeg,LAME等.这就牵涉到如何用Java调用C语言编写的库.
本文处理的是最简单的情形,简单调用一个C文件里的函数.
想写这篇文章很久了,但是之前遇到了一个坑,今天才万幸走出来.所以赶紧记录一下.
1 Java调用C代码的整体流程
Java调用C代码是通过JNI(JavaNativeInterface)这个手段来实现的,具体流程见下图:
image-20210418001615813.png
接下来我们用一个实例来说明如何实现Java调用C代码
2 准备工作
- 确认开发环境
小编这里使用的是
MacOS 11.0.1
Android Stuidio 4.1.2
Javac 的版本是14.0.1,这个很重要,影响到了第2步生成jni文件.之前参考帖子上用的都是javah命令,但是在小编这里就是不成功,可能就是版本问题.
- 明确两个重要工具的目录
ndk-build程序目录:
/Users/gikkiares/Library/Android/sdk/ndk/20.0.5594570/ndk-build
javac目录
/usr/bin/javac
如果在使用命令时,提示command not found:
就说明要么工具没有安装,要么安装了,但是没有加入到环境变量.
如果是前者,重新安装对应工具.
如果是后者,将工具的目录加入到环境变量,或者使用命令的全路径.
- 创建一个新的Android项目
我们在该项目中,实现用java调用c代码.
3 Java调用C代码示例
3.1 编写java桥接类
为了简单起见,我们新建一个CManager.java
CManager类负责两个事情:
1,对java层,提供了java形式的接口
2,实现的方式为c语言.
该文件内容为:
package com.tinywind.gajdemo.module.cmanager;
public class CManager {
public static CManager sharedInstance = new CManager();
public native String getMessageFromC();
public native int sum(int a, int b);
}
我们注意到以下几点:
- 我们使用了单例模式.
- 定义了两个用native关键字修饰的方法.
1.使用native关键字,表示我们实现该方法的语言不是java而是c/c++.
2,getMessageFromC,简单地用c语言返回一个字符串
3,sum函数,用c语言实现两个数相加.
- 我们只声明了函数,并没有实现.
3.2 生成jni风格的头文件.
小编之前一直卡在这一步.一直在尝试用javah命令生成,单总是提示找不到类.
估计可能和小编用的javac版本是14.0.1有关.
总之,错误的方式成千上万,正确的方式只有那么一种,我们记住正确的就好了,错误的就让他随风而去吧~
//切换到CManager.java所在的目录
cd ${ProjectPath}/app/src/main/java/com/tinywind/gajdemo/module/cmanager
//-h .指定了在当前目录中输出jni风格的头文件
javac CManager.java -h .
这一步完成之后,我们得到了一个.class文件和.h文件
image-20210417173817694.png
.class文件对我们来说没用,可以直接删除.
这个很长的h文件,就是我们的jhi头文件.
3.3 编写C文件
新建目录
src/main/jni
需要将jni的头文件移动到这个目录中.
然后新建一个文件CManager.c
,内容为:
#include "com_tinywind_gajdemo_module_cmanager_CManager.h"
//C字符串转java字符串
jstring cstringToJstring(JNIEnv* env, const char* pStr) {
int strLen = strlen(pStr);
jclass cls_string = (*env)->FindClass(env, "java/lang/String");
// 获取java String类方法String(byte[],String)的构造器,用于将本地byte[]数组转换为一个新String
jmethodID methodId = (*env)->GetMethodID(env, cls_string, "<init>", "([BLjava/lang/String;)V");
jbyteArray byteArray = (*env)->NewByteArray(env, strLen);
jstring encode = (*env)->NewStringUTF(env, "utf-8");
(*env)->SetByteArrayRegion(env, byteArray, 0, strLen, (jbyte*)pStr);
return (jstring)(*env)->NewObject(env, cls_string, methodId, byteArray, encode);
}
JNIEXPORT jstring JNICALL Java_com_tinywind_gajdemo_module_cmanager_CManager_getMessageFromC
(JNIEnv * env, jobject obj) {
char * str = "Hello,this is a message from c!";
return cstringToJstring(env, str);
}
JNIEXPORT jint JNICALL Java_com_tinywind_gajdemo_module_cmanager_CManager_sum
(JNIEnv * env, jobject obj, jint a, jint b) {
return a + b;
}
需要注意以下几点:
- c文件中要引入jni头文件
#include "com_tinywind_gajdemo_module_cmanager_CManager.h"
- 函数的原型从jni头文件中拷贝,但是要注意jni头文件中形参的名称是省略的,我们需要加上去.
- c语言的字符串和jni的jstring是不同类型的需要转换.
3.4 生成动态库
在jni目录中新建Android.mk文件,目录看起来是这个样子:
image-20210418005701072.png
在Android.mk文件中输入以下内容:
LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
LOCAL_MODULE := libcmanager
LOCAL_SRC_FILES := ./CManager.c
include $(BUILD_SHARED_LIBRARY)
- LOCAL_MODULE指定了so库的名字
要注意的是,写得是libcmanager,但是后期导入时只需要写cmanager.
- LOCAL_SRC_FILES指定了so库的源文件
其他的项目暂时不太清楚
然后执行以下命令:
//然后切换到目录:
${ProjectPath}/app/src/main
//编译c文件为so动态链接库
${NdkDir}/ndk-build
image-20210418074249795.png
编译成功之后,项目里在src/main目录下会多出libs目录,里面对应不同的架构,产生了对应的so库.
3.5 加载动态链接库
加载动态链接库最简单的方法,就是在main目录下创建jniLibs文件夹,然后将libs中目全部加入进去就额可以了.
系统会自动加载jniLibs里的so动态库.
3.6 调用Native方法
最后,我们调用Native方法,只要把桥接的Java类CManager当做普通的java类去调用就可以了:
String string = CManager.sharedInstance.getMessageFromC();
int sub = CManager.sharedInstance.sum(1,1);
我们断点查看执行结果
image-20210418075408617.png我们可以看到,c的世界向java的世界发来了贺电"Hello,this is a message from c",然后也友好地教导了我们1+1=2.
这就是java调用c代码的最简单模型.
4 总结
- 关于Markdown的一个问题
希望markdown有一个没有级别的标题,现在是要么1级标题,2级标题.
我希望能有一个没有级别的标题.
- 关于模板
模板,就是套路.不管是在编程界还是社交界,都是需要各种模板.
模板够多,才能玩的六.
所以本文牵涉的内容很简单,但是也是一个很重要的模板
网友评论