初學JNI-NDK編程

        我們在做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工程就結束了。

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章