Android开发之JNI调用本地C库专题(一):JNI的使用

JNI,是用于开发本地C函数库的技术。用于链接JAVA和C或者C++语言的桥梁。在部分android项目开发中,我们是需要用到这项技术的。那么废话不多说,进入正题。

开发JNI,需要用到NDK,这个大家应该都知道了。还需要一个linux的开发环境。一般而言,可以使用虚拟机装一个ubantu,博主以前就是搞linux开发的,这点还是比较熟悉。但是对于大部分android开发者而言,弄一个虚拟机成本太高。那么,我们需要搭建一个模拟linux的开发环境。这个博主就不说了,直接上链接

NDK环境搭建

以上博文其实只需要做完第三步即可,如果是下载安装谷歌官方集成的eclipse,第三步都可以不用做了。

好,当一切东西都准备好了之后,我们以一个例子来讲解如何开发一个JNI项目。


一、新建一个Android项目

这个正常使用,就是新建一个Android项目


二、C语言方法实现。

1、新建本地native方法

一般而言,需要用C语言实现的方法,我们需要用native关键字去修饰,这些方法可以放在任何一个类中,博主为方便,就都放入一个类中去。参考代码:
public class DataProvider {

	public native int add(int x, int y); //

	public native String sayHelloInC(String s);

	public native int[] intMethod(int[] iNum);

}

2、编译native方法

这里需要用C语言去实现三个方法,一般而言,我们用到JNI技术,都是用做加密。所以,上述三个方法应该是常用的方法。这个使用,我们需要将这个类用javah去编译生成C代码的头文件。首先,我们得在CMD窗口中进入到android项目中的src文件夹中(如果是JDK1.6,则需要进入到/bin/classes目录中,博主的是JDK1.7,所以进入的是src目录),如图所示
技术分享
然后执行 javah com.example.ndkpassdata.DataProvider(这里需要用到全路径全类名)
技术分享

然后我们刷新一下项目,会发现在src目录下生成了一个.h头文件,如图所示:
技术分享

3、创建JNI目录

在工程中新建一个名字为jni的文件夹,名字千万不要弄错,如图:
技术分享
将刚刚的头文件,copy到该文件夹下,然后新建一个名字一样的.c文件。点开 头文件后,我们发现刚刚写的三个方法都已经生成,如图所示:
技术分享

4、编写.c文件

这个时候,我们需要在新建的.c文件中,写入这三个方法。.c文件如何写,相信会C语言的同学应该都明白,关于c代码中如何转换java传入过来的参数和调用java中的方法,可以参考jni.h的头文件,里面有详细接口调用方法。这里博主就不在描述,直接上所有的代码,有详细注释:

#include <stdio.h>
#include "com_example_ndkpassdata_DataProvider.h"
#include <android/log.h>
#include <string.h>
#define LOG_TAG "clog"
#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, __VA_ARGS__)
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__)

//将java语言中的字符串格式转换为C语言中的字符串格式。
char* Jstring2CStr(JNIEnv* env, jstring jstr) {
	char* rtn = NULL;
	jclass clsstring = (*env)->FindClass(env, "java/lang/String");
	jstring strencode = (*env)->NewStringUTF(env, "GB2312");
	jmethodID mid = (*env)->GetMethodID(env, clsstring, "getBytes",
			"(Ljava/lang/String;)[B");
	jbyteArray barr = (jbyteArray)(*env)->CallObjectMethod(env, jstr, mid,
			strencode); // String .getByte("GB2312");
	jsize alen = (*env)->GetArrayLength(env, barr);
	jbyte* ba = (*env)->GetByteArrayElements(env, barr, JNI_FALSE);
	if (alen > 0) {
		rtn = (char*) malloc(alen + 1);         //"\0"
		memcpy(rtn, ba, alen);
		rtn[alen] = 0;
	}
	(*env)->ReleaseByteArrayElements(env, barr, ba, 0);  //
	return rtn;
}

JNIEXPORT jint JNICALL Java_com_example_ndkpassdata_DataProvider_add(
		JNIEnv * env, jobject jobject, jint x, jint y) {
	// 想在logcat控制台上 打印日志
	LOGD("x=%d", x);
	LOGI("y=%d", y);
	// log.i(TAG,"sss");
	return x + y;

}

JNIEXPORT jstring JNICALL Java_com_example_ndkpassdata_DataProvider_sayHelloInC(
		JNIEnv * env, jobject jobject, jstring str) {

	char* c = "hello";
	// 在C语言中不能直接操作java中的字符串
	// 把java中的字符串转换成c语言中 char数组
	char* cstr = Jstring2CStr(env, str);

	strcat(cstr, c);
	LOGD("%s", cstr);
	return (*env)->NewStringUTF(env, cstr);
}

JNIEXPORT jintArray JNICALL Java_com_example_ndkpassdata_DataProvider_intMethod(
		JNIEnv * env, jobject jobject, jintArray jarray) {
	// jArray  遍历数组   jint*       (*GetIntArrayElements)(JNIEnv*, jintArray, jboolean*);
	// 数组的长度    jsize       (*GetArrayLength)(JNIEnv*, jarray);
	// 对数组中每个元素 +5
	int length = (*env)->GetArrayLength(env, jarray);
	//拿到指针初始位置
	int* array = (*env)->GetIntArrayElements(env, jarray, 0);
	int i = 0;
	for (; i < length; i++) {
		*(array + i) += 5;
	}
	return jarray;
}

5、编写android.mk文件

该文件的写法请到ndk目录下的docs目录,打开ANDROID-MK.html,里面有使用方法说。这里博主就说说一般必须写的。
LOCAL_PATH := $(call my-dir)   // 返回当前c代码目录
include $(CLEAR_VARS)        // 清楚了所有 已local 开头的配置文件 唯独不清楚LOCAL_PATH
LOCAL_MODULE    := hello   // 库函数的名字  严格遵守makefile 格式  lib  .so  如果前面加lib 不会自动生成了
   LOCAL_SRC_FILES := Hello.c  //源文件名称,就是刚刚新建的那个.c文件的名称。
include $(BUILD_SHARED_LIBRARY)  // 加入库函数
由于在之前的代码中使用了C语言的日志函数log,所以在.mk文件中需要加入库引用的声明,代码如下:
 LOCAL_PATH := $(call my-dir)

   include $(CLEAR_VARS)

   LOCAL_MODULE    := libhello
   LOCAL_SRC_FILES := Hello.c
	LOCAL_LDLIBS += -llog

   include $(BUILD_SHARED_LIBRARY)

6、编译C文件

当android文件写好之后,一切的准备工作都已经就绪,这个时候我们只需要调用NDK去编译该项目即可上次.so动态库文件。这个时候需要启动Cygwin,然后来到该项目的目录下,调用ndk-build命令即可。过程如图所示:

技术分享
当编译完成之后我们刷新项目,会发现多出了一个obj文件夹,在libs文件夹中也会多出一个armeabs文件夹,在这里面就有我们刚刚编译生成的库。

三、C语言函数库方法调用

那么编译好之后,我们需要调用刚刚的方法,这个时候就简单了。在调用的地方,需要加入一个静态代码块,利用System.loadLibrary("hello");方法将刚刚生产的库文件导入。详细看代码:
public class MainActivity extends Activity {
	DataProvider provider;
	static {
		System.loadLibrary("hello");
	}

	@Override
	protected void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		setContentView(R.layout.activity_main);
		provider = new DataProvider();

	}

	public void click1(View view) {
		int result = provider.add(6, 8);
		System.out.println(result);
	}

	public void click2(View view) {
		String str = provider.sayHelloInC("freedom");
		Toast.makeText(getApplicationContext(), str, 0).show();

	}

	public void click3(View view) {
		int[] arr = new int[] { 5, 6, 7, 8, 9 };
		provider.intMethod(arr);
		for (int i : arr) {
			System.out.println(i);
		}

	}
}

好了,至此利用JNI开发本地native方法的流程已经讲解完毕,需要值得注意的是,如果本地.c文件有变更,我们需要调用ndk-build去重新编译.c文件,这个时候最好将本地缓存目录obj文件夹删除掉。希望能帮助到看到此文的人。


郑重声明:本站内容如果来自互联网及其他传播媒体,其版权均属原媒体及文章作者所有。转载目的在于传递更多信息及用于网络分享,并不代表本站赞同其观点和对其真实性负责,也不构成任何其他建议。