版權聲明:本文為部落客原創文章,轉載請标明出處。 https://blog.csdn.net/chaoyu168/article/details/51209699
Android系統不允許一個純粹使用C/C++的程式出現,它要求必須是通過Java代碼嵌入Native C/C++——即通過JNI的方式來使用本地(Native)代碼。是以JNI對Android底層開發人員非常重要。
如何将.so檔案打包到.APK
讓我們 先 從最簡單的情況開始,假如已有一個JNI實作——libxxx.so檔案,那麼如何在APK中使用它呢?
實作步驟如下:
1、在你的項目根目錄下建立libs/armeabi目錄;
2、将libxxx.so檔案copy到 libs/armeabi/下;
3、此時ADT插件自動編譯輸出的.apk檔案中已經包括.so檔案了;
4、安裝APK檔案,即可直接使用JNI中的方法;
我想還需要簡單說明一下libxxx.so的命名規則,沿襲Linux傳統,lib.so是類庫檔案名稱的格式,但在Java的System.loadLibrary(" something ")方法中指定庫名稱時,不能包括 字首—— lib,以及字尾——.so。
準備編寫自己的JNI子產品
你一定想知道如何編寫自己的xxx.so,不過這涉及了太多有關JNI的知識。簡單的說:JNI是Java平台定義的用于和宿主平台上的本地代碼進行互動的“Java标準”,它通常有兩個使用場景:1.使用(之前使用c/c++、delphi開發的)遺留代碼;2.為了更好、更直接地與硬體互動并獲得更高性能 。
1、首先建立含有native方法的Java類:
package com.okwap.testjni;
public final class MyJNI {
//native方法,
public static native String sayHello(String name);
}
2、通過javah指令生成.h檔案,内容如下(com_okwap_testjni.h檔案):
#include
#ifndef _Included_com_okwap_testjni_MyJNI
#define _Included_com_okwap_testjni_MyJNI
#ifdef __cplusplus
extern "C" {
#endif
JNIEXPORT jstring JNICALL Java_com_okwap_testjni_MyJNI_sayHello
(JNIEnv *, jclass, jstring);
#ifdef __cplusplus
}
#endif
#endif
這是一個标準的C語言頭檔案,其中的JNIEXPORT、JNICALL是JNI關鍵字(事實上它是沒有任何内容的宏,僅用于訓示性說明),而jint、jstring是JNI環境下對int及java.lang.String類型的映射。這些關鍵字的定義都可以在jni.h中看到。
3、在 com_okwap_testjni.c檔案中實作以上方法:
#include
#include
#include "com_okwap_testjni.h"
JNIEXPORT jstring JNICALL Java_com_okwap_testjni_MyJNI_sayHello(JNIEnv* env, jclass, jstring str){
//從jstring類型取得c語言環境下的char*類型
const char* name = (*env)->GetStringUTFChars(env, str, 0);
//本地常量字元串
char* hello = "你好,";
//動态配置設定目标字元串空間
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~!");
//釋放動态配置設定的記憶體
free(result);
//
return str;
}
4、編譯——兩種不同的編譯環境
以上的C語言代碼要編譯成最終.so動态庫檔案,有兩種途徑:
Android NDK :全稱是Native Developer Kit,是用于編譯本地JNI源碼的工具,為開發人員将本地方法整合到Android應用中提供了友善。事實上NDK和完整源碼編譯環境一樣,都使用Android的編譯系統——即通過Android.mk檔案控制編譯。NDK可以運作在Linux、Mac、Window(+cygwin)三個平台上。有關NDK的使用方法及更多細節請參考以下資料:
完整源碼編譯環境 :Android平台提供有基于make的編譯系統,為App編寫正确的Android.mk檔案就可使用該編譯系統。該環境需要通過git從官方網站擷取完整源碼副本并成功編譯,更多細節請參考:
http://source.android.com/index.html不管你選擇以上兩種方法的哪一個,都必須編寫自己的Android.mk檔案,有關該檔案的編寫請參考相關文檔。
JNI元件的入口函數——JNI_OnLoad()、JNI_OnUnload()
JNI元件被成功加載和解除安裝時,會進行函數回調,當VM執行到System.loadLibrary(xxx)函數時,首先會去執行JNI元件中的JNI_OnLoad()函數,而當VM釋放該元件時會呼叫JNI_OnUnload()函數。先看示例代碼:
//onLoad方法,在System.loadLibrary()執行時被調用
jint JNI_OnLoad(JavaVM* vm, void* reserved){
LOGI("JNI_OnLoad startup~~!");
return JNI_VERSION_1_4;
}
//onUnLoad方法,在JNI元件被釋放時調用
void JNI_OnUnload(JavaVM* vm, void* reserved){
LOGE("call JNI_OnUnload ~~!!");
}
JNI_OnLoad()有兩個重要的作用:
指定JNI版本:告訴VM該元件使用那一個JNI版本(若未提供JNI_OnLoad()函數,VM會預設該使用最老的JNI 1.1版),如果要使用新版本的JNI,例如JNI 1.4版,則必須由JNI_OnLoad()函數傳回常量JNI_VERSION_1_4(該常量定義在jni.h中) 來告知VM。
初始化設定,當VM執行到System.loadLibrary()函數時,會立即先呼叫JNI_OnLoad()方法,是以在該方法中進行各種資源的初始化操作最為恰當。
JNI_OnUnload()的作用與JNI_OnLoad()對應,當VM釋放JNI元件時會呼叫它,是以在該方法中進行善後清理,資源釋放的動作最為合适。
使用registerNativeMethods方法
對Java程式員來說,可能我們總是會遵循:1.編寫帶有native方法的Java類;--->2.使用javah指令生成.h頭檔案;--->3.編寫代碼實作頭檔案中的方法,這樣的“官方” 流程,但也許有人無法忍受那“醜陋”的方法名稱,RegisterNatives方法能幫助你把c/c++中的方法隐射到Java中的native方法,而無需遵循特定的方法命名格式。來看一段示例代碼吧:
//定義目标類名稱
static const char *className = "com/okwap/testjni/MyJNI";
//定義方法隐射關系
static JNINativeMethod methods[] = {
{"sayHello", "(Ljava/lang/String;)Ljava/lang/String;", (void*)sayHello},
};
jint JNI_OnLoad(JavaVM* vm, void* reserved){
//聲明變量
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;
}
//建立方法隐射關系
//取得方法長度 www.2cto.com
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;
}
建立c/c++方法和Java方法之間映射關系的關鍵是 JNINativeMethod 結構,該結構定義在jni.h中,具體定義如下:
typedef struct {
const char* name;//java方法名稱
const char* signature; //java方法簽名
void* fnPtr;//c/c++的函數指針
} JNINativeMethod
參照上文示例中初始化該結構的代碼:
//定義方法隐射關系
static JNINativeMethod methods[] = {
{"sayHello", "(Ljava/lang/String;)Ljava/lang/String;", (void*)sayHello},
};
其中比較難以了解的是第二個參數——signature字段的取值,實際上這些字元與函數的參數類型/傳回類型一一對應,其中"()" 中的字元表示參數,後面的則代表傳回值。例如"()V" 就表示void func(),"(II)V" 表示 void func(int, int),具體的每一個字元的對應關系如下:
字元 Java類型 C/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
數組則以"["開始,用兩個字元表示:
字元 java類型 c/c++類型
[Z jbooleanArray boolean[]
[I jintArray int[]
[F jfloatArray float[]
[B jbyteArray byte[]
[C jcharArray char[]
[S jshortArray short[]
[D jdoubleArray double[]
[J jlongArray long[]
上面的都是基本類型,如果參數是Java類,則以"L"開頭,以";"結尾,中間是用"/"隔開包及類名,而其對應的C函數的參數則為jobject,一個例外是String類,它對應C類型jstring,例如:Ljava/lang /String; 、Ljava/net/Socket; 等,如果JAVA函數位于一個嵌入類(也被稱為内部類),則用$作為類名間的分隔符,例如:"Landroid/os/FileUtils$FileStatus;"。
使用registerNativeMethods方法不僅僅是為了改變那醜陋的長方法名,最重要的是可以提高效率,因為當Java類别透過VM呼叫到本地函數時,通常是依靠VM去動态尋找.so中的本地函數(是以它們才需要特定規則的命名格式),如果某方法需要連續呼叫很多次,則每次都要尋找一遍,是以使用RegisterNatives将本地函數向VM進行登記,可以讓其更有效率的找到函數。
registerNativeMethods方法的另一個重要用途是,運作時動态調整本地函數與Java函數值之間的映射關系,隻需要多次調用registerNativeMethods()方法,并傳入不同的映射表參數即可。
JNI中的日志輸出
你一定非常熟悉在Java代碼中使用Log.x(TAG,“message”)系列方法,在c/c++代碼中也一樣,不過首先你要include相關頭檔案。遺憾的是你使用不同的編譯環境( 請參考上文中兩種編譯環境的介紹) ,對應的頭檔案略有不同。。
如果是在完整源碼編譯環境下,隻要include 頭檔案,就可以使用對應的LOGI、LOGD等方法了,同時請定義LOG_TAG,LOG_NDEBUG等宏值,示例代碼如下:
#define LOG_TAG "HelloJni"
#define LOG_NDEBUG 0
#define LOG_NIDEBUG 0
#define LOG_NDDEBUG 0
#include
#include
#include
jstring Java_com_inc_android_ime_HelloJni_stringFromJNI(JNIEnv* env,jobject thiz){
LOGI("Call stringFromJNI!\n");
return (*env)->NewStringUTF(env, "Hello from JNI (中文)!");
}
與日志相關的.h頭檔案,在以下源碼路徑:
myeclair\frameworks\base\include\utils\Log.h
myeclair\system\core\include\cutils\log.h
如果你是在NDK環境下編譯,則需要#include ,示例代碼如下:
#ifndef __JNILOGGER_H_
#define __JNILOGGER_H_
#include
#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
你可以
下載下傳以上頭檔案,來統一兩種不同環境下的使用差異。另外,不要忘了在你的Android.mk檔案中加入對類庫的應用,兩種環境下分别是
ifeq ($(HOST_OS),windows)
#NDK環境下
LOCAL_LDLIBS := -llog
else
#完整