Android JNI&NDK程式設計小結及建議

qianby發表於2021-09-09

前言

由於網上關於JNI/NDK相關的知識點介紹的比較零散而且不具備參照性,所以寫了這篇JNI/NDK筆記,便於作為隨時查閱的工具型別的文章,本文主要的介紹了在平時專案中常用的命令、JNI資料型別、簽名等,便於查閱相關資料。文末相關參考資料比較適合剛接觸或者不熟悉Android NDK開發的朋友參閱。

常用命令

javac 編譯java原始檔生成.class檔案

由於JNI對應的標頭檔案由javah工具根據對應的.class檔案生成,所以在進行JNI程式設計之前,寫好Java程式碼後需要先編譯,在使用javah生成對應的標頭檔案

javah -jni自動生成標頭檔案

舉例說明:

  • 生成普通的JNI標頭檔案

    javah -classpath path -jni -d outputdirpath com.mrljdx.JavaNativeCode
  • 在Java函式中包含Android相關的引數程式碼,則需要在classpath中新增android.jar包的絕對路徑地址

    javah -classpath path:$ANDROID_HOME/path/android.jar -jni -d outputdirpath com.mrljdx.JavaNativeCodeWithAndroid

javap -s -p 檢視函式簽名

-s: 顯示簽名(只顯示public型別的簽名) -p:顯示所有函式、成員變數的簽名

舉例說明:

javap -classpath pacakage_path_dir -s -p com.mrljdx.JavaCode

JNI資料型別和型別簽名

資料型別

JNI的資料型別包括:基本型別引用型別。這一點和Java的語言特性一致,基本型別包括jboolean、jchar、jint、jlong、jbyte、jshort、jfloat、jdouble、void,與Java型別的對應關係如下:

JNI型別 Java型別 描述
jboolean boolean 無符號8位整型
jbyte byte 有符號8位整型
jchar char 無符號16位整型
jshort short 有符號16位整型
jint int 32位整型
jlong long 64位整型
jfloat float 32位整型
jdouble double 64位整型
void void 無型別

JNI中引用型別主要有類、物件和陣列,這點也上符合Java的語法規範,對應的關係如下:

JNI 型別 Java引用型別 描述
jobject Object Object型別
jclass Class Class型別
jstring String String型別
jobjectArray Object[] 物件陣列
jbooleanArray boolean[] boolean陣列
jbyteArray byte[] byte陣列
jcharArray char[] char陣列
jshortArray short[] short陣列
jintArray int[] int陣列
jlongArray long[] long陣列
jfloatArray float[] float陣列
jdoubleArray double[] double陣列
jthrowable Throwable Throwable

JNI型別簽名

JNI的型別簽名標識了一個特定的Java型別,這個型別可以是類和方法,也可以是資料型別。

  • 型別簽名
    類的簽名採用”L+包名+類名+;”標識,包名中將.替換為/即可。
    比如String類的簽名:
    Ljava/lang/String;
    注意末尾的;屬於簽名的一部分。
    再比如Android中Context類的簽名:
    Landroid/content/Context;

  • 基本資料型別簽名
    基本資料型別的簽名採用一系列大寫字母來標識,如下:

Java型別 簽名
boolean Z
byte B
char C
short S
int I
long J
float F
double D
void V

可以發現除了 long基本資料型別的簽名為J之外其他的都比較容易辨識,估計是由於之前的類型別的簽名開頭為L+包名+類名+;設計者為了區分所以簽名為J

  • 陣列的型別簽名
    陣列的型別簽名比起類型別和基本資料型別的要稍微複雜一點,不過還是很好理解的。對於陣列來說,它的簽名為[+型別簽名,舉例說明:
    String[] 陣列型別對應的簽名:
    [Ljava/lang/String;
    可以發現,就是在String的類簽名前加了個[
    同理基本資料型別簽名int[]的簽名:
    [I
    注意這裡基本型別後面是不帶分號的。
    那麼多維陣列呢?可以類推,int[][] 的簽名為[[I ,而String[][]的簽名為[[Ljava/lang/String;

  • 方法的簽名
    在JNI中會經常需要在C/C++程式碼中呼叫Java的函式,這時候就會用到方法的簽名。方法的簽名為(+引數型別簽名+)+返回值型別簽名,比如:
    方法: boolean login(String username,String password)的方法簽名如下:
    (Ljava/lang/String;Ljava/lang/String)B 如果這裡不理解的話,請再去看看之前關於基本型別,類型別的簽名部分內容。

小技巧:使用 類似於javap -classpath pathdir -s -p com.sample.JavaCode 的 javap -s -p 命令也可以幫助檢視一些類中各種方法和成員變數的簽名。

JNI相關命名解釋

  • 函式名的格式遵循規則:Java_包名_類名_方法名

  • JNIEXPORT、JNICALL、JNIEnv和jobject 都是JNI標準中所定義的型別或者宏

  • JNIEnv * : 指向JNI環境的指標,可以透過JNIEnv * 訪問JNI提供的介面方法

  • JNIEXPORT、JNICALL:是jni.h中所定義的宏。

注:JNIEnv * 可以簡單的理解為Java和C/C++ 之間相互呼叫的橋樑,我們可以透過JNIEnv * 呼叫C/C++定義的方法,也可以在C/C++中透過JNIEnv * 來呼叫Java類中的方法。下面將會講到C/C++中呼叫Java的方法,注意JNIEnv *的作用。

在C/C++中呼叫Java方法

首先說明一點,在Android開發過程中使用NDK主要是為了提高程式碼的安全性,有些遊戲公司可能是為了方便利用已有的C/C++開源庫來進行平臺移植,其實在效能提升方面,NDK的作用並不是很明顯。所以有時候一些在Java中實現起來非常簡單的程式碼放在JNI裡面做會顯得吃力不討好,所以乾脆就直接在JNI中呼叫Java的方法,我們只把加密和驗證的一些邏輯寫到JNI層就行了。
在JNI中呼叫Java方法流程如下:

  1. 在Java中定義一個靜態方法供JNI呼叫,注意要是靜態的。

  2. 在JNI中利用env來呼叫Java中定義的靜態方法

  3. 呼叫宣告好的靜態方法

可能流程說的比較抽象,用程式碼簡單說明一下:

  1. 定義靜態方法:

    //對應包名:com.mrljdx.jni.HelloJNIpublic static void helloJava() {    
        System.out.println("Hello JavaCode");
    }
  2. JNI宣告靜態方法:

    static void static_helloJava(JNIEnv *env){
          jclass clazz = env->FindClass("com/mrljdx/jni/HelloJNI");
          jmethodID mid = env->GetStaticMethodID(clazz, "helloJava", "()V");
          env->CallStaticVoidMethod(clazz, mid);
    }
  3. 呼叫宣告好的靜態方法:

    static_helloJava(env);

在AndroidStudio中NDK程式設計配置注意事項:

  1. 在專案的gradle.properties中新增ndk支援:

    android.useDeprecatedNdk=true
  2. 配置build.gradle看程式碼註釋:

    defaultConfig {    
         minSdkVersion 9   
         targetSdkVersion 23   
         versionCode 1    
         versionName "1.0"    
        //配置ndk 支援   
        ndk {        
              //編譯的so庫名稱 libsecurity.so
              moduleName "security"       
              //指定編譯後的庫支援的平臺
              abiFilters "armeabi", "mips", "x86", "armeabi-v7a"  
              //用於指定應用應該使用哪個標準庫,此處新增c++庫支援   
              stl "stlport_static"    
          }
    }
  3. 在AndroidStudio中寫JNI程式碼有一個比較爽的地方,就是Android.mk系統會在編譯時自動幫你生成,你只需要配置build.gradle就行了。注意jni相關程式碼需要放在src/main/jni目錄下。如果對gradle配置不瞭解可以參考我的部落格:

小結

在我們做產品的時候,應該考慮該用JNI&NDK的時候就用,一切出發點是基於使用者的體驗和資料安全,我覺得在以下幾種情況下建議使用NDK:

  1. 重用現有的程式碼,比如C/C++的程式碼在Android中的重用。

  2. 資料安全,比如將Http的請求加密和解密演算法放在NDK中去實現,這樣可以提高應用的安全。

  3. 提升效能,由於Android裝置製造商在手機中給每個應用分配了可用的最大RAM,有時候為了效能考慮,可以透過Native程式碼向系統來“借”一些記憶體,儘量少的使用系統分配給應用的記憶體。(參考Infoq:)

參考文章及書籍

  •  : 一本不厚的書,關於Android相關的效能最佳化建議都是滿滿的乾貨,適合有一定開發經驗的人參考。

  •  : 關於google官方的培訓課程提出了在使用NDK做本地開發時的一些最佳化建議。

  •  : 一個系列的文章,適合剛接觸JNI/NDK開發的朋友學習。

  •  :關於JNI的介紹比少,如果你對Android開發想深入理解一些View衝突、屬性動畫那麼可以推薦一看。如果只想學習JNI相關內容,推薦看  的內容。

END

原文連結:http://www.apkbus.com/blog-705730-61428.html

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/3549/viewspace-2814778/,如需轉載,請註明出處,否則將追究法律責任。

相關文章