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

1、Jni基礎知識

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

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

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

  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.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[] = {
    //參數:1、Java聲明的native方法名;2、方法簽名;3、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); //釋放全局變量
}

2、Jni基本使用

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

2.1、字符串使用
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.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數組

3、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”); //1、
jmethodID1 = env->GetMethodID(cls, "<init>", "()V”); //2、
jobject1 = env->NewObject(cls, jmethodID1); //3、
cls_parent = env->FindClass("com.alex.kotlin.jni.ChildAction”); //4、
jmethodID2 = env->GetMethodID(cls_parent, "action()", "()V”); //5、
env->CallNonvirtualVoidMethod(jobject1, cls_parent, jmethodID2); //6、

4、引用類型

  • 局部引用
  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、其他知識

5.1、緩存
  • 使用時緩存
  1. 使用靜態字段緩存數據,在第一次加載初始化,之後直接使用緩存數據
  2. 不能緩存局部引用,局部引用釋放後容易造成緩存的空指針
  • 靜態初始化時緩存
  1. 在引入庫時直接調用native方法initIDs
  2. 在c文件中實現initIds方法,在其中緩存資源
static { 
    System.loadLibrary("AccessCache"); 
    initIDs(); 
}
5.2、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; 
     }

6、使用Jni生成So庫文件

對於我們正常開發來說,直接使用Jni的場景很少,一般Jni的方法都會封裝在So庫中供Java層調用,現在就根據上面的Jni知識利用AS生成一個So庫。

6.1、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.2、創建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高級開發這必備基礎知識,希望此篇文章的總結對需要的同學有所幫助;

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