Android NDK開發(二)——從Hello World學起

         轉載請註明出處:http://blog.csdn.net/allen315410/article/details/41805719 

        上篇文章講述了Android NDK開發的一些基本概念,以及NDK的環境搭建,相信看過的朋友NDK開發環境搭建應該是沒有問題了,還沒有搭建或者不知道怎麼搭建的朋友請點擊這裏。那麼這篇文章,我們跟剛學Java編程語言一樣,從世界知名程序“Hello World!”開始,開發出我們的第一個NDK程序。


NDK目錄簡單介紹  

        在進行NDK開發之前,我們有必須熟悉一下NDK目錄下包含哪些東西,以及這些東西對開發來說有什麼作用?那麼現在打開NDK的解壓目錄,查看一下解壓目錄下的文件:

1,samples目錄。這個目錄包含了Google爲NDK開發撰寫的一些小例子,包括本地JNI開發,圖片處理,多個庫文件開發等等,這些例子雖小但面面俱到,能看懂samples目錄下的小例子程序,那麼對於NDK開發來說,就很好應付了。

2,docs目錄。這個目錄下存放的都是Google給開發者提供的文檔,指導開發者怎樣在Android環境下進行NDK開發,這個非常重要。

3,sources目錄。由於Android是開源操作系統,作爲Android的一部分的NDK,同樣也是開源的,這個目錄下存放的是NDK源碼。

4,platforms目錄。裏面存放的是當前ndk版本所支持的所有android平臺的版本,做NDK開發的C代碼也是可以指定由某個特定版本平臺下編譯,該platforms目錄下存放的是不同版本所包含的C的庫文件和頭文件,不同版本有些微小的變化。

5,prebuilt目錄。這是提供給在Windows下開發ndk程序的一些工具集。

6,build目錄。裏面存放大量的Linux編程腳本和Windows下的批處理文件,用來完成ndk開發中的交叉編譯。  


具體開發

1,NDK開發步驟

        首先,我先列出NDK開發的簡單步驟,然後再以此爲大綱,用一個Hello World的實例講述一下NDK開發:

(1)創建一個android工程

(2)JAVA代碼中寫聲明native 方法 public native String helloFromJNI();

(3)創建jni目錄,編寫c代碼,方法名字要對應在c代碼中導入jni.h頭文件

(4)編寫Android.mk文件

(5)Ndk編譯生成動態庫

(6)Java代碼load 動態庫.調用native代碼


2,NDK開發具體實踐

     下面就按照上述的步驟建立一個HelloWorld小案例來一步一步實現NDK開發

1,創建一個Android工程,並且在Java代碼中聲明一個native方法:
public class MainActivity extends Activity {

	public native String javaFromJNI();

	@Override
	protected void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		setContentView(R.layout.activity_main);
		findViewById(R.id.button).setOnClickListener(new OnClickListener() {

			@Override
			public void onClick(View v) {
				Toast.makeText(MainActivity.this, javaFromJNI(),
						Toast.LENGTH_SHORT).show();
			}
		});
	}

}
2,創建jni目錄,編寫c代碼,方法名字要對應在c代碼中導入jni.h頭文件

#include<stdio.h>
#include<jni.h>

jstring Java_com_example_ndk_MainActivity_javaFromJNI(JNIEnv* env, jobject obj) {
	return (*(*env)).NewStringUTF(env, "hello jni!");
}
         關於這個本地的C代碼怎麼寫,還是需要一些C語言的基礎的。沒有也可以,我們可以參考一下ndk解壓目錄下的platforms\android-19\arch-arm\usr\include目錄下的jni.h文件,也就是本地C代碼需要include的那個,用記事本打開看看裏面的內容。先來說一下JNI代碼的簡單格式:

方法簽名規則:返回值類型 Java_包名_類名_native方法名(JNIEnv* env, jobject obj)
返回值類型就是JNI頭文件中事先定義好的自定義C類型,直接拿來使用即可:



其後的參數列表是固定的(JNIEnv* env, jobject obj)形式,關於JNIEnv請在下面的定義:



可以看到啊,這個JNIEnv原來是一個名作JNINativeInterface的結構體,這個結構體定義了很多的數據類型,那麼我們返回字符串的類型或者方法是哪一個呢?

  jstring     (*NewStringUTF)(JNIEnv*, const char*);
以上就是我在JNINativeInterface結構體找到的返回字符串的方法,參數爲JNINativeInterface指針和一個字符串,正如上面JNI代碼使用的那樣調用即可。

好,以上我們創建好了JNI本地代碼,我們編譯一下試試吧!打開cygwin,切換到工程目錄下,執行ndk-build命令:


仔細看一下報錯的日誌,告訴我們/jni目錄下缺少了一個叫Android.mk的文件,所以導致無法編譯。

3,編寫Android.mk文件

這個Android.mk文件怎麼寫呢?這時候我們得打開NDK的文檔來看看了,位置E:/NDK/android-ndk-r10d/docs/Start_Here.html,找到


好,我們就先在jni目錄下創建一個Android.mk的文件,將上面的這段話複製粘貼進去,將LOCAL_MODULE和LOCAL_SRC_FILES修改成我們自己的名稱:

    LOCAL_PATH := $(call my-dir)

    include $(CLEAR_VARS)

    LOCAL_MODULE    := Hello
    LOCAL_SRC_FILES := Hello.c

    include $(BUILD_SHARED_LIBRARY)
4,ndk編譯生成動態庫
然後在cygwin中編譯一下:


可以看到編譯通過了,下面刷新一下工程,就可以看到工程libs目錄下多了個libHello.so的文件,這個就是Android認識的動態庫了。



5,Java代碼load 動態庫.調用native代碼

編譯出來這個libHello.so文件後,就需要在Java代碼中加載這個.so的庫文件了,代碼很簡單,然後Toast一下看看效果:

public class MainActivity extends Activity {

	static {
		System.loadLibrary("Hello");
	}
	public native String javaFromJNI();

	@Override
	protected void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		setContentView(R.layout.activity_main);
		findViewById(R.id.button).setOnClickListener(new OnClickListener() {

			@Override
			public void onClick(View v) {
				Toast.makeText(MainActivity.this, javaFromJNI(),
						Toast.LENGTH_SHORT).show();
			}
		});
	}

}
System.loadLibrary(String 文件名);是用來加載動態庫的方法,其中參數類型是字符串,參數是Android.mk文件中LOCAL_MODULE定義的名稱。


運行效果上圖所示,到這裏,一個簡單的ndk開發的Hello World就完成了。友情提示:本示例程序不支持x86架構的cpu,測試請開啓arm模擬器!

使用javah命令幫助生成方法簽名

      已知native代碼中的方法簽名規則是這樣的:返回值類型 Java_包名_類名_native方法名(JNIEnv* env, jobject obj);但是有如以下特殊情況,Java的方法名中是可以帶下劃線“_”的,例如如下這樣的定義native方法:

public native String java_From_JNI();
假如我們按照上述的規則,在C代碼中套用,定義出這樣的C函數:

jstring Java_com_example_ndk_MainActivity_java_From_JNI(JNIEnv* env, jobject obj)
        這樣定義的方法簽名顯然是不合適的,這樣會造成編譯環境誤以爲MainActivity類下有個java內部類,其中又包含From內部類,From內部類下有個叫JNI的方法,實際上並沒有這個方法,所以編譯的時候肯定是會報錯的。那麼這個例子是個個例而已,其實按照上述的方法簽名規則來看,C語言中定義native方法比較麻煩,很容易讓人手敲失誤,導致程序運行不了,其實我們可以用JDK提供好的javah工具來自動爲我們生成方法簽名,步驟如下:

1,在windows命令模式中,切換到工程包下class字節碼文件所在的目錄下,本示例的路徑是D:\workspace-mime\NDKHelloWorld\bin\classes

先執行“ cd /d D:\workspace-mime\NDKHelloWorld\bin\classes ”命令進入到class字節碼文件的包名根目錄下

然後執行“ javah com.example.ndk.MainActivity ”

會得到如下圖的一個.h文件:

用記事本打開這個文件

/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class com_example_ndk_MainActivity */

#ifndef _Included_com_example_ndk_MainActivity
#define _Included_com_example_ndk_MainActivity
#ifdef __cplusplus
extern "C" {
#endif
/*
 * Class:     com_example_ndk_MainActivity
 * Method:    javaFromJNI
 * Signature: ()Ljava/lang/String;
 */
JNIEXPORT jstring JNICALL Java_com_example_ndk_MainActivity_javaFromJNI
  (JNIEnv *, jobject);

/*
 * Class:     com_example_ndk_MainActivity
 * Method:    java_From_JNI
 * Signature: ()Ljava/lang/String;
 */
JNIEXPORT jstring JNICALL Java_com_example_ndk_MainActivity_java_1From_1JNI
  (JNIEnv *, jobject);

#ifdef __cplusplus
}
#endif
#endif
        上面就是我們需要的方法簽名了,這就是javah工具自動爲我們生成的native頭文件,下面我們需要引用這個頭文件到工程中去。將這個頭文件直接剪切,粘貼到工程的jni的目錄下,然後重寫一個Hello.c的C代碼,將#include"com_example_ndk_MainActivity.h"放在代碼的頭部,表示引入剛剛生成好的頭文件,注:在C語言中#include<xx.h>表示引用C語言環境(編譯)自帶的頭文件,#include"xx.h"表示引用當前自定義的頭文件。引用好頭文件之後,將頭文件中的兩個方法簽名拷貝進來,實現邏輯:

#include<stdio.h>
#include<jni.h>
#include"com_example_ndk_MainActivity.h"

JNIEXPORT jstring JNICALL Java_com_example_ndk_MainActivity_javaFromJNI(
		JNIEnv* env, jobject obj) {
	return (*env)->NewStringUTF(env, "hello jni!");
}

/*
 * Class:     com_example_ndk_MainActivity
 * Method:    java_From_JNI
 * Signature: ()Ljava/lang/String;
 */
JNIEXPORT jstring JNICALL Java_com_example_ndk_MainActivity_java_1From_1JNI(
		JNIEnv* env, jobject obj) {
	return (*env)->NewStringUTF(env, "hello_jni__");
}
重新編譯:


重新編譯之後,我們clean一下工程,然後refresh一下工程,在libs目錄下就可以找到我們重新編譯的新的libHello.so文件,最後在Java代碼中實現操作(省略)。


Android.mk簡介

        一個Android.mk file用來向編譯系統描述你的源代碼。具體來說:該文件是GNU Makefile的一小部分,會被編譯系統解析一次或多次。你可以在每一個Android.mk file中定義一個或多個模塊,你也可以在幾個模塊中使用同一個源代碼文件。編譯系統爲你處理許多細節問題。例如,你不需要在你的Android.mk中列出頭文件和依賴文件。NDK編譯系統將會爲你自動處理這些問題。這也意味着,在升級NDK後,你應該得到新的toolchain/platform支持,而且不需要改變你的Android.mk文件。

#交叉編譯器在編譯C/C++代碼所依賴的配置文件,linux下makefile的語法子集
    
    #獲取當前Android.mk的路徑
    LOCAL_PATH := $(call my-dir)
    #變量的初始化操作 特點:不會重新初始化LOCAL_PATH的變量
    include $(CLEAR_VARS)
    #指定編譯後生成的.so文件名,makefile語法約定文件名加前綴lib和後綴.so
    LOCAL_MODULE    := Hello
    #指定native代碼文件
    LOCAL_SRC_FILES := Hello.c
    #指定native代碼編譯成動態庫.so或者指定編譯成靜態庫.a
    include $(BUILD_SHARED_LIBRARY)
參數介紹:

LOCAL_MODULE: 就是你要生成的庫的名字,這個名字要是唯一的.不能有空格.
                                      編譯後系統會自動在前面加上lib的頭, 比如說我們的Hello 就編譯成了libHello.so
                                      還有個特點就是如果你起名叫libHello 編譯後ndk就不會給你的module名字前加上lib了
                                      但是你最後調用的時候 還是調用Hello這個庫

LOCAL_SRC_FILES:這個是指定你要編譯哪些文件
                                         不需要指定頭文件 ,引用哪些依賴, 因爲編譯器會自動找到這些依賴 自動編譯

include $(BUILD_SHARED_LIBRARY)  BUILD_STATIC_LIBRARY
                                         .so 編譯後生成的庫的類型,如果是靜態庫.a 配置include $(BUILD_STATIC_LIBRARY)

LOCAL_CPP_EXTENSION := cc :指定c++文件的擴展名
LOCAL_MODULE    := ndkfoo 
LOCAL_SRC_FILES := ndkfoo.cc

LOCAL_LDLIBS += -llog -lvmsagent -lmpnet -lmpxml -lH264Android
                                         指定需要加載一些別的什麼庫. 

另:關於Android.mk文件的介紹和用法可以參考Google NDK提供的文檔,位置是ndk解壓目錄下的docs目錄下,Programmers_Guide/html/md_3__key__topics__building__chapter_1-section_8__android_8mk.html。



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