JNI與NDK簡析(一)



 

1 JNI 簡介

在Android Framework中,需要提供一種媒介或 橋梁,將Java層(上層)與C/C++層(下層)有機的聯系起來,使得他們互相協調完成某些任務。而充當這種媒介的就是Java本地接口(JNI,Java Native Interface)。

JNI提供一些列的接口,允許Java類與C/C++等本地編輯語言(在JNI中,這些語言被稱為 本地語言)編寫的應用 程序、模塊 、庫進行交互操作。比如,在Java類中使用C語言庫中的函數或在C語言中使用 Java類庫,都需要借助JNI。

Android NDK是一個開發工具集,提供一系列工具快速開發C/C++的動態庫,並能自動將 .so/.dll 和 Java 應用一起打包到Apk;

NDK提供工具可以方便JNI調用C/C++,而且提供了交叉編譯器可以修改.mk文件生成特定CPU平台的動態庫,並能將so和java應用一起打包到apk中;簡單說就是JNI負責Java與C/C++進行互相操作,NDK提供工具方便在Android平台使用JNI;
 

2 JNI 的使用場景

JNI通常有下列使用場景:

▨  注重處理速度:

  與本地代碼(C/C++等)相比,Java代碼的執行速度回慢一些。如果對某段程序的執行速度有較高的要求,建議使用C/C++編寫代碼。而后在Java中通過JNI 調用基於C/C++編寫的部分。在開發圖像處理或信號處理這類對CPU處理速度有較高要求的程序時,使用C/C++等本地語言編寫的相應模塊,執行效率 更高,性能也好得多。

▨ 硬件控制:

  為了更好第控制硬件,硬件控制代碼通常使用C語言編寫。

 已有C/C++代碼的復用:

  在編程過程中,常常會使用一些已經編寫好的C/C++代碼,及提高編寫效率,又確保程序的安全性與健壯性,這類在第三方庫里較為常見,現在許多第三方庫都是有C/C++庫編寫的,比如Ffmpeg。

 代碼保護:

  由於APK的Java層代碼很容易反編譯,而C/C++庫反編譯難度很大。

 平台之間移植應用。

 

在實際Android應用開發中,開發者通常使用Android  SDK開發Java程序。而對於性能要求較高的,常常使用Android提供的 NDK(Native Development Kit) 開發基於C/C++的本地庫。而后再通過 JNI將Java程序與C/C++程序集成在一起。NDK提供了一些列的工具,幫助開發者快速開發C/C++動態庫。

 

3 Java中調用C函數庫

3.1 在 Android Studio 新建項目

第一步:我們需要下載兩個至關重要的Tools,一個是CMake,一個是NDK。

 

第二步:新建 Project

 

創建完成后,如下圖:

 

C/C++文件一般存放於cpp目錄下。接下來我們看下配置文件 build.gradle:

 

CMakeLists.txt(代碼 3.1-1):

 1 cmake_minimum_required(VERSION 3.4.1) # Android Studio最低要求版本  2 
 3 add_library( # 當前庫名稱.  4              native-lib
 5 
 6              # 將庫設置為共享庫。
 7              SHARED
 8 
 9              # 加載該庫里的文件.
10              native-lib.cpp)

  

3.2 多目錄,多層次目錄時的配置問題

 

這時,cpp.CMakeLists.txt 設置如下所示(代碼 3.2-1):

1 cmake_minimum_required(VERSION 3.4.1) #指定編譯器版本 2 
3 #指定子文件夾
4 add_subdirectory(first)
5 add_subdirectory(second)

 

cpp.first.CMakeLists.txt(代碼 3.2-2) 

 1 set(LIBRARY first-lib) # 定義庫名稱 LIBRARY = first-lib
 2 
 3 file(GLOB_RECURSE cpp_first "./*.cpp") # first目錄下的所有 .cpp 文件  4 
 5 add_library( # 設置庫的名稱。  6         ${LIBRARY}
 7 
 8         # 將庫設置為共享庫。
 9         SHARED
10 
11         # 提供源文件的相對路徑。
12         ${cpp_first}
13         )
14 
15 find_library( #設置路徑變量的名稱。 16         log-lib
17 
18         # 指定您希望CMake定位的NDK庫的名稱。
19         log)
20 
21 target_link_libraries( #指定目標庫。 22         ${LIBRARY}
23 
24         # 將目標庫鏈接到NDK中包含的日志庫。
25         ${log-lib})

  

3.3 下面看看 java 代碼是如何實現的

以類 dinn.cappjni.HelloJNI.java 為例(代碼 3.3-1):

 1 package dinn.cappjni;
 2 
 3 public class HelloJNI {
 4 
 5     static {
 6         System.loadLibrary("first-lib"); // 加載本地庫“first-lib”,即 代碼 3.2-2
 7     }
 8 
 9     public native void printHello(); // ① 使用【native】關鍵字申明本地方法,該方法與用C++編寫的JNI本地函數相對應。
10 
11     public native String printString(String str);
12 }

 

那么C++代碼如何寫呢?我們可通過命令(javah  -jni xxx)生成(注意目錄要定位到 java 這層,也可通過 -classpath 重寫定位位置。):

 

生成成功后,刷新工程目錄,就可以看見生成的文件:

 

然后,我們可以將文件移動到cpp目錄下。下面是 dinn_cappjni_HelloJNI.h  的內容(代碼 3.3-2):

 1 /* DO NOT EDIT THIS FILE - it is machine generated */
 2 #include <jni.h>
 3 /* Header for class dinn_cappjni_HelloJNI */
 4 
 5 #ifndef _Included_dinn_cappjni_HelloJNI
 6 #define _Included_dinn_cappjni_HelloJNI
 7 #ifdef __cplusplus
 8 extern "C" {  // extern "C"的主要作用就是為了能夠正確實現C++代碼調用其他C語言代碼。
 9 #endif
10 /*
11  * Class:     dinn_cappjni_HelloJNI
12  * Method:    printHello
13  * Signature: ()V
14  */
15 JNIEXPORT void JNICALL Java_dinn_cappjni_HelloJNI_printHello (JNIEnv *, jobject);
16 
17 /*
18  * Class:     dinn_cappjni_HelloJNI
19  * Method:    printString
20  * Signature: (Ljava/lang/String;)Ljava/lang/String;
21  */
22 JNIEXPORT jstring JNICALL Java_dinn_cappjni_HelloJNI_printString  (JNIEnv *, jobject, jstring);
23 
24 #ifdef __cplusplus
25 }
26 #endif
27 #endif

 

接下來,我們實現 dinn_cappjni_HelloJNI.cpp 的內容(代碼3.3-3):

 1 #include <string>
 2 #include "dinn_cappjni_HelloJNI.h"
 3 
 4 extern "C" {
 5 /*
 6  * Class:     dinn_appdemojni_HelloJNI
 7  * Method:    printhello
 8  * Signature: ()V
 9  */
10 JNIEXPORT void JNICALL Java_dinn_cappjni_HelloJNI_printHello(JNIEnv *env, jobject obj) {
11     printf("Hello JNI!");
12     return;
13 }
14 
15 /*
16  * Class:     dinn_appdemojni_HelloJNI
17  * Method:    printString
18  * Signature: (Ljava/lang/String;)V
19  */
20 JNIEXPORT jstring JNICALL 21 Java_dinn_cappjni_HelloJNI_printString(JNIEnv *env, jobject obj, jstring str) {
22     // 將String字符串轉換成 C字符串
23     const char *chars = env->GetStringUTFChars(str, 0);
24     printf("%s! \n", chars);
25 
26     std::string strOld = chars;
27     std::string strNew = "您輸入的是:" + strOld;
28     return env->NewStringUTF(strNew.c_str());
29 }
30 }

 

最后在 Java 中使用時其實很簡單,直接調用類HelloJNI中的方法即可,如(代碼 3.3-4)所示:

(new HelloJNI()).printHello();

 

 至此,我們實現了 Java 調取本地 C/C++ 函數。那么 本地 C/C++ 庫又是怎么調用 Java 的方法呢?

 

4 在 C 中調用 Java 方法

 4.1 新建 Java文件 JniTest.java 

 1 public class JniTest {
 2 
 3     private String content;
 4 
 5     public JniTest(String content) {
 6         this.content = content;
 7     }
 8 
 9     // 此方法由本地函數調用
10     public String getContent() {
11         return content;
12     }
13 }

  

4.2 在Java文件 HelloJNI.java 中新增獲取對象JniTest的方法 createJniTestObject(), 注意要是靜態方法:

1 public static native JniTest createJniTestObject();

  

4.3 生成該Java方法所對應的C++函數:

1 JNIEXPORT jobject JNICALL 2 Java_dinn_cappjni_HelloJNI_createJniTestObject(JNIEnv *, jclass);

 

 此時我們注意到,生成的C++函數中的第二個參數為 jclass 類型,不再是 jobject。原因是什么呢?想 弄清楚這個,我們需要了解第二個參數的含義。前面的 jobject 類型變量用來保存調用本地方法的對象的引用。而此時的 java 方法為靜態(static)的,而靜態方法可以不用創建對象,可通過類名直接獲取到,因此這里函數的第二個參數為 jclass 類型。

 

4.4 dinn_cappjni_HelloJNI.cpp 

 1 #include <string>
 2 #include <android/log.h> // 引用日志的包
 3 #include "dinn_cappjni_HelloJNI.h"
 4 
 5 extern "C" {
 6 #define TAG "日志【C】"
 7 #define LOGI(...) __android_log_print(ANDROID_LOG_INFO, TAG, __VA_ARGS__)
 8 
 9 // ...
10 
11 JNIEXPORT jobject JNICALL 12 Java_dinn_cappjni_HelloJNI_createJniTestObject(JNIEnv *env, jclass clazz) {
13     // 查找生成對象的類
14     jclass targetClass = env->FindClass("dinn/cappjni/JniTest"); // 這里要包含類的包名 15 
16     // 查找構造方法
17     jmethodID mid = env->GetMethodID(targetClass, "<init>", "(Ljava/lang/String;)V");
18     if (mid == NULL) return NULL;
19 
20     // 生成 JniTest 對象(返回對象的引用)
21     jobject newObject = env->NewObject(targetClass, mid, env->NewStringUTF("【生成 JniTest 對象】"));
22 
23     // 調用對象方法 getContent();
24     mid = env->GetMethodID(targetClass, "getContent", "()Ljava/lang/String;");
25     if (mid == NULL) return newObject;
26     jstring str = (jstring) env->CallObjectMethod(newObject, mid);
27 
28     LOGI("【CPP】類JniTest中的變量content = %s\n", env->GetStringUTFChars(str, 0)); // 打印日志 29     return newObject;
30 }
31 
32 }

 說明:

第17行: env->GetMethodID(targetClass, "<init>", "(Ljava/lang/String;)V");

◆ 其中第二個參數<init>表示構造方法。如果是非構造方法,直接寫方法名稱,如第24行。

◆ 第三個參數表示Java變量/方法中的參數的簽名。在調用某些JNI函數是,要求提供指定的成員變量或成員方法的簽名。當然,開發者可以根據JNI規范中的Java簽名生成規則,直接創建簽名。但不建議這么做,java系統會為類的成員變量或成員方法生成簽名。使用時,只需要使用javap 命令(Java反編譯器),即可輕松獲取指定的成員變量或成員方法的簽名。

形式:javap [選項] '類名(.class后綴的文件,我們可通過javac編譯得到,或者在Android Studio中的build\intermediates\...目錄下找到)'

選項:-s 輸出java簽名

     -p 輸出所有類及成員

以JniTest.class為例,如下圖所示:

  

紅線處即是該成員方法的簽名。

 

最后在Java中我們可以獲取到 dinn_cappjni_HelloJNI.cpp 返回的JniTest對象。

1 JniTest jniTest = HelloJNI.createJniTestObject();
2 if (jniTest != null)
3     Log.i("日志(Java)", jniTest.getContent()));

 

 
       


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM