上節講解了NDK 開發環境搭建的方法,這節講解下NDK編程的相關知識。
如何將.so文件打包到.APK
1、在你的項目根目錄下建立libs/armeabi目錄;
2、將libxxx.so文件copy到 libs/armeabi/下;
3、此時ADT插件自動編譯輸出的.apk文件中已經包括.so文件了;
4、安裝APK文件,即可直接使用JNI中的方法;
我想還需要簡單說明一下libxxx.so的命名規則,沿襲Linux傳統,lib<something>.so是類庫文件名稱的格式,但在Java的System.loadLibrary(" something ")方法中指定庫名稱時,不能包括 前綴—— lib,以及後綴——.so。
當然,如果直接在模擬器上開發的話,將生成的.so 文件直接 adb remount, adb push *.so /system/lib 即可.但是這個需要ROOT權限,感覺不實用,也不方便。
編寫自己的NDK 應用
1、首先創建含有native方法的Java類:
public class MainActivity extends Activity {
static {
System.loadLibrary("myjni");
}
public native String stringFromJni();
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
TextView textView = new TextView(this);
String tranString = stringFromJni();
textView.setText(tranString);
setContentView(textView);
}
}
2、在C文件中直接實現Native 方法
當然,也可以採用javah 生成頭文件,然後再根據相關函數名寫C源代碼文件,但是JNI的函數命名規則我們已經清楚了,直接自己寫就OK!
具體做法:
在java 工程下建立 jni 文件夾, 下面加建立 test.c 和 Android.mk 文件即可。
test.c:
#include <string.h>
#include <jni.h>
jstring Java_com_sj_ndktest_MainActivity_stringFromJni(JNIEnv * env,
jobject this) {
return (*env)->NewStringUTF(env, "Hello from My-JNI !");
}
Android.mk:
LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
LOCAL_MODULE := myjni
LOCAL_SRC_FILES := myjni.c
include $(BUILD_SHARED_LIBRARY)
編譯——兩種不同的編譯環境
2、完整源碼編譯環境 :Android平臺提供有基於make的編譯系統,爲App編寫正確的Android.mk文件就可使用該編譯系統。該環境需要通過git從官方網站獲取完整源碼副本併成功編譯,更多細節請參考:http://source.android.com/index.html
不管你選擇以上兩種方法的哪一個,都必須編寫自己的Android.mk文件,有關該文件的編寫請參考相關文檔。
這裏我們採用 NDK 環境編譯,Cygwin 下 $NDK/ndk-build 即可。完成後會發現 文件夾下多了 libs 和 obj 文件夾,這樣就對了。Eclipse 下直接Build RUN工程,構建APK,其會將so 文件自動打包,安裝,完成。
NDK命令介紹:
ndk-build
ndk-build clean --> 清空所編譯出的二進制文件們。
ndk-build -B V=1 --> 強制完全重新編譯,並顯示命令
JNI組件的常見函數——JNI_OnLoad()、JNI_OnUnload()
JNI組件被成功加載和卸載時,會進行函數回調,當VM執行到System.loadLibrary(xxx)函數時,首先會去執行JNI組件中的JNI_OnLoad()函數,而當VM釋放該組件時會呼叫JNI_OnUnload()函數。
JNI_OnLoad()有兩個重要的作用:
(1)指定JNI版本:告訴VM該組件使用那一個JNI版本(若未提供JNI_OnLoad()函數,VM會默認該使用最老的JNI 1.1版),如果要使用新版本的JNI,例如JNI 1.4版,則必須由JNI_OnLoad()函數返回常量JNI_VERSION_1_4(該常量定義在jni.h中) 來告知VM。
(2)初始化設定,當VM執行到System.loadLibrary()函數時,會立即先調用JNI_OnLoad()方法,因此在該方法中進行各種資源的初始化操作最爲恰當。
JNI_OnUnload()作用:
JNI_OnUnload()的作用與JNI_OnLoad()對應,當VM釋放JNI組件時會調用它,因此在該方法中進行善後清理,資源釋放的動作最爲合適。
本地方法註冊——RegisterNatives方法
本地方法註冊的作用:本地方法名不必固定按照 Java_包名_類名_方法名 的格式,我們完全可以自定義方法名,然後註冊到虛擬機就可以。
當Java類別透過VM呼叫到本地函數時,通常是依靠VM去動態尋找.so中的本地函數(因此它們才需要特定規則的命名格式),如果某方法需要連續呼叫很多次,則每次都要尋找一遍,所以使用RegisterNatives將本地函數向VM進行登記,可以讓其更有效率的找到函數。RegisterNatives方法的另一個重要用途是,運行時動態調整本地函數與Java函數值之間的映射關係,只需要多次調用RegisterNatives()方法,並傳入不同的映射表參數即可。
本地方法註冊實質是要建立本地方法和JAVA方法之間的映射關係,而建立c/c++方法和Java方法之間映射關係的關鍵是 JNINativeMethod 結構,該結構定義在jni.h中,具體定義如下:
/*
* used in RegisterNatives to describe native method name, signature,
* and function pointer.
*/
typedef struct {
char *name;
char *signature;
void *fnPtr;
} JNINativeMethod;
定義映射關係數組:
static JNINativeMethod methods[] = { { "produceString",
"(Ljava/lang/String;)Ljava/lang/String;", (void*) C_produceString } };
其中第一項即 name 項爲java 方法名(不需要包含包名,直接寫方法名即可);第三項爲本地方法名;第二項看着比較複雜,其代表的是函數簽名信息,括號內部是參數類型,後面是返回值類型,具體規則如下:
1)基本類型對應關係:
標識符 Jni 類型 C 類型
V void void
Z jboolean boolean
I jint int
J jlong long
D jdouble double
F jfloat float
B jbyte byte
C jchar char
S jshort short
2)基本類型數組:(則以 [ 開始,用兩個字符表示)
標識串 Jni 類型 C 類型
[Z jbooleanArray boolean[]
[I jintArray int[]
[J jlongArray long[]
[D jdoubleArray double[]
[F jfloatArray float[]
[B jbyteArray byte[]
[C jcharArray char[]
[S jshortArray short[]
3)類(class):(則以 L 開頭,以 ; 結尾,中間是用 / 隔開的 包 及 類名)
標識串 Java 類型 Jni 類型
L包1/包n/類名; 類名 jobject
例子:
Ljava/net/Socket; Socket jobject
4)例外(String 類):
標識串 Java 類型 Jni 類型
Ljava/lang/String; String jstring
5)嵌套類(類位於另一個類之中,則用$作爲類名間的分隔符)
標識串 Java 類型 Jni 類型
L包1/包n/類名$嵌套類名; 類名 jobject
例子:
Landroid/os/FileUtils$FileStatus; FileStatus jobject
具體我覺得沒比較死記硬背,碰到了去查就可以,看多了就記住了。也可以用 java提供的一個javap的工具幫助生成函數或變量的簽名信息,用法如下:
javap -s -p xxx
xxx 爲class文件,有了它,就不用記上面的類型表示了。
生成文件如下 eg:
public com.sj.ndktest.MainActivity();
Signature: ()V
public native java.lang.String stringFromJni();
Signature: ()Ljava/lang/String;
public native java.lang.String produceString(java.lang.String);
Signature: (Ljava/lang/String;)Ljava/lang/String;
public void onCreate(android.os.Bundle);
Signature: (Landroid/os/Bundle;)V
RegisterNatives具體用法代碼示例:
//定義目標類名稱
static const char *className = "com/sj/ndktest/MainActivity";
//定義方法隱射關係
static JNINativeMethod methods[] = { { "produceString",
"(Ljava/lang/String;)Ljava/lang/String;", (void*) C_produceString } };
//onLoad方法,在System.loadLibrary()執行時被調用
jint JNI_OnLoad(JavaVM* vm, void* reserved) {
LOGI("JNI_OnLoad startup~~!");
//聲明變量
jint result = JNI_ERR;
JNIEnv* env = NULL;
jclass clazz;
int methodsLenght;
//獲取JNI環境對象
if ((*vm)->GetEnv(vm, (void**) &env, JNI_VERSION_1_4) != JNI_OK) {
LOGE("ERROR: GetEnv failed\n");
return JNI_ERR;
}
assert(env != NULL);
//註冊本地方法.Load 目標類
clazz = (*env)->FindClass(env, className);
if (clazz == NULL) {
LOGE("Native registration unable to find class '%s'", className);
return JNI_ERR;
}
//建立方法隱射關係
//取得方法長度
methodsLenght = sizeof(methods) / sizeof(methods[0]);
if ((*env)->RegisterNatives(env, clazz, methods, methodsLenght) < 0) {
LOGE("RegisterNatives failed for '%s'", className);
return JNI_ERR;
}
result = JNI_VERSION_1_4;
return result;
}
NDK 日誌和調試
上面的代碼中看到LOGE 符號,這個是NDK 的日誌輸出,類似於 android 中的 log.e(...) ,其實質也是調用了 android 的日誌輸出。
NDK 使用日誌輸出必須引入頭文件 #include <android/log.h>,如果是在完整源碼環境下編譯,引入 include <utils/Log.h> 即可。
具體使用方法:
#ifndef __JNILOGGER_H_
#define __JNILOGGER_H_
#include <android/log.h>
#ifdef _cplusplus
extern "C" {
#endif
#ifndef LOG_TAG
#define LOG_TAG "MY_LOG_TAG"
#endif
#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG,LOG_TAG,__VA_ARGS__)
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO,LOG_TAG,__VA_ARGS__)
#define LOGW(...) __android_log_print(ANDROID_LOG_WARN,LOG_TAG,__VA_ARGS__)
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR,LOG_TAG,__VA_ARGS__)
#define LOGF(...) __android_log_print(ANDROID_LOG_FATAL,LOG_TAG,__VA_ARGS__)
#ifdef __cplusplus
}
#endif
#endif
/* __JNILOGGER_H_ */
調試的話,方法有兩種:一種就是上面的LOG日誌輸出,另一種利用 ndk-gdb 工具調試,使用方法與 gdb 工具一致。
Android MK 文件寫法
示例文件:
# Copyright (C) 2009 The Android Open Source Project
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
LOCAL_MODULE := myjni
LOCAL_SRC_FILES := myjni.c
LOCAL_LDLIBS := -llog
include $(BUILD_SHARED_LIBRARY)
LOCAL_LDLIBS 指的是需要鏈接的庫文件
#宏函數'my-dir',編譯系統提供,用於返回當前路徑(即包含Android.mk文件的路徑)
LOCAL_PATH -編譯時的目錄
$(call目錄,目錄….)目錄引入操作符
如該目錄下有個文件夾名稱src,則可以這樣寫$(call
src),那麼就會得到src目錄的完整路徑
include $(CLEAR_VARS) -清除之前的一些系統變量
# 當前模塊的名稱/編譯的目標對象。編譯系統會自動產生合適的前綴和後綴
LOCAL_MODULE
-編譯生成的目標對象
# 包含將要編譯打包進模塊中的C或者C++源代碼文件(無需列出頭文件和包含文件)
LOCAL_SRC_FILES-編譯的源文件
LOCAL_C_INCLUDES-需要包含的頭文件目錄
LOCAL_SHARED_LIBRARIES-鏈接時需要的外部庫
LOCAL_PRELINK_MODULE-是否需要prelink處理
# BUILD_SHARED_LIBRARY表示編譯生成共享庫,是編譯系統提供的變量,指向一個GNU Makefile腳本,
# 負責收集自從上次調用'include ($CLEAR_VARS)'以來,定義在LOCA_*變量中的所有信息,並且決定編譯什麼,如果正確去編譯。
# 另: BUILD_STATIC_LIBRARY表示生成靜態庫: lib$(LOCAL_MODULE).a; BUILD_EXECUTABLE表示生成可執行文件。
include $(BUILD_SHARED_LIBRARY)-指明要編譯成動態庫
android.mk編譯模塊添加具體方法參考:http://blog.csdn.net/yili_xie/article/details/4906865
以上就是我的總結,附上完整源代碼:
MainActivity.java:
package com.sj.ndktest;
import android.app.Activity;
import android.os.Bundle;
import android.os.Process;
import android.util.Log;
import android.view.Menu;
import android.widget.TextView;
public class MainActivity extends Activity {
static {
System.loadLibrary("myjni");
}
public native String stringFromJni();
public native String produceString(String string);
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
TextView textView = new TextView(this);
String tranString = stringFromJni();
String produString = produceString("hello ");
textView.setText(tranString + "\n" + produString);
setContentView(textView);
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.activity_main, menu);
return true;
}
@Override
public void onBackPressed() {
super.onBackPressed();
finish();
Process.killProcess(Process.myPid());
}
@Override
public void onDestroy() {
super.onDestroy();
}
}
myjni.c :
#include <string.h>
#include <jni.h>
#include <stdlib.h>
#include <assert.h>
#ifndef __JNILOGGER_H_
#define __JNILOGGER_H_
#include <android/log.h>
#ifdef _cplusplus
extern "C" {
#endif
#ifndef LOG_TAG
#define LOG_TAG "MY_LOG_TAG"
#endif
#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG,LOG_TAG,__VA_ARGS__)
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO,LOG_TAG,__VA_ARGS__)
#define LOGW(...) __android_log_print(ANDROID_LOG_WARN,LOG_TAG,__VA_ARGS__)
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR,LOG_TAG,__VA_ARGS__)
#define LOGF(...) __android_log_print(ANDROID_LOG_FATAL,LOG_TAG,__VA_ARGS__)
#ifdef __cplusplus
}
#endif
#endif
/* __JNILOGGER_H_ */
jstring Java_com_sj_ndktest_MainActivity_stringFromJni(JNIEnv * env,
jobject this) {
LOGE("123");
return (*env)->NewStringUTF(env, "Hello from My-JNI !");
}
jstring C_produceString(JNIEnv * env,
jobject this, jstring str) {
//從jstring類型取得c語言環境下的char*類型
const char* name = (*env)->GetStringUTFChars(env, str, 0);
LOGE(name);
//output:hello
//本地常量字符串
char* hello = "YEAH";
//動態分配目標字符串空間
char* result = malloc((strlen(name) + strlen(hello) + 1) * sizeof(char));
memset(result, 0, sizeof(result));
//字符串鏈接
strcat(result, hello);
strcat(result, name);
//釋放jni分配的內存
(*env)->ReleaseStringUTFChars(env, str, name);
//生成返回值對象
str = (*env)->NewStringUTF(env, " JNI~!");
//strcat(str, result);
//釋放動態分配的內存
LOGE(result);
//output:YEAHhello
free(result);
return str;
}
//定義目標類名稱
static const char *className = "com/sj/ndktest/MainActivity";
//定義方法隱射關係
static JNINativeMethod methods[] = { { "produceString",
"(Ljava/lang/String;)Ljava/lang/String;", (void*) C_produceString } };
//onLoad方法,在System.loadLibrary()執行時被調用
jint JNI_OnLoad(JavaVM* vm, void* reserved) {
LOGI("JNI_OnLoad startup~~!");
//聲明變量
jint result = JNI_ERR;
JNIEnv* env = NULL;
jclass clazz;
int methodsLenght;
//獲取JNI環境對象
if ((*vm)->GetEnv(vm, (void**) &env, JNI_VERSION_1_4) != JNI_OK) {
LOGE("ERROR: GetEnv failed\n");
return JNI_ERR;
}
assert(env != NULL);
//註冊本地方法.Load 目標類
clazz = (*env)->FindClass(env, className);
if (clazz == NULL) {
LOGE("Native registration unable to find class '%s'", className);
return JNI_ERR;
}
//建立方法隱射關係
//取得方法長度
methodsLenght = sizeof(methods) / sizeof(methods[0]);
if ((*env)->RegisterNatives(env, clazz, methods, methodsLenght) < 0) {
LOGE("RegisterNatives failed for '%s'", className);
return JNI_ERR;
}
result = JNI_VERSION_1_4;
return result;
}
//onUnLoad方法,在JNI組件被釋放時調用
void JNI_OnUnload(JavaVM* vm, void* reserved) {
LOGE("call JNI_OnUnload ~~!!");
}
Android.mk :
# Copyright (C) 2009 The Android Open Source Project
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
LOCAL_MODULE := myjni
LOCAL_SRC_FILES := myjni.c
LOCAL_LDLIBS := -llog
include $(BUILD_SHARED_LIBRARY)
參考地址,在此表示感謝:
http://mobile.51cto.com/android-267538_2.htm
http://blog.sina.com.cn/s/blog_4c451e0e0101339i.html
http://www.cnblogs.com/bastard/archive/2012/05/19/2508913.html
http://jykenan.iteye.com/blog/1140965
http://blog.csdn.net/gongyangyang100/article/details/7418436