Android游戲開發實踐(1)之NDK與JNI開發01
NDK是Native Developement Kit的縮寫,顧名思義,NDK是Google提供的一套原生Java代碼與本地C/C++代碼“交互”的開發工具集。而Android是運行在Dalvik虛擬機之上,支持通過JNI的方式調用本地C/C++動態鏈接庫。C/C++有著較高的性能和移植性,通過這種調用機制就可以實現多平臺開發、多語言混編的Android應用了。當然,這些都是基于JNI實現的。在游戲開發中,這種需求更是必不可少。
1、認識JNI
JNI是Java Native Interface的縮寫,也稱為Java本地接口。是JVM規范中的一部分,因此,我們可以將任何實現了JVM規范的JNI程序在Java虛擬機中運行。這里的本地接口,主要指的是C/C++所現實的接口。因此,也使得我們可以通過這種方式重用C/C++開發的代碼或模塊。
具體關于JNI的詳細介紹,可以參見JNI的官方文檔。
Java Native Interface Specification:
http://docs.oracle.com/javase/7/docs/technotes/guides/jni/spec/jniTOC.html
2、JNI的類型和數據結構
實現原生Java代碼與本地C/C++代碼,一個重要的環節是將原生Java的類型和數據結構映射成本地C/C++支持的相應的類型和數據結構。
(1)Java基本數據類型與原生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引用數據類型與原生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 |
通過上面的對應關系可以發現,本地類型的命名基本上是在Java原生類型明明的前面加上了個j
,組成j-type
格式的新類型命名,還是很直觀的。
(3)JNI引用類型的類關系圖,如下:
(上圖源自:Java Native Interface Specification文檔)
3、JNI函數的簽名
在函數的聲明中,由函數的參數,返回值類型共同構成了函數的簽名。因此,將Java函數映射到本地C/C++中的對應也要遵循相應的規則。
(1)函數數據類型的簽名關系如下:
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加全類名表示。
- 數組類型的簽名,只取中括號左半邊。
(2)JNI函數簽名格式比較
Java函數原型:
return-value fun(params1, params2, params3)
return-value:表示返回值
params:表示參數
對應函數簽名格式為:
(params1params2params3)return-value
注意:
- JNI函數簽名中間都沒逗號,沒有空格
- 返回值在
()
后面- 如果參數是引用類型,那么參數應該寫為:L加全類名加分號。例如:
Ljava/lang/String;
根據這種規則,知道Java函數原型就能判斷出對應的JNI函數的簽名格式:
// 原型為:
boolean isLoading();
// 簽名格式為:
()Z
// 原型為:
void setLevel(int level);
// 簽名格式為:
(I)V
// 原型為:
char getCharFunc(int index, String str, int[] value);
// 簽名格式為:
(ILjava/lang/String;[I)C
4、JNI開發流程
1.簡要開發步驟
JNI的具體開發流程總結起來分為這么幾步:
(1)在原生java類中聲明native
方法。native表明該方法為一個本地方法,由C/C++實現。
(2)使用javac
命令將帶有聲明native方法的類,編譯成class字節碼。javac
是jdk自帶的一個命令,一般在javapath/bin
(javapath為java安裝目錄)路徑下。
(3)使用javah
命令將編譯好的class生成本地C/C++代碼的.h
頭文件。同樣,javah
也是jdk自帶的一個命令。
(4)實現.h
頭文件中的方法。
(5)將本地代碼編譯成動態庫。注意,不同平臺的動態庫是不一樣的。
(6)在java工程中引用編譯好的動態庫。
2.開發實例
按照上面的開發步驟作為指導,來一步步實現個簡單的JNI的例子。
(1)新建名為HelloJNI的java工程,并新建一個聲明了native方法的類。(這里就以Eclipse作為開發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
的使用參數介紹,可以輸入javah -help
。
如果,執行成功,會在當前目錄下生成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平臺動態庫為.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
的動態庫。將HelloJni.dll
所在的路徑添加到環境變量,這樣每次重新生成,在任意目錄都能訪問。
(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來加載動態庫。注意,動態庫的名字不需要加.dll
。
運行java工程,這時候會提示Exception in thread “main” java.lang.UnsatisfiedLinkError: no HelloJni in java.library.path
。這時候,需要重啟下Eclipse。因為,剛配置的環境變量。重啟下,Eclipse才能識別。
重啟完畢,運行java工程。控制臺會輸入:
Hello JNI
表明整個JNI調用成功。
第一篇就介紹這么多,大體明白了JNI的整個開發流程及基本規則。下一篇將介紹下在Android NDK環境下的交叉編譯及調用過程。