Android進階知識樹——JNI和So庫開發

一、Jni基礎知識

JNI是Java Native Interface的縮寫,意思是Java的本地接口,這個本地接口主要指Java能夠經過本地接口去和其餘的編程語言通訊,有時在開發某個功能時想使用以前的技術積累或封裝好的模塊,但不幸的是以前不是用Java開發的,那對於此中狀況該如何處理呢?對於通過時間驗證的可靠程序不可能輕易重寫和修改,因此就須要JNI做爲程序的中轉樞紐;java

  • Jni使用場景
  1. 須要調用Java語言不支持的依賴時,
  2. 整合非Java語言開發的系統,如C、C++
  3. 節省運行時間提升運行效率,如:音視頻等
  • Jni類型和Java類型的映射關係

既然Jni是Java和其餘語言的溝通橋樑,那麼它既必須有一套基礎協議做爲與Java代碼溝通的基礎,這個基礎就是類型的映射和簽名,類型映射就是在Java類型和Jni中的類型創建一一對應關係,從而實現兩者的類型可讀性和惟一性,簽名指Java中類型、方法、屬性等在Java中的展示形式,根據最終的簽名查找方法的對應關係;android

  1. native方法與Jni映射實例
public static native String action(short s , int i, long l, float f, double d, char c, boolean z, byte b, String str, Object obj, ArrayList<String> array, int[] arr, Action action);
//生成的Jni對應的代碼
JNIEXPORT jstring JNICALL Java_com_alex_kotlin_jni_JniTest_action (JNIEnv *, jclass, jshort, jint, jlong, jfloat, jdouble, jchar, jboolean, jbyte, jstring, jobject, jobject, jintArray, jobject);
複製代碼
  1. 基本數據映射關係
    在這裏插入圖片描述
  2. 引用類型關係映射表
    在這裏插入圖片描述
  • Jni方法名:Java_類全路徑_方法名
//native
public native void test();
//jni
JNIEXPORT jstring JNICALL Java_com_alex_kotlin_jni_JniTest_test (JNIEnv *, jobject);
複製代碼

上面是Java代碼中聲明的test()轉換後的Jni方法,此方法名稱在編譯javah文件時生成,在實現的C文件中重寫並實現便可,方法的命名規則:編程

  1. Java:表示C++實現Java方法的前綴
  2. com_alex_kotlin_jni_JniTest:JniTest的類名全路徑
  3. test:native方法名稱
  • 參數規則
  1. JNIEnv *:每一個native函數的入口參數,執行JVM函數表的指針,JNIEnv便可在Native環境中使用Java資源
  2. jobject:調用java中native方法的實例或class對象,若是native方法是普通方法,則該參數是jobject,若是是靜態方法,則是jclass
  3. 剩餘參數爲native方法的傳入參數,此處爲JNI中的映射類型(參照介紹映射關係)
  • Jni簽名
  1. 數據類型簽名:見上面對照表
  2. 方法簽名:將參數簽名和返回值類型簽名組合一塊兒做爲方法簽名
JNIEXPORT jstring JNICALL Java_com_alex_kotlin_jni_JniTest_setTest (JNIEnv *env, jobject cls, jstring j_str) {
}
方法簽名:(Ljava/lang/Object,Ljava/lang/String)Lava/lang/String
複製代碼
1.一、 JNI 函數註冊
  • 靜態註冊

靜態註冊JNI方法很簡單,咱們在Java中聲明native方法後,會執行Java命令編譯和生成Jni方法:數組

javac ***
javah ***
複製代碼

在執行javah的命令後,系統會在之間文件處建立.h文件,當咱們在Java層調用native方法時,系統就會根據JNI方法命名規則,按照JNI方法名尋找對應的方法,在找到JNI方法後保存JNI指針創建方法間的聯繫,下次調用時直接使用函數指針就能夠,不過靜態註冊在初次調用時須要查找再創建關聯,影響效率,與靜態註冊對應的就是動態註冊,不須要編譯和查找關聯;緩存

  • 動態註冊
  1. 在C++文件中實現native方法,此時方法名並無嚴格要求
JNIEXPORT jstring JNICALL native_method(JNIEnv *env, jobject) {
    return env->NewStringUTF("Register method in Jni");
};
複製代碼
  1. 建立註冊的方法數組,在數組中創建Java方法和Jni方法的對應關係
static JNINativeMethod methods[] = {
    //參數:一、Java聲明的native方法名;二、方法簽名;三、c中實現的方法名
    {"method", "()Ljava/lang/String;", (void *) native_method},
};
複製代碼
  1. 在重寫的JNI_OnLoad()中調用註冊方法實現註冊
// 聲明動態註冊對應的Java類的路徑
static const char *const PACKAGE_NAME = "com/alex/kotlin/jni/JniTest";

JNIEXPORT jint JNI_OnLoad(JavaVM *vm, void *) {
    JNIEnv *env;  //獲取JNIEnv
    if (JNI_OK != vm->GetEnv(reinterpret_cast<void **>(&env), JNI_VERSION_1_6)) {
        return JNI_ERR;
    }
    jclass jclass1 = env->FindClass(PACKAGE_NAME); //根據類名獲取jclass
    if (jclass1 == NULL) {
        return JNI_ERR;
    }
    jclassGlobal = static_cast<jclass>(env->NewWeakGlobalRef(jclass1)); //建立全局緩存jclass
    env->DeleteLocalRef(jclass1); //釋放局部變量
    if (JNI_OK != env->RegisterNatives(jclassGlobal, method, 1)) { //註冊方法
        return JNI_ERR;
    }
    return JNI_VERSION_1_6;
}
複製代碼

在建立的C++文件中重寫Jni.h中的JNI_OnLoad()方法,在JNI_OnLoad中首先根據報名獲取Java類的jclass對象,而後全局緩存jclass對象,最後調用RegisterNatives()方法傳入jclass和關係數組實現方法的註冊編程語言

  • 在UnLoad()中解除方法註解
JNIEXPORT void JNICALL JNI_OnUnload(JavaVM *vm, void *reserved) {
    JNIEnv *env = NULL;
    if (vm->GetEnv((void **) &env, JNI_VERSION_1_6) != JNI_OK) {
        return;
    }
    env->UnregisterNatives(jclassGlobal); //解除註冊
    env->DeleteGlobalRef(jclassGlobal); //釋放全局變量
}
複製代碼

二、Jni基本使用

在介紹完JNI基礎知識後,一塊兒來學習下JNI在開發中的基本使用,也就是Jni的基本語法,其實在上面動態註冊時已經使用到了一些,這裏根據使用頻率介紹下最經常使用的方法;ide

2.一、字符串使用
JNIEXPORT jstring JNICALL Java_com_alex_kotlin_jni_JniTest_setTest (JNIEnv *env, jobject cls, jstring j_str) {
    const char *c_str = NULL;
    char buff[128] = {};
    jboolean copy;
    c_str = env->GetStringUTFChars(j_str, &copy); // 字符串訪問
    if(c_str == NULL){ 
        return NULL;
    }
    sprintf(buff, "Jni %s", c_str); //字符串輸出
    env->ReleaseStringUTFChars(j_str, c_str); //字符串釋放
    return env->NewStringUTF(buff); // 字符串建立
}
複製代碼
  • 訪問字符串:GetStringUTFChars(j_str, &copy)
  1. j_str:訪問的本地字符串,這裏爲參數傳入
  2. copy:表示引用是否拷貝,若是設置true則拷貝一份使用,false指向源字符串指針
  3. 對字符串的拷貝可能會由於內存問題而失敗,在讀取本地字符串以後,必定要檢查字符串是否爲NULL再使用,爲空則返回
  4. ReleaseStringUTFChars釋放字符串資源,GetStringUTFChars/ReleaseStringUTFChars處理UTF編碼的字符串
  • 釋放引用的資源:ReleaseStringUTFChars(j_str, c_str);
  1. 在C語言中獲取字符串,再使用完畢後須要釋放資源,不然形成內存溢出
  2. 通常Get***和Release***成對使用,針對不容的編碼選擇不一樣的方法
  • 建立字符串:NewStringUTF()
env->NewStringUTF(「this is string !");
複製代碼
  • 其他字符串方法
  1. GetStringChars和ReleaseStringChars:用於建立和釋放Unicode格式編碼的字符串
const jchar *c_str = NULL;
c_str = env->GetStringChars(j_str, &copy);
if (c_str == NULL) {
    return NULL;
}
sprintf(buff,"Jni %s",c_str);  //將字符串緩存到buff中
env->ReleaseStringChars(j_str, c_str);
複製代碼
  1. GetStringLength:獲取Unicode編碼的字符串的長度
jsize lenUtf = env->GetStringLength(j_str);
複製代碼
  1. GetStringUTFLength:獲取UTF編碼的字符串的長度
jsize lenUtf = env->GetStringUTFLength(j_str);
複製代碼
  1. GetStringCritical和ReleaseStringCritical:提升JVM返回源字符串直接指針的可能性,前面的獲取字符串會拷貝並分配內存,此方法直接讀取字符串無需分配內存,但不能在中間臨界區間調用Jni方法和線程阻塞程序
const jchar *c_str = NULL;
    char buff[128] = {};
    jboolean copy;
    c_str = env->GetStringCritical(j_str, &copy); //讀取字符串
    if (c_str == NULL) {
        return NULL;
    }
    sprintf(buff,"Jni %s",c_str);
    env->ReleaseStringCritical(j_str, c_str); //釋放字符串
    return env->NewStringUTF(buff);
複製代碼
  1. GetStringRegion和GetStringUTFRegion:截取UTF和Unicode格式字符串的部份內容,會將源字符串拷貝到緩存區中
JNIEXPORT jstring JNICALL Java_com_alex_kotlin_jni_JniTest_setTest(JNIEnv *env, jobject cls, jstring j_str) {
    char buff[128] = {};
    jsize lenUtf = env->GetStringUTFLength(j_str);   //讀取字符串長度
    env->GetStringUTFRegion(j_str, 0, 4, buff);   //截取字符串0~4緩存到buff中
    return env->NewStringUTF(buff);   //建立字符串
}
//輸入 "From Native !」 ,輸出結果:「From」
複製代碼
2.二、數組操做
  • 訪問Java傳入的數組
Get<PrimitiveType>ArrayElements(ArrayType array, jboolean *isCopy)
複製代碼
  1. 參數isCopy表示是否拷貝返回副本,若是返回的指針指向Java數組地址而非副本,此時會阻止Java對數組的回收,但建立副本時可能因內存問題建立失敗,使用前應檢查NULL
  • 釋放數組使用:ReleaseArrayElements
  1. 獲取數組和釋放數組必須配對使用
  2. Release中的最後參數mode針對拷貝副本時可設置三種形式,可根據是否須要回寫數組進行選擇
* 0:將 elems 內容回寫到 Java 數組並釋放 elems 佔用的空間,回寫的意思是會去修改原Java中的數組
* JNI_COMMIT:將 elems 內容回寫到 Java 數組,但不釋放 elems 的空間;
* JNI_ABORT:不回寫 elems 內容到 Java 數組,釋放 elems 的空間。
複製代碼
  • 使用實例
jint *array ;
jboolean  jboolean1;
array = env->GetIntArrayElements(jintArray1, &jboolean1);  //獲取集合
if (array == NULL){
    return;
}
array[2] = 5;  //修改集合中的參數
env->ReleaseIntArrayElements(jintArray1,array,0); //釋放array集合並寫入Java集合

//在Java中調用Jni
int[] number = new int[]{1,2,3,4,5};
Log.e("Before Jni=====",number[2] + "");
test.getAction(number);  //調用Jni方法
Log.e("After Jni=====",number[2] + "");

2019-04-29 22:09:07.698 15989-15989/com.alex.kotlin.jni E/Before Jni=====: 3
2019-04-29 22:09:07.698 15989-15989/com.alex.kotlin.jni E/After Jni=====: 5  //3改變程5
//使用JNI_ABORT
2019-04-29 22:11:55.485 16108-16108/com.alex.kotlin.jni E/Before Jni=====: 3
2019-04-29 22:11:55.485 16108-16108/com.alex.kotlin.jni E/After Jni=====: 3 //不變
複製代碼

由上面的運行結果知道,在ReleaseIntArrayElements()傳入0時,Java層傳入的數組在執行方法後被改變,從而驗證了將數據回寫到Java的結論,在使用JNI_ABORT時,Java中數據並未發生改變;函數

  • GetArrayLength(j_array):獲取Array的長度
  • malloc(len):申請len長度的緩存區
  • memset (c_array,0,len):初始化長度爲len的array集合
  • memcpy (buffer, data, len):將數組中指定長度的數據拷貝到buff數組中
jbyte* data = env->GetByteArrayElements(javaArray, NULL);
    if (data != NULL) {
        memcpy(buffer, data, len);
        env->ReleaseByteArrayElements(javaArray, data, JNI_ABORT);
    }
複製代碼
  • GetIntArrayRegion(env,j_array,0,arr_len,c_array):複製源集合中指定長度的數據到目標集合中,和上面塊拷貝功能一致
env->GetByteArrayRegion(javaArray, 0, len, buffer);
複製代碼
  • 訪問對象數組
  1. FindClass(env,"[I」):獲取Int數據引用類型
  2. NewObjectArray(env,size,clsIntArray,NULL):建立一個數組對象
  3. NewIntArray(env,size):建立一個Int數組

三、C/C++與Java的訪問

  • C訪問Java中靜態方法,實現步驟
  1. 根據訪問的類路徑調用FindClass()獲取Class對象
  2. 根據class、方法名、參數、返回值等條件獲取Java方法MethodId
  3. 跟據class、methodId執行Java方法
public class Action {
   public static void actionStatic(){
      Log.e("=======","Static method from action in Java ") ;
   }
}
//Jni方法
JNIEXPORT void JNICALL Java_com_alex_kotlin_jni_JniTest_test
        (JNIEnv *env, jobject) {
    jclass cls = NULL;
    jmethodID method_Id = NULL;
    cls = env->FindClass("com/alex/kotlin/jni/Action」); //查找Action類文件
    method_Id = env->GetStaticMethodID(cls, "actionStatic", "()V」); //根據類、方法名、參數條件獲取MethodId
    env->CallStaticVoidMethod(cls,method_Id); //調用靜態方法
    env->DeleteGlobalRef(cls); //釋放class
}
2019-04-27 17:25:27.580 10961-10961/com.alex.kotlin.jni E/=======: Static method from action in Java
複製代碼
  • 訪問Java成員方法,訪問步驟:
  1. 根據類的全路徑查找class
  2. 根據方法簽名,查找構造函數的方法methodId
  3. 執行構造函數方,建立類的實例
  4. 根據方法名、參數、返回值查找調用方法的id
  5. 使用建立的實例調用相應的方法
public class Action {
   public void action(){
      Log.e("=======","Method from action in Java ") ;
   }
}
//Jni中代碼
jclass clss = NULL;
jmethodID method_Id = NULL;
jmethodID construct_Id = NULL;
jobject obj = NULL;
clss = env->FindClass("com/alex/kotlin/jni/Action」);  //查找Action類
construct_Id = env->GetMethodID(clss, "<init>", "()V」);  //獲取構造函數方法的Id
obj = env->NewObject(clss, construct_Id);  //建立構造Action實例
method_Id = env->GetMethodID(clss, "action", "()V」);  //建立action()方法的Id
env->CallVoidMethod(obj,method_Id); //調用action()方法

2019-04-27 17:42:31.774 11880-11880/com.alex.kotlin.jni E/=======: Method from action in Java
複製代碼
  • 訪問Java靜態實例,訪問步驟:
  1. 查找類的class
  2. 根據屬性名稱、屬性的數據類型獲取fieldId
  3. 根據fieldId訪問屬性
public class Action {
   private static int number = 100;
   public int getNumber() {
     return number;
  }
  public void setNumber(int number) {
     Action.number = number;
  }
}
複製代碼
  1. 聲明訪問變量的native方法
public native void getStaticInstance();
複製代碼
  1. 在Jni方法中訪問靜態變量
cls = env->FindClass("com/alex/kotlin/jni/Action」);  //獲取功能類
field_id = env->GetStaticFieldID(cls, "number", "I」);  //獲取靜態變量Id
number = env->GetStaticIntField(cls, field_id); //從類中獲取靜態變量
jint num = 555;
env->SetStaticIntField(cls, field_id, num); //爲靜態變量賦值
複製代碼
  1. 調用和執行方法
Action action = new Action();
action.setMessage("Message in Java");
action.setNumber(123);

Log.e("Before======", action.getNumber() + "");
test.getStaticInstance();
Log.e("After Jni======", action.getNumber() + "");

2019-04-27 21:32:51.592 15022-15022/com.alex.kotlin.jni E/Before======: 123
2019-04-27 21:32:51.592 15022-15022/com.alex.kotlin.jni E/After Jni======: 555
複製代碼
  • 訪問Java成員實例(須要傳入實例)
  1. 根據傳入的object獲取類的class
  2. 根據參數名、參數類型獲取fieldId
  3. 根據fieldId和object訪問屬性
public class Action {
   private String message = null;
   public String getMessage() {
      return message;
   }
   public void setMessage(String message) {
      this.message = message;
   }
}
複製代碼
  1. 聲明訪問變量的native方法
public native void getInstance(Action action);
複製代碼
  1. Jni中讀取成員變量
cls = env->GetObjectClass(obj); //從參數object中獲取class
field_id = env->GetFieldID(cls, "message", "Ljava/lang/String;」); //根據參數名和類型獲取Field_Id
str = static_cast<jstring>(env->GetObjectField(obj, field_id));  //根據field_Id從obj中獲取實例
new_str = env->NewStringUTF("Message in Jni」); //建立新的String
env->SetObjectField(obj, field_id, new_str);  //設置Obj中的數值
複製代碼
  1. 訪問靜態實例
Log.e("Before======", action.getMessage() + "");
test.getInstance(action);
Log.e("After Jni======", action.getMessage() + "");

2019-04-27 21:32:51.592 15022-15022/com.alex.kotlin.jni E/Before======: Message in Java
2019-04-27 21:32:51.592 15022-15022/com.alex.kotlin.jni E/After Jni======: Message in Jni
複製代碼
  • 訪問構造函數:由上面的實例知道訪問構造函數分三步
  1. FindClass()查找類的jclass
  2. GetMethodID(clss, "", "()V」):獲取構造函數的MethosId
  3. NewObject(clss, construct_Id):建立實例
  • 訪問父類的方法
  1. FindClass()查找類的jclass
  2. GetMethodID(clss, "", "()V」):獲取構造函數的MethosId
  3. NewObject(clss, construct_Id):建立類的實例
  4. 使用FindClas s查找父類的jclass對象
  5. 獲取父類方法的MethodId
  6. 調用父類方法,此處須要同時傳入子類和父類的對象
public class Action {
   public void action(){
      Log.e("==========","Action in Action");
   }
}
public class ChildAction extends Action {
   @Override
   public void action() {
      Log.e("==========","Action in ChildAction");
   }
}

cls = env->FindClass("com.alex.kotlin.jni.Action」); //一、
jmethodID1 = env->GetMethodID(cls, "<init>", "()V」); //二、
jobject1 = env->NewObject(cls, jmethodID1); //三、
cls_parent = env->FindClass("com.alex.kotlin.jni.ChildAction」); //四、
jmethodID2 = env->GetMethodID(cls_parent, "action()", "()V」); //五、
env->CallNonvirtualVoidMethod(jobject1, cls_parent, jmethodID2); //六、
複製代碼

四、引用類型

  • 局部引用
  1. 局部引用不能跨方法和跨線程使用,至關於Java中的局部變量
  2. 會阻止GC的回收,在方法結束返回Java後對象沒被引用會自動釋放,若是被引用則阻止回收
  3. 在方法中使用基本方法建立的都是局部變量,可使用DeleteLocalRef釋放
  4. 局部變量使用完後要及時釋放資源,不然當變量個數超過512個時拋出異常
  5. 推薦使用Push/PopLocalFrame(建立一個局部變量的引用棧),在方法的入口使用PushLocalFrame,在每一個返回的地方調用PopLocalFrame,這樣在函數中建立的任何變量都會被釋放;
  • 全局引用
  1. 能夠跨方法、跨線程使用,至關於Java中的成員屬性
  2. 只能經過NewGlobalRef函數建立
  3. 會組織對象被GC回收,只有手動調用DeleteGlobalRef釋放
  • 弱全局引用
  1. 使用NewGlobalWeakRef建立,使用DeleteGlobalWeakRef釋放
  2. 能夠跨方法、跨線程使用
  3. 不會阻止GC回收,當內存不足時會被回收,至關於Java中的若引用
  4. 在使用時須要檢查引用的對象是否被回收,因此每次引用以前須要作非空判斷
  • 引用比較
  1. IsSameObject(env, obj1, obj2):若是obj1和obj2指向相同的對象,則返回JNI_TRUE(或者1),不然返回JNI_FALSE(或者0)
  2. 局部引用和全局引用使用IsSameObject與NULL比較判斷是否爲null
  3. IsSameObject用於弱全局引用與NULL比較時,返回值的意義是不一樣於局部引用和全局引用,此時判斷對象是否被回收
  • 管理引用規則
  1. 不要形成全局引用和弱引用的增長
  2. 不要在函數軌跡上遺漏任何的局部引用
  3. 在方法對外返回實例時,要註釋清楚返回的是全局、局部變量

五、其餘知識

5.一、緩存
  • 使用時緩存
  1. 使用靜態字段緩存數據,在第一次加載初始化,以後直接使用緩存數據
  2. 不能緩存局部引用,局部引用釋放後容易形成緩存的空指針
  • 靜態初始化時緩存
  1. 在引入庫時直接調用native方法initIDs
  2. 在c文件中實現initIds方法,在其中緩存資源
static { 
    System.loadLibrary("AccessCache"); 
    initIDs(); 
}
複製代碼
5.二、Jni異常處理
  • Jni中沒有像try…catch()異常處理機制
  • Jni中拋出異常程序不會馬上執行,此時爲了中止程序調用ThrowNew()手動拋出異常後調用return結束函數
  • Jni中捕獲異常的方法
  1. ExceptionCheck():檢查Java是否拋出異常
  2. ExceptionDescribe():打印Java異常堆棧信息
  3. ExceptionClear():清除異常堆棧信息
  4. ThrowNew():手動拋出一個java.lang.Exception異常
//Java 拋出異常代碼
public void actionException() throws Exception {
   throw new Exception("Java 拋出異常");
}
//C文件中調用
jmethodID1 = env->GetMethodID(cls, "actionException", "()V");
env->CallVoidMethod(obj, jmethodID1);
if (env->ExceptionCheck()) { //檢查並拋出異常
    env->ExceptionDescribe();
    env->ExceptionClear();
    env->ThrowNew(env->FindClass("java/lang/Exception"), "Jni 拋出異常");
}
//運行結果
2019-04-29 19:12:54.919 32350-32350/com.alex.kotlin.jni W/System.err: java.lang.Exception: Java 拋出異常
2019-04-29 19:12:54.920 32350-32350/com.alex.kotlin.jni E/AndroidRuntime: FATAL EXCEPTION: main
    Process: com.alex.kotlin.jni, PID: 32350
    java.lang.RuntimeException: Unable to start activity ComponentInfo{com.alex.kotlin.jni/com.alex.kotlin.jni.MainActivity}: java.lang.Exception: Jni 拋出異常
複製代碼
  • 使用ExceptionOccurred捕獲異常
env->CallVoidMethod(obj, jmethodID1);
if (env->ExceptionOccurred()) {
    env->ExceptionDescribe();
    env->ExceptionClear();
    env->ThrowNew(env->FindClass("java/lang/Exception"), "Jni 拋出異常");
}
複製代碼
  • 異常發生後釋放資源
if ((*env)->ExceptionCheck(env)) { /* 異常檢查 */
         (*env)->ReleaseStringChars(env, jstr, cstr); // 發生異常後釋放前面所分配的內存
         return; 
     }
複製代碼

六、使用Jni生成So庫文件

對於咱們正常開發來講,直接使用Jni的場景不多,通常Jni的方法都會封裝在So庫中供Java層調用,如今就根據上面的Jni知識利用AS生成一個So庫。工具

6.一、Android Studio 配置Jni

  • 下載NDK和構建工具
  1. 在Android Studio的SDK Manager中選擇LLDB、CMake、NDK下載
    在這裏插入圖片描述
  • 配置ndk-build
  1. 在local.properties文件中配置ndk文件路徑
ndk.dir=/Users/wuliangliang/Library/Android/sdk/ndk-bundle
複製代碼
  • 建立C++項目
  1. 建立新的項目選擇C++工程
  2. 建立完成後,項目中首先會有一個cpp文件夾,其中已配置好Jni開發的Demo(能夠做爲參考)
  3. 在main文件夾下建立jni文件夾
  • 配置CMakeList.txt文件,C代碼生成so庫的配置文件
  1. 建立text或複製cpp文件夾下的CMakeList.txt
cmake_minimum_required(VERSION 3.4.1) :配置So庫的最小版本
add_library(  //每一個C文件具有一個add_library
        jni-lib  // 配置So庫名稱
        SHARED  //設置So庫設置爲Shared共享庫
        test.cpp)  // 源C文件的相對路徑
find_library(  //,
        log-lib
        log)
target_link_libraries(  
        jni-lib  // 指定目標庫
        ${log-lib}) // 將目標庫鏈接到NDK的日誌庫
複製代碼

上面配置中:學習

  1. add_library()做用是建立並命名一個So庫,「jni-lib」就是最終生成So庫的名程,最終生成的So庫名稱爲「linjni-lib.so」文件
  2. test.cpp爲對應So庫的C代碼文件,系統在編譯時會自動定位和關聯此文件,
  3. 因爲NDK已是CMake搜索的一部分,因此在使用時只須要向NDK設置要使用庫的名稱,在配置文件中使用find_library()定位Ndk,並將其路徑存儲爲變量設,在以後構建腳本時使用此變量待指NDK,此處設置的log-lib就是NDK的變量
  4. 爲了保證原生庫能夠在log中調用函數,須要使用target_link_libraries()命令關聯庫
  • build.gradle中配置:關聯CMakeList的配置文件路徑和Jni文件路徑
android{
 externalNativeBuild { 
    cmake {
        path "src/main/jni/CMakeLists.txt」 //配置CMakeList的文件路徑
    }
  }
  sourceSets { main { jni.srcDirs = ['src/main/jni', 'src/main/jni/'] } } //配置Jni文件輸出路徑
}
複製代碼

6.二、建立Native代碼,生成並使用So庫

  • 建立Java文件並聲明native方法
public class JniTest {
   static {
      System.loadLibrary("jni-lib」); //引入so庫
   }
   public static native String test();  //配置native方法
}
複製代碼
  • 在jni文件夾下生成編譯後的.h文件
  • 建立cpp文件實現native方法
JNIEXPORT void JNICALL Java_com_alex_kotlin_jni_JniTest_test (JNIEnv *env, jobject) {
    jclass cls = NULL;
    jmethodID method_Id = NULL;
    cls = env->FindClass("com/alex/kotlin/jni/Action");
    if (cls == NULL) {
        return;
    }
    method_Id = env->GetStaticMethodID(cls, "test", "()V");
    env->CallStaticVoidMethod(cls, method_Id);
    env->DeleteGlobalRef(cls);
}
複製代碼

這裏調用FindClass()根據類名獲取Class對象,而後使用全局變量保存Class對象,而後查找並調用actionStatic()

  • 根據so庫名稱配置CMakeList.txt後執行Make Project,系統會自動在build文件夾下建立so庫
    在這裏插入圖片描述
  • 調用native方法
JniTest test = new JniTest();
tv.setText(test.test());
複製代碼

關於Jni的基本知識點和用法基本介紹完了,在通常的開發中可能使用的很少,但想作進一步的功能或優化時就常常會使用到,因此Jni也成爲Android高級開發這必備基礎知識,但願此篇文章的總結對須要的同窗有所幫助;

相關文章
相關標籤/搜索