Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> 關於Android編程 >> Android---簡單的JNI實例

Android---簡單的JNI實例

編輯:關於Android編程

一、JNI概述
JNI 是Java Native Interface的縮寫,中文翻譯為“Java本地調用”,JNI 是本地編程接口。它使得在 Java 虛擬機 (VM) 內部運行的 Java 代碼能夠與用其它編程語言(如 C、C++ 和匯編語言)編寫的應用程序和庫進行互操作。就是說,JNI是一種技術,通過這種技術可以做到兩點:
1)Java程序中的函數可以調用Native語言寫的函數,Native一般指的是C/C++編寫的函數。

2)Native程序中的函數可以調用Java層的函數,也就是說C/C++程序可以調用Java函數。


二、JNI應用

1、JNI本地調用:(以例子介紹)

java:(HelloWorld.java)

class HelloWorld {
	private native void print();
	public static void main(String[] args){
		new HelloWorld().print();
	}

	static {
		System.loadLibrary("HelloWorld");
	}
}

新建一個java文件,可以看出第二行有個native,說明這個函數是java和其他語言(c/c++)協作時用的,並用其他語言實現。在文件中有 System.loadLibrary,它是對本地方法的加載,原則上是在調用native函數前,任何時候,任何地方加載都可以,它的參數是動態庫的名字。
然後需要用命令javac HelloWorld.java生成HelloWorld.class文件,
下一步是利用javah -jni -classpath . HelloWorld生成對應C/C++的HelloWorld.h文件,設置classpath的目的在於告訴java環境,在哪些目錄下可以找到所要執行的java程序所需要的類或者包,此處不用聲明.class文件名,在對應目錄下編譯器會自動找到。查看.h文件:

*******
/* 
 * Class:     HelloWorld 
 * Method:    print 
 * Signature: ()V 
 */ 
JNIEXPORT void JNICALL Java_HelloWorld_print 
  (JNIEnv *, jobject);
*******

注釋部分是對應文件的類、native方法和方法簽名。方法的簽名是由方法的參數和返回值類型共同構成的,如.h文件中,Signature:()V,其中()中代表的是方法參數,V代表的是方法無返回值。java程序中參數類型和Signature有不同的對應值,可網上查閱,此處不贅述。
此處對應java中方法的jni函數,JNIEXPORT void JNICALL Java_HelloWorld_print (JNIEnv *, jobject);這裡的JNIEXPORT和JNICALL都是JNI關鍵字,表示此函數是要被JNI調用的。
JNIEnv是一個與線程相關的代表JNI環境的結構體,用來調用JAVA的函數或操作Jobject對象等

\

第二個參數的意義取決於該方法是靜態還是實例方法,當本地方法作為一個實例方法時,第二個參數相當於對象本身,即this,當本地方法作為一個靜態方法時,指向所在類。
C:(HelloWorld.c)
<喎?/kf/ware/vc/" target="_blank" class="keylink">vcD4KPHA+PC9wPgo8cHJlIGNsYXNzPQ=="brush:java;">#include #include #include "HelloWorld.h" JNIEXPORT void JNICALL Java_HelloWorld_print(JNIEnv *env, jobject obj) { printf("Hello World!\n"); return; } 此時.c文件中的函數要跟.H文件中一樣,這樣在JAVA調用時才能根據庫找到對應的JNI函數,進而調用執行該函數。
下面是生成動態庫。
$:gcc -I/usr/lib/jvm/java-6-sun/include/linux/ -I/usr/lib/jvm/java-6-sun/include/ -fPIC -shared -o libHelloWorld.so HelloWorld.c
gcc是c程序的編譯命令,-I用來指定路徑下的相關頭文件,最後執行java HelloWorld就可以完成調用。
2、android下jni調用(eclipse)
(1)ubuntu下Android NDK開發環境搭建(僅參考我本地搭建方法)
①下載NDK,http://dl.google.com/android/ndk/android-ndk-r4b-linux-x86.zip
②解壓到當前目錄
$pwd <--查看當前目錄 ~/android-ndk-r4b
③配置環境變量
$gedit ~/.bashrc :在打開的文件末尾添加以下內容
NDK=~/android-ndk-r4d
export NDK
④在當前環境下讀取並執行~/.bashrc中的命令:
$source ~/.bashrc
⑤查看是否生效
$echo $NDK
~/android-ndk-r4b/
(2)注冊JNI函數
①靜態注冊
java:

package com.example.hellojni; 

import android.app.Activity; 
import android.widget.TextView; 
import android.os.Bundle; 

public class HelloJni extends Activity 
{ 
    @Override 
    public void onCreate(Bundle savedInstanceState) 
    { 
        super.onCreate(savedInstanceState); 

        TextView  tv = new TextView(this); 
        tv.setText( stringFromJNI() ); 
        setContentView(tv); 
    } 
    public native String  stringFromJNI(); 
    static { 
        System.loadLibrary("hello-jni"); 
    } 
}
此時會在工程目錄中/bin/classes/com/example/hellojni中會自動生成.class文件,
此時可以利用命令javah -jni -classpath bin/classes/ com.example.hellojni.HelloJni生成.h文件,可見當前文件夾下生成了一個有包名類名組成的com_example_hellojni_HelloJni.h文件,
新建一個jni文件夾,並新建一個hello-jni.c和Android.mk。
Android.mk

LOCAL_PATH := $(call my-dir) 
include $(CLEAR_VARS) 
LOCAL_MODULE    := hello-jni 
LOCAL_SRC_FILES := hello-jni.c 
include $(BUILD_SHARED_LIBRARY)
代碼如上,android.mk是android提供的一種makefile文件,用來指定編譯生成的so庫名、引用的頭文件等。LOCAL_PATH 用於給出當前文件的路徑;LOCAL_MODULE是模塊的名字,必須唯一切不能包含空格;LOCAL_SRC_FILES指定要編譯的源文件列表;LOCAL_SHARED_LIBRARIES則表示模塊在運行時要以來的共享庫,在鏈接時需要。
hello-jni.c

#include  
#include  
jstring 
Java_com_example_hellojni_HelloJni_stringFromJNI( JNIEnv* env, 
                                                  jobject thiz ) 
{ 
    return (*env)->NewStringUTF(env, "Hello from JNI !"); 
}
這個文件中的函數名必須和生成的.h文件中的函數名一致,否則可能會無法找到,也就是Java、對應的包名類名和函數名組成了新函數名。
執行ndk:/home/wesley/android-ndk-r4b/ndk-build,在obj文件中生成庫,在eclipse中全部加載運行就可以在模擬器中顯示。
總結:當Java層調用 stringFromJNI函數時,它會從對應的JNI庫中尋找Java_com_example_hellojni_HelloJni_stringFromJNI函數,如果沒有,便回報錯,如果找到,則會為這個stringFromJNI和Java_com_example_hellojni_HelloJni_stringFromJNI建立一個關聯關系,其實就是保存JNI層函數的函數指針,以後調用stringFromJNI函數時,直接使用這個函數指針就可以了,當然這個工作是由虛擬機來完成的。
②動態注冊
從靜態注冊中可以看出來,它有幾個弊端,首先就是需要編譯所有聲明了native函數的Java類,每個所生成的class文件都得用javah生成頭文件;其次javah生成的JNI層函數名特別長,書寫不便;最後初次調用native函數時要根據函數名搜索對應的JNI層函數來建立關聯關系,這樣很影響效率。故動態注冊,直接讓native函數知道JNI層對應函數的函數指針。
上面的工程改成動態注冊只要改C文件:
HelloWorld.c:
#include 
#include 
#include 
#include 
#include 

jstring native_hello(JNIEnv* env, jobject thiz)
{
	return (*env)->NewStringUTF(env, "動態注冊JNI");
}

/*
*方法對應表
*/
static JNINativeMethod gMethods[] = {
	{"stringFromJNI","()Ljava/lang/String;", (void*)native_hello},
};

/*
*為某一個類注冊本地方法
*/
static int registerNativeMethods(JNIEnv* env, const char* className, 
				 JNINativeMethod* gMethods, int numMethods){
	jclass clazz; 
	/*
	env指向一個JNIEnv結構體,classname對應為Java類名,由於JNINativeMethod中	使用的函數不是全路徑名,所以要指明是哪個類。
	*/
	clazz = (*env)->FindClass(env, className);
	if(clazz == NULL){
		return JNI_FALSE;
	}

	//實際上是調用JNIEnv的RegisterNatives函數完成注冊
	if((*env)->RegisterNatives(env, clazz, gMethods, numMethods) < 0){
		return JNI_FALSE;
	}
	return JNI_TRUE;
}

/*
*為所有類注冊本地方法
*/
static int registerNatives(JNIEnv* env){
	const char* kClassName = "com/example/hellojni/HelloJni";
	return registerNativeMethods(env, kClassName, gMethods, 
				sizeof(gMethods)/sizeof(gMethods[0]));
}

/*
*System.loadLibrary("lib")時調用,如果成功返回JNI版本,失敗返回-1
*該函書的第一個參數類型為JavaVM,是虛擬機在JNI層的代表
*每個Java進程只有一個這樣的JavaVM
*/
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 -1;
	}
	assert(env != NULL);
	
	//動態注冊JNI函數
	if(!registerNatives(env)){
		return -1;
	}
	result = JNI_VERSION_1_4;
	
	return result;
}
在jni.h中定義了JNINativeMethod結構體,利用這個結構體就可以把java中native函數和此處的函數名對應。那為什麼需要這個簽名信息呢?因為Java支持函數重載,也就是說,可以定義同名但不同參數的函數。但僅僅根據函數名是沒法找到具體函數的,JNI技術中就將參數類型和返回值類型的組合成為了一個函數的簽名信息,有了簽名信息和函數名,就能順利地找到Java中的函數了。
typedef struct {
	//java中native函數的名字,不用帶包路徑
char *name; //Java函數的簽名信息。
char *signature; //JNI層對應的函數指針
void *fnPtr;
} JNINativeMethod;
此結構體中的signature可以通過命令來取得,在class文件所在的目錄執行javap -p-s HelloWorld。
然後加載庫就可以完成動態注冊。
總結:當Java層通過System.loadLibrary加載萬JNI動態庫後,緊接著會查找該庫中一個叫JNI_OnLoad的函數,如果有,就調用它,而動態注冊的工作就是從這裡完成的。

(3)通過JNIEnv操作jobject:
java對象是由成員變量和成員函數組成的,操作jobject的本質就應當是操作這些對象的成員變量和成員函數。它們是類的屬性,所以在JNI規則中,用jfieldID和jmethodID來表示Java累的成員變量和成員函數,可通過JNIEnv的下面兩個函數得到:
jfieldID GetFieldID(jclass clazz, const char *name, const char *sig)
jmethodID GetMethodID(jclass clazz, const char *name, const char *sig)
其中,jclass代表Java類,name表示成員函數或成員變量的名字,sig為這個函數和變量的簽名信息。將獲取的ID可以保存以下,這樣可以提高效率。
(4)垃圾回收
①Local Reference:本地引用。在JNI層函數中使用的非全局引用對象都是ocal Reference,它包括函數調用時傳入的jobject和JNI層函數中創建的jobject。ocal Reference最大的特點就是一旦JNI層函數返回,這些jobject就可能被垃圾回收。
②Global Reference:全局引用,這種對象如不住動釋放,它永遠不會被垃圾回收。
③Weak Global Reference:弱全局引用,一種特殊的Global Reference,在運行過程中可能會被垃圾回收,所以在使用之前,需JNIEnv的IsSameObject判斷是否被回收了。



  1. 上一頁:
  2. 下一頁:
熱門文章
閱讀排行版
Copyright © Android教程網 All Rights Reserved