在某些情況下,Java編程已經不能滿足我們的需要,比如一個復雜的算法處理,這時候就需要用到jni(java native interface)技術;
jni開發工具ndk的安裝:
在最新的ndk版本中,安裝ndk很簡單,只需要裝ndk的路徑配置到系統環境變量中即可;
在編譯的時候,進入工程根目錄;執行命令 ndk-build 即可完成編譯;
下面就通過一個例子一步一步的來初步學習jni
一、HelloWorld
新建一個工程,你甚至不需要其它額外的設置,然後在工程中添加一個jni目錄,然後就可以開始了;
1.新建一個java類HelloWorld.java
package com.jni; public class HelloWorld { static { System.loadLibrary("helloworld"); } public native String helloworld(); }
在HelloWorld中,定義了一個方法helloworld(),只不過這個方法被申明成了native的,並沒有具體的實現,具體功能我們在接下來的cpp文件中實現;
2.在jni目錄下添加一個helloworld.cpp
#include <jni.h> #include <Android/log.h> #include <string.h> #ifndef _Included_com_jni_HelloWorld // 1 #define _Included_com_jni_HelloWorld #ifdef __cplusplus // 2 extern "C" { #endif // 2 JNIEXPORT jstring JNICALL Java_com_jni_HelloWorld_helloworld(JNIEnv *, jobject); #ifdef __cplusplus // 3 } #endif // 3 #endif // 1 JNIEXPORT jstring JNICALL Java_com_jni_HelloWorld_helloworld(JNIEnv * env, jobject obj) { return env->NewStringUTF("helloworld"); }
從上面這個cpp文件中可以很明白的看出,它有一個方法,具體包括方法申明和方法實現兩個部分;但是相信大家也都看出來了,方法的命令很怪異,怎麼這麼長的方法名?
我們在這裡先思考一個問題,java類中的方法是如何調用c++中的方法的呢?要解決這個問題,就得先來看這個長長的方法名;
其實這是jni的一個規范之一,用於映射java方法和c/c++中的方法對應;
再來看在cpp中定義的函數名:Java_com_jni_HelloWorld_helloworld
其實不難看出,java文件與cpp文件中函數名的配對定義方式為Java + 包名 + java類名 + 方法/函數名,中間用_分隔;其中兩個參數分別是:
這裡的helloworld方法,其實就只是返回了一個單詞"helloworld";
3.在jni目錄下添加一個Android.mk文件
LOCAL_PATH := $(call my-dir) include $(CLEAR_VARS) LOCAL_MODULE := helloworld LOCAL_SRC_FILES := helloworld.cpp include $(BUILD_SHARED_LIBRARY)
4.在命令行下進入工程目錄執行 ndk-build 命令,然後運行程序,調用HelloWorld實例的helloworld方法就可以得到它的返回字符串了;
二、jni調用Java類的方法(1)
通過上面的helloworld練手之後,我們來看一下jni調用java類裡面的方法的實現;
1.新建設一個MethodCall.java文件如下
public class MethodCall { final String tag = "MethodCall"; static { System.loadLibrary("methodcall"); } public native String jniCallMethod1(); public native String jniCallMethod2(); public native String jniCallStaticMethod(); public void javaMethod1() { Log.e(tag, "javaMethod1"); } public String javaMethod2() { Log.e(tag, "javaMethod2"); return "javaMethod2"; } public static void javaStaticMethod(String input) { Log.e("MethodCall", "" + input); } }
該類有6個方法,其中有3個是java類的方法,另外3個是native方法,3個native方法分別去調用3個java方法;
2.添加三個native方法具體實現 methodcall.cpp
#include <jni.h> #include <android/log.h> #include <string.h> #ifndef _Included_com_jni_MethodCall #define _Included_com_jni_MethodCall #ifdef __cplusplus extern "C" { #endif JNIEXPORT jstring JNICALL Java_com_jni_MethodCall_jniCallMethod1(JNIEnv *, jobject); JNIEXPORT jstring JNICALL Java_com_jni_MethodCall_jniCallMethod2(JNIEnv *, jobject); JNIEXPORT jstring JNICALL Java_com_jni_MethodCall_jniCallStaticMethod(JNIEnv *, jobject); #ifdef __cplusplus } #endif #endif JNIEXPORT jstring JNICALL Java_com_jni_MethodCall_jniCallMethod1(JNIEnv * env, jobject obj) { jmethodID mid; // 方法標識id jclass cls = env->GetObjectClass(obj); // 類的對象實例 mid = env->GetMethodID(cls, "javaMethod1", "()V"); env->CallVoidMethod(obj, mid); return env->NewStringUTF("jniCallMethod1"); } JNIEXPORT jstring JNICALL Java_com_jni_MethodCall_jniCallMethod2(JNIEnv * env, jobject obj) { jmethodID mid; // 方法標識id jclass cls = env->GetObjectClass(obj); // 類的對象實例 mid = env->GetMethodID(cls, "javaMethod2", "()Ljava/lang/String;"); jstring js = (jstring) env->CallObjectMethod(obj, mid); return js; } JNIEXPORT jstring JNICALL Java_com_jni_MethodCall_jniCallStaticMethod( JNIEnv * env, jobject obj) { jmethodID mid; // 方法標識id jclass cls = env->GetObjectClass(obj); // 類的對象實例 mid = env->GetStaticMethodID(cls, "javaStaticMethod", "(Ljava/lang/String;)V"); jstring input = env->NewStringUTF("jniCallStaticMethod->>javaStaticMethod"); env->CallStaticVoidMethod(cls, mid, input); return env->NewStringUTF("jniCallStaticMethod"); }
該cpp文件中有3個方法(我這裡把方法名都寫得很明白直觀,相信不需要注釋都知道是調用的哪一個java方法)
我們知道,在java編程中,對一個類的調用,其實是先創建一個類的對象實例,然後再調用它的方法(這裡指的是非static方法) ,那麼我們是如何在c/c++文件中調用java方法的呢?
回到上面的HelloWorld,我們講方法名的時候,下邊有隨便提到的方法的參數,其中,第二個參數obj其實就是我們在java中使用的類的實例,到這裡,相信是如何調用java方法的大家都明白了吧;
在java中,每一個方法其實都有一個id,我們在c/c++中不能直接通過obj來調用一個java方法,我們要先獲取方法的id,通過GetMethodID()來獲取,需要傳入類的類型,方法名,方法的簽名(方法簽名在文章後面會講到簽名規則);然後再在線程裡面調用java方法,通過env->Call****Method();需要傳入對象實例,方法id,或者其它參數;(上面只展示了幾個這種方法,其它的方法如果大家有需要用到可以自行查找資料解決);
3.編寫Android.mk文件,在Android.mk文件後面添加如下內容
include $(CLEAR_VARS) LOCAL_MODULE := methodcall LOCAL_SRC_FILES := methodcall.cpp include $(BUILD_SHARED_LIBRARY)
4.執行ndk-build 命令,下面是分別執行3個jniCall****方法的結果
三、jni調用Java類的方法(1)
上面是c++調用java方法的例子,下面再帖一個c調用java方法的例子
1.Java文件 MethodCall1.java
package com.jni; public class MethodCall1 { static { System.loadLibrary("methodcall1"); } public static int value = 0; public static void javaMethod() { value = 12; } public native int jniCalljavaMethod(); }
2.methodcall.c
#include <string.h> #include <jni.h> jint Java_com_jni_MethodCall1_jniCalljavaMethod(JNIEnv* env, jobject thiz) //env:當前該線程的內容,包含線程全部的東西;thiz:當前類的實例,指.java文件的內容 { jint si; jfieldID fid; // 一個字段,實際上對應java類裡面的一個字段或屬性; jclass cls = (*env)->GetObjectClass(env, thiz); // 類的對象實例 jmethodID mid = (*env)->GetStaticMethodID(env, cls, "javaMethod", "()V"); // 一個方法的id //(I)V (I)I if (mid == NULL) { return -1; } (*env)->CallStaticVoidMethod(env, cls, mid); //調用callback方法 fid = (*env)->GetStaticFieldID(env, cls, "value", "I"); //取出value字段 if (fid == NULL) { return -2; } si = (*env)->GetStaticIntField(env, cls, fid); //取出字段對應的值(fid字段對應的值) return si; // return (*env)->NewStringUTF(env, "init success"); }
3.完善Android.mk文件,參照二裡面第四步;
4.運行代碼
MethodCall1 mc1 = new MethodCall1(); Log.e(tag, MethodCall1.value + "->" + mc1.jniCalljavaMethod());
四、方法簽名規則
上面是各種數據類型對應的簽名字符
方法的簽名組成:"(參數簽名)" + "返回值簽名"
例如Java方法long fun(int n, String str, int[] arr);
根據上面的簽名規則可以得到其簽名為:(ILjava/lang/String;[I)J
上面的簽名分為兩部分:括號裡面為函數的參數,參數的內容分三部分"I","Ljava/lang/String;","[I",之間沒有空格;括號外邊是函數的返回類型簽名。需要注意的是如果函數返回類型為void則其中返回類型簽名為V;
五、動態注冊函數
前面二和三都是c/c++裡面方法的名稱來映射函數,其實jni還為我們提供了動態注冊函數的功能;
1.添加java文件 DynamicRegisterMethod.java
package com.jni; public class DynamicRegisterMethod { static { System.loadLibrary("dynamicregistermethod"); } public native String dynamicRegisterMethod(); }
2.添加 c 文件
#include <string.h> #include <jni.h> #ifndef _Included_org_spring_SpringUtils #define _Included_org_spring_SpringUtils jstring JNICALL java_dynamicRegisterMethod(JNIEnv * env, jobject obj) { return (*env)->NewStringUTF(env, "dynamicRegisterMethod"); } static JNINativeMethod gmethods[] = { { "dynamicRegisterMethod", "()Ljava/lang/String;", (void*) java_dynamicRegisterMethod } }; static int registerNativeMethods(JNIEnv * env, const char* className, JNINativeMethod* gMethods, int numMethods) { jclass clazz; clazz = (*env)->FindClass(env, className); if (clazz == NULL) return JNI_FALSE; if (((*env)->RegisterNatives(env, clazz, gMethods, numMethods) < 0)) { return JNI_FALSE; } return JNI_TRUE; } static int registerNatives(JNIEnv* env) { if (!registerNativeMethods(env, "com/jni/DynamicRegisterMethod", gmethods, sizeof(gmethods) / sizeof(gmethods[0]))) { return JNI_FALSE; } return JNI_TRUE; } jint JNI_OnLoad(JavaVM* vm, void* reserved) { jint result = -1; JNIEnv* env = NULL; if ((*vm)->GetEnv(vm, (void **) &env, JNI_VERSION_1_4)) { goto fail; } if (registerNatives(env) != JNI_TRUE) { goto fail; } result = JNI_VERSION_1_4; fail: return result; } #endif
3.在Android.mk文件中進行編譯的配置(省略,參考前面的例子)
4.ndk-build編譯項目
DynamicRegisterMethod drm = new DynamicRegisterMethod(); Log.e(tag, drm.dynamicRegisterMethod());
執行結果:
可以看到通過動態注冊方法的方式,也是成功的調用了 native 方法;
六、加入鏈接庫
在程序開發過程中,會頻繁的用到調試,方式有很多種,下面要講的這一種是通過log打印信息來打印程序運行時的一些狀態數值;
修改Android.mk文件,添加一句代碼
include $(CLEAR_VARS) LOCAL_LDLIBS += -llog //LDLIBS:連接libs,後面跟的參數為需要鏈接的libs,-llog表示Android中的Log庫; include $(BUILD_SHARED_LIBRARY)
加入了log庫之後,即可通過c/c++文件直接打印log信息;
在c/c++中調用log打印輸出信息:
#include <android/log.h>
__android_log_print(ANDROID_LOG_ERROR, "hello", "livingstone"); __android_log_print(ANDROID_LOG_DEBUG, "hello", "livingstone %d" ,23);
更多Android相關信息見Android 專題頁面 http://www.linuxidc.com/topicnews.aspx?tid=11