1. 程式人生 > >NDK-JNI實戰教程(三) 從比Hello World稍複雜點兒的NDK例子說說模板

NDK-JNI實戰教程(三) 從比Hello World稍複雜點兒的NDK例子說說模板

PS一句:最終還是選擇CSDN來整理髮表這幾年的知識點,該文章平行遷移到CSDN。因為CSDN也支援MarkDown語法了,牛逼啊!

這裡寫圖片描述

第一部分

概述

學習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則很容易被破解。

這是這篇文章要介紹的程式碼工程的幾個主要資料夾檔案分佈情況:

JNI

淺析:正常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如下:

JNI

淺析:這裡你會看到在執行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其實透露出了一個新的知識點,下文會介紹的。

執行程式結果如下:

JNI

淺析:傳入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!!!

這裡寫圖片描述