Android開發之JNI調用本地C庫專題(一):JNI的使用

JNI,是用於開發本地C函數庫的技術。用於鏈接JAVA和C或者C++語言的橋樑。在部分android項目開發中,我們是需要用到這項技術的。在升級APP的時候,我們有時間需要用到增量更新技術,這個也是基於JNI技術實現的,詳情請點擊:基於JNI技術實現增量更新

那麼廢話不多說,進入正題。

開發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文件夾刪除掉。希望能幫助到看到此文的人。


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章