Android開發(fā)進(jìn)階:Android NDK介紹
導(dǎo)讀
為了在Android OS系統(tǒng)上開發(fā)應(yīng)用程序,Google提供了兩種開發(fā)包:SDK和NDK。你可以從Google官方查閱到有許多關(guān)于SDK 的優(yōu)秀的書籍、文章作為參考,但Google沒有提供足夠的NDK資料。在現(xiàn)有的書籍中,我認(rèn)為Cinar O.寫于2012年 的”Pro Android C++ with the NDK”值得一讀。
本文旨在幫助那些缺乏Android NDK經(jīng)驗但又想擴(kuò)充這方面知識的人們。我所關(guān)注的是JNI(本地編程接口,簡稱JNI)。本文分上下兩篇,在上篇中,會從JNI為接口開始講起;下篇會進(jìn)行回顧,并給出帶兩個文件讀寫功能的實例。
什么是 Android NDK?
Android NDK(Native Development Kit )是一套工具集合,允許你用像C/C++語言那樣實現(xiàn)應(yīng)用程序的一部分。
何時使用NDK?
Google僅在極少數(shù)情況下建議使用NDK,有如下使用場景:
- 必須提高性能(例如,對大量數(shù)據(jù)進(jìn)行排序)。
- 使用第三方庫。舉例說明:許多第三方庫由C/C++語言編寫,而Android應(yīng)用程序需要使用現(xiàn)有的第三方庫,如Ffmpeg、OpenCV這樣的庫。
- 底層程序設(shè)計(例如,應(yīng)用程序不依賴Dalvik Java虛擬機(jī))。
什么是JNI?
JNI是一種在Java虛擬機(jī)控制下執(zhí)行代碼的標(biāo)準(zhǔn)機(jī)制。代碼被編寫成匯編程序或者C/C++程序,并組裝為動態(tài)庫。也就允許了非靜態(tài)綁定用法。這提供了一個在Java平臺上調(diào)用C/C++的一種途徑,反之亦然。
JNI的優(yōu)勢
與其他類似接口(Netscape Java運(yùn)行接口、Microsoft的原始本地接口、COM/Java接口)相比,JNI主要的競爭優(yōu)勢在于:它在設(shè)計之初就確保了二進(jìn)制的兼容 性,JNI編寫的應(yīng)用程序兼容性以及在某些具體平臺上的Java虛擬機(jī)兼容性(當(dāng)談及JNI,這里并不特別針對Dalvik;JNI由Oracle開發(fā), 適用于所有Java虛擬機(jī))。這就是為什么C/C++編譯后的代碼無論在任何平臺上都能執(zhí)行。不過,一些早期版本并不支持二進(jìn)制兼容。
二進(jìn)制兼容性是一種程序兼容性類型,允許一個程序在不改變其可執(zhí)行文件的條件下在不同的編譯環(huán)境中工作。
JNI組織結(jié)構(gòu)
這張JNI函數(shù)表的組成就像C++的虛函數(shù)表。虛擬機(jī)可以運(yùn)行多張函數(shù)表,舉例來說,一張調(diào)試函數(shù)表,另一張是調(diào)用函數(shù)表。JNI接口指針僅在當(dāng)前線程中起作用。這意味著指針不能從一個線程進(jìn)入另一個線程。然而,可以在不同的線程中調(diào)用本地方法。
示例代碼:
- jdouble Java_pkg_Cls_f__ILjava_lang_String_2 (JNIEnv *env, jobject obj, jint i, jstring s)
- {
- const char *str = (*env)->GetStringUTFChars(env, s, 0);
- (*env)->ReleaseStringUTFChars(env, s, str);
- return 10;
- }
- *env — 一個接口指針。
- obj — 在本地方法中聲明的對象引用。
- i和s — 用于傳遞的參數(shù)。
原始類型(Primitive Type)在虛擬機(jī)和本機(jī)代碼進(jìn)行拷貝,對象之間使用引用進(jìn)行傳遞。VM(虛擬機(jī))要追蹤所有傳遞給本地代碼的對象引用。GC無法釋放所有傳遞給本地代碼的對象引用。與此同時,本機(jī)代碼應(yīng)該通知VM不需要的對象引用。
局部引用和全局引用
JNI定義了三種引用類型:局部引用、全局引用和全局弱引用。局部引用在方法完成之前是有效的。所有通過JNI函數(shù)返回的Java對象都是本地引 用。程序員希望VM會清空所有的局部引用,然而局部引用僅在其創(chuàng)建的線程里可用。如果有必要,局部引用可以通過接口中的DeleteLocalRef JNI方法立即釋放:
- jclass clazz;
- clazz = (*env)->FindClass(env, "java/lang/String");
- ...
- (*env)->DeleteLocalRef(env, clazz)
全局引用在完全釋放之前都是有效的。要創(chuàng)建一個全局引用,需要調(diào)用NewGlobalRef方法。如果全局引用并不是必須的,可以通過DeleteGlobalRef方法刪除:
- jclass localClazz;
- jclass globalClazz;
- ...
- localClazz = (*env)->FindClass(env, "java/lang/String");
- globalClazz = (*env)->NewGlobalRef(env, localClazz);
- ...
- (*env)->DeleteLocalRef(env, localClazz);
錯誤
JNI不會檢查NullPointerException、IllegalArgumentException這樣的錯誤,原因是:
- 導(dǎo)致性能下降。
- 在絕大多數(shù)C的庫函數(shù)中,很難避免錯誤發(fā)生。
JNI允許用戶使用Java異常處理。大部分JNI方法會返回錯誤代碼但本身并不會報出異常。因此,很有必要在代碼本身進(jìn)行處理,將異常拋給Java。在JNI內(nèi)部,首先會檢查調(diào)用函數(shù)返回的錯誤代碼,之后會調(diào)用ExpectOccurred()返回一個錯誤對象。
- jthrowable ExceptionOccurred(JNIEnv *env);
例如:一些操作數(shù)組的JNI函數(shù)不會報錯,因此可以調(diào)用ArrayIndexOutofBoundsException或ArrayStoreExpection方法報告異常。
JNI原始類型
JNI有自己的原始數(shù)據(jù)類型和數(shù)據(jù)引用類型。
Java類型 |
本地類型(JNI) |
描述 |
boolean(布爾型) | jboolean | 無符號8個比特 |
byte(字節(jié)型) | jbyte | 有符號8個比特 |
char(字符型) | jchar | 無符號16個比特 |
short(短整型) | jshort | 有符號16個比特 |
int(整型) | jint | 有符號32個比特 |
long(長整型) | jlong | 有符號64個比特 |
float(浮點(diǎn)型) | jfloat | 32個比特 |
double(雙精度浮點(diǎn)型) | jdouble | 64個比特 |
void(空型) | void | N/A |
JNI引用類型
改進(jìn)的UTF-8編碼
JNI使用改進(jìn)的UTF-8字符串來表示不同的字符類型。Java使用UTF-16編碼。UTF-8編碼主要使用于C語言,因為它的編碼用\u000表示為0xc0,而不是通常的0×00。非空ASCII字符改進(jìn)后的字符串編碼中可以用一個字節(jié)表示。
#p#
JNI函數(shù):
JNI接口不僅有自己的數(shù)據(jù)集(dataset)也有自己的函數(shù)?;仡欉@些數(shù)據(jù)集和函數(shù)需要花費(fèi)我們很多時間??梢詮墓俜轿臋n中找到更多信息:
http://docs.oracle.com/javase/6/docs/technotes/guides/jni/spec/functions.html
JNI函數(shù)使用示例
下面會通過一個簡短的示例確保你對這些資料所講的內(nèi)容有了正確的理解:
- #include <jni.h>
- ...
- JavaVM *jvm;
- JNIEnv *env;
- JavaVMInitArgs vm_args;
- JavaVMOption* options = new JavaVMOption[1];
- options[0].optionString = "-Djava.class.path=/usr/lib/java";
- vm_args.version = JNI_VERSION_1_6;
- vm_args.nOptions = 1;
- vm_args.options = options;
- vm_args.ignoreUnrecognized = false;
- JNI_CreateJavaVM(&jvm, &env, &vm_args);
- delete options;
- jclass cls = env->FindClass("Main");
- jmethodID mid = env->GetStaticMethodID(cls, "test", "(I)V");
- env->CallStaticVoidMethod(cls, mid, 100);
- jvm->DestroyJavaVM();
讓我們來逐個分析字符串:
- JavaVM — 提供了一個接口,可以調(diào)用函數(shù)創(chuàng)建、刪除Java虛擬機(jī)。
- JNIEnv — 確保了大多數(shù)的JNI函數(shù)。
- JavaVMlnitArgs — Java虛擬機(jī)參數(shù)。
- JavaVMOption — Java虛擬機(jī)選項。
JNI的_CreateJavaVM()方法初始化Java虛擬機(jī)并向JNI接口返回一個指針。
JNI_DestroyJavaVM()方法可以載入創(chuàng)建好的Java虛擬機(jī)。
線程
內(nèi)核負(fù)責(zé)管理所有在Linux上運(yùn)行的線程;線程通過AttachCurrentThread和AttachCurrentThreadAsDaemon函數(shù)附加到Java虛擬機(jī)。如果線程沒有被添加成功,則不能訪問JNIEnv。 Android系統(tǒng)不能停止JNI創(chuàng)建的線程,即使GC(Garbage Collection)在運(yùn)行釋放內(nèi)存時也不行。直到調(diào)用DetachCurrentThread方法,該線程才會從Java虛擬機(jī)脫離。
***步
你的項目結(jié)構(gòu)應(yīng)該如圖3所示:
在圖3中,所有本地代碼都存儲到一個jni的文件夾。在新建一個工程后,Libs文件夾會被分為四個子文件夾。這意味著一個子目錄對應(yīng)一種處理器架構(gòu),庫的數(shù)量取決于處理器架構(gòu)的數(shù)量。
要創(chuàng)建一個本地項目和一個Android項目可以參照以下面的步驟:
- 創(chuàng)建一個jni文件夾 — 包含本地代碼的項目源代碼根目錄。
- 創(chuàng)建一個Android.mk文件用來構(gòu)建項目。
- 創(chuàng)建一個Application.mk文件用來存儲編譯參數(shù)。雖然這不是必須的配置,但是推薦你這么做。這樣會使得編譯設(shè)置更加靈活。
- 創(chuàng)建一個ndk-build文件以此來顯示編譯過程(同樣這一步也不是必須的)。
Android.mk
就像前面提到的,Android.mk是編譯本地項目的makefile。Android.mk把代碼按照模塊進(jìn)行了劃分,把靜態(tài)庫(static library)拷貝到項目的libs文件夾,生成共享庫(shared library)和獨(dú)立的可執(zhí)行文件。
最精簡的配置示例:
- LOCAL_PATH := $(call my-dir)
- include $(CLEAR_VARS)
- LOCAL_MODULE := NDKBegining
- LOCAL_SRC_FILES := ndkBegining.c
- include $(BUILD_SHARED_LIBRARY)
讓我們來仔細(xì)看看:
- LOCAL_PATH:-$(call my-dir) — 調(diào)用函數(shù)宏my-dir返回當(dāng)前文件所在路徑。
- include $(CLEAR_VARS) — 清除所有LOCAL_PATH以外的變量。這是必須的步驟,考慮到所有編譯控制文件都位于同一個GNU MAKE執(zhí)行環(huán)境中,所有變量都是全局的。
- LOCAL_MODULE — 輸出模塊名稱。在上述例子中,輸出模塊叫做NDKBegining。但是在生成以后,會在libs文件夾中創(chuàng)建libNDKbegining庫。同 時,Android系統(tǒng)會為其添加一個前綴名lib,例如一個被命名為”foo”的共享庫模塊,將會生成”libfoo.so”文件。 但是在Java代 碼中使用庫時應(yīng)該忽略前綴名(也就是說,名稱應(yīng)該和makefile一樣)。
- LOCAL_SRC_FILE — 列出編譯所需要的源文件。
- include $(BUILD_SHARED_LIBARY) — 輸出模塊的類型。
你可以在Android.mk文件中設(shè)置自定義變量;但是必須遵守語法命名規(guī)則:LOCAL_、PRIVATE_、NDK_、APP_、my-dir。Google建議自定義示例前綴使用MY_,例如:
MY_SOURCE := NDKBegining.c
這樣就調(diào)用了一個變量$(MY_SOURCE)。變量同樣也可以被連接起來,例如:
LOCAL_SRC_FILES += $(MY_SOURCE)
Application.mk
這個makefile中定義了好幾種變量讓編譯更加靈活:
- APP_OPTM — 這個變量是可選的,用于指定程序是“release”還是“debug”。在構(gòu)建應(yīng)用程序模塊時,該變量用來優(yōu)化構(gòu)建過程。你可以在調(diào)試中指定“release”,不過“debug”支持的配置選項更多。
- APP_BUILD_SCRI為Android.mk定義了另一條路徑。
- APP_ABI — 最重要的變量之一。它指定了編譯模塊時使用的目標(biāo)處理器架構(gòu)。默認(rèn)情況下,APP_ABI會設(shè)置為“armeabi”,對應(yīng)于ARMv5TE架構(gòu)。例如, 如果要支持 ARMv7,就需要設(shè)置為“armeabi-v7a”。對于IA-32-x86和MIPS-mips這樣支持多體系架構(gòu)的系統(tǒng),應(yīng)該把 APP_ABI設(shè)置為“armeabi armeabi-v7a x86 mips”。在NDK修訂版本7或更高的版本中,可以簡單的設(shè)置APP_ABI := “all rather enumerating all the architectures”。
- APP_PLATFORM — 為目標(biāo)平臺名稱;
- APP_STL — Android提供了一個最精簡的libstdc c++運(yùn)行庫,因此開發(fā)人員使用的c++功能是非常有限的。然而使用APP_STL變量就可以使這些庫支持?jǐn)U展功能。
- NDK_TOOLCHAIN_VERSION-GCC — 選擇的GCC編譯器版本(默認(rèn)情況下設(shè)置為4.6)。
NDK-BUILDS
NDK-build是一個GNU Make的包裝容器。在NDK 4以后,ndk-build支持以下參數(shù):
- clean — 清除所有已生成的二進(jìn)制文件。
- NDK_DEBUG=1 — 生成可調(diào)式的代碼。
- NDK_LOG=1 — 顯示日志信息(用于調(diào)試)。
- NDK_HOST_32BIT=1 — 使Android系統(tǒng)支持64位版本(例如,NDK_PATH\toolchains\mipsel-linux-android-4.8\prebuilt\windows-x86_64,等等)。
- NDK_APPLICATION_MK=<file> — 指定Application.mk路徑。
在 NDK v5中,引入了NDK_DEBUG。當(dāng)NDK_DEBUG設(shè)置為“1”時,便會生成可調(diào)試版本。如果沒有設(shè)置NDK_DEBUG,ndk-build會默 認(rèn)驗證是否有在AndroidMainfest.xml文件中設(shè)置 android:debuggable=“true” 屬性。如果你使用的是NDK v8以后的版本,Google不建議你在AndoirdMainfest.xml文件中使用 android:debuggable 屬性(當(dāng)你使用“ant debug”或ADT插件生成調(diào)試版本時,會自動添加“NDK_DEBUG=1”)。
默認(rèn)情況下,設(shè)置了支持64位版本。你也可以通過設(shè)置“NDK_HOST_32BIT=1”強(qiáng)制使用一個32位的工具鏈來使用32位應(yīng)用程序。不過,谷歌仍建議使用64位的應(yīng)用程序來提升大型程序的性能。
如何建立一個項目?
這 是個令人頭疼的步驟。你要安裝CDT插件并下載cygwin或mingw編譯器和Android NDK,在Eclipse設(shè)置里配置這些東西,但***還 是不能運(yùn)行。我***次開始使用Android NDK時,配置這些東西花了我3天時間。***發(fā)現(xiàn)問題出在Cygwin編譯器身上:應(yīng)該為項目文件夾設(shè)置讀、寫、可執(zhí)行的所有權(quán)限。
現(xiàn)在可就簡單多咯!只需要照著這個鏈接到網(wǎng)址:http://developer.android.com/sdk/index.html 下載ADT包,這里面有開始編譯環(huán)節(jié)需要用到的所有東西。
從Java代碼中調(diào)用本地方法
要從Java中調(diào)用本地代碼,首先你要在Java類中定義本地方法。例如:
- native String nativeGetStringFromFile(String path) throws IOException;
- native void nativeWriteByteArrayToFile(String path, byte[] b) throws IOException
你得在方法前使用“native”關(guān)鍵字。,這樣編譯器就知道這是JNI的入口點(diǎn)。這些方法會在C/C++文件中實現(xiàn)。Google建議用 “native+x”這樣的命名方式,“x”代表著方法的實際名稱。還有,在實現(xiàn)這些方法前你還得手動生成一個頭文件。你可以手動執(zhí)行此操作或者使用 JDK的 javah工具生成頭文件。然后讓我們將進(jìn)一步探討如何不用控制臺,直接使用標(biāo)準(zhǔn)的Eclipse開發(fā)環(huán)境:
- 打開Eclipse,選擇Run -> External-tool-External -> External tools configurations。
- 新建配置。
- 指定javah.exe在jdk里的絕對路徑(例如,C:\Program Files (x86)\Java\jdk1.6.0_35\bin\javah.exe)。
- 在工作目錄中指定bin/class目錄的路徑(例如,«${workspace_loc:/NDKBegin/bin/classes}»)。
- 填入如下參數(shù):“-jni ${java_type_name}” (注意,輸入時不需要帶引號)。
現(xiàn)在你可以運(yùn)行了。你的頭文件應(yīng)該放在bin/classes目錄下。下一步,復(fù)制這些文件到本地工程的jni目錄。打開工程的配置菜單并選擇 Andorid Tools這一項 — 添加本地庫(Add Native Library)。這樣我們就可以使用jni.h頭文件中包含的函數(shù)了。在此之后,你還要創(chuàng)建一個.cpp的文件(有時候 Eclipse會默認(rèn)生成),并且方法實現(xiàn)已經(jīng)在頭文件中定義。
考慮到文章長度和可讀性,我并沒有加入簡單的代碼示例,所以你在這里找不到。如果需要,請訪問這個鏈接https://github.com/viacheslavtitov/NDKBegining。