1、前言
jni是java調用原生語言來進行開發的一座橋梁,原生語言一般是指c,c++語言,即jni機制可以讓java語言調用c,c++語言,也可以讓c,c++語言調用java語言。這樣的相互調用,互相結合,主要是出現在對性能要求較高的應用上。在android中,由於它的開發語言也是java,所以也可以利用原生語言進行開發,對jni的了解和使用有助於我們在做應用的時候,對於時間性能要求較高的代碼段,可以用原生語言來開發,然後java通過jni機制來調用它,就可以達到很好的效果。
總的來說,jni是一種機制,一種可以然java和原生語言互相調用的機制。
學習jni開發需要有c++或者c基礎。
2、基本環境要求
利用jni機制開發,對於開發環境是有特別要求的,因為c,c++語言的開發也需要環境的支持。
單純的android開發:jdk,sdk,eclipse,adt。
如果要在eclipse上進行jni開發,那麼需要:ndk,cygwin,ant。
本文接下來的例子都是使用eclipse的方法進行開發,使用android studio的朋友,由於編譯器自身的集成,利用jni機制進行開發會更加方便,但是為了更深入的了解jni機制,本文使用的是eclipse。
對於android的基本開發環境的安裝就不提了,jni需要的開發環境這裡就提一下,方便完全沒基礎的朋友安裝環境。
2.1 ant的安裝
ant是一個命令行構建工具,它主要是驅動目標進行任務的進行。在jni中一般是用來驅動命令。它的安裝可以去ant下載官網下載。一般下載zip壓縮包即可。然後解壓安裝到我們指定的目錄(自由的目錄位置)。最關鍵的一步是添加環境變量。
假如你的ant根目錄是E:\apache-ant-1.9.7。那麼你只需要復制這個文件夾下的bin路徑,添加到高級環境變量的path變量中。注意與path變量本身存在的值必須添加分號進行分割。
比如:
隨後我們用命令行工具輸入ant -version命令。如果安裝成功就會輸出ant的版本號。
2.2cygwin的安裝
由於android原生語言開發環境包(NDK)是基於類unix系統運行的,它包含了許多shell腳本,而它們又是不可以直接在window系統運行的,因此需要安裝cygwin模擬類unix系統的環境。cygwin的下載地址
由於筆者的安裝環境以及完成,所以並不能圖文並茂德截圖講解。為了方便大家的安裝,筆者決定直接使用參考資料的截圖。
讀者只需要按照筆者的截圖順序去做就可以安裝成功了,cygwin一定要安裝成功,否則會導致ndk運行的失敗,這也是為什麼筆者花這麼多圖的原因。
2.3 NDK的安裝
NDK即android原生語言開發環境包。下載地址:NDK的下載
我們下載好壓縮包解壓到我們指定的目錄,然後需要添加環境變量,比如筆者的NDK安裝在:E:\android-ndk-r11b。那麼只需要在path變量裡面添加此路徑即可,記得用分號分隔。
隨後我們在命令行窗口輸入ndk-build命令。如果有以下輸出說明安裝成功。
2.4 在eclipse裡面配置jni開發環境
需要指定NDK的目錄路徑。筆者的如下:
3、實踐過程進行總結
實踐才是檢驗真理的唯一標准,通過上面的環境配置,我們就可以搭建一個可以開發jni的android環境了,接下來就一步一步引導大家去開發一個jni實例。
首先新建一個空白的android項目。
我們新建一個類,專門處理native函數。筆者命名為CppUtils。內容如下:
public class CppUtils {
static {
System.loadLibrary("cppUtils");
}
/**
* 從CPP獲取字符串
*
* @return
*/
public native String getStringFromCPP();
}
這裡是簡單的從c++原生代碼中獲取字符串。
3.1 System.loadLibrary
java在java.lang.System包提供了兩個靜態方法用於加載共享庫(一種含原生語言實現的可供android程序調用的庫),分別是load,loadLibrary兩個方法,由於我們在程序啟動的時候就需要加載共享庫,因此放在靜態代碼塊中加載。這兩個方法的參數是共享庫名稱。注意共享庫為了跨平台使用,它的文件名稱會包含一些前綴,而共享庫文件的後綴是so。比如我們加載cppUtils共享庫,其實他的全名是:libcppUtils.so。
3.2 native標簽
native標簽用於告訴java編輯器,它的方法是由原生語言實現的,因此不需要去實現它。native方法用分號結束。即如果你希望一個方法用原生語言實現,那麼你就給它聲明為native方法。
3.3 生成原生語言頭文件
由於原生語言頭文件需要根據字節碼文件來進行分析,所以,在生成頭文件之前,我們必須對項目進行build。之後打開我們項目的bin/classes的文件夾,筆者的文件夾如下圖:
接著我們就要針對CppUtils.class進行分析生成頭文件,在我們對編寫原生語言頭文件的時候,最好借助工具生成,而不是手寫,這樣出錯的概率才會更低,否則很容易發生jni橋無法將java函數與原生方法聯系起來的錯誤。生成頭文件的方法,就是使用命令行工具。比如筆者這裡就是,先進入自己項目要分析的java文件的目錄下,然後生成頭文件。生成頭文件的命令如下:
javah -classpath bin/classes com.example.jnibolg.CppUtils
完整的操作過程看下圖:
然後回到eclipse中,刷新下我們的項目,我們會發現多了一個以h結尾的文件,這個就是機器生成的頭文件。
關於原聲函數的實現以及祥光頭文件,我們需要放在jni文件夾中,因此,我們接下來需要在項目中建立jni文件夾,並將相關文件放進去。
這樣我們的原生文件就生成了。
3.4 分析頭文件
接下來我們需要分析頭文件的內容,有助於幫助我們了解整個實現過程。首先我們看頭文件的代碼:
/* DO NOT EDIT THIS FILE - it is machine generated */
#include
/* Header for class com_example_jnibolg_CppUtils */
#ifndef _Included_com_example_jnibolg_CppUtils
#define _Included_com_example_jnibolg_CppUtils
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class: com_example_jnibolg_CppUtils
* Method: getStringFromCPP
* Signature: ()Ljava/lang/String;
*/JNIEXPORT jstring JNICALL Java_com_example_jnibolg_CppUtils_getStringFromCPP
(JNIEnv *, jobject);
#ifdef __cplusplus}#endif#endif
首先我們可以看到jni.h頭文件被包含了,這個頭文件包含了jni機制為了實現從java對象到原生語言的映射的規則。因此,我們一切java調用原生函數,或者原聲函數調用java,都必須通過它來實現。
其次我們關注這個函數聲明:
JNIEXPORT jstring JNICALL Java_com_example_jnibolg_CppUtils_getStringFromCPP
(JNIEnv *, jobject);
這個函數聲明說明了,它實現的是jnibolg包下的CppUtils類的getStringFromCPP方法,返回的是jstring類型,這是一個jni類型,映射到java的string類型。這裡不詳細解析jni類型映射,以免變得復雜,主要以實現一個例子了解整個流程為主。
JNIEnv 是一個指針,指向jni對象。通過它,就可以調用jni.h頭文件包含的所有函數。即,它就是一個指向jni對象的指針。
jobject表示當前函數所實現方法所屬的java對象,這裡指的是CppUtils的一個實例。
有了頭文件,那麼接下來,我們需要用到NDK了,這就需要獲取NDK的支持。
3.5 獲取NDK的支持
右擊我們的項目,找到android tools,點擊add native support,這樣就可以獲取NDK開發包的支持了。點擊之後,會需要你填寫一個名稱,這個名稱將會用作共享庫的名稱,同時這個名字也是我們CppUtils中System.loadLibrary所包含的文件名稱。
確定之後,我們看jni文件夾,會生成NDK支持的文件。
其中cppUtils.cpp是我們要進行實現的C++文件。Android.mk是NDK的makefile文件,通過他,可以將原生語言的實現生成為共享庫。我們之前安裝的cygwin目的就是支持NDK的系統構建。我們打開我們NDK的目錄,筆者的如下:
可以發現NDK有很多makefile文件,這些文件都是用於幫助構成共享庫的。
3.6 分析mk文件的內容
我們打開Android.mk文件,內容如下:
LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
LOCAL_MODULE := cppUtils
LOCAL_SRC_FILES := cppUtils.cpp
include $(BUILD_SHARED_LIBRARY)
LOCAL_PATH,這是一個用於定位源文件的宏,在Android.mk文件中,它必須是第一個變量。
include $(CLEAR_VARS),作用是清除命名沖突,因為Android構建系統在單次執行中會構建多個文件和模塊,為了避免LOCAL_
模式的變量名沖突,必須包含這條指令。
LOCAL_MODULE,指的是生成的共享庫的名稱,為了適應不同的架構,生成的共享庫會含有lib前綴。
LOCAL_SRC_FILES,指的是生成共享庫的源文件,多個源文件之間用空格隔開。
include $(BUILD_SHARED_LIBRARY)指令表示生成一個共享庫。
關於共享庫的生成,可以有更復雜的組織,比如多個共享庫依賴某個靜態庫.....這裡不詳細講解,只為了讓大家理解整個流程。我們要知道的就是,基本的生成流程都是按照上面這幾條指令順序來的。
3.7實現原生代碼
我們打開cppUtils.cpp文件,會發現是空的。如果包含了jni.h頭文件指令,我們把他刪掉,因為我們即將要實現的頭文件已經包含了jni,h頭文件,所以我們的源文件無需再次包含。
接著我們將頭文件需要實現的函數聲明復制過來,為了避免出錯,強烈建議復制過來。然後修改成下面這個樣子。
#include "com_example_jniblog_CppUtils.h"
JNIEXPORT jstring JNICALL Java_com_example_jniblog_CppUtils_getStringFromCpp
(JNIEnv * env, jobject jthis)
{
return env->NewStringUTF("來自C++");
}
3.8調用native函數
做完了上面這些,就可以函數調用了。調用和正常的java調用沒有差別的。比如這裡就是:
private TextView text;
CppUtils cppUtils = new CppUtils();
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
text = (TextView) findViewById(R.id.text);
text.setText("從C++獲取字符串:" + cppUtils.getStringFromCpp());
}
3.9 總結
從整個流程下來,我們可以清晰的知道每一步要怎麼做為什麼這麼做這麼做的意義,雖然並沒有深入講解,但是對於整個jni流程來說,是一個很好的了解。