雖然Google官方推薦使用基於Java程式語言的Android SDK來開發Android App,但是Android SDK卻沒有辦法完全地開發出能在Android系統上執行的所有功能,而且也因為Java語言編譯出來的Bytecode在Runtime時需要再次直譯成機械碼,因此Android SDK效能並沒有說很好。還好Android有提供NDK,可以使用Java的JNI(Java Native Interface),讓Android的Java程式也能夠使用C/C++的Native Code。
開發環境
首先是IDE(Integrated Development Environment)的選擇,大部分的人都是使用Eclipse,所以能查到的資料也比較多,故在這裡也是使用Eclipse。為了使Eclipse能夠同時開發Android與C/C++,必須要替Eclipse安裝ADT(Android Development Tools)與CDT(C/C++ Development Tooling)插件。要讓Android能夠使用JNI,還要再安裝Android Native Development Tools,這樣Eclipse才可以支援Android NDK。當然,Android NDK也是需要的。
安裝ADT與Android Native Development Tools
在Eclipse的「Install New Software」中,使用以下來源網址,就可以找到Android的開發工具。
安裝CDT
到Eclipse的CDT網頁,就可以下載到符合自己Eclipse版本的CDT,當然也可以像ADT一樣使用Eclipse的「Install New Software」來安裝。
下載與設定Android NDK
下載Android NDK請參考這篇文章。下載並解壓縮後,要讓Eclipse知道Android NDK的位置,因此要在Eclipse的「Preferences」中,在「Android」分類下找到「NDK」,並指定NDK的根目錄路徑。
讓Android專案支援JNI
在Android專案項目上按下滑鼠右鍵,出現選單後,選擇「Android Tools」->「Add Native Support」。接著輸入函式庫的名稱,就是.so檔案的名稱。在一個Android專案下透過JNI可以使用多個不同的.so檔案,.so檔案的命名可以幫助設計師了解到這個檔案的用途為何。
替專案加入Native Support後,專案下會多了一個「jni」目錄,這個目錄便是C/C++程式的工作目錄,所有C/C++程式,或是相關的資源都可以放進這個目錄裡面。
JNI的使用方法
使用javah建立C/C++的標頭檔(*.h)
建置好支援JNI的Android專案後,先別急著開始寫C/C++程式,應該要先規劃好Java程式中哪些地方需要使用到由C/C++程式語言實作的方法或是函數,這些方法都需要先使用native修飾子來進行宣告,並且使用System類別下的loadLibrary方法來讀入指定名稱的.so函式庫。用以下程式為例:
package org.magiclen.androidjni;
import android.app.Activity;
import android.os.Bundle;
import android.widget.TextView;
public class MainActivity extends Activity {
// 宣告由C/C++實作的方法
private native String helloString(String toWhat);
// 讀取函式庫
static {
System.loadLibrary("NativeMath");
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
TextView root = new TextView(this);
root.setText(helloString("MagicJNI"));
setContentView(root);
}
}
接著再使用JDK提供的「javah」工具,來將指定類別轉換成JNI使用的標頭檔。使用終端機,將工作目錄移動到Android專案的「src」目錄下,接著使用以下指令來產生.h檔案:
例如:
執行成功後,會在目前的工作目錄下產生.h檔案。檔名的話,會將完整類別名稱的「.」改為「_」。
使用.h標頭檔來實作程式
將使用javah產生出來的.h檔案移動到「jni」目錄下,然後在別的.c或是.cpp檔案中include,並實作出其中宣告的函數。程式如下:
#include <jni.h>
#include <stdio.h>
#include "org_magiclen_androidjni_MainActivity.h"
JNIEXPORT jstring JNICALL Java_org_magiclen_androidjni_MainActivity_helloString(
JNIEnv* env, jobject obj, jstring str){
const char* toWhat = env->GetStringUTFChars(str, JNI_FALSE);
char hello[80];
sprintf(hello,"Hello, %s!", toWhat);
return env->NewStringUTF(hello);
}
編譯JNI的C/C++程式
雖然Eclipse應該會在編譯Android SDK時,自動使用NDK來編譯JNI下的C/C++程式,但是十分的不可靠,常常會遇到奇怪的問題導致專案一直編譯失敗。如果要編譯JNI的C/C++建議還是在終端機下動作。Android NDK提供「ndk-build」工具,位於NDK根目錄下,可以快速地編譯Android專案下的C/C++程式,而且使用方式很簡單,只要將終端機的工作目錄移動到Android NDK下,再執行以下指令即可開始編譯:
ndk-build跟makefile類似,不會編譯已經編過且原始碼未被改變的部份,如果想要清空先前的編譯結果,可以使用以下指令來清除:
使用NDK編出.so檔之後,就可以使用Android SDK來編譯出Android App。執行結果如下:
替不同平台編譯出不同的.so檔
預設的情況下,NDK只會編譯出armeabi架構的.so檔案,可以用在任何ARM架構的Android裝置上。如果要針對ARMv7架構優化,或是使.so檔能在x86架構或是MIPS架構上執行的話,可以在「jni」目錄下,增加「Application.mk」檔案,並且設定「APP_ABI」參數的值。
例如要編譯出ARM與x86的.so檔案,可以這樣設定:
如果要直接支援所有的架構,可以這樣設定:
有些程式碼可能會不太能在MIPS上執行,寫成以下這樣子會比較保險:
接著再使用「ndk-build」工具來編譯,就可以自動編出不同架構下的.so檔案了。
JNI下的Java資料型別
JNI下的C/C++程式如果要使用Java的資料型別,型別名稱前必須要加上「j」,例如:「int」變成「jint」,「long long」變成「jlong」,「String」變成「jstring」,都是小寫字母。這裡要注意的是,如果是物件型別(非基本資料型別)的話,只有特定幾個物件可以直接加上「j」,像是「String」或是「Array」,不然的話都是「jobject」。
若要呼叫jobject的方法要怎麼辦呢?可以使用JNIEnv的GetMethodID來取得方法的ID,再依照方法的回傳值型態來決定使用JNIEnv的哪種call。以下程式,使用jni呼叫SDK上textView.setText(String)的功能。
package org.magiclen.androidjni;
import android.app.Activity;
import android.os.Bundle;
import android.widget.TextView;
public class MainActivity extends Activity {
// 宣告由C/C++實作的方法
private native void showHelloString(TextView tv, String toWhat);
// 讀取函式庫
static {
System.loadLibrary("NativeMath");
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
TextView root = new TextView(this);
setContentView(root);
showHelloString(root, "MagicJNI");
}
}
#include <jni.h>
#include <stdio.h>
#include "org_magiclen_androidjni_MainActivity.h"
JNIEXPORT void JNICALL Java_org_magiclen_androidjni_MainActivity_showHelloString(
JNIEnv* env, jobject obj, jobject tv, jstring str) {
const char* toWhat = env->GetStringUTFChars(str, JNI_FALSE);
char hello[80];
sprintf(hello, "Hello, %s!", toWhat);
jstring helloString = env->NewStringUTF(hello);
jclass textViewClass = env->FindClass("android/widget/TextView");
jmethodID setText = env->GetMethodID(textViewClass, "setText",
"(Ljava/lang/CharSequence;)V");
env->CallVoidMethod(tv, setText, helloString);
}
NDK與JNI用起來不簡單,而且很容易造成記憶體洩漏(Memory Leak),以及程式閃退(出現Error,而不是歡樂的Exception)的狀況,使用前還是建議先詳讀Google官方提供的NDK文件。