NDK-JNI實戰教程(三) 從比Hello World稍復雜點兒的NDK例子說說模板
【工匠若水 http://blog.csdn.net/yanbober】 閱讀前一篇《NDK-JNI實戰教程(二) JNI官方中文資料》 http://blog.csdn.net/yanbober/article/details/45310365
第一部分
概述
學習JNI NDK你需要有java與C或者C++基礎。因為NDK幾乎就是java與C或者C++的混合編程互調,JNI在其中只是扮演了一個不同語種間對接握手調運的規則而已。就像C語言嵌入調運執行匯編程序一樣,需要一種規則來約束溝通。這個例子是我在閑時繼續使用Android Studio擼的,不難,適合入門。不要一下子被這么幾個文件嚇著了。重點是為了通過這個例子引出來幾個Android NDK開發的重要基礎模板知識點。所以內在代碼邏輯看上去可能十分僵硬不合理,代碼風格可能也不是十分規范,還請多多指點交流,然后擼的更多。
需要知識點:C語言基礎,C語言動態參數宏,Java基礎,JNI基本概念
代碼及工程文件介紹
這個例子是一個簡單的場景模擬實現;我們通過在app java層傳入一個name到c庫中,c庫通過app傳入的name經過保密的自定義加密算法(本代碼沒實現,只是模擬)處理生成一個客戶化定制的key反饋給app層使用。這樣至于通過name得到key的具體加密機制被編譯成了so文件,很難被破解。而如果使用java則很容易被破解。
這是這篇文章要介紹的代碼工程的幾個主要文件夾文件分布情況:
淺析:正常NDK工程目錄結構,其中jni目錄下只是多包涵了兩個文件夾而已。在這里在jni根目錄下的兩個文件就是jni核心文件,起到C與Java的互聯互通作用;utils目錄是我自己加入的一個常用工具目錄,里面放置一些通用代碼,譬如這里的android_log_print.h用來打印log;local_logic_c目錄是我放置的用C語言實現的加密邏輯代碼,其中包含實現和頭文件。你的jni目錄結構也可以隨意組織,符合自己習慣效率就行。在這里需要注意的一點是Android JNI下面c代碼使用printf打印是不顯示的,所以才需要像我加入的宏,使用android提供的log打印函數,不過在編譯時請記得加入log依賴的官方lib。
io.github.yanbober.ndkapplication包中MainActivity主Activity代碼:
package io.github.yanbober.ndkapplication; import android.os.Bundle; import android.support.v7.app.ActionBarActivity; import android.widget.TextView; public class MainActivity extends ActionBarActivity { private TextView mTextView; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); mTextView = (TextView) this.findViewById(R.id.test); NdkJniUtils jni = new NdkJniUtils(); //傳入name="vip"到jni代碼模擬拿到加密后的key mTextView.setText(jni.generateKey("vip")); } }
淺析:這就是App的傳統界面了,一個UI傳入name=”vip”,調運native方法取到轉換好的key顯示在TextView里,沒啥技術難度。
io.github.yanbober.ndkapplication包中NdkJniUtils類代碼:
package io.github.yanbober.ndkapplication; public class NdkJniUtils { public native String generateKey(String name); static { System.loadLibrary("YanboberJniLibName"); } }
淺析:這個類就是定義本地native方法,編譯以后通過javah生成這個文件的h頭文件,如下文。其中static塊作用就不說了吧。System.loadLibrary(“YanboberJniLibName”);就是加載你編譯生成的庫文件,注意庫生成在lib目下默認會添加lib前綴,形如:libXxx.so,我們在load函數里傳入的名字只需要Xxx就行。
jni根目錄下通過系列教程一中javah生成的頭文件io_github_yanbober_ndkapplication_NdkJniUtils.h內容:
/* DO NOT EDIT THIS FILE - it is machine generated */ #include <jni.h> #ifndef _Included_io_github_yanbober_ndkapplication_NdkJniUtils #define _Included_io_github_yanbober_ndkapplication_NdkJniUtils #ifdef __cplusplus extern "C" { #endif JNIEXPORT jstring JNICALL Java_io_github_yanbober_ndkapplication_NdkJniUtils_generateKey(JNIEnv *, jobject, jstring); #ifdef __cplusplus } #endif #endif
淺析:通過javah生成的頭文件,不明白的參考系列教程一中。
jni根目錄下通過系列教程一中類似test生成的jni接口c文件jni_interface.c內容:
#include <jni.h> #include <string.h> #include "io_github_yanbober_ndkapplication_NdkJniUtils.h" #include "./utils/android_log_print.h" #include "./local_logic_c/easy_encrypt.h" JNIEXPORT jstring JNICALL Java_io_github_yanbober_ndkapplication_NdkJniUtils_generateKey (JNIEnv *env, jobject obj, jstring name){ //聲明局部量 char key[KEY_SIZE] = {0}; memset(key, 0, sizeof(key)); char temp[KEY_NAME_SIZE] = {0}; //將java傳入的name轉換為本地utf的char* const char* pName = (*env)->GetStringUTFChars(env, name, NULL); if (NULL != pName) { strcpy(temp, pName); strcpy(key, generateKeyRAS(temp)); //java的name對象不需要再使用,通知虛擬機回收name (*env)->ReleaseStringUTFChars(env, name, pName); } return (*env)->NewStringUTF(env, key); }
淺析:jni”接口封裝實現”文件,我就叫這名吧,可能好理解些,別把jni想的太高大上。這里面就是實現h文件聲明的函數。一些基本參數可以查閱系列教程二文檔,復制關鍵字在教程二里搜索查閱即可。主要流程就是通過GetStringUTFChars拿到java傳入的String的name轉換后的char* utf-8指針;把name通過generateKeyRAS傳入C語言實現的加密邏輯代碼中處理,同時通過ReleaseStringUTFChars告訴虛擬機不需要持有name的引用,以便Java釋放String的name;完事將C語言處理生成的key通過NewStringUTF轉換返回給java層使用。
jni目錄下utils子目錄下的log打印工具宏android_log_print.h文件內容:
/* * 作者:工匠若水 * 說明:Android JNI Log打印宏定義文件 */ #ifndef _ANDROID_LOG_PRINT_H_ #define _ANDROID_LOG_PRINT_H_ #include <android/log.h> #define IS_DEBUG #ifdef IS_DEBUG #define LOG_TAG ("CUSTOMER_NDK_JNI") #define LOGV(...) ((void)__android_log_print(ANDROID_LOG_VERBOSE, LOG_TAG, __VA_ARGS__)) #define LOGD(...) ((void)__android_log_print(ANDROID_LOG_DEBUG , LOG_TAG, __VA_ARGS__)) #define LOGI(...) ((void)__android_log_print(ANDROID_LOG_INFO , LOG_TAG, __VA_ARGS__)) #define LOGW(...) ((void)__android_log_print(ANDROID_LOG_WARN , LOG_TAG, __VA_ARGS__)) #define LOGE(...) ((void)__android_log_print(ANDROID_LOG_ERROR , LOG_TAG, __VA_ARGS__)) #else #define LOGV(LOG_TAG, ...) NULL #define LOGD(LOG_TAG, ...) NULL #define LOGI(LOG_TAG, ...) NULL #define LOGW(LOG_TAG, ...) NULL #define LOGE(LOG_TAG, ...) NULL #endif #endif
淺析:這個文件是我自己寫JNI時每次直接使用的文件,就是一個工具文件一樣。目的是因為Android的JNI使用printf函數打印的東西是沒法顯示,這里這么轉化其實對應的就是java層打印Log的函數Log.d(), Log.i(), Log.w(),Log.e(), Log.f()。原因是因為Android的java層和C++ framework層都提供了Log函數,但是JNI環境下打印稍有不同,使用的是__android_log_print并且用NDK環境編譯和android源碼framework環境編譯選擇鏈接Android.mk庫也不同。所以你會發現Google NDK官方sample代碼中也是類似處理的,這里只是簡單封裝的更實用而已。需要一點C語言知識理解。如果你喜歡再往深里折騰,那我再提一點吧,那就是自己去android系統源碼的system/core/include/cutils/log.h去看看吧,如果是在完整源碼編譯環境下,只要include
jni目錄下local_logic_c子目錄中本地C語言實現的邏輯目錄下的接口頭文件easy_encrypt.h內容:
#ifndef _EASY_ENCRYPT_H_ #define _EASY_ENCRYPT_H_ /* * 作者:晏博(工匠若水) * * 功能:通過name獲取加密后的key * 類型:測試代碼 */ #define KEY_NAME_SIZE (6) #define KEY_SIZE (129) char* generateKeyRAS(char* name); #endif /* _EASY_ENCRYPT_H_ */
淺析:這就是標準的C語言模塊了,這是邏輯的h文件,不解釋。
jni目錄下local_logic_c子目錄中本地C語言實現的邏輯目錄下的接口邏輯實現文件easy_encrypt.c內容:
#include <string.h> #include "easy_encrypt.h" #include "./../utils/android_log_print.h" /* * 功能:通過傳入name生成加密后唯一的key值 * * name 傳入小于KEY_NAME_SIZE的字符串 * return 通過name生成的驗證key值 */ char* generateKeyRAS(char* name) { //判斷形參是否有效 if (NULL == name || strlen(name) > KEY_NAME_SIZE) { LOGD("function generateKey must have a ok name!\n"); return NULL; } //聲明局部變量 int index = 0; int loop = 0; char temp[KEY_SIZE] = {"\0"}; //清空數組內存 memset(temp, 0, sizeof(temp)); //將傳進來的name拷貝到零時空間 strcpy(temp, name); //進行通過name轉化生成key的邏輯,這里是模擬測試,實際算法比這復雜 for (index=0; index<KEY_SIZE-1; index++) { temp[index] = 93; LOGD("---------------temp[%d]=%c", index, temp[index]); } return temp; }
淺析:這就是標準的C語言模塊了,這是邏輯的c文件,模擬實現了加密算法而已。
build.gradle文件中android.defaultConfig中新加如下代碼(其他使用AS編譯設置參見本系列教程一):
ndk{ moduleName "YanboberJniLibName" ldLibs "log", "z", "m" //添加依賴庫文件,因為有log打印等 abiFilters "armeabi", "armeabi-v7a", "x86" }
淺析:不解釋。
編譯代碼運行在LogCat中可以看見主要的幾條Log如下:
淺析:這里你會看到在運行app時:
- 嘗試加載so文件 Trying to load lib /data/app-lib/io.github.yanbober.ndkapplication-2/libYanboberJniLibName.so 0xa6a4e120
- 加載了so文件 Added shared lib /data/app-lib/io.github.yanbober.ndkapplication-2/libYanboberJniLibName.so 0xa6a4e120
- 先不解釋這句話 No JNI_OnLoad found in /data/app-lib/io.github.yanbober.ndkapplication-2/libYanboberJniLibName.so 0xa6a4e120, skipping init
上面說“先不解釋這句話”的No JNI_OnLoad found……skipping init其實透露出了一個新的知識點,下文會介紹的。
運行程序結果如下:
淺析:傳入name加密后得到的key顯示。
總結
以上第一部分就是JNI開發常見的基本結構模板,實際開發代碼量和文件和目錄結構都會比這復雜,這只是一個雛形用來領悟重點。
第二部分
概述
如果你已經大致理解掌握了第一部分內容,那基本OK了。接下來要扯蛋的就是第一部分遺留的歷史問題和其他提升技能。
首先,不知道還記不記得第一部分編譯代碼運行在LogCat中可以看見主要的幾條Log。“No JNI_OnLoad found……skipping init”這句話是不是還是依舊耿耿于懷呢?那么接下來咱們放大招來kill它。
從Load這個蛋疼的詞說起
Android OS加載JNI Lib的方法有兩種:
- 通過JNI_OnLoad。
- 如果JNI Lib實現中沒有定義JNI_OnLoad,則dvm調用dvmResolveNativeMethod進行動態解析。
PS:咱們上面第一部分就是dvm調用dvmResolveNativeMethod進行動態解析,所以log打印No JNI_OnLoad found。
從網上查到的深入解析(此解析模塊代碼引用自網絡)
JNI_OnLoad機制分析
System.loadLibrary調用流程如下所示:
System.loadLibrary->Runtime.loadLibrary->(Java)nativeLoad->(C: java_lang_Runtime.cpp)Dalvik_java_lang_Runtime_nativeLoad->dvmLoadNativeCode->(dalvik/vm/Native.cpp)
接著如下:
- dlopen(pathName, RTLD_LAZY) (把.so mmap到進程空間,并把func等相關信息填充到soinfo中)
- dlsym(handle, “JNI_OnLoad”)
- JNI_OnLoad->RegisterNatives->dvmRegisterJNIMethod(ClassObject* clazz, const char* methodName, const char* signature, void* fnPtr)->dvmUseJNIBridge(method, fnPtr)->(method->nativeFunc = func)
JNI函數在進程空間中的起始地址被保存在ClassObject->directMethods中。
struct ClassObject : Object { /* static, private, and <init> methods */ int directMethodCount; Method* directMethods; /* virtual methods defined in this class; invoked through vtable */ int virtualMethodCount; Method* virtualMethods; }
此ClassObject通過gDvm.jniGlobalRefTable或gDvm.jniWeakGlobalRefLock獲取。
dvmResolveNativeMethod延遲解析機制
如果JNI Lib中沒有JNI_OnLoad,即在執行System.loadLibrary時,無法把此JNI Lib實現的函數在進程中的地址增加到ClassObject->directMethods。則直到需要調用的時候才會解析這些javah風格的函數 。這樣的函數dvmResolveNativeMethod(dalvik/vm/Native.cpp)來進行解析,其執行流程如下所示:
void dvmResolveNativeMethod(const u4* args, JValue* pResult, const Method* method, Thread* self)->(Resolve a native method and invoke it.)
接著如下:
- void* func = lookupSharedLibMethod(method)(根據signature在所有已經打開的.so中尋找此函數實現)dvmHashForeach(gDvm.nativeLibs, findMethodInLib,(void*) method)->findMethodInLib(void* vlib, void* vmethod)->dlsym(pLib->handle, mangleCM)
- dvmUseJNIBridge((Method*) method, func)
- (*method->nativeFunc)(args, pResult, method, self);(調用執行)
說完蛋疼Load基礎后該準么辦?
答案其實就是推薦Android OS加載JNI Lib的方法的通過JNI_OnLoad。因為通過它你可以干許多自定義的事,譬如實現自己的本地注冊等。因為在上面的解析中已經看到了JNI_OnLoad->RegisterNatives->…這兩個關鍵方法。具體細節咱們現在再說說。
先來看JNI_OnLoad函數
JNI_OnLoad()函數主要的用途有兩點:
- 通知VM此C組件使用的JNI版本。如果你的.so文件沒有提供JNI_OnLoad()函數,VM會默認該.so使用最老的JNI 1.1版本。而新版的JNI做了許多擴充,如果需要使用JNI的新版功能,例如JNI 1.4的java.nio.ByteBuffer, 就必須藉由JNI_OnLoad()函數來告知VM。
- 因為VM執行到System.loadLibrary()函數時,會立即先調運JNI_OnLoad(),所以C組件的開發者可以由JNI_OnLoad()來進行C組件內的初期值之設定(Initialization)。
既然有JNI_OnLoad(),那就有相呼應的函數,那就是JNI_OnUnload(),當VM釋放JNI組件時會呼叫它,因此在該方法中進行善后清理,資源釋放的動作最為合適。
再來看RegisterNatives函數
在上面第一部分時我們看見通過javah命令生成的io_github_yanbober_ndkapplication_NdkJniUtils.h里函數的名字好長,看著就蛋疼。你肯定也想過怎么這么長,而且當有時候項目需求原因導致類名變了的時候,函數名必須一個一個的改,更加蛋疼。我第一次接觸時那時候自己經驗不足,就遇上了這個蛋疼問題。淚奔啊!
既然這樣那就有解決辦法的,那就是RegisterNatives大招。接下來來看下這個大招:
App的Java程序尋找c本地方法的過程一般是依賴VM去尋找*.so里的本地函數,如果需要連續調運很多次,每次都要尋找一遍,會多花許多時間。因此為了解決這個問題我們可以自行將本地函數向VM進行登記,然后讓VM自行調registerNativeMethods()函數。
VM自行調registerNativeMethods()函數的作用主要有兩點:
- 更加有效率去找到C語言的函數
- 可以在執行期間進行抽換,因為自定義的JNINativeMethod類型的methods[]數組是一個名稱-函數指針對照表,在程序執行時,可以多次調運registerNativeMethods()函數來更換本地函數指針,從而達到彈性抽換本地函數的效果。
上面提到的JNINativeMethod結構是c/c++方法和Java方法之間映射關系的關鍵結構,該結構定義在jni.h中,具體定義如下:
typedef struct { const char* name;//java方法名稱 const char* signature; //java方法簽名 void* fnPtr;//c/c++的函數指針 } JNINativeMethod;
所謂自定義的JNINativeMethod類型的methods[]數組自然也就類似長下面這樣了:
static JNINativeMethod methods[] = { {"generateKey", "(Ljava/lang/String;)Ljava/lang/String;", (void*)generateKey}, };
以上也就是所謂的動態注冊JNI了。
好了,該補腦的也差不多了,很空洞很枯燥,空虛寂寞冷啊;接下來進入實戰吧,通過對第一部分代碼的改變來輕松理解這部分扯淡的內容。
代碼實例分析
我們對第一部分的jni根目錄下的c代碼修改如下:
#include <jni.h> #include <string.h> #include <assert.h> #include "io_github_yanbober_ndkapplication_NdkJniUtils.h" #include "./utils/android_log_print.h" #include "./local_logic_c/easy_encrypt.h" JNIEXPORT jstring JNICALL native_generate_key(JNIEnv *env, jobject obj, jstring name) { //聲明局部量 char key[KEY_SIZE] = {0}; memset(key, 0, sizeof(key)); char temp[KEY_NAME_SIZE] = {0}; //將java傳入的name轉換為本地utf的char* const char* pName = (*env)->GetStringUTFChars(env, name, NULL); if (NULL != pName) { strcpy(temp, pName); strcpy(key, generateKeyRAS(temp)); //java的name對象不需要再使用,通知虛擬機回收name (*env)->ReleaseStringUTFChars(env, name, pName); } return (*env)->NewStringUTF(env, key); } //參數映射表 static JNINativeMethod methods[] = { {"nativeGenerateKey", "(Ljava/lang/String;)Ljava/lang/String;", (void*)native_generate_key}, //這里可以有很多其他映射函數 }; //自定義函數,為某一個類注冊本地方法,調運JNI注冊方法 static int registerNativeMethods(JNIEnv* env , const char* className , JNINativeMethod* gMethods, int numMethods) { jclass clazz; clazz = (*env)->FindClass(env, className); if (clazz == NULL) { return JNI_FALSE; } //JNI函數,參見系列教程2 if ((*env)->RegisterNatives(env, clazz, gMethods, numMethods) < 0) { return JNI_FALSE; } return JNI_TRUE; } //自定義函數 static int registerNatives(JNIEnv* env) { const char* kClassName = "io/github/yanbober/ndkapplication/NdkJniUtils";//指定要注冊的類 return registerNativeMethods(env, kClassName, methods, sizeof(methods) / sizeof(methods[0])); } JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void* reserved) { LOGD("customer---------------------------JNI_OnLoad-----into.\n"); JNIEnv* env = NULL; jint result = -1; if ((*vm)->GetEnv(vm, (void**) &env, JNI_VERSION_1_4) != JNI_OK) { return -1; } assert(env != NULL); //動態注冊,自定義函數 if (!registerNatives(env)) { return -1; } return JNI_VERSION_1_4; }
相應的h頭文件修改如下:
/* DO NOT EDIT THIS FILE - it is machine generated */ #include <jni.h> #ifndef _Included_io_github_yanbober_ndkapplication_NdkJniUtils #define _Included_io_github_yanbober_ndkapplication_NdkJniUtils #ifdef __cplusplus extern "C" { #endif JNIEXPORT jstring JNICALL native_generate_key(JNIEnv *env, jobject obj, jstring name); #ifdef __cplusplus } #endif #endif
對應的java文件中native方法名字換為映射表中的nativeGenerateKey即可。
以上代碼不做詳細解釋,代碼中有注釋,同時可以參考該系列第二篇博客。
總結
至此一個比Hello World稍微復雜一丁點兒的例子就分析的差不多了。整個JNI的基本雛形也就差不多這樣子。下一篇會從其他角度來啃。T_T!!!
【工匠若水 http://blog.csdn.net/yanbober】 繼續閱讀《待續新章。。。》