Android游戲開發(fā)實踐(1)之NDK與JNI開發(fā)01
NDK是Native Developement Kit的縮寫,顧名思義,NDK是Google提供的一套原生Java代碼與本地C/C++代碼“交互”的開發(fā)工具集。而Android是運行在Dalvik虛擬機之上,支持通過JNI的方式調用本地C/C++動態(tài)鏈接庫。C/C++有著較高的性能和移植性,通過這種調用機制就可以實現(xiàn)多平臺開發(fā)、多語言混編的Android應用了。當然,這些都是基于JNI實現(xiàn)的。在游戲開發(fā)中,這種需求更是必不可少。
1、認識JNI
JNI是Java Native Interface的縮寫,也稱為Java本地接口。是JVM規(guī)范中的一部分,因此,我們可以將任何實現(xiàn)了JVM規(guī)范的JNI程序在Java虛擬機中運行。這里的本地接口,主要指的是C/C++所現(xiàn)實的接口。因此,也使得我們可以通過這種方式重用C/C++開發(fā)的代碼或模塊。
具體關于JNI的詳細介紹,可以參見JNI的官方文檔。
Java Native Interface Specification:
http://docs.oracle.com/javase/7/docs/technotes/guides/jni/spec/jniTOC.html
2、JNI的類型和數(shù)據(jù)結構
實現(xiàn)原生Java代碼與本地C/C++代碼,一個重要的環(huán)節(jié)是將原生Java的類型和數(shù)據(jù)結構映射成本地C/C++支持的相應的類型和數(shù)據(jù)結構。
(1)Java基本數(shù)據(jù)類型與原生C/C++類型對應關系如下:
| Java類型 | 本地類型 | 說明 |
|---|---|---|
| boolean | jboolean | 無符號,8位 |
| byte | jbyte | 無符號,8位 |
| char | jchar | 無符號,16位 |
| short | jshort | 有符號,16位 |
| int | jint | 有符號,32位 |
| long | jlong | 有符號,64位 |
| float | jfloat | 32位 |
| double | jdouble | 64位 |
| void | void | N/A |
(2)Java引用數(shù)據(jù)類型與原生C/C++類型對應關系如下:
| Java類型 | 本地類型 |
|---|---|
| Object | jobject |
| Class | jclass |
| String | jstring |
| Object[] | jobjectArray |
| boolean[] | jbooleanArray |
| byte[] | jbyteArray |
| char[] | jcharArray |
| short[] | jshortArray |
| int[] | jintArray |
| long[] | jlongArray |
| float[] | jfloatArray |
| double[] | jdoubleArray |
通過上面的對應關系可以發(fā)現(xiàn),本地類型的命名基本上是在Java原生類型明明的前面加上了個j,組成j-type格式的新類型命名,還是很直觀的。
(3)JNI引用類型的類關系圖,如下:

(上圖源自:Java Native Interface Specification文檔)
3、JNI函數(shù)的簽名
在函數(shù)的聲明中,由函數(shù)的參數(shù),返回值類型共同構成了函數(shù)的簽名。因此,將Java函數(shù)映射到本地C/C++中的對應也要遵循相應的規(guī)則。
(1)函數(shù)數(shù)據(jù)類型的簽名關系如下:
| Java類型 | 類型簽名 |
|---|---|
| boolean | Z |
| byte | B |
| char | C |
| short | S |
| int | I |
| long | J |
| float | F |
| double | D |
| void | V |
| full-qualified-class(全限定的類) | L |
| [] | [ |
| boolean[] | [Z |
| byte[] | [B |
| char[] | [C |
| short[] | [S |
| int[] | [I |
| long[] | [J |
| float[] | [F |
| double[] | [D |
注意:
- full-qualified-class(全限定的類):指的是引用類型,用L加全類名表示。
- 數(shù)組類型的簽名,只取中括號左半邊。
(2)JNI函數(shù)簽名格式比較
Java函數(shù)原型:
return-value fun(params1, params2, params3)
return-value:表示返回值
params:表示參數(shù)
對應函數(shù)簽名格式為:
(params1params2params3)return-value
注意:
- JNI函數(shù)簽名中間都沒逗號,沒有空格
- 返回值在
()后面- 如果參數(shù)是引用類型,那么參數(shù)應該寫為:L加全類名加分號。例如:
Ljava/lang/String;
根據(jù)這種規(guī)則,知道Java函數(shù)原型就能判斷出對應的JNI函數(shù)的簽名格式:
// 原型為:
boolean isLoading();
// 簽名格式為:
()Z
// 原型為:
void setLevel(int level);
// 簽名格式為:
(I)V
// 原型為:
char getCharFunc(int index, String str, int[] value);
// 簽名格式為:
(ILjava/lang/String;[I)C
4、JNI開發(fā)流程
1.簡要開發(fā)步驟
JNI的具體開發(fā)流程總結起來分為這么幾步:
(1)在原生java類中聲明native方法。native表明該方法為一個本地方法,由C/C++實現(xiàn)。
(2)使用javac命令將帶有聲明native方法的類,編譯成class字節(jié)碼。javac是jdk自帶的一個命令,一般在javapath/bin(javapath為java安裝目錄)路徑下。
(3)使用javah命令將編譯好的class生成本地C/C++代碼的.h頭文件。同樣,javah也是jdk自帶的一個命令。
(4)實現(xiàn).h頭文件中的方法。
(5)將本地代碼編譯成動態(tài)庫。注意,不同平臺的動態(tài)庫是不一樣的。
(6)在java工程中引用編譯好的動態(tài)庫。
2.開發(fā)實例
按照上面的開發(fā)步驟作為指導,來一步步實現(xiàn)個簡單的JNI的例子。
(1)新建名為HelloJNI的java工程,并新建一個聲明了native方法的類。(這里就以Eclipse作為開發(fā)IDE舉例了)
package com.hellojni.test;
public class HelloJni {
public native void printJni();
public static void main(String[] args) {
}
}
(2)使用javac編譯該HelloJni的類編譯為.class文件。當然,這步也可以由Eclipse來完成即可。
(3)使用javah命令生成頭文件。在命令行終端下輸入如下命令:
javah -classpath E:\workplace\java\HelloJNI\src com.hellojni.test.HelloJni
classpath:是指定加載類的路徑
com.hellojni.test.HelloJni:為完整類名。注意,不需要帶java
具體javah的使用參數(shù)介紹,可以輸入javah -help。
如果,執(zhí)行成功,會在當前目錄下生成com_hellojni_test_HelloJni.h的頭文件。
/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class com_hellojni_test_HelloJni */
#ifndef _Included_com_hellojni_test_HelloJni
#define _Included_com_hellojni_test_HelloJni
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class: com_hellojni_test_HelloJni
* Method: printJni
* Signature: ()V
*/
JNIEXPORT void JNICALL Java_com_hellojni_test_HelloJni_printJni(JNIEnv *, jobject);
#ifdef __cplusplus
}
#endif
#endif
可以看到javah自動為我們生成了一個Java_com_hellojni_test_HelloJni_printJni的方法。格式是:Java_Packagename_Classname_Methodname。
首先,這里引入了jni.h的頭文件。這個是jdk自帶的一個頭文件,一般在javapath/include(javapath為java安裝目錄)。
(4)打開VS新建一個Win32控制臺應用程序,應用程序類型選擇DLL(Win平臺動態(tài)庫為.dll)。并將生成的Java_com_hellojni_test_HelloJni_printJni.h頭文件拷貝到該工程目錄下。
然后,再將該頭文件添加到工程中。如圖:

編譯生成一下。會提示找不到
jni.h。因此,把jni.h拷貝到工程目錄下,并加入到項目中。jni.h一般在javapath/include(javapath為java安裝目錄)路徑下。
重新編譯生成下,會提示找不到jni_md.h。這個文件在,javapath/include/win32路徑下??截愒撐募偌尤牍こ?。并修改Java_com_hellojni_test_HelloJni_printJni.h頭文件。
將#include <jni.h>修改為#include "jni.h",在當前目錄下找jni.h頭文件。
新建一個hellojni.cpp的源文件。如下:
#include "stdafx.h"
#include <iostream>
#include "com_hellojni_test_HelloJni.h"
using namespace std;
JNIEXPORT void JNICALL Java_com_hellojni_test_HelloJni_printJni(JNIEnv *env, jobject obj)
{
cout<<"Hello JNI"<<endl;
}
(5)再將工程重新生成下,成功的話,會在工程的Debug目錄下生成一個HelloJni.dll的動態(tài)庫。將HelloJni.dll所在的路徑添加到環(huán)境變量,這樣每次重新生成,在任意目錄都能訪問。
(6)在java工程中引用剛生成的HelloJni.dll。并加入如下代碼:
package com.hellojni.test;
public class HelloJni {
public native void printJni();
public static void main(String[] args) {
System.loadLibrary("HelloJni");
HelloJni hello = new HelloJni();
hello.printJni();
}
}
調用System.loadLibrary來加載動態(tài)庫。注意,動態(tài)庫的名字不需要加.dll。
運行java工程,這時候會提示Exception in thread “main” java.lang.UnsatisfiedLinkError: no HelloJni in java.library.path。這時候,需要重啟下Eclipse。因為,剛配置的環(huán)境變量。重啟下,Eclipse才能識別。
重啟完畢,運行java工程??刂婆_會輸入:
Hello JNI
表明整個JNI調用成功。
第一篇就介紹這么多,大體明白了JNI的整個開發(fā)流程及基本規(guī)則。下一篇將介紹下在Android NDK環(huán)境下的交叉編譯及調用過程。