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

jopen 8年前發布 | 53K 次閱讀 Android開發 移動開發

【工匠若水 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則很容易被破解。

這是這篇文章要介紹的代碼工程的幾個主要文件夾文件分布情況:

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!!!

【工匠若水 http://blog.csdn.net/yanbober】 繼續閱讀《待續新章。。。》

這里寫圖片描述

來自: http://blog.csdn.net//yanbober/article/details/45310589

 本文由用戶 jopen 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!