一步一步學習androidNDK程式設計(hello world)
上一篇部落格,已經搭建好了windows下的linux環境(cygwine),這次我們試著寫一個hello world。首先需要去android的官網下載android-ndk壓縮包,之後解壓,進入解壓後的目錄,我們發現有一個ndk-build的指令碼檔案,這個指令碼檔案就是我們用交叉編譯的檔案。我們通過 "./ndk-build" 來執行該命令,如下圖:
因為目前我們沒有編譯任何c程式碼,所以會提示"could not find application project directory"這樣的錯誤,這說明我們的ndk的環境,已經是ok的。但是如果我們每次執行該命令"ndk-build",難道都必須進入ndk的解壓縮目錄下來執行嗎?答案是"no",我們可以配置ndk的環境變數,就像配置java環境變數一樣。
我們開啟cygwine的安裝目錄,我的是在c盤下的cygwine目錄下,也是預設的目錄,在該目錄下有一個etc資料夾,在etc資料夾下有一個profile的檔案,我們開啟它
將PATH原先的路徑:"PATH="/usr/local/bin:/usr/bin:" ,我們在"bin:"之後新增ndk的解壓縮的目錄,我的是在f:盤下,如下圖:紅色標註的就是我新新增的ndk的路徑:
完成之後,儲存,然後重新開啟cygwine,在任意目錄輸入"ndk-build" ,檢視是否配置好了“ndk-build”的環境變數。
===============================================================================================
接下來,我們來寫一個hello world,首先在eclipse下新建一個android工程,這裡我取名為"helloworld",建立好工程以後,我們需要在java程式碼中申明一個native方法,如下:
public native String helloWorldNdk();
這裡注意,括號後邊不能新增大括號,因為我們只是宣告而並未實現該方法,native關鍵字,表明該方法的實現是在c語言層面實現的。
接下來我們呢需要在helloworld工程下建立一個jni目錄。在該目錄下新建一個hello.c檔案,在編寫hello.c檔案之前,我們需要用javah生成public native String helloWorldNdk(); 方法對應的標頭檔案,由於我們在MainActivity中宣告該方法,當我們用javac編譯的時候,由於用到了R檔案,導致生成位元組碼失敗,由於我們只是需要.h標頭檔案,所以我們可以新建一個java工程,來生成.h檔案,如下圖:
其中JavahTest.java中的內容,就是我們在Mainactivty中宣告的native方法,如下:
package com.test.example;
public class JavahTest {
public native String helloWorldNdk();
}
接下來,我們生成.h檔案,首先執行javac生成.class位元組碼。
javac JavahTest.java
執行以上命令,會生成.class位元組碼,接下來生成.h檔案,如下圖:
此時,會生成一個com_test_example_JavahTest.h檔案,該檔案內容如下:
#include <jni.h>
/* Header for class com_test_example_JavahTest */
#ifndef _Included_com_test_example_JavahTest
#define _Included_com_test_example_JavahTest
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class: com_test_example_JavahTest
* Method: helloWorldNdk
* Signature: ()Ljava/lang/String;
*/
JNIEXPORT jstring JNICALL Java_com_test_example_JavahTest_helloWorldNdk
(JNIEnv *, jobject);
#ifdef __cplusplus
}
#endif
#endif
其中jstring JNICALL Java_com_test_example_JavahTest_helloWorldNdk (JNIEnv *, jobject);
就是我們需要在hello.c中宣告的方法,這裡需要注意,由於我們的native方法是在MainActivity中宣告的,所以,我們需要將jstring JNICALL Java_com_test_example_JavahTest_helloWorldNdk (JNIEnv *, jobject);中的JavahTest替換成MainActivity,如下:
jstring Java_com_example_helloworld_MainActivity_helloWorldNdk(JNIEnv* env ,jobject obj)
接下來就是完整實現hello.c的程式碼,如下:
#include<stdio.h>
#include<jni.h>
jstring Java_com_example_helloworld_MainActivity_helloWorldNdk(JNIEnv* env ,jobject obj) {
return (*env)->NewStringUTF(env,"hello world");
}
可以看到這裡我們返回"hello world"字串。
記住,需要編譯該android工程中的c檔案,我們還需要編寫Android.mk檔案,同樣在jni目錄下,新建一個Android.mk檔案,內容如下:
LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
LOCAL_MODULE := hello
LOCAL_SRC_FILES := hello.c
include $(BUILD_SHARED_LIBRARY)
其中LOCAL_MODULE是我們需要編譯的模組名稱,這個名稱隨便命名的,LOCAL_SRC_FILES是我們需要編譯的原始檔
當hello.c和Android.mk檔案都建立好了以後,我們就可以編譯該hello.c檔案了,開啟cygwine,進入該android工程,執行"ndk-build"命令,即可生成libhello.so檔案,如下圖:
同時,我們發現在helloworld的android工程中,生成了以下檔案:
在libs目錄下生成的libhello.so檔案就是一個可以執行的二進位制檔案。下面我們就要在java程式碼中使用該二進位制檔案。我們通過靜態程式碼塊經該二進位制檔案載入進來。如下:
static{
System.loadLibrary("hello");
}
這裡需要注意的是:這裡的"hello",就是我們在Android.mk檔案中的 LOCAL_MODULE的值
接下來就是,調運之前生成的二進位制檔案,我們只需要在MainActivity.java中這樣寫即可
Toast.makeText(this,helloWorldNdk(),Toast.LENGTH_LONG).show();
此時,當執行我們的helloworld工程,即會彈出toast,顯示我們在hello.c中返回的字串。
到這裡,一個最基本的ndk實現就完成了,現在我在加上一個宣告的方法:
public native String hello_World_Ndk();
那麼,我們的hello.c中的方法應該怎麼寫呢?參照之前的寫法:
jstring Java_com_example_helloworld_MainActivity_hello_World_Ndk
這裡需要注意,這種寫法是錯誤的,ndk會以為我們是在MainActivity中的內部類hello,以及hello中的內部類World中的方法Ndk,這樣顯然是不對的,ndk為這種情況提供了一個標準,我們需要在方法中的每一個下劃線之後加上數字1即可,如下:
jstring Java_com_example_helloworld_MainActivity_hello_1World_1Ndk(JNIEnv* env ,jobject obj) {
return (*env)->NewStringUTF(env,"i am from china");
}
這一點,我們可以通過javah來生成,我在d:盤下新建一個MainActivity.java,內容如下:
public class MainActivity {
public native String hello_World_Ndk();
}
然後,我們進入d:盤執行如下命令,生成.h檔案,如下圖:
此時在d:盤下生成了一個MainActivity.h檔案,內容如下:
JNIEXPORT jstring JNICALL Java_MainActivity_hello_1World_1Ndk
(JNIEnv *, jobject);
可以看到是以這樣命名的。這裡有一點需要注意的是,如果我們的類是有包名的話,此時運用javah來生成.h檔案的時候,首先要將生成的.class檔案拷貝到對應的包地下,然後執行如下命令:
javah 包名.類名 這樣才可以生成.h檔案。
那麼現在呢,有了以上這些基礎之後呢,就可以為MainActivity.java中宣告的native方法直接生成.h標頭檔案了,cmd進入命令列,首先進入helloworld工程的bin/classes目錄下,執行如下命令即可:
javah com.example.helloworld.MainActivity
此時,會在bin/classes資料夾下新生成一個com_example_helloworld_MainActivity.h檔案,我們將該檔案拷貝到jni目錄下,如下圖:
這個時候,我們的hello.c就可以這樣寫了:
#include<stdio.h>
#include<jni.h>
#include "com_example_helloworld_MainActivity.h"
/*
jstring Java_com_example_helloworld_MainActivity_helloWorldNdk(JNIEnv* env ,jobject obj) {
return (*env)->NewStringUTF(env,"hello world");
}
jstring Java_com_example_helloworld_MainActivity_hello_1World_1Ndk(JNIEnv* env ,jobject obj) {
return (*env)->NewStringUTF(env,"i am from china");
}
*/
JNIEXPORT jstring JNICALL Java_com_example_helloworld_MainActivity_helloWorldNdk
(JNIEnv * env, jobject obj) {
return (*env)->NewStringUTF(env,"hello world two");
}
JNIEXPORT jstring JNICALL Java_com_example_helloworld_MainActivity_hello_1World_1Ndk
(JNIEnv * env, jobject obj) {
return (*env)->NewStringUTF(env,"i am from china two");
}
此時執行helloworld工程,toast會一次彈出"hello world two"和"i am from china two"
總結一下:
1.首先需要宣告native方法:
public native String helloWorldNdk();
public native String hello_World_Ndk();
2.然後運用javah生成對應的.h標頭檔案
3.根據.h標頭檔案,編寫hello.c程式碼
4.編寫Android.mk檔案
#交叉編譯編譯c/c++程式碼所依賴的配置檔案
#獲取當前Android.mk的路徑
LOCAL_PATH := $(call my-dir)
#變數初始化操作
include $(CLEAR_VARS)
#libhello.so 其實生成的libhello.so就是在我們這個模組的名稱前面加上lib後邊加上.so
LOCAL_MODULE := hello
LOCAL_SRC_FILES := hello.c
include $(BUILD_SHARED_LIBRARY)
5.在java中通過靜態快引入二進位制檔案:
static{
System.loadLibrary("hello");
}
原始碼下載