安卓逆向_15( 一 ) --- JNI 和 NDK

 

From:較詳細的介紹JNI:https://blog.csdn.net/lizhifa2011/article/details/21021177

From:https://www.jb51.net/article/126111.htm

 

NDK 官方文檔:https://developer.android.google.cn/training/articles/perf-jni
JNI / NDK 開發指南:https://wiki.jikexueyuan.com/project/jni-ndk-developer-guide/overview.html
Java Native Interface Specification—Contents:https://docs.oracle.com/javase/1.5.0/docs/guide/jni/spec/jniTOC.html
菜鳥教程  之 JNI 入門教程:https://www.runoob.com/w3cnote/jni-getting-started-tutorials.html
Android JNI學習(1、2、3、4、5 ):https://www.jianshu.com/p/b4431ac22ec2


JNI官方中文資料:https://blog.csdn.net/yishifu/article/details/52180448
JNI 入門教程( 菜鳥教程 ):https://www.runoob.com/w3cnote/jni-getting-started-tutorials.html
JNI 實戰全面解析:https://blog.csdn.net/yuzhou_zang/article/details/78410632
Java 與 c++ 通過 JNI 的完美結合:https://blog.csdn.net/xiaoxiaoyusheng2012/article/details/56672173
使用 jni 調用 C++ 的過程:https://www.cnblogs.com/mssyj/p/12148739.html
VS2019 C++的跨平臺開發——Android .so 開發:https://blog.csdn.net/luoyu510183/article/details/94590497
Android Studio 開發 JNI 示例:https://blog.csdn.net/wzhseu/article/details/79683045
JNI開發總結:https://cloud.tencent.com/developer/article/1356493
Android JNI原理分析:http://gityuan.com/2016/05/28/android-jni/

 

 

 

較詳細的 JNI 簡介

 

 

JNI 是本地語言編程接口。它允許運行在 JVM 中的 Java 代碼和用C、C++或彙編寫的本地代碼相互操作。

在 Java中,有時候我們不得不要去使用其他語言的代碼,比如說:

  • 1、你的應用需要訪問系統的各個特性和設備,這些特性和設備通過java平臺是無法訪問的。
  • 2、你已經有了大量的測試過和調試過的用另一種語言編寫的代碼,並且知道如何將其導出到所有的目標平臺上。
  • 3、通過基礎測試,你已經發現所編寫的 Java 代碼比用其他語言編寫的等價代碼要慢得多。

Java 平臺有一個用於和本地C代碼進行互操作的 API,稱爲 Java本地接口JNI)。

 

JNI 有什麼用?

        JNI 是 Java Native Interface 的縮寫,它提供了若干的 API 實現了Java和其他語言的通信(主要是 C/C++)。通俗來說,就是JAVA 調用 C/C++ 函數的接口。如果你要想調用C系列的函數,你就必須遵守這樣的約定。

        JNI 最常見的兩個應用:從 Java 程序調用 C/C++,以及從 C/C++ 程序調用Java代碼。JNI 是一個雙向的接口:開發者不僅可以通過 JNI 在 Java 代碼中訪問 Native 模塊,還可以在 Native 代碼中嵌入一個 JVM,並通過 JNI 訪問運行於其中的 Java 模塊。可見 JNI 擔任了一個橋樑的角色,它將 JVM 與 Native 模塊聯繫起來,從而實現了 Java 代碼與 Native 代碼的互訪

 

 

1、JNI 組織結構

JNI 函數表的組成就像 C++的虛函數表,虛擬機可以運行多張函數表。
JNI 接口指針僅在當前線程中起作用,指針不能從一個線程進入另一個線程,但可以在不同的線程中調用本地方法。

 

 

2、原始數據

Jobject 對象 和 引用類型

1、基本數據類型

以下是java的基本數據類型和jni中的基本數據類型的比較,及各類型的字節。

圖表:

Java類型 本地類型(JNI) 描述
boolean(布爾型) jboolean 無符號8個比特
byte(字節型) jbyte 有符號8個比特
char(字符型) jchar 無符號16個比特
short(短整型) jshort 有符號16個比特
int(整型) jint 有符號32個比特
long(長整型) jlong 有符號64個比特
float(浮點型) jfloat 32個比特
double(雙精度浮點型) jdouble 64個比特
void(空型) void N/A

2、引用類型

Java 中不同的引用類型在 JNI 當中也有對應的引用類型,以下樹形表示:

當在C語言中使用時,所有的 JNI 引用類型都被定義爲 jobject 類型。typedef jobject jclass;

jvalue 類型

jvalue 類型是一個 基本數據類型引用類型 的集合,定義方式如下:

 typedef union jvalue {
     jboolean z;
     jbyte    b;
     jchar    c;
     jshort   s;
     jint     i;
     jlong    j;
     jfloat   f;
     jdouble  d;
     jobject  l;
 } jvalue;

class 的說明

  • (1):類和接口的描述符在 java 當中使用 ".",如:java.lang.String。而在 JNI 當中是用 "/",如:java/lang/String
  • (2):數組類型的引用類型用 "[" 表示。如  int[] (java中的表示法)    [I ([ 大寫的i 是JNI中的表示法,[ 的個數表示數組的維數  二維則是  [[ I )
  • (3):域的說明,和 java 比較如下表:

    注意:引用類型的域 用L開頭,並且以”;”作爲結尾。數組類型和class說明的一樣。

  • (4):Method 說明JNI 中的方法的聲明規則:先寫參數列表,再寫返回類型,以下是例子。

域描述符

Java 語言
Z boolean
B byte
C char
S short
I int
J long
F float
D double

引用類型則爲 L + 該類型類描述符 + 。

數組,其爲 :  [ + 其類型的域描述符 + 。

String類型的域描述符爲 Ljava/lang/String;    
  
[ + 其類型的域描述符 + ;  
int[ ]     其描述符爲[I  
float[ ]   其描述符爲[F  
String[ ]  其描述符爲[Ljava/lang/String;  
Object[ ]類型的域描述符爲[Ljava/lang/Object;  
int  [ ][ ] 其描述符爲[[I  
float[ ][ ] 其描述符爲[[F  

將參數類型的域描述符按照申明順序放入一對括號中後跟返回值類型的域描述符,規則如下: (參數的域描述符的疊加)返回類型描述符。對於,沒有返回值的,用V(表示void型)表示。

舉例如下:

Java層方法                                               JNI函數簽名  
                String test ( )                                              Ljava/lang/String;  
                int f (int i, Object object)                            (ILjava/lang/Object;)I  
                void set (byte[ ] bytes)                                ([B)V  

JNIEnv與JavaVM 

JNIEnv 概念 : 是一個線程相關的結構體, 該結構體代表了 Java 在本線程的運行環境 ; 

JNIEnv 與 JavaVM : 注意區分這兩個概念; 
-- JavaVM : JavaVM 是 Java虛擬機在 JNI 層的代表, JNI 全局只有一個;
-- JNIEnv : JavaVM 在線程中的代表, 每個線程都有一個, JNI 中可能有很多個 JNIEnv;

JNIEnv 作用 : 
-- 調用 Java 函數 : JNIEnv 代表 Java 運行環境, 可以使用 JNIEnv 調用 Java 中的代碼;
-- 操作 Java 對象 : Java 對象傳入 JNI 層就是 Jobject 對象, 需要使用 JNIEnv 來操作這個 Java 對象;

 

JNIEnv 體系結構 

線程相關 : JNIEnv 是線程相關的, 即 在 每個線程中 都有一個 JNIEnv 指針, 每個JNIEnv 都是線程專有的, 其它線程不能使用本線程中的 JNIEnv, 線程 A 不能調用 線程 B 的 JNIEnv;

JNIEnv 不能跨線程 : 
-- 當前線程有效 : JNIEnv 只在當前線程有效, JNIEnv 不能在 線程之間進行傳遞, 在同一個線程中, 多次調用 JNI層方法, 傳入的 JNIEnv 是相同的;
-- 本地方法匹配多JNIEnv : 在 Java 層定義的本地方法, 可以在不同的線程調用, 因此 可以接受不同的 JNIEnv;

JNIEnv 結構 : 由上面的代碼可以得出, JNIEnv 是一個指針,  指向一個線程相關的結構, 線程相關結構指向 JNI 函數指針 數組, 這個數組中存放了大量的 JNI 函數指針, 這些指針指向了具體的 JNI 函數; 

 

注意:JNIEnv只在當前線程中有效。本地方法不能將JNIEnv從一個線程傳遞到另一個線程中。相同的 Java 線程中對本地方法多次調用時,傳遞給該本地方法的JNIEnv是相同的。但是,一個本地方法可被不同的 Java 線程所調用,因此可以接受不同的 JNIEnv。

UTF-8編碼

JNI使用改進的UTF-8字符串來表示不同的字符類型。Java使用UTF-16編碼。UTF-8編碼主要使用於C語言,因爲它的編碼用\u000表示爲0xc0,而不是通常的0×00。非空ASCII字符改進後的字符串編碼中可以用一個字節表示。

錯誤

JNI不會檢查NullPointerException、IllegalArgumentException這樣的錯誤,原因是:導致性能下降。

在絕大多數C的庫函數中,很難避免錯誤發生。
JNI允許用戶使用Java異常處理。大部分JNI方法會返回錯誤代碼但本身並不會報出異常。因此,很有必要在代碼本身進行處理,將異常拋給Java。在JNI內部,首先會檢查調用函數返回的錯誤代碼,之後會調用ExpectOccurred()返回一個錯誤對象。

  1. jthrowable ExceptionOccurred(JNIEnv *env);  
  2. 例如:一些操作數組的JNI函數不會報錯,因此可以調用ArrayIndexOutofBoundsException或ArrayStoreExpection方法報告異常。  

 

3、JNI 函數 實戰

1、*.so的入口函數

JNI_OnLoad() 與J NI_OnUnload()
當 Android 的 VM(Virtual Machine) 執行到 System.loadLibrary() 函數時,首先會去執行 C 組件裏的 JNI_OnLoad() 函數。它的用途有二:

  • (1) 告訴 VM 此 C 組件使用那一個 JNI 版本。如果你的 *.so 沒有提供 JNI_OnLoad() 函數,VM 會默認該 *.so 是使用最老的JNI 1.1 版本。由於新版的 JNI 做了許多擴充,如果需要使用 JNI 的新版功能,例如 JNI 1.4 的 java.nio.ByteBuffer,就必須藉由 JNI_OnLoad() 函數來告知 VM 。
  • (2) 由於 VM 執行到 System.loadLibrary() 函數時,就會立即先呼叫 JNI_OnLoad(),所以 C 組件的開發者可以藉由JNI_OnLoad() 來進行 C 組件內的初期值之設定 (Initialization) 。

 

2、返回值

jstring str = env->newStringUTF("HelloJNI");  //直接使用該JNI構造一個jstring對象返回    
return str ;    

示例:

jobjectArray ret = 0;  
jsize len = 5;  
jstring str;  
string value("hello");  
   
ret = (jobjectArray)(env->NewObjectArray(len, env->FindClass("java/lang/String"), 0));  
for(int i = 0; i < len; i++)  
{  
    str = env->NewStringUTF(value..c_str());  
    env->SetObjectArrayElement(ret, i, str);  
}  
return ret; 返回數組  

示例:

jclass    m_cls   = env->FindClass("com/ldq/ScanResult");    
   
   jmethodID m_mid   = env->GetMethodID(m_cls,"<init>","()V");    
       
   jfieldID  m_fid_1 = env->GetFieldID(m_cls,"ssid","Ljava/lang/String;");    
   jfieldID  m_fid_2 = env->GetFieldID(m_cls,"mac","Ljava/lang/String;");    
   jfieldID  m_fid_3 = env->GetFieldID(m_cls,"level","I");    
   
   jobject   m_obj   = env->NewObject(m_cls,m_mid);    
   
                       env->SetObjectField(m_obj,m_fid_1,env->NewStringUTF("AP1"));    
                       env->SetObjectField(m_obj,m_fid_2,env->NewStringUTF("00-11-22-33-44-55"));    
                       env->SetIntField(m_obj,m_fid_3,-50);    
   return m_obj;  返回自定義對象  

示例:

jclass list_cls = env->FindClass("Ljava/util/ArrayList;");//獲得ArrayList類引用    
    
    if(listcls == NULL)    
    {    
        cout << "listcls is null \n" ;    
    }    
    jmethodID list_costruct = env->GetMethodID(list_cls , "<init>","()V"); //獲得得構造函數Id    
    
    jobject list_obj = env->NewObject(list_cls , list_costruct); //創建一個Arraylist集合對象    
    //或得Arraylist類中的 add()方法ID,其方法原型爲: boolean add(Object object) ;    
    jmethodID list_add  = env->GetMethodID(list_cls,"add","(Ljava/lang/Object;)Z");     
      
    jclass stu_cls = env->FindClass("Lcom/feixun/jni/Student;");//獲得Student類引用    
    //獲得該類型的構造函數  函數名爲 <init> 返回類型必須爲 void 即 V    
    jmethodID stu_costruct = env->GetMethodID(stu_cls , "<init>", "(ILjava/lang/String;)V");    
    
    for(int i = 0 ; i < 3 ; i++)    
    {    
        jstring str = env->NewStringUTF("Native");    
        //通過調用該對象的構造函數來new 一個 Student實例    
        jobject stu_obj = env->NewObject(stucls , stu_costruct , 10,str);  //構造一個對象    
            
        env->CallBooleanMethod(list_obj , list_add , stu_obj); //執行Arraylist類實例的add方法,添加一個stu對象    
    }    
    
    return list_obj ;   返回對象集合  

 

3、操作Java層的類

//獲得jfieldID 以及 該字段的初始值    
   jfieldID  nameFieldId ;    
    
   jclass cls = env->GetObjectClass(obj);  //獲得Java層該對象實例的類引用,即HelloJNI類引用    
    
   nameFieldId = env->GetFieldID(cls , "name" , "Ljava/lang/String;"); //獲得屬性句柄    
    
   if(nameFieldId == NULL)    
   {    
       cout << " 沒有得到name 的句柄Id \n;" ;    
   }    
   jstring javaNameStr = (jstring)env->GetObjectField(obj ,nameFieldId);  // 獲得該屬性的值    
   const char * c_javaName = env->GetStringUTFChars(javaNameStr , NULL);  //轉換爲 char *類型    
   string str_name = c_javaName ;      
   cout << "the name from java is " << str_name << endl ; //輸出顯示    
   env->ReleaseStringUTFChars(javaNameStr , c_javaName);  //釋放局部引用    
    
   //構造一個jString對象    
   char * c_ptr_name = "I come from Native" ;    
       
   jstring cName = env->NewStringUTF(c_ptr_name); //構造一個jstring對象    
    
   env->SetObjectField(obj , nameFieldId , cName); // 設置該字段的值   

 

4、回調Java層方法

jstring str = NULL;    
      
  jclass clz = env->FindClass("cc/androidos/jni/JniTest");    
  //獲取clz的構造函數並生成一個對象    
  jmethodID ctor = env->GetMethodID(clz, "<init>", "()V");    
  jobject obj = env->NewObject(clz, ctor);    
  
  // 如果是數組類型,則在類型前加[,如整形數組int[] intArray,則對應類型爲[I,整形數組String[] strArray對應爲[Ljava/lang/String;    
  jmethodID mid = env->GetMethodID(clz, "sayHelloFromJava", "(Ljava/lang/String;II[I)I");    
  if (mid)    
  {    
      LOGI("mid is get");    
      jstring str1 = env->NewStringUTF("I am Native");    
      jint index1 = 10;    
      jint index2 = 12;    
      //env->CallVoidMethod(obj, mid, str1, index1, index2);    
  
      // 數組類型轉換 testIntArray能不能不申請內存空間    
      jintArray testIntArray = env->NewIntArray(10);    
      jint *test = new jint[10];    
      for(int i = 0; i < 10; ++i)    
      {    
          *(test+i) = i + 100;    
      }    
      env->SetIntArrayRegion(testIntArray, 0, 10, test);    
  
  
      jint javaIndex = env->CallIntMethod(obj, mid, str1, index1, index2, testIntArray);    
      LOGI("javaIndex = %d", javaIndex);    
      delete[] test;    
      test = NULL;    
  }    

示例代碼:

static void event_callback(int eventId,const char* description) {  //主進程回調可以,線程中回調失敗。  
    if (gEventHandle == NULL)  
        return;  
      
    JNIEnv *env;  
    bool isAttached = false;  
  
    if (myVm->GetEnv((void**) &env, JNI_VERSION_1_2) < 0) { //獲取當前的JNIEnv  
        if (myVm->AttachCurrentThread(&env, NULL) < 0)  
            return;  
        isAttached = true;  
    }  
  
    jclass cls = env->GetObjectClass(gEventHandle); //獲取類對象  
    if (!cls) {  
        LOGE("EventHandler: failed to get class reference");  
        return;  
    }  
  
    jmethodID methodID = env->GetStaticMethodID(cls, "callbackStatic",  
        "(ILjava/lang/String;)V");  //靜態方法或成員方法  
    if (methodID) {  
        jstring content = env->NewStringUTF(description);  
        env->CallVoidMethod(gEventHandle, methodID,eventId,  
            content);  
        env->ReleaseStringUTFChars(content,description);  
    } else {  
        LOGE("EventHandler: failed to get the callback method");  
    }  
  
    if (isAttached)  
        myVm->DetachCurrentThread();  
}  

 

線程中回調
把 c/c++ 中所有線程的創建,由 pthread_create 函數替換爲由 Java 層的創建線程的函數 AndroidRuntime::createJavaThread。

static pthread_t create_thread_callback(const char* name, void (*start)(void *), void* arg)    
{    
    return (pthread_t)AndroidRuntime::createJavaThread(name, start, arg);    
}   
  
  
static void checkAndClearExceptionFromCallback(JNIEnv* env, const char* methodName) {  //異常檢測和排除  
    if (env->ExceptionCheck()) {    
        LOGE("An exception was thrown by callback '%s'.", methodName);    
        LOGE_EX(env);    
        env->ExceptionClear();    
    }    
}    
    
static void receive_callback(unsigned char *buf, int len)  //回調  
{    
    int i;    
    JNIEnv* env = AndroidRuntime::getJNIEnv();    
    jcharArray array = env->NewCharArray(len);    
    jchar *pArray ;    
        
    if(array == NULL){    
        LOGE("receive_callback: NewCharArray error.");    
        return;     
    }    
    
    pArray = (jchar*)calloc(len, sizeof(jchar));    
    if(pArray == NULL){    
        LOGE("receive_callback: calloc error.");    
        return;     
    }    
    
    //copy buffer to jchar array    
    for(i = 0; i < len; i++)    
    {    
        *(pArray + i) = *(buf + i);    
    }    
    //copy buffer to jcharArray    
    env->SetCharArrayRegion(array,0,len,pArray);    
    //invoke java callback method    
    env->CallVoidMethod(mCallbacksObj, method_receive,array,len);    
    //release resource    
    env->DeleteLocalRef(array);    
    free(pArray);    
    pArray = NULL;    
        
    checkAndClearExceptionFromCallback(env, __FUNCTION__);    
}  
  
  
public void Receive(char buffer[],int length){  //java層函數  
        String msg = new String(buffer);    
        msg = "received from jni callback" + msg;    
        Log.d("Test", msg);    
    }  

示例代碼:

jclass cls = env->GetObjectClass(obj);//獲得Java類實例    
jmethodID callbackID = env->GetMethodID(cls , "callback" , "(Ljava/lang/String;)V") ;//或得該回調方法句柄    
  
if(callbackID == NULL)    
{    
     cout << "getMethodId is failed \n" << endl ;    
}    
  
jstring native_desc = env->NewStringUTF(" I am Native");    
  
env->CallVoidMethod(obj , callbackID , native_desc); //回調該方法,並且  

 

5、傳對象到JNI調用

jclass stu_cls = env->GetObjectClass(obj_stu); //或得Student類引用    
  
  if(stu_cls == NULL)    
  {    
      cout << "GetObjectClass failed \n" ;    
  }    
  //下面這些函數操作,我們都見過的。O(∩_∩)O~    
  jfieldID ageFieldID = env->GetFieldID(stucls,"age","I"); //獲得得Student類的屬性id     
  jfieldID nameFieldID = env->GetFieldID(stucls,"name","Ljava/lang/String;"); // 獲得屬性ID    
  
  jint age = env->GetIntField(objstu , ageFieldID);  //獲得屬性值    
  jstring name = (jstring)env->GetObjectField(objstu , nameFieldID);//獲得屬性值    
  
  const char * c_name = env->GetStringUTFChars(name ,NULL);//轉換成 char *    
   
  string str_name = c_name ;     
  env->ReleaseStringUTFChars(name,c_name); //釋放引用    
      
  cout << " at Native age is :" << age << " # name is " << str_name << endl ;     

 

6、與C++互轉

jbytearray 轉 c++byte 數組

jbyte * arrayBody = env->GetByteArrayElements(data,0);     
jsize theArrayLengthJ = env->GetArrayLength(data);     
BYTE * starter = (BYTE *)arrayBody;     

jbyteArray 轉 c++中的BYTE[] 

jbyte * olddata = (jbyte*)env->GetByteArrayElements(strIn, 0);    
jsize  oldsize = env->GetArrayLength(strIn);    
BYTE* bytearr = (BYTE*)olddata;    
int len = (int)oldsize;    

C++中的BYTE[]轉jbyteArray 

jbyte *by = (jbyte*)pData;    
jbyteArray jarray = env->NewByteArray(nOutSize);    
env->SetByteArrayRegin(jarray, 0, nOutSize, by);    

jbyteArray 轉 char * 

char* data = (char*)env->GetByteArrayElements(strIn, 0);    

char* 轉jstring

jstring WindowsTojstring(JNIEnv* env, char* str_tmp)    
{    
 jstring rtn=0;    
 int slen = (int)strlen(str_tmp);    
 unsigned short* buffer=0;    
 if(slen == 0)    
 {    
  rtn = env->NewStringUTF(str_tmp);    
 }    
 else    
 {    
  int length = MultiByteToWideChar(CP_ACP, 0, (LPCSTR)str_tmp, slen, NULL, 0);    
  buffer = (unsigned short*)malloc(length*2+1);    
  if(MultiByteToWideChar(CP_ACP, 0, (LPCSTR)str_tmp, slen, (LPWSTR)buffer, length) > 0)    
  {    
   rtn = env->NewString((jchar*)buffer, length);    
  }    
 }    
 if(buffer)    
 {    
  free(buffer);    
 }    
 return rtn;    
}    

char* jstring互轉

JNIEXPORT jstring JNICALL Java_com_explorer_jni_SambaTreeNative_getDetailsBy    
  (JNIEnv *env, jobject jobj, jstring pc_server, jstring server_user, jstring server_passwd)    
{    
    const char *pc = env->GetStringUTFChars(pc_server, NULL);    
    const char *user = env->GetStringUTFChars(server_user, NULL);    
    const char *passwd = env->GetStringUTFChars(server_passwd, NULL);    
    const char *details = smbtree::getPara(pc, user, passwd);    
    jstring jDetails = env->NewStringUTF(details);    
    return jDetails;    
}    

 

 

4、Android.mk、Application.mk

1、Android.mk

Android.mk文件是GNU Makefile的一小部分,它用來對Android程序進行編譯,Android.mk中的變量都是全局的,解析過程會被定義。

一個Android.mk文件可以編譯多個模塊,模塊包括:APK程序、JAVA庫、C\C++應用程序、C\C++靜態庫、C\C++共享庫。

簡單實例如下:

LOCAL_PATH := $(call my-dir)  #表示是當前文件的路徑  
include $(CLEAR_VARS)       #指定讓GNU MAKEFILE該腳本爲你清除許多 LOCAL_XXX 變量  
LOCAL_MODULE:= helloworld   #標識你在 Android.mk 文件中描述的每個模塊  
MY_SOURCES := foo.c         #自定義變量  
ifneq ($(MY_CONFIG_BAR),)  
 MY_SOURCES += bar.c  
endif  
LOCAL_SRC_FILES += $(MY_SOURCES)    #包含將要編譯打包進模塊中的 C 或 C++源代碼文件  
include $(BUILD_SHARED_LIBRARY) #根據LOCAL_XXX系列變量中的值,來編譯生成共享庫(動態鏈接庫)  

 

GNU Make系統變量

變量 描述
CLEAR_VARS 指向一個編譯腳本,幾乎所有未定義的 LOCAL_XXX 變量都在"Module-description"節中列出。必須在開始一個新模塊之前包含這個腳本:include$(CLEAR_VARS),用於重置除LOCAL_PATH變量外的,所有LOCAL_XXX系列變量。
BUILD_SHARED_LIBRARY 指向編譯腳本,根據所有的在 LOCAL_XXX 變量把列出的源代碼文件編譯成一個共享庫。
BUILD_STATIC_LIBRARY 一個 BUILD_SHARED_LIBRARY 變量用於編譯一個靜態庫。靜態庫不會複製到的APK包中,但是能夠用於編譯共享庫。
TARGET_ARCH 目標 CPU平臺的名字,  和 android 開放源碼中指定的那樣。如果是arm,表示要生成 ARM 兼容的指令,與 CPU架構的修訂版無關。
TARGET_PLATFORM Android.mk 解析的時候,目標 Android 平臺的名字.詳情可參考/development/ndk/docs/stable- apis.txt.
TARGET_ARCH_ABI 支持目標平臺
TARGET_ABI 目標平臺和 ABI 的組合,它事實上被定義成$(TARGET_PLATFORM)-$(TARGET_ARCH_ABI)  ,在想要在真實的設備中針對一個特別的目標系統進行測試時,會有用。在默認的情況下,它會是'android-3-arm'。

模塊描述變量

變量 描述
LOCAL_PATH 這個變量用於給出當前文件的路徑。必須在 Android.mk 的開頭定義,可以這樣使用:LOCAL_PATH := $(call my-dir)  這個變量不會被$(CLEAR_VARS)清除,因此每
個 Android.mk 只需要定義一次(即使在一個文件中定義了幾個模塊的情況下)。
LOCAL_MODULE 這是模塊的名字,它必須是唯一的,而且不能包含空格。必須在包含任一的$(BUILD_XXXX)腳本之前定義它。模塊的名字決定了生成文件的名字。例如,如果一個一個共享庫模塊的名字是,那麼生成文件的名字就是 lib.so。但是,在的 NDK 生成文件中(或者 Android.mk 或者 Application.mk),應該只涉及(引用)有正常名字的其他模塊。
LOCAL_SRC_FILES 這是要編譯的源代碼文件列表。只要列出要傳遞給編譯器的文件,因爲編譯系統自動計算依賴。注意源代碼文件名稱都是相對於 LOCAL_PATH的,你可以使用路徑部分。
LOCAL_CPP_EXTENSION 這是一個可選變量, 用來指定C++代碼文件的擴展名,默認是'.cpp',但是可以改變它。
LOCAL_C_INCLUDES 可選變量,表示頭文件的搜索路徑。
LOCAL_CFLAGS 可選的編譯器選項,在編譯 C 代碼文件的時候使用。
LOCAL_CXXFLAGS 與 LOCAL_CFLAGS同理,針對 C++源文件。
LOCAL_CPPFLAGS 與 LOCAL_CFLAGS同理,但是對 C 和 C++ source files都適用。
LOCAL_STATIC_LIBRARIES 表示該模塊需要使用哪些靜態庫,以便在編譯時進行鏈接。
LOCAL_SHARED_LIBRARIES 表示模塊在運行時要依賴的共享庫(動態庫),在鏈接時就需要,以便在生成文件時嵌入其相應的信息。注意:它不會附加列出的模塊到編譯圖,也就是仍然需要在Application.mk 中把它們添加到程序要求的模塊中。
LOCAL_LDLIBS 編譯模塊時要使用的附加的鏈接器選項。這對於使用‘-l’前綴傳遞指定庫的名字是有用的。
LOCAL_ALLOW_UNDEFINED_SYMBOLS 默認情況下, 在試圖編譯一個共享庫時,任何未定義的引用將導致一個“未定義的符號”錯誤。
LOCAL_ARM_MODE 默認情況下, arm目標二進制會以 thumb 的形式生成(16 位),你可以通過設置這個變量爲 arm如果你希望你的 module 是以 32 位指令的形式。
LOCAL_MODULE_PATH 和 LOCAL_UNSTRIPPED_PATH 在 Android.mk 文件中, 還可以用LOCAL_MODULE_PATH 和LOCAL_UNSTRIPPED_PATH指定最後的目標安裝路徑.
不同的文件系統路徑用以下的宏進行選擇:
  TARGET_ROOT_OUT:表示根文件系統。
   TARGET_OUT:表示 system文件系統。
   TARGET_OUT_DATA:表示 data文件系統。
用法如:LOCAL_MODULE_PATH :=$(TARGET_ROOT_OUT) 
至於LOCAL_MODULE_PATH 和LOCAL_UNSTRIPPED_PATH的區別,暫時還不清楚。

GNU Make 功能宏

變量 描述
my-dir 返回當前 Android.mk 所在的目錄的路徑,相對於 NDK 編譯系統的頂層。
all-subdir-makefiles 返回一個位於當前'my-dir'路徑的子目錄中的所有Android.mk的列表。
this-makefile 返回當前Makefile 的路徑(即這個函數調用的地方)
parent-makefile 返回調用樹中父 Makefile 路徑。即包含當前Makefile的Makefile 路徑。
grand-parent-makefile 返回調用樹中父Makefile的父Makefile的路徑

範例:

2、

編譯一個簡單的APK

LOCAL_PATH := $(call my-dir)  
include $(CLEAR_VARS)  
# Build all java files in the java subdirectory  
LOCAL_SRC_FILES := $(call all-subdir-java-files)  
# Name of the APK to build  
LOCAL_PACKAGE_NAME := LocalPackage  
# Tell it to build an APK  
include $(BUILD_PACKAGE)  

編譯一個依賴靜態.jar文件的APK 

LOCAL_PATH := $(call my-dir)  
  include $(CLEAR_VARS)  
  # List of static libraries to include in the package  
  LOCAL_STATIC_JAVA_LIBRARIES := static-library  
  # Build all java files in the java subdirectory  
  LOCAL_SRC_FILES := $(call all-subdir-java-files)  
  # Name of the APK to build  
  LOCAL_PACKAGE_NAME := LocalPackage  
  # Tell it to build an APK  
  include $(BUILD_PACKAGE)  
 注:LOCAL_STATIC_JAVA_LIBRARIES 後面應是你的APK程序所需要的JAVA庫的JAR文件名。  

編譯一個需要platform key簽名的APK

LOCAL_PATH := $(call my-dir)  
  include $(CLEAR_VARS)  
  # Build all java files in the java subdirectory  
  LOCAL_SRC_FILES := $(call all-subdir-java-files)  
  # Name of the APK to build  
  LOCAL_PACKAGE_NAME := LocalPackage  
  LOCAL_CERTIFICATE := platform  
  # Tell it to build an APK  
  include $(BUILD_PACKAGE)  
 注:LOCAL_CERTIFICATE 後面應該是簽名文件的文件名  

編譯一個需要特殊vendor key簽名的APK 

LOCAL_PATH := $(call my-dir)  
 include $(CLEAR_VARS)  
 # Build all java files in the java subdirectory  
 LOCAL_SRC_FILES := $(call all-subdir-java-files)  
 # Name of the APK to build  
 LOCAL_PACKAGE_NAME := LocalPackage  
 LOCAL_CERTIFICATE := vendor/example/certs/app  
 # Tell it to build an APK  
 include $(BUILD_PACKAGE)  

裝載一個普通的第三方APK

LOCAL_PATH := $(call my-dir)  
 include $(CLEAR_VARS)  
 # Module name should match apk name to be installed.  
 LOCAL_MODULE := LocalModuleName  
 LOCAL_SRC_FILES := $(LOCAL_MODULE).apk  
 LOCAL_MODULE_CLASS := APPS  
 LOCAL_MODULE_SUFFIX := $(COMMON_ANDROID_PACKAGE_SUFFIX)  
 LOCAL_CERTIFICATE := platform  
 include $(BUILD_PREBUILT)   

裝載需要.so(動態庫)的第三方apk

LOCAL_PATH := $(my-dir)  
include $(CLEAR_VARS)  
LOCAL_MODULE := baiduinput_android_v1.1_1000e  
LOCAL_SRC_FILES := $(LOCAL_MODULE).apk  
LOCAL_MODULE_CLASS := APPS  
LOCAL_MODULE_SUFFIX := $(COMMON_ANDROID_PACKAGE_SUFFIX)  
LOCAL_CERTIFICATE := platform  
include $(BUILD_PREBUILT)  
   
#################################################################  
####### copy the library to /system/lib #########################  
#################################################################  
include $(CLEAR_VARS)  
LOCAL_MODULE := libinputcore.so  
LOCAL_MODULE_CLASS := SHARED_LIBRARIES  
LOCAL_MODULE_PATH := $(TARGET_OUT_SHARED_LIBRARIES)  
LOCAL_SRC_FILES := lib/$(LOCAL_MODULE)  
OVERRIDE_BUILD_MODULE_PATH := $(TARGET_OUT_INTERMEDIATE_LIBRARIES)  
include $(BUILD_PREBUILT)  

編譯一個靜態java庫 

  LOCAL_PATH := $(call my-dir)  
  include $(CLEAR_VARS)  
  # Build all java files in the java subdirectory  
  LOCAL_SRC_FILES := $(call all-subdir-java-files)  
  # Any libraries that this library depends on  
  LOCAL_JAVA_LIBRARIES := android.test.runner  
  # The name of the jar file to create  
  LOCAL_MODULE := sample  
  # Build a static jar file.  
  include $(BUILD_STATIC_JAVA_LIBRARY)  
注:LOCAL_JAVA_LIBRARIES表示生成的java庫的jar文件名。  

編譯C/C++應用程序模板

LOCAL_PATH := $(call my-dir)  
#include $(CLEAR_VARS)  
LOCAL_SRC_FILES := main.c  
LOCAL_MODULE := test_exe  
#LOCAL_C_INCLUDES :=  
#LOCAL_STATIC_LIBRARIES :=  
#LOCAL_SHARED_LIBRARIES :=  
include $(BUILD_EXECUTABLE)  
注:‘:=’是賦值的意思,'+='是追加的意思,‘$’表示引用某變量的值  
LOCAL_SRC_FILES中加入源文件路徑,LOCAL_C_INCLUDES中加入需要的頭文件搜索路徑  
LOCAL_STATIC_LIBRARIES 加入所需要鏈接的靜態庫(*.a)的名稱,  
LOCAL_SHARED_LIBRARIES 中加入所需要鏈接的動態庫(*.so)的名稱,  
LOCAL_MODULE表示模塊最終的名稱,BUILD_EXECUTABLE 表示以一個可執行程序的方式進行編譯。  
(4)編譯C\C++靜態庫  
LOCAL_PATH := $(call my-dir)  
include $(CLEAR_VARS)  
LOCAL_SRC_FILES := \  
 helloworld.c  
LOCAL_MODULE:= libtest_static  
 #LOCAL_C_INCLUDES :=  
#LOCAL_STATIC_LIBRARIES :=  
#LOCAL_SHARED_LIBRARIES :=  
include $(BUILD_STATIC_LIBRARY)  
和上面相似,BUILD_STATIC_LIBRARY 表示編譯一個靜態庫。  

編譯C\C++動態庫的模板

LOCAL_PATH := $(call my-dir)  
include $(CLEAR_VARS)  
LOCAL_SRC_FILES := helloworld.c  
LOCAL_MODULE := libtest_shared  
TARGET_PRELINK_MODULES := false  
#LOCAL_C_INCLUDES :=  
#LOCAL_STATIC_LIBRARIES :=  
#LOCAL_SHARED_LIBRARIES :=  
include $(BUILD_SHARED_LIBRARY)  
和上面相似,BUILD_SHARED_LIBRARY 表示編譯一個共享庫。  
以上三者的生成結果分別在如下目錄中,generic 依具體 target 會變:  
out/target/product/generic/obj/APPS  
out/target/product/generic/obj/JAVA_LIBRARIES  
out/target/product/generic/obj/EXECUTABLE  
out/target/product/generic/obj/STATIC_LIBRARY  
out/target/product/generic/obj/SHARED_LIBRARY  
每個模塊的目標文件夾分別爲:  
1)APK程序:XXX_intermediates  
2)JAVA庫程序:XXX_intermediates  
這裏的XXX  
 3)C\C++可執行程序:XXX_intermediates  
 4)C\C++靜態庫: XXX_static_intermediates  
 5)C\C++動態庫: XXX_shared_intermediates  

實例:

LOCAL_PATH := $(call my-dir)  #項目地址  
include $(CLEAR_VARS)       #清除變量  
  
LOCAL_MODULE    := libvlcjni    #庫  
  
#源文件  
LOCAL_SRC_FILES := libvlcjni.c libvlcjni-util.c libvlcjni-track.c libvlcjni-medialist.c aout.c vout.c libvlcjni-equalizer.c native_crash_handler.c  
LOCAL_SRC_FILES += thumbnailer.c pthread-condattr.c pthread-rwlocks.c pthread-once.c eventfd.c sem.c  
LOCAL_SRC_FILES += pipe2.c  
LOCAL_SRC_FILES += wchar/wcpcpy.c  
LOCAL_SRC_FILES += wchar/wcpncpy.c  
LOCAL_SRC_FILES += wchar/wcscasecmp.c  
LOCAL_SRC_FILES += wchar/wcscat.c  
LOCAL_SRC_FILES += wchar/wcschr.c  
LOCAL_SRC_FILES += wchar/wcscmp.c  
LOCAL_SRC_FILES += wchar/wcscoll.c  
LOCAL_SRC_FILES += wchar/wcscpy.c  
LOCAL_SRC_FILES += wchar/wcscspn.c  
LOCAL_SRC_FILES += wchar/wcsdup.c  
LOCAL_SRC_FILES += wchar/wcslcat.c  
LOCAL_SRC_FILES += wchar/wcslcpy.c  
LOCAL_SRC_FILES += wchar/wcslen.c  
LOCAL_SRC_FILES += wchar/wcsncasecmp.c  
LOCAL_SRC_FILES += wchar/wcsncat.c  
LOCAL_SRC_FILES += wchar/wcsncmp.c  
LOCAL_SRC_FILES += wchar/wcsncpy.c  
LOCAL_SRC_FILES += wchar/wcsnlen.c  
LOCAL_SRC_FILES += wchar/wcspbrk.c  
LOCAL_SRC_FILES += wchar/wcsrchr.c  
LOCAL_SRC_FILES += wchar/wcsspn.c  
LOCAL_SRC_FILES += wchar/wcsstr.c  
LOCAL_SRC_FILES += wchar/wcstok.c  
LOCAL_SRC_FILES += wchar/wcswidth.c  
LOCAL_SRC_FILES += wchar/wcsxfrm.c  
LOCAL_SRC_FILES += wchar/wmemchr.c  
LOCAL_SRC_FILES += wchar/wmemcmp.c  
LOCAL_SRC_FILES += wchar/wmemcpy.c  
LOCAL_SRC_FILES += wchar/wmemmove.c  
LOCAL_SRC_FILES += wchar/wmemset.c  
  
  
LOCAL_C_INCLUDES := $(VLC_SRC_DIR)/include  #包含頭  
  
ARCH=$(ANDROID_ABI) #變量 平臺  
  
CPP_STATIC=$(ANDROID_NDK)/sources/cxx-stl/gnu-libstdc++$(CXXSTL)/libs/$(ARCH)/libgnustl_static.a #應用靜態庫  
  
LOCAL_CFLAGS := -std=gnu99  #編譯器標識  
ifeq ($(ARCH), armeabi)  
    LOCAL_CFLAGS += -DHAVE_ARMEABI  
    # Needed by ARMv6 Thumb1 (the System Control coprocessor/CP15 is mandatory on ARMv6)  
    # On newer ARM architectures we can use Thumb2  
    LOCAL_ARM_MODE := arm  
endif  
ifeq ($(ARCH), armeabi-v7a)  
    LOCAL_CFLAGS += -DHAVE_ARMEABI_V7A  
endif  
LOCAL_LDLIBS := -L$(VLC_CONTRIB)/lib \  #使用本地庫  
    $(VLC_MODULES) \  
    $(VLC_BUILD_DIR)/lib/.libs/libvlc.a \  
    $(VLC_BUILD_DIR)/src/.libs/libvlccore.a \  
    $(VLC_BUILD_DIR)/compat/.libs/libcompat.a \  
    -ldl -lz -lm -llog \  
    -ldvbpsi -lebml -lmatroska -ltag \  
    -logg -lFLAC -ltheora -lvorbis \  
    -lmpeg2 -la52 \  
    -lavformat -lavcodec -lswscale -lavutil -lpostproc -lgsm -lopenjpeg \  
    -lliveMedia -lUsageEnvironment -lBasicUsageEnvironment -lgroupsock \  
    -lspeex -lspeexdsp \  
    -lxml2 -lpng -lgnutls -lgcrypt -lgpg-error \  
    -lnettle -lhogweed -lgmp \  
    -lfreetype -liconv -lass -lfribidi -lopus \  
    -lEGL -lGLESv2 -ljpeg \  
    -ldvdnav -ldvdread -ldvdcss \  
    $(CPP_STATIC)  
  
include $(BUILD_SHARED_LIBRARY) #編譯成動態庫  
  
  
include $(CLEAR_VARS)   #清除變量  
  
LOCAL_MODULE     := libiomx-gingerbread    
LOCAL_SRC_FILES  := ../$(VLC_SRC_DIR)/modules/codec/omxil/iomx.cpp  
LOCAL_C_INCLUDES := $(VLC_SRC_DIR)/modules/codec/omxil $(ANDROID_SYS_HEADERS_GINGERBREAD)/frameworks/base/include $(ANDROID_SYS_HEADERS_GINGERBREAD)/system/core/include  
LOCAL_CFLAGS     := -Wno-psabi  
LOCAL_LDLIBS     := -L$(ANDROID_LIBS) -lgcc -lstagefright -lmedia -lutils -lbinder  
  
include $(BUILD_SHARED_LIBRARY)  
  
include $(CLEAR_VARS)  
  
LOCAL_MODULE     := libiomx-hc  
LOCAL_SRC_FILES  := ../$(VLC_SRC_DIR)/modules/codec/omxil/iomx.cpp  
LOCAL_C_INCLUDES := $(VLC_SRC_DIR)/modules/codec/omxil $(ANDROID_SYS_HEADERS_HC)/frameworks/base/include $(ANDROID_SYS_HEADERS_HC)/frameworks/base/native/include $(ANDROID_SYS_HEADERS_HC)/system/core/include $(ANDROID_SYS_HEADERS_HC)/hardware/libhardware/include  
LOCAL_CFLAGS     := -Wno-psabi  
LOCAL_LDLIBS     := -L$(ANDROID_LIBS) -lgcc -lstagefright -lmedia -lutils -lbinder  
  
include $(BUILD_SHARED_LIBRARY)  
  
include $(CLEAR_VARS)  
  
LOCAL_MODULE     := libiomx-ics  
LOCAL_SRC_FILES  := ../$(VLC_SRC_DIR)/modules/codec/omxil/iomx.cpp  
LOCAL_C_INCLUDES := $(VLC_SRC_DIR)/modules/codec/omxil $(ANDROID_SYS_HEADERS_ICS)/frameworks/base/include $(ANDROID_SYS_HEADERS_ICS)/frameworks/base/native/include $(ANDROID_SYS_HEADERS_ICS)/system/core/include $(ANDROID_SYS_HEADERS_ICS)/hardware/libhardware/include  
LOCAL_CFLAGS     := -Wno-psabi  
LOCAL_LDLIBS     := -L$(ANDROID_LIBS) -lgcc -lstagefright -lmedia -lutils -lbinder  
  
include $(BUILD_SHARED_LIBRARY)  

 

2、Application.mk

Application.mk目的是描述在你的應用程序中所需要的模塊(即靜態庫或動態庫)。

變量 描述
APP_PROJECT_PATH 這個變量是強制性的,並且會給出應用程序工程的根目錄的一個絕對路徑。
APP_MODULES 這個變量是可選的,如果沒有定義,NDK將由在Android.mk中聲明的默認的模塊編譯,並且包含所有的子文件(makefile文件)如果APP_MODULES定義了,它不許是一個空格分隔的模塊列表,這個模塊名字被定義在Android.mk文件中的LOCAL_MODULE中。
APP_OPTIM 這個變量是可選的,用來義“release”或"debug"。在編譯您的應用程序模塊的時候,可以用來改變優先級。
APP_CFLAGS 當編譯模塊中有任何C文件或者C++文件的時候,C編譯器的信號就會被髮出。
APP_CXXFLAGS APP_CPPFLAGS的別名,已經考慮在將在未來的版本中廢除了
APP_CPPFLAGS 當編譯的只有C++源文件的時候,可以通過這個C++編譯器來設置
APP_BUILD_SCRIPT 默認情況下,NDK編譯系統會在$(APP_PROJECT_PATH)/jni目錄下尋找名爲Android.mk文件:
$(APP_PROJECT_PATH)/jni/Android.mk
APP_ABI 默認情況下,NDK的編譯系統回味"armeabi"ABI生成機器代碼。
APP_STL 默認情況下,NDK的編譯系統爲最小的C++運行時庫(/system/lib/libstdc++.so)提供C++頭文件。然而,NDK的C++的實現,可以讓你使用或着鏈接在自己的應用程序中。
例如:
APP_STL := stlport_static    --> static STLport library
APP_STL := stlport_shared    --> shared STLport library
APP_STL := system            --> default C++ runtime library

實例:

APP_OPTIM := release   //調試版還是發行版  
APP_PLATFORM := android-8  //平臺  
APP_STL := gnustl_static  //C++運行時庫  
APP_CPPFLAGS += -frtti      //編譯標識  
APP_CPPFLAGS += -fexceptions  //編譯標識 異常  
APP_CPPFLAGS += -DANDROID   //編譯標識  
APP_MODULES := test     //靜態模塊  

 

JNI內存泄漏

JAVA 編程中的內存泄漏,從泄漏的內存位置角度可以分爲兩種:JVM 中 Java Heap 的內存泄漏;JVM 內存中 native memory 的內存泄漏。

Java Heap 的內存泄漏:

Java 對象存儲在 JVM 進程空間中的 Java Heap 中,Java Heap 可以在 JVM 運行過程中動態變化。如果 Java 對象越來越多,佔據 Java Heap 的空間也越來越大,JVM 會在運行時擴充 Java Heap 的容量。如果 Java Heap 容量擴充到上限,並且在 GC 後仍然沒有足夠空間分配新的 Java 對象,便會拋出 out of memory 異常,導致 JVM 進程崩潰。
Java Heap 中 out of memory 異常的出現有兩種原因①程序過於龐大,致使過多 Java 對象的同時存在;②程序編寫的錯誤導致 Java Heap 內存泄漏。

JVM 中 native memory 的內存泄漏

從操作系統角度看,JVM 在運行時和其它進程沒有本質區別。在系統級別上,它們具有同樣的調度機制,同樣的內存分配方式,同樣的內存格局。
JVM 進程空間中,Java Heap 以外的內存空間稱爲 JVM 的 native memory。進程的很多資源都是存儲在 JVM 的 native memory 中,例如載入的代碼映像,線程的堆棧,線程的管理控制塊,JVM 的靜態數據、全局數據等等。也包括 JNI 程序中 native code 分配到的資源。
在 JVM 運行中,多數進程資源從 native memory 中動態分配。當越來越多的資源在 native memory 中分配,佔據越來越多 native memory 空間並且達到 native memory 上限時,JVM 會拋出異常,使 JVM 進程異常退出。而此時 Java Heap 往往還沒有達到上限。
多種原因可能導致 JVM 的 native memory 內存泄漏。
例如:
JVM 在運行中過多的線程被創建,並且在同時運行。
JVM 爲線程分配的資源就可能耗盡 native memory 的容量。
JNI 編程錯誤也可能導致 native memory 的內存泄漏。

Native Code 本身的內存泄漏

JNI 編程首先是一門具體的編程語言,或者 C 語言,或者 C++,或者彙編,或者其它 native 的編程語言。每門編程語言環境都實現了自身的內存管理機制。因此,JNI 程序開發者要遵循 native 語言本身的內存管理機制,避免造成內存泄漏。以 C 語言爲例,當用 malloc() 在進程堆中動態分配內存時,JNI 程序在使用完後,應當調用 free() 將內存釋放。總之,所有在 native 語言編程中應當注意的內存泄漏規則,在 JNI 編程中依然適應。
Native 語言本身引入的內存泄漏會造成 native memory 的內存,嚴重情況下會造成 native memory 的 out of memory。

Global Reference 引入的內存泄漏

JNI 編程還要同時遵循 JNI 的規範標準,JVM 附加了 JNI 編程特有的內存管理機制。
JNI 中的 Local Reference 只在 native method 執行時存在,當 native method 執行完後自動失效。這種自動失效,使得對 Local Reference 的使用相對簡單,native method 執行完後,它們所引用的 Java 對象的 reference count 會相應減 1。不會造成 Java Heap 中 Java 對象的內存泄漏。
而 Global Reference 對 Java 對象的引用一直有效,因此它們引用的 Java 對象會一直存在 Java Heap 中。程序員在使用 Global Reference 時,需要仔細維護對 Global Reference 的使用。如果一定要使用 Global Reference,務必確保在不用的時候刪除。就像在 C 語言中,調用 malloc() 動態分配一塊內存之後,調用 free() 釋放一樣。否則,Global Reference 引用的 Java 對象將永遠停留在 Java Heap 中,造成 Java Heap 的內存泄漏。

LocalReference 的深入理解

Local Reference 在 native method 執行完成後,會自動被釋放,似乎不會造成任何的內存泄漏。但這是錯誤的。


泄漏實例1:創建大量的 JNI Local Reference

Java 代碼部分  
 class TestLocalReference {   
 private native void nativeMethod(int i);   
 public static void main(String args[]) {   
         TestLocalReference c = new TestLocalReference();   
         //call the jni native method   
         c.nativeMethod(1000000);   
 }    
 static {   
 //load the jni library   
 System.loadLibrary("StaticMethodCall");   
 }   
 }   
  
  
 JNI 代碼,nativeMethod(int i) 的 C 語言實現  
 #include<stdio.h>   
 #include<jni.h>   
 #include"TestLocalReference.h"  
 JNIEXPORT void JNICALL Java_TestLocalReference_nativeMethod   
 (JNIEnv * env, jobject obj, jint count)   
 {   
 jint i = 0;   
 jstring str;   
  
  
 for(; i<count; i++)   
         str = (*env)->NewStringUTF(env, "0");   
 }   
運行結果  
 JVMCI161: FATAL ERROR in native method: Out of memory when expanding   
 local ref table beyond capacity   
 at TestLocalReference.nativeMethod(Native Method)   
 at TestLocalReference.main(TestLocalReference.java:9)  

泄漏實例2:建立一個 String 對象,返回給調用函數。

JNI 代碼,nativeMethod(int i) 的 C 語言實現  
 #include<stdio.h>   
 #include<jni.h>   
 #include"TestLocalReference.h"  
 jstring CreateStringUTF(JNIEnv * env)   
 {   
 return (*env)->NewStringUTF(env, "0");   
 }   
 JNIEXPORT void JNICALL Java_TestLocalReference_nativeMethod   
 (JNIEnv * env, jobject obj, jint count)   
 {   
 jint i = 0;   
 for(; i<count; i++)   
 {   
         str = CreateStringUTF(env);   
 }   
 }   
運行結果  
 JVMCI161: FATAL ERROR in native method: Out of memory when expanding local ref   
 table beyond  capacity   
 at TestLocalReference.nativeMethod(Native Method)   
 at TestLocalReference.main(TestLocalReference.java:9)  

編譯問題:SLES/OpenSLES.h: No such file or directory
解決方法:ndk-build TARGET_PLATFORM=android-9

 

編譯斷點問題:有沒有好用的斷點工具

解決方法:visualGDB 神器

 

 

 

 

 

Windows 下 JNI 的使用教程

 

參考:IntelliJ idea 2018 平臺下JNI編程調用C++算法(一):https://www.cnblogs.com/lucychen/p/9771236.html

JNI 的使用大致有以下4個步驟:

  • 一、在 Java 中寫 native 方法
  • 二、用 javah 命令生成 C/C++ 頭文件。( 注意:windows 系統生成的動態鏈接庫是 .dll 文件,Linux 是 .so 文件。JDK10 中將 javah 工具取消了,需要使用 javac -h 替代,這是與 jdk8 不同的地方。 )
  • 三、寫對應的 C/C++ 程序,實現頭文件中聲明的方法,並編譯成庫文件
  • 四、在 Java 中加載這個庫文件並使用

注意:Windows 平臺需要注意操作系統位數,32 位 dll 無法在 64位 上被調用。

 

 

一、在 Java 中寫 native 方法

 

主要步驟

  1. 創建一個 java 項目,在其中編寫一個帶有 native 方法的類
  2. 利用 idea 生成 .h 頭文件。  
  3. 在 vs 中創建一個動態鏈接庫應用程序的解決方案
  4. 在解決方案中創建 C++ 文件,實現頭文件中的方法
  5. 生成 動態 鏈接庫
  6. 回到 idea,運行 java 項目,排錯重複以上步驟直到運行成功

 

1. 在 idea 創建 java 項目

實現一個簡單的 testHello_1() 函數 和 靜態的 testHell0_2() 函數,在 C++ 中實現  testHello_1() 和 testHell0_2()。

注意:java 代碼都不要放到默認包下(就是不寫 package 語句就會放到默認包),默認包下的方法在其他地方都不能調用!!

步驟如下:

  1. 在 idea 創建 java 項目(例如:jni_demo),在 src 目錄下新建一個 package(示例 包名  com.jni.test )。
  2. 在包下創建一個類,用來編寫 native 方法和 main 函數。示例 類名 JNIDemo
  3. 聲明  native 方法,native 方法就是聲明一個非 java 實現的方法,比如用 C/C++ 實現。本地方法可以是靜態的,也可以不聲明爲靜態的。

圖示:

示例代碼:

package com.jni.test;

public class JNIDemo {
    public native void testHello_1();
    public static native int testHello_2();

    public static void main(String[] args) {
        try {
            // System.loadLibrary("JNIPROJECT.dll");
            System.load("D:\\jni_demo\\src\\com\\jni\\test\\JNIPROJECT.dll");

            JNIDemo jniDemo =new JNIDemo();
            jniDemo.testHello_1();

            int retVal = testHello_2();
            System.out.println("retVal : " + retVal);
        }
        catch (Exception ex) {
            ex.printStackTrace();
        }
    }
}

其中 testHello_1 是一個類方法,testHello_2 是一個靜態方法,前面都有 native 代表是一個本地函數。

main 函數中,調用 testHello_1 函數 和 testHello_2  函數。下面的 static 代碼塊暫且不談。

代碼寫好後,build 一下項目,生成 class文件,build 後,可在左側目錄看到 out/production 目錄下生成了對應 class 文件。

 

注:

load 和 loadLibrary 區別

  1. 它們都可以用來裝載庫文件,不論是 JNI 庫文件還是非 JNI 庫文件。在任何本地方法被調用之前必須先用這個兩個方法之一把相應的 JNI 庫文件裝載。
  2. System.load 參數爲庫文件的絕對路徑,可以是任意路徑。例如,你可以這樣載入一個 windows 平臺下 JNI 庫 文件:System.load("C:\\Documents and Settings\\TestJNI.dll");
  3. System.loadLibrary 參數爲庫文件名,不包含庫文件的擴展名。例如,你可以這樣載入一個 windows 平臺下 JNI 庫 文件System. loadLibrary ("TestJNI"); 這裏,TestJNI.dll 必須是在 java.library.path 這一 jvm 變量所指向的路徑中。
    可以通過如下方法來獲得該變量的值:System.getProperty("java.library.path");
     默認情況下,在 Windows 平臺下,該值包含如下位置:
            1)和 jre 相關的一些目錄
            2)程序當前目錄
            3)Windows 目錄
            4)系統目錄(system32)
            5)系統環境變量 path 指定目錄。

classpath 與 java.library.path 區別

classpath 路徑下,只能是 jar 或者 class 文件,否者會報錯,因爲他們會被 load 到 JVM 

build ---> build project,

 

 

 

2.生成 頭文件

 

靜態註冊動態註冊

爲什麼需要註冊?其實就是給 Java 的 native 函數找到底層 C/C++ 實現的函數指針。

  • 靜態註冊:通過包名、類名一致來確認,Java 有一個命令 javah,專門生成某一個 JAVA 文件所有的 native 函數的頭文件(h文件), 靜態方法註冊 JNI 有哪些缺點?1:必須遵循某些規則。 2:名字過長。 3:多個 class 需 Javah 多遍。 4:運行時去找效率不高
  • 動態註冊 :在 JNI 層實現的,JAVA 層不需要關心,因爲在 system.load 時就會去掉 JNI_OnLoad,有就註冊,沒就不註冊。
  • 區別:靜態註冊是用到時加載,動態註冊一開始就加載好了,這個可以從 DVM 的源代碼看出來。

 

生成 JNI 頭文件。(此處有兩種方法:2.1手動輸入javah命令生成頭文件、2.2 一鍵生成頭文件)

 

2.1 手動輸入 javah 命令生成頭文件

打開 cmd,進入 src 目錄,運行 javah 命令,生成 C/C++ 頭文件,注意:要帶上 java 包名

命令格式:javah -classpath 要加載的類的路徑 -jni 包名.類名

執行完命令之後,會在 src  目錄生成一個 .h 文件:

在 IntelliJ IDEA 圖示:

頭文件完整代碼:

/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class com_jni_test_JNIDemo */

#ifndef _Included_com_jni_test_JNIDemo
#define _Included_com_jni_test_JNIDemo
#ifdef __cplusplus
extern "C" {
#endif
/*
 * Class:     com_jni_test_JNIDemo
 * Method:    testHello_1
 * Signature: ()V
 */
JNIEXPORT void JNICALL Java_com_jni_test_JNIDemo_testHello_11
  (JNIEnv *, jobject);

/*
 * Class:     com_jni_test_JNIDemo
 * Method:    testHello_2
 * Signature: ()I
 */
JNIEXPORT jint JNICALL Java_com_jni_test_JNIDemo_testHello_12
  (JNIEnv *, jclass);

#ifdef __cplusplus
}
#endif
#endif

頭文件 說明:

  1. 包含了 jni.h 頭文件。
  2. 在類中 聲明 的常量(static final)類型會在頭文件中以宏的形式出現,這一點還是很方便的。

  3. 函數的註釋還是比較全的,包括了:
    1. 對應的 class
    2. 對應的 java 方法名
    3. 對應 java 方法 的 簽名
  4. 方法的聲明顯得有點奇怪,由以下及部分組成:
    1. JNIEXPORT 這是函數的導出方式
    2. jint 返回值類型( jint 由 jni.h定義,對應 int
    3. JNICALL 函數的調用方式也就是彙編級別參數的傳入方式
    4.  Java_com_jni_test_JNIDemo_testHello_11  超級長的函數名!!!
      格式是 :Java_ + 類全名 + _ + JAVA中聲明的 native 方法名。其中會把包名中的點(.)替換成下劃線(_),同時爲了避免衝突把 下劃線 替換成 _1
    5. 方法的參數,上面的這個方法在 JAVA 的聲明中實際上是沒有參數的,其中的 JNIENV 顧名思義是 JNI 環境,和具體的線程綁定。而第二個參數 jclass 其實是 java 中的 Class 因爲上面是一個 static 方法,因此第二個參數是jclass。如果是一個實例方法則對應第二個參數是 jobject,相當於 java 中的 this 

 

2.2 一鍵生成頭文件

頭文件可以使用命令行生成(見參考文獻),或者熟悉格式後自己手寫。但是如果希望能夠隨便點一下就生成頭文件,於是,找到了一種 用idea工具生成頭文件的方法,那就是 External Tools。External Tools 其實就是將手動輸入的命令存下來,本質也是運行 javah,後面跟着配置參數,這些參數存在 External Tools,避免每次手動輸入。

  • 添加 External Tools。File -> Settings -> Tools -> ExternalTools,點擊添加
  • 編輯 Tools
              Name: Generate Header File   
              Program: $JDKPath$/bin/javah 
              Arguments: -jni -classpath $OutputPath$ -d ./jni $FileClass$
              Working directory: $ProjectFileDir$

        Name:External Tools 的名稱,喜歡什麼起什麼,只要自己明白
        Program是javah工具所在地址,即jdk所在路徑下的bin,該參數是指tool採用的運行工具是javah
        Arguments設置的是javah的參數,具體可在命令行中查看javah的幫助,查看每個函數含義
        Working directory:項目名稱

  • 生成頭文件
    保存工具後,右擊需要生成頭文件的類,即我們的SimpleHello,選擇External Tool,點擊我們剛剛創建的tool。
    然後你就會發現我們的目錄中多了一個jni文件夾,jni文件夾裏面有一個名字長長的.h文件,成功!

提示:該方法適用於 jdk8,jdk10 中取消了 javah,的使用 javac -h。

 

jni.h 是什麼 ?

  • jni.h 頭文件一般位於 $JAVA_HOME/jd{jdk-version}/include 目錄內下面的一個文件,jni.h 裏面存儲了大量的函數和對象,這是 JNI 中所有的 類型、函數、宏 等定義的地方。C/C++ 世界的 JNI 規則就是由他制定的。它有個很好的方法就是通過 native 接口名來獲取 C/C++ 函數。
  • 另外還有個 %JAVA_HOME%\bin\include\win32 下的 jni_md.h 

打個比方類似如下:public static String getCMethod(String javaMethodName);

它可以根據你的 java接口,找到 C函數並調用。但這就意味着你不能在 C 裏隨意寫函數名,因爲如果你寫的 java 方法叫 native aaa(); C函數也叫aaa(); 但 jni.h 通過 getCMethod(String javaMethodName) 去找的結果是 xxx(); 那這樣就無法調用了。

既然不能隨意寫,怎麼辦?

沒事,jdk 提供了一個通過 java 方法生成 C/C++ 函數接口名的工具 javah。

 

javah 是什麼?

javah 就是提供具有 native method 的 java 對象的 C/C++ 函數接口。javah  命令可以提供一個 C/C++ 函數的接口。

 

然後就是在 C/C++ 中實現這個方法就可以了。

但是在動手前現大致瞭解以下 jni.h 制定的遊戲規則。javah 生成的頭文件裏面使用的類型都是 jni.h 定義的,目的是做到 平臺無關,比如保證在所有平臺上 jint 都是 32位 的有符號整型。

基本對應關係如下:

jni 類型 JAVA 類型 對應 本地類型 類型簽名
jboolean boolean uint8_t Z
jbyte byte char B
jcahr char uint16_t C
jshort short int16_t S
jint int int32_t I
jlong long int64_t J
jfloat float float F
jdouble double double D
void void void V

引用類型對應關係:

java 類型 JNI 類型 java 類型 JNI 類型
所有的實例引用 jobject java.lang.Class jclass
java.lang.String jstring Ocject[] jobjectArray
java.lang.Throwable jthrowable 基本類型[] jxxxArray

通過表格發現,除了上面定義的 StringClassThrowable,其他的類(除了數組)都是以 jobject 的形式出現的!事實上jstring, jclass 也都是 object 的子類。所以這裏還是和 java 層一樣,一切皆 jobject。(當然,如果 jni 在 C 語言中編譯的話是沒有繼承的概念的,此時 jstring,jclass 等其實就是 jobject !用了 typedef 轉換而已!!)

接下來是 JNIEnv * 這個指針,他提供了 JNI 中的一系列操作的接口函數。

JNI 中操作 jobject

其實也就是在 native 層操作 java 層的實例。 要操作一個實例無疑是:

  1. 獲取/設置 (即 get/set )成員變量(field)的值

  2. 調用成員方法(method)

怎麼得到 field 和 method?

通過使用 jfieldID jmethodID: 在 JNI 中使用類似於放射的方式來進行 field 和 method 的操作。JNI 中使用 jfieldID 和jmethodID 來表示成員變量和成員方法,獲取方式是:

jfieldID GetFieldID(jclass clazz, const char *name, const char *sig);
jfieldID GetStaticFieldID(jclass clazz, const char *name, const char *sig);
jmethodID GetMethodID(jclass clazz, const char *name, const char *sig);
jmethodID GetStaticMethodID(jclass clazz, const char *name, const char *sig) ;

其中最後一個參數是簽名。 獲取 jclass 的方法 除了實用上面靜態方法的第二個參數外,還可以手動獲取。 jclass FindClass(const char *name) 需要注意的是 name 參數,他是一個類包括包名的全稱,但是需要把包名中的點.替換成斜槓/

有了 jfieldID 和 jmethodID 就知道狗蛋住哪了,現在去狗蛋家找他玩 ♪(^∇^*)

 

成員變量:

  1. get:

    1. <type> Get<type>Field(jobject , jfieldID);即可獲得對應的field,其中field的類型是type,可以是上面類型所敘述的任何一種。

    2. <type> GetStatic<type>Field(jobject , jfieldID);同1,唯一的區別是用來獲取靜態成員。

  2. set:

    1. void Set<type>Field(jobject obj, jfieldID fieldID, <type> val)

    2. void SetStatic<type>Field(jclass clazz, jfieldID fieldID, <type> value);

成員方法:

調用方法自然要把方法的參數傳遞進去,JNI中實現了三種參數的傳遞方式:

  1. Call<type>Method(jobject obj, jmethod jmethodID, ...)其中...是C中的可變長參數,類似於printf那樣,可以傳遞不定長個參數。於是你可以把java方法需要的參數在這裏面傳遞進去。

  2. Call<type>MethodV(jobject obj, jmethodID methodID, va_list args)其中的va_list也是C中可變長參數相關的內容(我不瞭解,不敢瞎說。。。偷懶粘一下Oracle的文檔)Programmers place all arguments to the method in an args argument of type va_list that immediately follows the methodID argument. The CallMethodV routine accepts the arguments, and, in turn, passes them to the Java method that the programmer wishes to invoke.

  3. Call<type>MethodA(jobject obj, jmethodID methodID, const jvalue * args)哎!這個我知道可以說兩句LOL~~這裏的jvalue通過查代碼發現就是JNI中各個數據類型的union,所以可以使用任何類型複製!所以參數的傳入方式是通過一個jvalue的數組,數組內的元素可以是任何jni類型。

然後問題又來了:(挖掘機技術到底哪家強?!o(*≧▽≦)ツ┏━┓) 如果傳進來的參數和java聲明的參數的不一致會怎麼樣!(即不符合方法簽名)這裏文檔中沒用明確解釋,但是說道: > Exceptions raised during the execution of the Java method.

typedef union jvalue {
    jboolean z;
    jbyte    b;
    jchar    c;
    jshort   s;
    jint     i;
    jlong    j;
    jfloat   f;
    jdouble  d;
    jobject  l;
} jvalue;
  1. 調用實例方法(instance method):
    1. <type> Call<type>Method(jobject obj, jmethodID methodID, ...);調用一個具有<type>類型返回值的方法。
    2. <type> Call<type>MethodV(jobject obj, jmethodID methodID, va_list args);
    3. Call<type>MethodA(jobject obj, jmethodID methodID, const jvalue * args)
  2. 調用靜態方法(static method):
    1. <type> CallStatic<type>Method(jobject obj, jmethodID methodID, ...);
    2. <type> CallStatic<type>MethodV(jobject obj, jmethodID methodID, va_list args);
    3. CallStatic<type>MethodA(jobject obj, jmethodID methodID, const jvalue * args)
  3. 調用父類方法(super.method),這個就有點不一樣了。多了一個jclass參數,jclass可以使obj的父類,也可以是obj自己的class,但是methodID必須是從jclass獲取到的,這樣就可以調用到父類的方法。
    1. <type> CallNonvirtual<type>Method(jobject obj, jclass clazz, jmethodID methodID, ...)
    2. <type> CallNonvirtual<type>MethodV(JNIEnv *env, jobject obj, jclass clazz, jmethodID methodID, va_list args);
    3. <type> CallNonvirtual<type>MethodA(JNIEnv *env, jobject obj, jclass clazz, jmethodID methodID, const jvalue *args);

#### 數組的操作

數組是一個很常用的數據類型,在但是在JNI中並不能直接操作jni數組(比如jshortArray,jfloatArray)。使用方法是:

  1. 獲取數組長度:jsize GetArrayLength(jarray array)
  2. 創建新數組: ArrayType New<PrimitiveType>Array(jsize length);
  3. 通過JNI數組獲取一個C/C++數組:<type>* Get<type>ArrayElements(jshortArray array, jboolean *isCopy)
  4. 指定原數組的範圍獲取一個C/C++數組(該方法只針對於原始數據數組,不包括Object數組):void Get<PrimitiveType>ArrayRegion(JNIEnv *env, ArrayType array, jsize start, jsize len, NativeType *buf);
  5. 設置數組元素:void Set<type>ArrayRegion(jshortArray array, jsize start, jsize len,const <type> *buf)。again,如果是Object數組需要使用:void SetObjectArrayElement(JNIEnv *env, jobjectArray array, jsize index, jobject value);
  6. 使用完之後,釋放數組:void Release<type>ArrayElements(jshortArray array, jshort *elems, jint mode)

有點要說明的:

  1. 面的3中的isCopy:當你調用getArrayElements時JVM(Runtime)可以直接返回數組的原始指針,或者是copy一份,返回給你,這是由JVM決定的。所以isCopy就是用來記錄這個的。他的值是JNI_TURE或者JNI_FALSE

  2. 6釋放數組。一定要釋放你所獲得數組。其中有一個mode參數,其有三個可選值,分別表示:

  • 0

    • 原始數組:允許原數組被垃圾回收。

    • copy: 數據會從get返回的buffer copy回去,同時buffer也會被釋放。

  • JNI_COMMIT

    • 原始數組:什麼也不做

    • copy: 數據會從get返回的buffer copy回去,同時buffer不會被釋放。

  • JNI_ABORT

    • 原始數組:允許原數組被垃圾回收。之前由JNI_COMMIT提交的對數組的修改將得以保留。

    • copy: buffer會被釋放,同時buffer中的修改將不會copy回數組!

####關於引用與垃圾回收 比如上面有個方法傳了一個jobject進來,然後我把她保存下來,方便以後使用。這樣做是不行噠!因爲他是一個LocalReference,所以不能保證jobject指向的真正的實例不被回收。也就是說有可能你用的時候那個指針已經是個野指針的。然後你的程序就直接Segment Fault了,呵呵。。。

在JNI中提供了三種類型的引用:

  1. Local Reference:即本地引用。在JNI層的函數,所有非全局引用對象都是Local Reference, 它包括函數調用是傳入的jobject和JNI成函數創建的jobject。Local Reference的特點是一旦JNI層的函數返回,這些jobject就可能被垃圾回收。
  2. Glocal Reference:全局引用,這些對象不會主動釋放,永遠不會被垃圾回收。
  3. Weak Glocal Reference:弱全局引用,一種特殊的Global Reference,在運行過程中有可能被垃圾回收。所以使用之前需要使用jboolean IsSameObject(jobject obj1, jobject obj2)判斷它是否已被回收。

Glocal Reference:
1. 創建:jobject NewGlobalRef(jobject lobj);
2. 釋放:void DeleteGlobalRef(jobject gref);

Local Reference:
LocalReference也有一個釋放的函數:void DeleteLocalRef(jobject obj),他會立即釋放Local Reference。 這個方法可能略顯多餘,其實也是有它的用處的。剛纔說Local Reference會再函數返回後釋放掉,但是假如函數返回前就有很多引用佔了很多內存,最好函數內就儘早釋放不必要的內存。

####關於JNI_OnLoad 開頭提到JNI_OnLoad是java1.2中新增加的方法,對應的還有一個JNI_OnUnload,分別是動態庫被JVM加載、卸載的時候調用的函數。有點類似於WIndows裏的DllMain。
前面提到的實現對應native的方法是實現javah生成的頭文件中定義的方法,這樣有幾個弊端:

  1. 函數名太長。很長。。相當長。。。
  2. 函數會被導出,也就誰說可以在動態庫的導出函數表裏面找到這些函數。這將有利於別人對動態庫的逆向工程,因此帶來安全問題。

現在有了JNI_OnLoad,情況好多了。你不光能在其中完成動態註冊native函數的工作還可以完成一些初始化工作。java對應的有了jint RegisterNatives(jclass clazz, const JNINativeMethod *methods,jint nMethods)函數。參數分別是:

  1. jclass clazz,於native層對應的java class

  2. const JNINativeMethod *methods這是一個數組,數組的元素是JNI定義的一個結構體JNINativeMethod

  3. 上面的數組的長度

JNINativeMethod:代碼中的定義如下:

/*
 * used in RegisterNatives to describe native method name, signature,
 * and function pointer.
 */

typedef struct {
    char *name;
    char *signature;
    void *fnPtr;
} JNINativeMethod;

所以他有三個字段,分別是

字段 含義
char *name java class中的native方法名,只需要方法名即可
char *signature 方法簽名
void *fnPtr 對應native方法的函數指針

於是現在你可以不用導出native函數了,而且可以隨意給函數命名,唯一要保證的是參數及返回值的統一。然後需要一個const JNINativeMethod *methods數組來完成映射工作。

看起來大概是這樣的:

//只需導出JNI_OnLoad和JNI_OnUnload(這個函數不實現也行)
/**
 * These are the exported function in this library.
*/
JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *reserved);
JNIEXPORT void JNICALL JNI_OnUnload(JavaVM *vm, void *reserved);

//爲了在動態庫中不用導出函數,全部聲明爲static
//native methods registered by JNI_OnLoad
static jint native_newInstance (JNIEnv *env, jclass);

//實現native方法
/*
* Class:     com_young_soundtouch_SoundTouch
* Method:    native_newInstance
* Signature: ()I
*/
static jint native_newInstance
(JNIEnv *env, jclass ) {
	int instanceID = ++sInstanceIdentifer;
	SoundTouchWrapper *instance = new SoundTouchWrapper();
	if (instance != NULL) {
		sInstancePool[instanceID] = instance;
		++sInstanceCount;
	}
	LOGDBG("create new SouncTouch instance:%d", instanceID);
	return instanceID;
}

//構造JNINativeMethod數組
static JNINativeMethod gsNativeMethods[] = {
		{
			"native_newInstance",
			"()I",
			reinterpret_cast<void *> (native_newInstance)
		}
};
//計算數組大小
static const int gsMethodCount = sizeof(gsNativeMethods) / sizeof(JNINativeMethod);

//JNI_OnLoad,註冊native方法。
JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *reserved) {
	JNIEnv* env;
	jclass clazz;
	LOGD("JNI_OnLoad called");
	if (vm->GetEnv(reinterpret_cast<void**>(&env), JNI_VERSION_1_6) != JNI_OK) {
		return -1;
	}
	//FULL_CLASS_NAME是個宏定義,定義了對應java類的全名(要把包名中的點(.)_替換成斜槓(/))
	clazz = env->FindClass(FULL_CLASS_NAME);
	LOGDBG("register method, method count:%d", gsMethodCount);
	//註冊JNI函數
	env->RegisterNatives(clazz, gsNativeMethods,
		gsMethodCount);
	//必須返回一個JNI_VERSION_1_1以上(不含)的版本號,否則直接加載失敗
	return JNI_VERSION_1_6;
}

###實戰技巧篇

這裏主要是巧用C中的宏來減少重複工作:

####迅速生成全名

//修改包名時只需要改以下的宏定義即可
#define FULL_CLASS_NAME "com/young/soundtouch/SoundTouch"
#define func(name) Java_ ## com_young_soundtouch_SoundTouch_ ## name
#define constance(cons) com_young_soundtouch_SoundTouch_ ## cons

比如func(native_1newInstance)展開成:Java_com_young_soundtouch_SoundTouch_native_1newInstance即JNI中需要導出的函數名(不過用動態註冊方式沒太大用了)

constance(AUDIO_FORMAT_PCM16)展開成com_young_soundtouch_SoundTouch_AUDIO_FORMAT_PCM16這個着實有用。

而且如果包名改了也可以很方便的適應之。

###安卓的log

//define __USE_ANDROID_LOG__ in makefile to enable android log
#if defined(__ANDROID__) && defined(__USE_ANDROID_LOG__)
#include <android/log.h>
#define LOGV(...)   __android_log_print((int)ANDROID_LOG_VERBOSE, "ST_jni", __VA_ARGS__)
#define LOGD(msg)  __android_log_print((int)ANDROID_LOG_DEBUG, "ST_jni_dbg", "line:%3d %s", __LINE__, msg)
#define LOGDBG(fmt, ...) __android_log_print((int)ANDROID_LOG_DEBUG, "ST_jni_dbg", "line:%3d " fmt, __LINE__, __VA_ARGS__)
#else
#define LOGV(...) 
#define LOGD(fmt) 
#define LOGDBG(fmt, ...) 
#endif

通過這樣的宏定義在打LOGD或者LOGDBG的時候還能自動加上行號!調試起來爽多了!

####C++中清理內存的方式

由於C++裏面需要手動清楚內存,因此我的解決方案是定義一個map,給每個實例一個id,用id把java中的對象和native中的對象綁定起來。在java層定義一個release方法,用來釋放本地的對象。 本地的 KEY-對象 映射 static std::map<int, SoundTouchWrapper*> sInstancePool;

####關於NDK 因爲安卓的約定是把本地代碼放到jni目錄下面,但是假如有多個jni lib的時候會比較混亂,所以方案是每一個lib都在jni裏面建一個子目錄,然後jni裏面的Android.mk就可以去構建子目錄中的lib了。

jni/Android.mk如下(超級簡單):

LOCAL_PATH := $(call my-dir)
include $(call all-subdir-makefiles)

然後在子目錄soundtouch_module中的Android.mk就可以像一般的Android.mk一樣書寫規則了。

同時記錄一下在Andoroid.mk中使用makefile內建函數wildcard的方法。 有時候源文件是一個目錄下的所有.cpp/.c文件,這時候wildcard來統配會很方便。但是Android.mk與普通的Makefile的不同在於:

  1. 調用Android.mkmingling的${CWD}並不是Android.ml所在的目錄。所以Android.mk中有一個變量LOCAL_PATH := $(call my-dir)來記錄當前 Android.mk所在的目錄。
  2. 同時還會把所有的LOCAL_SRC_FILES 前面加上$(LOCAL_PATH)這樣寫makefile的時候就可以用相對路徑了,提供了方便。但是這也導致了坑!

因爲1,直接使用相對路徑會導致wildcard匹配不到源文件。所以最好這麼寫FILE_LIST := $(wildcard $(LOCAL_PATH)/soundtouch_source/source/SoundTouch/*.cpp)。然而又因爲2,這樣還是不行的。所以還需要匹配之後把$(LOCAL_PATH)的部分去掉,因此還得這樣$(FILE_LIST:$(LOCAL_PATH)/%=%).

還有個小tip:LOCAL_CFLAGS中最好加上這個定義-fvisibility=hidden這樣就不會在動態庫中導出不必要的函數了。

###附錄簽名

JAVA中的函數簽名包括了函數的參數類型,返回值類型。因此即使是重載了的函數,其函數簽名也不一樣。java編譯器就會根據函數簽名來判斷你調用的到地址哪個方法。 簽名中表示類型是這樣的

1.基本類型都對應一個大寫字母,如下:

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

2.如果是類則是: L + 類全名(報名中的點(.)用(/)代替)+ ; 比如java.lang.String 對應的是 Ljava/lang/String;

3.如果是數組,則在前面加[然後加類型簽名,幾位數組就加幾個[ 比如int[]對應[I,boolean[][] 對應 [[Z,java.lang.Class[]對應[Ljava/lang/Class;

可以通過javap命令來獲取簽名(javah生成的頭文件註釋中也有簽名):javap -x -p <類全名> 坑爹的是java中並不能通過反射來獲取方法簽名,需要自己寫一個幫助類。 (其實我還寫了個小程序可以自動生成簽名,和JNI_OnLoad中註冊要用到的JNINativeMethod數組,從此再也不用糟心的去寫那該死的數組了。LOL~~~)

 

 

 

3. 在 VS 中創建解決方案

接下來打開 Visual studio 2019,新建動態鏈接庫: JniProject

填寫 項目名,項目所在目錄:

創建完成後再添加類:

 

設置項目包含目錄

  • 本來我是按照這篇文章複製jni.h等文件的,但是一直報錯“找不到 源 文件 jni.h”。搞來搞去總是不成,後來才發現,我在vs2017直接複製,jni.h並沒有到C++項目目錄下,而是仍然在原來的目錄裏,這與java的ide很不同啊。雖然被這個問題搞到差點摔桌子,但我轉念一想,在原來的目錄下就還不錯啊,省得我複製來複制去。於是刷刷刷設置了包含路徑

  • 點擊項目,我的項目叫 jniCppDemo,在菜單欄選擇:項目 -> 屬性 -> 配置屬性 -> VC++目錄 -> 包含目錄
  • 設置包含路徑:          
              設置 jni.h 所在路徑: C:\Program Files\Java\jdk1.8.0_181\include
              設置 jni_md.h 所在路徑: C:\Program Files\Java\jdk1.8.0_181\include\win32
              設置剛剛生成頭文件所在路徑: D:\javaWorkspace\jniJavaDemo\jni

 

如果不想設置 包含目錄,可以直接把文件( jni.h、com_jni_test_JNIDemo.h、jni_md.h )複製到工程目錄下.

JDK 安裝目錄的 include 目錄下有一個 jni.h 的文件,include 的 win32 目錄下有個 jni_md.h 文件,還有 java 工程的 src 目錄下的C 頭文件,一起拷貝到 C工程的 JniProject 目錄下:( JniProject ---> jni.h   com_jni_test_JNIdemo.h    jni_md.h )如下圖:

在 C項目的頭文件文件夾上面:右鍵 --- > 添加 ---> 現有項

選擇 jni.h、com_jni_test_JNIDemo.h、jni_md.h

添加完可以在 頭文件 目錄中看到

打開 com_jni_test_JNIDemo.h 文件

#include <jni.h> 修改爲 #include "jni.h" 錯誤提示消失。

 

 

4. 編寫 cpp 文件

然後在 TestJNI.cpp 文件中寫入如下代碼:

#include "pch.h"
#include "TestJNI.h"
#include "com_jni_test_JNIDemo.h"


JNIEXPORT void JNICALL Java_com_jni_test_JNIDemo_testHello_11
(JNIEnv*, jobject) {
	printf("this is C++ print : Java_com_jni_test_JNIDemo_testHello_11\n");
}


JNIEXPORT jint JNICALL Java_com_jni_test_JNIDemo_testHello_12
(JNIEnv*, jclass) {
	printf("this is C++ print : Java_com_jni_test_JNIDemo_testHello_12\n");
	return 100;
}

 

 

5. 生成 dll 文件

使用 C/C++ 實現本地方法生成動態庫文件(windows下擴展名爲 DDL,linux 下擴展名爲 so):

寫好了 cpp,就可以生成 dll。右擊項目生成/重新生成,就生成了 dll 文件。從控制檯輸出可看到 dll 的地址

注意:設置爲 64位

保存,運行,編譯生成 DLL 文件,在工程項目的 release 目錄中可以找到。

 

 

6. 運行 Java

示例代碼 1:

package com.jni.test;

public class JNIDemo {
    public native void testHello_1();
    public static native int testHello_2();

    public static void main(String[] args) {
        try {
            // System.loadLibrary("JNIPROJECT.dll");
            System.load("D:\\jni_demo\\src\\com\\jni\\test\\JNIPROJECT.dll");

            JNIDemo jniDemo =new JNIDemo();
            jniDemo.testHello_1();

            int retVal = testHello_2();
            System.out.println("retVal : " + retVal);
        }
        catch (Exception ex) {
            ex.printStackTrace();
        }
    }
}

運行截圖:

示例代碼 2:

package com.jni.test;

public class JNIDemo {
    public native void testHello_1();
    public static native int testHello_2();

    static {
        // System.loadLibrary("JNIPROJECT.dll");
        System.load("D:\\jni_demo\\src\\com\\jni\\test\\JNIPROJECT.dll");
    }

    public static void main(String[] args) {
        try {
            JNIDemo jniDemo =new JNIDemo();
            jniDemo.testHello_1();

            int retVal = testHello_2();
            System.out.println("retVal : " + retVal);
        }
        catch (Exception ex) {
            ex.printStackTrace();
        }
    }
}

運行截圖:

 

注意:

  • 1、一般在 static 代碼塊中加載動態鏈接庫
  • 2、如果將 DLL 路徑加入 PATH 環境變量的時候,eclipse是開着的,那麼要關閉 eclipse 再開,讓 eclipse 重新讀取環境變量
  • 3、必須在本類中使用native方法

 

 

 

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章