Android通过jni调用本地c/c++接口方法总结

2022-11-21 10:15:15 浏览数 (1)

网上有网友问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 开发揭秘》免费领取

0 人点赞