我們在做Android開發時,有時因爲實際需要,需要調用本地的c或c++庫,這個時候就會用到JNI,JNI是Java Native Interface的簡稱,可以理解爲:java本地調用。NDK全稱:Native Development Kit,NDK是一系列工具的集合。
使用JNI是有代價的,java程序可以做到與平臺無關。但如果java程序通過JNI調用了原生的代碼(比如c/c++等),java程序就喪失了平臺無關性。最起碼需要重新編譯原生代碼部分。所以所以應用JNI需要好好權衡,不到萬不得已,請不要選擇JNI,可以選擇替代方案,比如TCP/IP進行進程間通訊等等。這也是爲什麼谷歌的Android平臺的底層雖然用JNI實現,但是他不建議開發人員用JNI來開發Android上面的應用的原因。將會喪失Android上面的應用程序平臺無關性。
JNI的簡單示例
下面就是JNI實現一個經典的“Hello World”程序。該程序在Java中通過JNI調用c函數實現“Hello World”的輸出。創建該程序分爲以下步驟:
1、創建一個Java程序(HelloWorld.java)定義原生的c/c++函數。
2、用javac編譯HelloWorld.java生成HelloWorld.class。
3、用javah帶-jni參數編譯HelloWorld.class生成HelloWorld.h文件,該文件中定義了c的函數原型。在實現c函數的時候需要。
4、創建HelloWorld.c,實現HelloWorld.h定義的函數。
5、編譯HelloWorld.c生成libHelloWorld.so。
6、在java虛擬機運行java程序HelloWorld。
1. 創建HelloWorld.java
新建項目目錄:helloworld/com/android,在此目錄下新建HelloWorld.java,內容如下:
package com.android;
class HelloWorld {
private native void print();
public static void main(String args[])
{
new HelloWorld().print();
}
static
{
System.loadLibrary("HelloWorld");
}
}
注意print方法的聲明,關鍵字native表明該方法是一個原生代碼實現的,只需要聲明,不需要實現。另外注意static代碼段的System.loadLibrary調用,這段代碼表示在程序加載的時候,自動加載libHelloWorld.so庫。(注意so文件名是libHelloWorld)
2. 編譯HelloWorld.java
使用命令行進入HelloWorld.java所在目錄,執行如下命令:
javac HelloWorld.java
則在當前目錄下回生成HelloWorld.class文件。
3. 生成HelloWorld.h
命令行下進入到helloworld目錄下,執行:
javah -classpath . com.android.HelloWorld
這樣就在helloworld目錄下生成了:com_android_HelloWorld.h文件,內容如下:
/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class com_android_HelloWorld */
#ifndef _Included_com_android_HelloWorld
#define _Included_com_android_HelloWorld
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class: com_android_HelloWorld
* Method: print
* Signature: ()V
*/
JNIEXPORT void JNICALL Java_com_android_HelloWorld_print
(JNIEnv *, jobject);
#ifdef __cplusplus
}
#endif
#endif
4. 實現HelloWorld.c
創建HelloWorld.c,內容如下:
#include <jni.h>
#include <stdio.h>
#include "com_android_HelloWorld.h"
JNIEXPORT void JNICALL Java_com_android_HelloWorld_print(JNIEnv *, jobject)
{
printf("hello world!\n");
}
5. 編譯生成庫文件,windows生成dll庫,linux生成so庫。
windows:(dll)
可以用vs2005,創建DLL工程helloworld,生成dll庫。
將com_android_HelloWorld.h放到工程目錄下,將com_android_HelloWorld.h加入到工程,改變helloworld.cpp(內容與HelloWorld.c相同),內容如下:
// helloworld.cpp : 定義 DLL 應用程序的入口點。
//
#include "stdafx.h"
#include <jni.h>
#include <stdio.h>
#include "com_android_HelloWorld.h"
JNIEXPORT void JNICALL Java_com_android_HelloWorld_print(JNIEnv *, jobject)
{
printf("hello world!\n");
}
這裏需要頭文件jni.h,所以需要在工程指定包含路徑。指定方法:項目右鍵--》屬性--》c/c++ --》附加包含目錄:D:\eclipse\jdk1.7.0\include\win32;D:\eclipse\jdk1.7.0\include,這裏需要包含這兩個目錄。
做完這些之後就可以 “重新生成解決方案” 了。這樣就獲得了helloworld.dll,因爲之前HelloWorld.java里加載的庫名是“HelloWorld”,所以在這裏需要將名字改成“HelloWorld.dll”。將HelloWorld.dll複製到helloworld目錄下(這個目錄是HelloWorld.java所在的目錄),命令行進入到helloworld目錄,執行:
java com.android.HelloWorld
這時候久違的 hello world! 出現在了屏幕上。到此一個簡單的jni工程就完成了。
linux: (so)
以上講的是在windows環境下JNI編程,更多的時候我們並不是在windows而是在linux(android平臺就是基於linux),所以我們需要編譯成.so庫文件供java層調用,那麼如何生成.so文件呢,下邊就通過一個簡單的示例來演示如何生成.so。
首先創建Android工程(這裏SDK我用的是Android 4.1),工程名:TestJNI,創建包:com.example.testjni。更改自動生成的TestJNIActivity.java,內容如下:
package com.example.testjni;
import android.os.Bundle;
import android.app.Activity;
import android.view.Menu;
import android.widget.Button;
public class TestJNIActivity extends Activity {
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_test_jni);
Button button = (Button)findViewById(R.id.button1);
String str = TestJNI.getStringFromJNI("Hello World!");
button.setText(str);
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.activity_test_jni, menu);
return true;
}
}
這裏是在Activity中放入了一個Button,通過調用getStringFromJNI()方法,獲得字符串,然後將獲得的字符串設置爲Button上顯示的字符。
main.xml內容如下:
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent" >
<TextView
android:id="@+id/textView1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerHorizontal="true"
android:layout_centerVertical="true"
android:text="@string/hello_world"
tools:context=".TestJNIActivity" />
<Button
android:id="@+id/button1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_above="@+id/textView1"
android:layout_alignRight="@+id/textView1"
android:layout_marginBottom="41dp"
android:text="Button" />
</RelativeLayout>
在testjni目錄下新建類TestJNI,內容如下:
package com.example.testjni;
public class TestJNI {
static{
System.loadLibrary("testjni");
}
public static native String getStringFromJNI(String str);
}
這裏使用了native關鍵字,native關鍵字是聲明此方法爲本地方法,不需要java實現,而是需要本地語言去實現(如c/c++等)。System.loadLibarary("testjni")這行代碼是加載本地so庫文件。文件爲:libtestjni.so,注意這裏是“testjni”。
本想直接在TestJNIActivity.java中直接寫這個native方法,但後來使用javah生成.h頭文件的時候老報錯,說“錯誤:找不到類android.app.Activity”,估計是javah時沒加載到類路徑,這個問題沒解決,待以後再解決。所以就只有另外再寫了個TestJNI類,這個類裏頭就沒有導入其他任何類。下邊就開始生成.h頭文件了,命令行進入到testjni目錄,執行:
javah -classpath bin/classes -d jni com.example.testjni.TestJNI
這樣就會當前目錄(testjni)下生成jni文件夾,jni下邊有生成的頭文件:com_example_testjni_TestJNI.h,內容如下:
/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class com_example_testjni_TestJNI */
#ifndef _Included_com_example_testjni_TestJNI
#define _Included_com_example_testjni_TestJNI
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class: com_example_testjni_TestJNI
* Method: getStringFromJNI
* Signature: ()Ljava/lang/String;
*/
JNIEXPORT jstring JNICALL Java_com_example_testjni_TestJNI_getStringFromJNI
(JNIEnv *, jclass, jstring);
#ifdef __cplusplus
}
#endif
#endif
在jni目錄下添加testjni.cpp文件,內容如下:
#include "com_example_testjni_TestJNI.h"
#include <jni.h>
#include <string.h>
#include <stdio.h>
JNIEXPORT jstring JNICALL Java_com_example_testjni_TestJNI_getStringFromJNI(
JNIEnv *env, jclass jobject, jstring string) {
jboolean jb;
char cap[256];
const char* str = env->GetStringUTFChars(string, &jb);
sprintf(cap,"C++ read string: \n%s ",str);
env->ReleaseStringUTFChars(string, str);
return env->NewStringUTF(cap);
}
在jni目錄下添加Android.mk文件,內容如下:
LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
LOCAL_MODULE := testjni
LOCAL_SRC_FILES := testjni.cpp
include $(BUILD_SHARED_LIBRARY)
LOCAL_PATH:指定路徑
include $(CLEAR_VARS):清除當前編譯環境中的變量
LOCAL_MODULE:要生成的so庫文件名,生成後是:libtestjni.so
LOCAL_SRC_FILES:需要編譯的文件
include $(BUILD_SHARED_LIBRARY):生成庫文件的類型
在jni目錄下添加Application.mk文件,內容如下:
APP_ABI := armeabi armeabi-v7a mips x86
這裏APP_ABI是指定需要編譯出的版本(cpu架構類型),這裏我把這四種類型都寫上了,這裏如果沒有Application.mk文件的話,編譯器會默認生成armeabi,就像我機器的cpu是mips,我沒有Application文件,在運行應用程序的時候就提示說找不到libtestjni.so的錯誤,這個問題糾結了好久,後來才知道是這裏的問題。
做好這些之後,我們就可以編譯了,命令行進入到testjni目錄,直接輸入:ndk-build,做這個的前提是你安裝了NDK,我的NDK是:android-ndk-r8。安裝之後還需要將android-ndk-r8目錄加入到系統環境變量path中,否則會出現找不到ndk-build命令的錯誤提示。編譯完成後,你會發現在testjni目錄下多了個libs文件夾,該文件夾下有armeabi armeabi-v7 mips x86四個文件夾,每個文件夾下對應有libtestjni.so文件。
回到你的eclipse,右鍵工程--》Refresh(刷新),右鍵--》Run As--》Android Application,這時就能看到效果了,如下圖:
到此,eclipse環境下開發一個簡單的jni工程就結束了。