轉載請註明出處:http://blog.csdn.net/allen315410/article/details/41805719
上篇文章講述了Android NDK開發的一些基本概念,以及NDK的環境搭建,相信看過的朋友NDK開發環境搭建應該是沒有問題了,還沒有搭建或者不知道怎麼搭建的朋友請點擊這裏。那麼這篇文章,我們跟剛學Java編程語言一樣,從世界知名程序“Hello World!”開始,開發出我們的第一個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。