JNI(Java Native Interface)是java调用本地代码的接口技术,基本上我是用它来在android中调用C/C++,android提供了一套套件叫做NDK(Native Development Kit),使我们可以很方便生成我们需要的目标文件。

专门有网站是介绍使用JNI的,网址是这里

##使用举例

###JAVA端实现

info 首先需要明确apk还是运行在Dalvik虚拟机中的,我们暂时还不能编写纯粹C/C++的应用,除非不需要面向用户界面 info

我们可以指定一个类来完成JAVA和C的交互工作,例如com.lingavin.jnisample.JavaToJni.java

:::java
package com.lingavin.jnsample;

public class JavaToJni {
	static{
		//系统加载 libnativejni.so  
		System.loadLibrary("nativejni");
	}
	//对应native函数的接口,用native关键字来修饰	
	public static native void intToJni(int number); 
}

就这样一个简单的JAVA端部分完成了,程序通过调用JavaToJni.intToJni(num),就可以将int传入C/C++处理。

###native端实现

native端的实现才是重点内容,本次使用动态注册的方式和C语言来完成这项工作。

info jni本地代码的接口分为动态注册和静态注册,本文只体现动态注册 info

info 要注意用C和用C++写jni接口是有些区别的,主要是方法调用上,C调用env方法是(*env)->xxx(env,xxx),C++则为env->xxx(xxx) info

在本工程中创建一个jni/目录,进去后创建native_sample_jni.c,我们将在这个文件中完成代码编写。开始是JNI_OnLoad函数。

info JNI_OnLoad函数是必须要的,系统就是从这个函数开始注册工作的。这个可以被看做回调函数 info

:::c 
JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void* reserved){
	JNIEnv* env = NULL;
	jint result = -1;
	if((*vm)->GetEnv(vm, (void**)&env, JNI_VERSION_1_4) != JNI_OK){
		return result;
	}

	if(!registerNatives(env)){
		return result;
	}
	result = JNI_VERSION_1_4;
	return result;
}

看到JNI_OnLoad函数中调用了registerNatives函数,那就看看这个函数干了些什么。

:::c
static int registerNatives(JNIEnv* env){
	return registerNativeMethods(env,KClassPath,gMethods,
			sizeof(gMethods)/sizeof(gMethods[0]));
}

调用了registerNativeMethods函数,传入了KClassPathgMethod,这两个是很重要的变量。先看看registerNativeMethods做了些什么。

:::c
static int registerNativeMethods(JNIEnv* env,const char* className,
		JNINativeMethod* gMethods, int numMethods){
	jclass clz;
	clz = (*env)->FindClass(env, className);
	if(clz == NULL){
		return JNI_FALSE;
	}

	if((*env)->RegisterNatives(env, clz, gMethods, numMethods) < 0){
		return JNI_FALSE;
	}
	return JNI_TRUE;
}

从代码可以看到它的功能是根据存入的参数进行接口注册,那么这些参数是怎么定义的呢?重点来看看:

:::c
static const char* const KClassPath = "com/lingavin/jnisample/JavaToJni";
// java_method,  (in)return,  native_method
static JNINativeMethod gMethods[] = {
	{"intToJni","(I)V", (void*)nativeIntToJni},
};

kClassPath对应java工程的包名+包含native函数的类名,gMethods是一个数组,记录着java端的方法+(传入参数)返回类型+本地对应函数,这样我们的Java函数就和c函数对接起来了。

写到这里,其实还有很多知识没有提及,例如gMethods中(I)V是什么意思,后面会讲的

但是本地代码只是声明,没有实现,我们需要自己写实现方法,例如

:::c
void nativeIntToJni(JNIEnv* env, jobject clazz, jint num){
	__android_log_print(ANDROID_LOG_INFO,"TAG","native get num is %d",num);
}

JNIEnv和jobject是需要的,这两个的意思是java环境和调用这个函数的对象,后面的jint就是java传进来的int。基本上这个工程就实现了。

##具体知识

###参数类型

JAVA和C的参数类型表如下

JAVA类型描述符号本地类型备注
booleanZjbooleanunsigned 8 bits
byteBjbytesigned 8 bits
charCjcharunsigned 16 bits
shortSjshortsigned 16 bits
intIjintsigned 32 bits
longJjlongsigned 32 bits
floatFjfloat32 bits
doubleDjdouble64 bits
int[][IjIntArray
StringLjava/lang/String;jstring
ObjectLjava/lang/Object;jobject

Warning JAVA的char是16位无符号数,而C则是8位,如果你用char*来指向JAVA传进来的char数组,那就等着悲剧吧。Warning

有一些数据类型可以不通过转换直接使用,但有一些则不能,例如数组,string和object等。

##Android NDK

写完上面的代码,还需要把c文件编译成so,这个时候就要NDK出动了。NDK的介绍

NDK包括了:

1.从C / C++生成原生代码库所需要的工具和build files。

2.将一致的原生库嵌入可以在Android设备上部署的应用程序包文件(application packages files ,即.apk文件)中。

3.支持所有未来Android平台的一些列原生系统头文件和库

为了编译so,需要编写Android.mk,在JNI目录新建Android.mk

:::c
LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
LOCAL_MODULE := nativejni
LOCAL_SRC_FILES := native_sample_jni.c
LOCAL_MODULE_TAGS :=debug
LOCAL_ARM_MODE := arm
LOCAL_LDFLAGS = -llog
include $(BUILD_SHARED_LIBRARY)

编译出so后就可以编译apk,然后放到机器上调试看是否有打印信息了。

工程已经放到github上面,网址是:https://github.com/gavinlin/jni-sample