网上有网友问android的原生应用,上层java代码如何通过jni调用本地的c/c 接口或第三方动态库 ?之前搞过android应用开发和底层c/c 接口开发都是一个人搞定,觉得还是蛮简单的。其实没啥难度,如果觉得难只是因为你没有经历过,只要搞过一遍基本就记住了。这里总结下方法留作备忘,同时分享给有需要的小伙伴。
网上这方面介绍的文章有很多,但都较凌乱或者不够系统,啰里啰唆一大堆前戏,不如实战来的快。长篇大论真没必要,我们只想上手用,先用起来再说,其他需要了再深入。为了做到通俗易懂和尽可能的简单,直接举例说明吧。举一个详细的例子从头到尾完整实现一遍,保证看一遍就会上手会用。
总体方法就是通过JNI(Java Native Interface),即 Java 本地接口,使得 Java 与本地其他类型语言如 C、C 交互。也就是在 Java 中调用 C/C 代码,或者在 C/C 中调用 Java 代码,下面一一详细介绍。
调用其他三方动态库的使用过程,可以参见博主的另一篇文章介绍:
支付宝二维码脱机认证库在android的app下测试过程记录_特立独行的猫a的博客-CSDN博客
java调用JNI总结_特立独行的猫a的博客-CSDN博客
目标任务
举例需求如下:MAC生成算法保密,是在c层实现的。java层业务需调用底层c语言实现的接口。
Java层需要的接口如下:
代码语言:javascript复制byte[] calcDesMac64(byte[] key, byte[] data, int len)
环境准备
首先需要有编译c代码的环境,就是一套工具链和脚本。平常通过AndroidStudio搞android原生开发的都倒弄过环境,需要下载sdk开发包。但是如果涉及c/c 接口的本地代码,则还需要下载安装NDK,是 Android 的一个底层Native开发包。关于NDK的详细介绍这里就不科普了,文末有相关知识的引用,感兴趣的可以看看,我是觉得有点儿啰嗦。
下载安装NDK的方法,这里也不多介绍了,下载安装就是了。
实现步骤
一、定义java层需要用到的类和接口
首先需要定义好java层需要用到的类和接口,一旦定义好不能轻易变。由于是特殊的与底层交互的接口,最好单独指定一个特殊的包名称,并给出实现类的封装。如下:
代码语言:javascript复制package com.mypackage.jni;
public class CalcMac {
public static String TAG = CalcMac.class.getSimpleName();
static {
System.loadLibrary("CalcMac");
}
public static synchronized byte[] calcDesMac64(byte[] key, byte[] data, int len){
return Native_JniCalcDesMac64(key,data,len);
}
private static native final long Native_JniTest();
private static native final byte[] Native_JniCalcDesMac64(byte[] key,byte[] data,int len);
}
这一步操作比较简单,接下来就是需要把用到的CalcMac.so搞出来了。否则代码也编译不过呀,会提示System.loadLibrary找不到动态库CalcMac.so。
二、c层接口封装
这是关键的一步,需要处理好java代码和c代码之间的类型转换。关于java层和c层接口参数转换的知识,可以自行查阅资料或查看头文件,后面有机会单独总结下。Native层的c代码如下:
代码语言:javascript复制// Native层接口封装
static jbyteArray Jni_CalcDesMac64(JNIEnv *env, jobject obj, jbyteArray key,jbyteArray data,jint len){
U08 mac[8];
jbyte * pkey = NULL;
jbyte * pbuf = NULL;
pkey = (jbyte *)(*env)->GetByteArrayElements(env,key, NULL);
pbuf = (jbyte *)(*env)->GetByteArrayElements(env,data, NULL);
//c代码接口调用
CurCalc_DES_MAC64( (SINGLE_DES_ENCRYPTION|ZERO_CBC_IV), (U08*)pkey, 0, (U08*)pbuf, len , mac );
jbyteArray jarrMac =(*env)->NewByteArray(env,8);
(*env)->SetByteArrayRegion(env,jarrMac, 0,8,(jbyte*)mac);
return jarrMac;
}
// Native层接口封装
static jlong Jni_Test(JNIEnv *env, jobject obj)
{
U32 rcode = 0;
LOGD(">>> ..%s..%d..enter",__FUNCTION__,__LINE__);
LOGD(">>> ..%s..%d.exit",__FUNCTION__,__LINE__);
return rcode;
}
这样就完了吗?肯定不行啦,至少我们需要的CalcMac.so还没有生成。不过以下的步骤都是模板套路了,按照格式书写就行了。
三、接口注册
这一步也是很关键的部分,没有注册上层是无法调用底层接口的。这部分内容其实也很简单,就是模板套路,按照一定的要求书写就行了。
代码语言:javascript复制//定义批量注册的数组,是注册的关键部分
static const JNINativeMethod gMethods[] = {
{ "Native_JniTest","()J", (void*)Jni_Test},
{ "Native_JniCalcDesMac64","([B[BI)[B", (void*)Jni_CalcDesMac64}
};
// extern "C" {
JNIEXPORT jint JNI_OnLoad(JavaVM* vm,void *reserved)
{
JNIEnv *env =NULL;
jint result = -1;
static const char* kClassName= "com/mypackage/jni/CalcMac";
jclass clazz;
debug_level = 5;
LOGD(">>> ..%s..%d..enter",__FUNCTION__,__LINE__);
if( (*vm)->GetEnv(vm,(void**)&env,JNI_VERSION_1_4) != JNI_OK )
{
return result;
}
clazz = (*env)->FindClass(env,kClassName);
if( clazz == NULL )
{
LOGE("%d..Can't find class %s!n",__LINE__, kClassName);
return -1;
}
//FindTradeInfoFields(env);
if( (*env)->RegisterNatives( env,clazz, gMethods, sizeof(gMethods)/sizeof(gMethods[0]) ) != JNI_OK )
{
LOGE("Failed registering methods for %s!n", kClassName);
return -1;
}
LOGD(">>> ..%s..%d.exit",__FUNCTION__,__LINE__);
return JNI_VERSION_1_4;
}
// }
四、脚本编译
这一步也很关键,没有它前面步骤的努力也白费,通过这一步方能最终实现我们要的CalcMac.so,这一步也是模板套路。有些时候之所以觉得难,是因为你没有经历过。其实没什么特别难的事。
把需要编译的c代码或需要链接的三方库,写到编译脚本里组织下。
Android.mk文件如下:
Application.mk 文件内容如下:
代码语言:javascript复制APP_BUILD_SCRIPT := Android.mk
APP_ABI := armeabi-v7a
APP_PLATFORM := android-22
编译脚本如下,就一行指令:
代码语言:javascript复制ndk-build NDK_PROJECT_PATH=. NDK_APPLICATION_MK=Application.mk
--copy--
cp ./libs/arm64-v8a/libCalcMac.so D:GitAsWorkMyAppPrjappsrcmainjniLibsarm64-v8alibCalcMac.so
经过以上步骤,如果没有编译错误的话能够成功生成我们需要的libCalcMac.so 。如何使用?肯定不能随便放一个目录位置了,需要放置到特定的目录里。
五、如何使用
如果上述步骤成功生成了对应平台需要的so动态库,接下来使用就简单啦。把so库放置到对应的目录,让项目代码整体编译通过。so库放置的位置是有要求的,剩下都是一些配置的工作。
六、build.gradle中的配置
已经打好的so库文件或者以来第三方库的so文件,首先需要将so库文件放置在libs目录或者自定义的目录中(如有些人喜欢放在src目录下的jniLibs目录中),然后再module下的build.gradle中引用so库,具体如下:
代码语言:javascript复制android {
//...
defaultConfig {
//version,versioncode,applicationID等信息
ndk {
//针对自己项目的架构对应添加相应的so目录
//目前的手机架构基本上都是arm架构,x86的基本上没有,基本上是平板
abiFilters "armeabi-v7a",//arm架构的32位
"armeabi",//十年前的手机CPU架构,基本上已经不存在了
"arm64-v8a",//arm架构的64位
"x86",//x86架构的 32位
"x86_64"//x86架构的64位
}
}
//省略其他配置...
sourceSets {
main {
//这里的libs需要替换成你放置so库的目录,比如jniLibs
jniLibs.srcDirs = ['libs']
}
}
}
dependencies {
//省略其他配置....
}
需要注意的是看你的android系统的平台版本和内核版本,比如是32位的还是64位的,是armeabi-v7a还是arm64-v8a,这些都是有区别的,不同的类别编译出来的动态库不通用。以上示例,把libCalcMac.so放置到myappprj/ app / libs / armeabi目录下,就可以编译打包通过啦。
七、其他说明和注意事项
需要注意的地方,定义批量注册的数组是注册的关键部分。
其中的 "()I" 是干啥的?如果接口不带参数,所以签名是()I,如果我的接口方法带两个参数,这里签名应该是 (II)I, I表示的是int类型,否则java层通过JNI调用时,会报找不到方法。括号里面的是参数类型对应的符号,括号外面的返回值类型对应的符号。
JNI_Onload函数,当启动程序的时候会加载动态库文件,就会调用这个函数。接着在onload函数中,注册了nativemethods。 methods数组中第一个和第三个参数比较好理解,那么第二个参数呢?
其实第二个参数可以参考头文件,一模一样拉过来就好了。其中的意思就是()里的表示函数的参数,()表示没有参数,(II)表示两个参数,都是int型。后面跟的Ljava/lang/String表示返回值是String类型的,需要注意的是long类型对应的符号是"J",可不是想当然的"L",I表示的是int类型。关于这块儿文末链接里有对照表文章可以查看。
还有个地方要注意了,包名一定不能错,(*env)->FindClass(env,kClassName)这里kClassName包名一定得对应。
另外一点需注意的是上层应用层注意load顺序,先load第三方库,再load自己的库。
底层完整代码实现
代码语言:javascript复制// jni_CalcMac.c
#include "CurCalc_DES.h"
#include <jni.h>
#include <android/log.h>
static const char *TAG = "CalcMac_JNI";
static unsigned int debug_level = 5;
//#define PATH_CLASS_NAME "com/mypackage/jni/CardNc"
#define LOGD(fmt, args...)
do{ if (debug_level >= 3) __android_log_print(ANDROID_LOG_DEBUG, TAG, fmt, ##args); } while(0)
#define LOGI(fmt, args...)
do{ if (debug_level >= 2) __android_log_print(ANDROID_LOG_INFO, TAG, fmt, ##args); } while(0)
#define LOGE(fmt, args...)
do{ if (debug_level >= 1) __android_log_print(ANDROID_LOG_ERROR, TAG, fmt, ##args); } while(0)
#define LOGA(fmt, args...)
do{ if (debug_level >= 0) __android_log_print(ANDROID_LOG_ERROR, TAG, fmt, ##args); } while(0)
//unsigned int debug_level = 5;
static jbyteArray Jni_CalcDesMac64(JNIEnv *env, jobject obj, jbyteArray key,jbyteArray data,jint len){
U08 mac[8];
jbyte * pkey = NULL;
jbyte * pbuf = NULL;
pkey = (jbyte *)(*env)->GetByteArrayElements(env,key, NULL);
pbuf = (jbyte *)(*env)->GetByteArrayElements(env,data, NULL);
CurCalc_DES_MAC64( (SINGLE_DES_ENCRYPTION|ZERO_CBC_IV), (U08*)pkey, 0, (U08*)pbuf, len , mac );
jbyteArray jarrMac =(*env)->NewByteArray(env,8);
(*env)->SetByteArrayRegion(env,jarrMac, 0,8,(jbyte*)mac);
return jarrMac;
}
static jlong Jni_Test(JNIEnv *env, jobject obj)
{
U32 rcode = 0;
LOGD(">>> ..%s..%d..enter",__FUNCTION__,__LINE__);
LOGD(">>> ..%s..%d.exit",__FUNCTION__,__LINE__);
return rcode;
}
//定义批量注册的数组,是注册的关键部分
static const JNINativeMethod gMethods[] = {
{ "Native_JniTest","()J", (void*)Jni_Test},
{ "Native_JniCalcDesMac64","([B[BI)[B", (void*)Jni_CalcDesMac64}
};
// extern "C" {
JNIEXPORT jint JNI_OnLoad(JavaVM* vm,void *reserved)
{
JNIEnv *env =NULL;
jint result = -1;
static const char* kClassName= "com/mypackage/jni/CalcMac";
jclass clazz;
debug_level = 5;
LOGD(">>> ..%s..%d..enter",__FUNCTION__,__LINE__);
if( (*vm)->GetEnv(vm,(void**)&env,JNI_VERSION_1_4) != JNI_OK )
{
return result;
}
clazz = (*env)->FindClass(env,kClassName);
if( clazz == NULL )
{
LOGE("%d..Can't find class %s!n",__LINE__, kClassName);
return -1;
}
//FindTradeInfoFields(env);
if( (*env)->RegisterNatives( env,clazz, gMethods, sizeof(gMethods)/sizeof(gMethods[0]) ) != JNI_OK )
{
LOGE("Failed registering methods for %s!n", kClassName);
return -1;
}
LOGD(">>> ..%s..%d.exit",__FUNCTION__,__LINE__);
return JNI_VERSION_1_4;
}
// }
引用
Android NDK(一)- 认识 NDK - 简书
android ndk_百度百科
NDK 使用入门 | Android NDK | Android Developers
Android NDK开发(一) - 简书
Android NDK编程_Karson Tiger的博客-CSDN博客
JNI 数据类型与 Java 数据类型的映射关系_Martin89的博客-CSDN博客
JNI的数据类型及映射关系详解_普通网友的博客-CSDN博客_jni映射
Android NDK 从入门到精通(汇总篇)_阿飞__的博客-CSDN博客
JNI基础:JNI数据类型和类型描述符_阿飞__的博客-CSDN博客
java调用JNI总结_特立独行的猫a的博客-CSDN博客
支付宝二维码脱机认证库在android的app下测试过程记录_特立独行的猫a的博客-CSDN博客
安装及配置 NDK 和 CMake | Android 开发者 | Android Developers
armeabi-v7a armeabi arm64-v8a区别_ChampionDragon的博客-CSDN博客_armeabi-v7a
字节跳动总监知乎1716赞的AndroidFramework开发笔记 腾讯技术团队出品的《Android Framework 开发揭秘》免费领取