前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Android通过jni调用本地c/c++接口方法总结

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

作者头像
杨永贞
发布2022-11-21 10:15:15
1.6K0
发布2022-11-21 10:15:15
举报

网上有网友问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:\GitAsWork\MyAppPrj\app\src\main\jniLibs\arm64-v8a\libCalcMac.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 开发揭秘》免费领取

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2022-11-19,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 目标任务
  • 环境准备
  • 实现步骤
    • 一、定义java层需要用到的类和接口
      • 二、c层接口封装
        • 三、接口注册
          • 四、脚本编译
            • 五、如何使用
              • 六、build.gradle中的配置
                • 七、其他说明和注意事项
                • 底层完整代码实现
                • 引用
                相关产品与服务
                腾讯云 BI
                腾讯云 BI(Business Intelligence,BI)提供从数据源接入、数据建模到数据可视化分析全流程的BI能力,帮助经营者快速获取决策数据依据。系统采用敏捷自助式设计,使用者仅需通过简单拖拽即可完成原本复杂的报表开发过程,并支持报表的分享、推送等企业协作场景。
                领券
                问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档