這篇文章主要講解了 JNI 的基礎語法和交叉編譯的基本使用,經過這篇文章的學習就徹底能夠入門 Android 下 JNI 項目的開發了。java
從 JVM 角度,存在兩種類型的代碼:「Java」和「native」, native 通常指的是 c/c++,爲了使 java 和 native 端可以進行交互,java 設計了 JNI(java native interface)。 JNI 容許java虛擬機(VM)內運行的java代碼與C++、C++和彙編等其餘編程語言編寫的應用程序和庫進行互操做。linux
雖然大部分狀況下咱們的軟件徹底能夠由 java 來實現,可是某些場景下使用 native 代碼更加適合,好比:android
native 層使用 JNI 主要能夠作到:c++
使用 as 建立一個 native c++ 項目git
文件結構以下:程序員
能夠看到生成了一個 cpp 文件夾,裏面有 CMakeLists.txt, native-lib.cpp,CMakeLists後面再講,這裏先來看一下 native-lib.cpp 和 java 代碼。github
public class MainActivity extends AppCompatActivity { static { System.loadLibrary("native-lib"); } ... public native String stringFromJNI(); } 複製代碼
#include <jni.h> #include <string> extern "C" JNIEXPORT jstring JNICALL Java_com_wangzhen_jnitutorial_MainActivity_stringFromJNI(JNIEnv* env, jobject thiz) { std::string hello = "Hello from C++"; return env->NewStringUTF(hello.c_str()); } 複製代碼
能夠看到在 MainActivity 中先定義了一個 native 方法,而後編譯器在 cpp 文件中建立一個一個對應的方法Java_com_wangzhen_jnitutorial_MainActivity_stringFromJNI
。 它的命名規則就是 Java_packageName_methodName。shell
接下來咱們詳細的解讀一下 cpp 中的代碼。編程
在 c++ 中使用 c 代碼api
宏定義:#define JNIEXPORT __attribute__ ((visibility ("default")))
在 Linux/Unix/Mac os/Android 這種類 Unix 系統中,定義爲__attribute__ ((visibility ("default")))
GCC 有個visibility屬性, 該屬性是說, 啓用這個屬性:
宏定義,在 Linux/Unix/Mac os/Android 這種類 Unix 系統中,它是個空的宏定義: #define JNICALL
,因此在 android 上刪除它也能夠。 快捷生成 .h 代碼
struct _JNIEnv { /** * 定義了不少的函數指針 **/ const struct JNINativeInterface* functions; #if defined(__cplusplus) /// 經過類的名稱(類的全名,這時候包名不是用.號,而是用/來區分的)來獲取jclass jclass FindClass(const char* name) { return functions->FindClass(this, name); } ... } 複製代碼
JNIEnv 的結構圖以下:
JavaVM : JavaVM 是 Java虛擬機在 JNI 層的表明, JNI 全局只有一個
JNIEnv : JavaVM 在線程中的表明, 每一個線程都有一個, JNI 中可能有不少個 JNIEnv,同時 JNIEnv 具備線程相關性,也就是 B 線程沒法使用 A 線程的 JNIEnv
JVM 的結構圖以下:
這個 object 指向該 native 方法的 this 實例,好比咱們在 MainActivity 調用的下面的 native 函數中打印一下 thiz 的 className:
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR,"JNI",__VA_ARGS__); extern "C" JNIEXPORT jstring JNICALL Java_com_wangzhen_jnitutorial_MainActivity_stringFromJNI(JNIEnv *env, jobject thiz) { std::string hello = "Hello from C++"; // 1. 獲取 thiz 的 class,也就是 java 中的 Class 信息 jclass thisclazz = env->GetObjectClass(thiz); // 2. 根據 Class 獲取 getClass 方法的 methodID,第三個參數是簽名(params)return jmethodID mid_getClass = env->GetMethodID(thisclazz, "getClass", "()Ljava/lang/Class;"); // 3. 執行 getClass 方法,得到 Class 對象 jobject clazz_instance = env->CallObjectMethod(thiz, mid_getClass); // 4. 獲取 Class 實例 jclass clazz = env->GetObjectClass(clazz_instance); // 5. 根據 class 的 methodID jmethodID mid_getName = env->GetMethodID(clazz, "getName", "()Ljava/lang/String;"); // 6. 調用 getName 方法 jstring name = static_cast<jstring>(env->CallObjectMethod(clazz_instance, mid_getName)); LOGE("class name:%s", env->GetStringUTFChars(name, 0)); return env->NewStringUTF(hello.c_str()); } 複製代碼
打印結果以下:
Java Type | Native Type | Description |
---|---|---|
boolean | jboolean | unsigned 8 bits |
byte | jbyte | signed 8 bits |
char | jchar | unsigned 16 bits |
short | jshort | signed 16 bits |
int | jint | signed 32 bits |
long | jlong | signed 64 bits |
float | jfloat | 32 bits |
double | jdouble | 64 bits |
void | void | N/A |
這裏貼一張 oracle 文檔中的圖,雖然很醜但挺好:
JNIEvn 操做 java 對象時利用 java 中的反射,操做某個屬性都須要 field 和 method 的 id,這些 id 都是指針類型:
struct _jfieldID; /* opaque structure */ typedef struct _jfieldID *jfieldID; /* field IDs */ struct _jmethodID; /* opaque structure */ typedef struct _jmethodID *jmethodID; /* method IDs */ 複製代碼
將一個 Java int[] 對象傳入 C++ 中,如何操做這個數組呢?
JNIEXPORT void JNICALL Java_com_wangzhen_jnitutorial_MainActivity_setArray(JNIEnv *env, jobject thiz, jintArray array) { // 1.獲取數組長度 jint len = env->GetArrayLength(array); LOGE("array.length:%d", len); jboolean isCopy; // 2.獲取數組地址 // 第二個參數表明 javaArray -> c/c++ Array 轉換的方式: // 0: 把指向Java數組的指針直接傳回到本地代碼中 // 1: 新申請了內存,拷貝了數組 // 返回值: 數組的地址(首元素地址) jint *firstElement = env->GetIntArrayElements(array, &isCopy); LOGE("is copy array:%d", isCopy); // 3.遍歷數組(移動地址) for (int i = 0; i < len; ++i) { LOGE("array[%i] = %i", i, *(firstElement + i)); } // 4.使用後釋放數組 // 第一個參數是 jarray,第二個參數是 GetIntArrayElements 返回值 // 第三個參數表明 mode env->ReleaseIntArrayElements(array,firstElement,0); // 5. 建立一個 java 數組 jintArray newArray = env->NewIntArray(3); } 複製代碼
- mode = 0 刷新java數組 並 釋放c/c++數組
- mode = JNI_COMMIT (1) 只刷新java數組
- mode = JNI_ABORT (2) 只釋放c/c++數組
extern "C" JNIEXPORT void JNICALL Java_com_wangzhen_jnitutorial_MainActivity_setString(JNIEnv *env, jobject thiz, jstring str) { // 1.jstring -> char* // java 中的字符創是 unicode 編碼, c/C++ 是UTF編碼,因此須要轉換一下。第二個參數做用同上面 const char *c_str = env -> GetStringUTFChars(str,NULL); // 2.異常處理 if(c_str == NULL){ return; } // 3.當作一個 char 數組打印 jint len = env->GetStringLength(str); for (int i = 0; i < len; ++i) { LOGE("c_str: %c",*(c_str+i)); } // 4.釋放 env->ReleaseStringUTFChars(str,c_str); } 複製代碼
調用完 GetStringUTFChars 以後不要忘記安全檢查,由於 JVM 須要爲新誕生的字符串分配內存空間,當內存空間不夠分配的時候,會致使調用失敗,失敗後 GetStringUTFChars 會返回 NULL,並拋出一個OutOfMemoryError 異常。JNI 的異常和 Java 中的異常處理流程是不同的,Java 遇到異常若是沒有捕獲,程序會當即中止運行。而 JNI 遇到未決的異常不會改變程序的運行流程,也就是程序會繼續往下走,這樣後面針對這個字符串的全部操做都是很是危險的,所以,咱們須要用 return 語句跳事後面的代碼,並當即結束當前方法。
在 java 代碼中,MainActivity 有兩個成員變量:
public class MainActivity extends AppCompatActivity { String testField = "test1"; static int staticField = 1; } 複製代碼
// 1. 獲取類 class jclass clazz = env->GetObjectClass(thiz); // 2. 獲取成員變量 id jfieldID strFieldId = env->GetFieldID(clazz,"testField","Ljava/lang/String;"); // 3. 根據 id 獲取值 jstring jstr = static_cast<jstring>(env->GetObjectField(thiz, strFieldId)); const char* cStr = env->GetStringUTFChars(jstr,NULL); LOGE("獲取 MainActivity 的 String field :%s",cStr); // 4. 修改 String jstring newValue = env->NewStringUTF("新的字符創"); env-> SetObjectField(thiz,strFieldId,newValue); // 5. 釋放資源 env->ReleaseStringUTFChars(jstr,cStr); env->DeleteLocalRef(newValue); env->DeleteLocalRef(clazz); // 獲取靜態變量 jfieldID staticIntFieldId = env->GetStaticFieldID(clazz,"staticField","I"); jint staticJavaInt = env->GetStaticIntField(clazz,staticIntFieldId); 複製代碼
GetFieldID 和 GetStaticFieldID 須要三個參數:
Type | Signature Java Type |
---|---|
Z | boolean |
B | byte |
C | char |
S | short |
I | int |
J | long |
F | float |
D | double |
V | void |
L fully-qualified-class; | fully-qualified-class |
[type | type[] |
(arg-types) ret-type | method type |
基本數據類型的比較好理解,不如要獲取一個 int ,GetFieldID 須要傳入簽名就是 I;
若是是一個類,好比 String,簽名就是 L+全類名; :Ljava.lang.String;
若是是一個 int array,就要寫做 [I
若是要獲取一個方法,那麼方法的簽名是:(參數簽名)返回值簽名,參數若是是多個,中間不須要加間隔符,好比: | java 方法|JNI 簽名| |--|--| |void f (int n); |(I)V| |void f (String s,int n); |(Ljava/lang/String;I)V| |long f (int n, String s, int[] arr); |(ILjava/lang/String;[I)J|
操做 method 和 filed 很是類似,先獲取 MethodID,而後對應的 CallXXXMethod 方法
Java層返回值 | 方法族 | 本地返回類型NativeType |
---|---|---|
void | CallVoidMethod() | (無) |
引用類型 | CallObjectMethod( ) | jobect |
boolean | CallBooleanMethod ( ) | jboolean |
byte | CallByteMethod( ) | jbyte |
char | CallCharMethod( ) | jchar |
short | CallShortMethod( ) | jshort |
int | CallIntMethod( ) | jint |
long | CallLongMethod() | jlong |
float | CallFloatMethod() | jfloat |
double | CallDoubleMethod() | jdouble |
在 java 中咱們要想獲取 MainActivity 的 className 會這樣寫:
this.getClass().getName() 複製代碼
能夠看到須要先調用 getClass 方法獲取 Class 對象,而後調用 Class 對象的 getName 方法,咱們來看一下如何在 native 方法中調用:
extern "C" JNIEXPORT jstring JNICALL Java_com_wangzhen_jnitutorial_MainActivity_stringFromJNI(JNIEnv *env, jobject thiz) { std::string hello = "Hello from C++"; // 1. 獲取 thiz 的 class,也就是 java 中的 Class 信息 jclass thisclazz = env->GetObjectClass(thiz); // 2. 根據 Class 獲取 getClass 方法的 methodID,第三個參數是簽名(params)return jmethodID mid_getClass = env->GetMethodID(thisclazz, "getClass", "()Ljava/lang/Class;"); // 3. 執行 getClass 方法,得到 Class 對象 jobject clazz_instance = env->CallObjectMethod(thiz, mid_getClass); // 4. 獲取 Class 實例 jclass clazz = env->GetObjectClass(clazz_instance); // 5. 根據 class 的 methodID jmethodID mid_getName = env->GetMethodID(clazz, "getName", "()Ljava/lang/String;"); // 6. 調用 getName 方法 jstring name = static_cast<jstring>(env->CallObjectMethod(clazz_instance, mid_getName)); LOGE("class name:%s", env->GetStringUTFChars(name, 0)); // 7. 釋放資源 env->DeleteLocalRef(thisclazz); env->DeleteLocalRef(clazz); env->DeleteLocalRef(clazz_instance); env->DeleteLocalRef(name); return env->NewStringUTF(hello.c_str()); } 複製代碼
首先定義一個 java 類:
public class Person { private int age; private String name; public Person(int age, String name){ this.age = age; this.name = name; } public void print(){ Log.e("Person",name + age + "歲了"); } } 複製代碼
而後咱們再 JNI 中建立一個 Person 並調用它的 print 方法:
// 1. 獲取 Class jclass pClazz = env->FindClass("com/wangzhen/jnitutorial/Person"); // 2. 獲取構造方法,方法名固定爲<init> jmethodID constructID = env->GetMethodID(pClazz,"<init>","(ILjava/lang/String;)V"); if(constructID == NULL){ return; } // 3. 建立一個 Person 對象 jstring name = env->NewStringUTF("alex"); jobject person = env->NewObject(pClazz,constructID,1,name); jmethodID printId = env->GetMethodID(pClazz,"print","()V"); if(printId == NULL){ return; } env->CallVoidMethod(person,printId); // 4. 釋放資源 env->DeleteLocalRef(name); env->DeleteLocalRef(pClazz); env->DeleteLocalRef(person); 複製代碼
JNI 分爲三種引用:
上面的代碼片斷中最後都會有釋放資源的代碼,這是 c/c++ 編程的良好習慣,對於不一樣 JNI 引用有不一樣的釋放方式。
JNI 函數返回的全部 Java 對象都是局部引用,好比上面調用的 NewObject/FindClass/NewStringUTF 等等都是局部引用。
有了自動釋放以後爲何還須要手動釋放呢?主要考慮一下場景:
因此咱們應該養成手動釋放本地引用的好習慣。
在調用 GetStringUTFChars 函數從 JVM 內部獲取一個字符串以後,JVM 內部會分配一塊新的內存,用於存儲源字符串的拷貝,以便本地代碼訪問和修改。即然有內存分配,用完以後立刻釋放是一個編程的好習慣。經過調用ReleaseStringUTFChars 函數通知 JVM 這塊內存已經不使用了。
JNI 容許程序員從局部引用建立全局引用:
static jstring globalStr; if(globalStr == NULL){ jstring str = env->NewStringUTF("C++"); // 從局部變量 str 建立一個全局變量 globalStr = static_cast<jstring>(env->NewGlobalRef(str)); //局部能夠釋放,由於有了一個全局引用使用str,局部str也不會使用了 env->DeleteLocalRef(str); } 複製代碼
全局引用在顯式釋放以前保持有效,能夠經過 DeleteGlobalRef 來手動刪除全局引用調用。
與全局引用相似,弱引用能夠跨方法、線程使用。與全局引用不一樣的是,弱引用不會阻止GC回收它所指向的VM內部的對象
因此在使用弱引用時,必須先檢查緩存過的弱引用是指向活動的對象,仍是指向一個已經被GC的對象
static jclass globalClazz = NULL; //對於弱引用 若是引用的對象被回收返回 true,不然爲false //對於局部和全局引用則判斷是否引用java的null對象 jboolean isEqual = env->IsSameObject(globalClazz, NULL); if (globalClazz == NULL || isEqual) { jclass clazz = env->GetObjectClass(instance); globalClazz = static_cast<jclass>(env->NewWeakGlobalRef(clazz)); env->DeleteLocalRef(clazz); } 複製代碼
刪除使用 DeleteWeakGlobalRef
局部變量只能在當前線程使用,而全局引用能夠跨方法、跨線程使用,直到它被手動釋放纔會失效。
在 android 中有兩種方式加載動態庫:
好比下面代碼會報錯,在 java.library.path 下找不到 hello
static{ System.loadLibrary("Hello"); } 複製代碼
可使用下面代碼打印出 java.library.path ,而且吧 hello 拷貝到改路徑下:
public static void main(String[] args){ System.out.println(System.getProperty("java.library.path")); } 複製代碼
調用System.loadLibrary()函數時, 內部就會去查找so中的 JNI_OnLoad 函數,若是存在此函數則調用。 JNI_OnLoad 必須返回 JNI 的版本,好比 JNI_VERSION_1_六、JNI_VERSION_1_8。
JNI 匹配對應的 java 方法有兩種方式:
靜態註冊的名字須要包名,太長了,可使用動態註冊來縮短方法名。
好比咱們再 Java 中有兩個 native 方法:
public class MainActivity extends AppCompatActivity { public native void dynamicJavaFunc1(); public native int dynamicJavaFunc2(int i); } 複製代碼
在 native 代碼中,咱們不使用靜態註冊,而使用動態註冊
void dynamicNativeFunc1(){ LOGE("調用了 dynamicJavaFunc1"); } // 若是方法帶有參數,前面要加上 JNIEnv *env, jobject thisz jint dynamicNativeFunc2(JNIEnv *env, jobject thisz,jint i){ LOGE("調用了 dynamicTest2,參數是:%d",i); return 66; } // 須要動態註冊的方法數組 static const JNINativeMethod methods[] = { {"dynamicJavaFunc1","()V",(void*)dynamicNativeFunc1}, {"dynamicJavaFunc2","(I)I",(int*)dynamicNativeFunc2}, }; // 須要動態註冊native方法的類名 static const char *mClassName = "com/wangzhen/jnitutorial/MainActivity"; jint JNI_OnLoad(JavaVM* vm, void* reserved){ JNIEnv* env = NULL; // 1. 獲取 JNIEnv,這個地方要注意第一個參數是個二級指針 int result = vm->GetEnv(reinterpret_cast<void **>(&env), JNI_VERSION_1_6); // 2. 是否獲取成功 if(result != JNI_OK){ LOGE("獲取 env 失敗"); return JNI_VERSION_1_6; } // 3. 註冊方法 jclass classMainActivity = env->FindClass(mClassName); // sizeof(methods)/ sizeof(JNINativeMethod) result = env->RegisterNatives(classMainActivity,methods, 2); if(result != JNI_OK){ LOGE("註冊方法失敗") return JNI_VERSION_1_2; } return JNI_VERSION_1_6; } 複製代碼
這樣咱們再 MainActivity 中調用 dynamicJavaFunc1 方法就會調用 native 中的 dynamicNativeFunc1 方法。
前面介紹過 JNIEnv* 是和線程相關的,那麼若是在 c++ 中新建一個線程A,在線程A 中能夠直接使用 JNIEnv* 嗎? 答案是否認的,若是想在 native 線程中使用 JNIEnv* 須要使用 JVM 的 AttachCurrentThread 方法進行綁定:
JavaVM *_vm; jint JNI_OnLoad(JavaVM* vm, void* reserved){ _vm = vm; return JNI_VERSION_1_6; } void* threadTask(void* args){ JNIEnv *env; jint result = _vm->AttachCurrentThread(&env,0); if (result != JNI_OK){ return 0; } // ... // 線程 task 執行完後不要忘記分離 _vm->DetachCurrentThread(); } extern "C" JNIEXPORT void JNICALL Java_com_wangzhen_jnitutorial_MainActivity_nativeThreadTest(JNIEnv *env, jobject thiz) { pthread_t pid; pthread_create(&pid,0,threadTask,0); } 複製代碼
在一個平臺上編譯出另外一個平臺上能夠執行的二級制文件的過程叫作交叉編譯。好比在 MacOS 上編譯出 android 上可用的庫文件。 若是想要編譯出能夠在 android 平臺上運行的庫文件就須要使用 ndk。
linux 平臺上的庫文件分爲兩種:
Android 原生開發套件 (NDK):這套工具使您能在 Android 應用中使用 C 和 C++ 代碼。 CMake:一款外部編譯工具,可與 Gradle 搭配使用來編譯原生庫。若是您只計劃使用 ndk-build,則不須要此組件。 LLDB:Android Studio 用於調試原生代碼的調試程序。
原生開發套件 (NDK) 是一套工具,使您可以在 Android 應用中使用 C 和 C++ 代碼,並提供衆多平臺庫。 咱們能夠在 sdk/ndk-bundle 中查看 ndk 的目錄結構,下面列舉出三個重要的成員:
ndk 爲何要提供多平臺呢? 不一樣的 Android 設備使用不一樣的 CPU,而不一樣的 CPU 支持不一樣的指令集。更具體的內容參考官方文檔
在 ndk 目錄下的 toolchains 下有多個平臺的編譯工具,好比在 /arm-linux-androideabi-4.9/prebuilt/darwin-x86_64/bin 下能夠找到 arm-linux-androideabi-gcc 執行文件,利用 ndk 的這個 gcc 能夠編譯出在 android(arm 架構) 上運行的動態庫:
arm-linux-androideabi-gcc -fPIC -shared test.c -o libtest.so
複製代碼
參數含義 -fPIC: 產生與位置無關代碼 -shared:編譯動態庫,若是去掉表明靜態庫 test.c:須要編譯的 c 文件 -o:輸出 libtest.so:庫文件名
獨立工具鏈 版本比較新的 ndk 下已經找不到 gcc 了,若是想用的話須要參考獨立工具鏈。 好比執行
$NDK/build/tools/make_standalone_toolchain.py --arch arm --api 21 --install-dir/$yourDir
能夠產生 arm 的獨立工具鏈
$NDK 表明 ndk 的絕對路徑, $yourDir 表明輸出文件路徑
當源文件不少的時候,手動編譯既麻煩又容易出錯,此時出現了 makefile 編譯。
makefile 就是「自動化編譯」:一個工程中的源文件不計數,其按類型、功能、模塊分別放在若干個目錄中,makefile定義了一系列的規則來指定,哪些文件須要先編譯,哪些文件須要後編譯,如何進行連接等等操做。 Android 使用 Android.mk 文件來配置 makefile,下面是一個最簡單的 Android.mk:
# 源文件在的位置。宏函數 my-dir 返回當前目錄(包含 Android.mk 文件自己的目錄)的路徑。
LOCAL_PATH := $(call my-dir)
# 引入其餘makefile文件。CLEAR_VARS 變量指向特殊 GNU Makefile,可爲您清除許多 LOCAL_XXX 變量
# 不會清理 LOCAL_PATH 變量
include $(CLEAR_VARS)
# 指定庫名稱,若是模塊名稱的開頭已經是 lib,則構建系統不會附加額外的前綴 lib;而是按原樣採用模塊名稱,並添加 .so 擴展名。
LOCAL_MODULE := hello
# 包含要構建到模塊中的 C 和/或 C++ 源文件列表 以空格分開
LOCAL_SRC_FILES := hello.c
# 構建動態庫
include $(BUILD_SHARED_LIBRARY)
複製代碼
咱們配置好了 Android.mk 文件後如何告訴編譯器這是咱們的配置文件呢? 這時候須要在 app/build.gradle 文件中進行相關的配置:
apply plugin: 'com.android.application' android { compileSdkVersion 29 defaultConfig { ... // 應該將源文件編譯成幾個 CPU so externalNativeBuild{ ndkBuild{ abiFilters 'x86','armeabi-v7a' } } // 須要打包進 apk 幾種 so ndk { abiFilters 'x86','armeabi-v7a' } } // 配置 native 構建腳本位置 externalNativeBuild{ ndkBuild{ path "src/main/jni/Android.mk" } } // 指定 ndk 版本 ndkVersion "20.0.5594570" ... } dependencies { implementation fileTree(dir: "libs", include: ["*.jar"]) ... } 複製代碼
Google 推薦開發者使用 cmake 來代替 makefile 進行交叉編譯了,makefile 在引入第三方預編譯好的 so 的時候會在 android 6.0 版本先後有些差別,好比在 6.0 以前須要手動 System.loadLibrary 第三方 so,在以後則不須要。 關於 makefile 還有不少配置參數,這裏不在講解,更多參考官方文檔。
在 6.0 如下,System.loadLibrary 不會自動加載 so 內部依賴的 so 在 6.0 如下,System.loadLibrary 會自動加載 so 內部依賴的 so 因此使用 mk 的話須要作版本兼容
CMake是一個跨平臺的構建工具,能夠用簡單的語句來描述全部平臺的安裝(編譯過程)。可以輸出各類各樣的makefile或者project文件。Cmake 並不直接建構出最終的軟件,而是產生其餘工具的腳本(如Makefile ),而後再依這個工具的構建方式使用。 Android Studio利用CMake生成的是ninja,ninja是一個小型的關注速度的構建系統。咱們不須要關心ninja的腳本,知道怎麼配置cmake就能夠了。
Make的腳本名默認是CMakeLists.txt,當咱們用 android studio new project 勾選 include c/c++ 的時候,會默認生成如下文件:
|- app |-- src |--- main |---- cpp |----- CMakeLists.txt |----- native-lib.cpp
先來看一下 CMakeLists.txt:
# 設置 cmake 最小支持版本 cmake_minimum_required(VERSION 3.4.1) # 建立一個庫 add_library( # 庫名稱,好比如今會生成 native-lib.so native-lib # 設置是動態庫(SHARED)仍是靜態庫(STATIC) SHARED # 設置源文件的相對路徑 native-lib.cpp ) # 搜索並指定預構建庫並將路徑存儲爲變量。 # NDK中已經有一部分預構建庫(好比 log),而且ndk庫已是被配置爲cmake搜索路徑的一部分 # 能夠不寫 直接在 target_link_libraries 寫上log find_library( # 設置路徑變量的名稱 log-lib # 指定要CMake定位的NDK庫的名稱 log ) # 指定CMake應連接到目標庫的庫。你能夠連接多個庫,例如構建腳本、預構建的第三方庫或系統庫。 target_link_libraries( # Specifies the target library. native-lib ${log-lib} ) 複製代碼
咱們再來看下 gradle 中的配置:
android { compileSdkVersion 29 buildToolsVersion "29.0.1" defaultConfig { ... testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" // 設置編譯版本 externalNativeBuild { cmake { abiFilters "armeabi-v7a","x86" } } } ... // 設置配置文件路徑 externalNativeBuild { cmake { path "src/main/cpp/CMakeLists.txt" version "3.10.2" } } } 複製代碼
這樣在編譯產物中就能夠看到兩個版本的 so:
好比咱們添加一個 extra.h:
#ifndef JNITUTORIAL_EXTRA_H #define JNITUTORIAL_EXTRA_H const char * getString(){ return "string from extra"; } #endif //JNITUTORIAL_EXTRA_H 複製代碼
而後在 native-lib.cpp 中使用:
#include <jni.h> #include <string> #include <android/log.h> #include "extra.h" // __VA_ARGS__ 表明... 可變參數 #define LOGE(...) __android_log_print(ANDROID_LOG_ERROR,"JNI",__VA_ARGS__); extern "C" JNIEXPORT jstring JNICALL Java_com_wangzhen_jnitutorial_MainActivity_stringFromJNI(JNIEnv *env, jobject thiz) { // std::string hello = "Hello from new C++"; std::string hello = getString(); return env->NewStringUTF(hello.c_str()); } 複製代碼
源文件已經寫好了,這時候要修改一下 CMakeLists.txt:
add_library( native-lib SHARED native-lib.cpp // 添加 extra.h extra.h ) #================================== # 固然若是源文件很是多,而且可能在不一樣的文件夾下,像上面明確的引入各個文件就會很是繁瑣,此時能夠批量引入 # 若是文件太多,能夠批量加載,下面時將 cpp 文件夾下全部的源文件定義成了 SOURCE(後面的源文件使用相對路徑) file(GLOB SOURCE *.cpp *.h) add_library( native-lib SHARED # 引入 SOURCE 下的全部源文件 ${SOURCE} ) 複製代碼
那麼如何添加第三方的動態庫呢?
動態庫必須放到 src/main/jniLibs/xxabi 目錄下才能被打包到 apk 中,這裏用的是虛擬機,因此用的是 x86 平臺,因此咱們放置一個第三方庫 libexternal.so 到 src/main/jniLibs/x86 下面。 libexternal.so 中只有一個 hello.c ,裏面只有一個方法:
const char * getExternalString(){ return "string from external"; } 複製代碼
這裏將 CMakeLists.txt 從新放到了 app 目錄下,和 src 同級,這樣方便找到 jniLibs 下面的庫。 因此別忘了修改 gradle
externalNativeBuild { cmake { path "CMakeLists.txt" version "3.10.2" } } 複製代碼
cmake_minimum_required(VERSION 3.4.1) # 若是文件太多,能夠批量加載,下面時將 cpp 文件夾下全部的源文件定義成了 SOURCE(後面的源文件使用相對路徑) file(GLOB SOURCE src/main/cpp/*.cpp src/main/cpp/*.h) add_library( native-lib SHARED # 引入 SOURCE 下的全部源文件 ${SOURCE} ) set_target_properties(native-lib PROPERTIES LINKER_LANGUAGE CXX) #add_library( # Sets the name of the library. # native-lib # # # Sets the library as a shared library. # SHARED # # # Provides a relative path to your source file(s). # native-lib.cpp # extra.h ) find_library( log-lib log ) # ==================引入外部 so=================== message("ANDROID_ABI : ${ANDROID_ABI}") message("CMAKE_SOURCE_DIR : ${CMAKE_SOURCE_DIR}") message("PROJECT_SOURCE_DIR : ${PROJECT_SOURCE_DIR}") # external 表明第三方 so - libexternal.so # SHARED 表明動態庫,靜態庫是 STATIC; # IMPORTED: 表示是以導入的形式添加進來(預編譯庫) add_library(external SHARED IMPORTED) #設置 external 的 導入路徑(IMPORTED_LOCATION) 屬性,不可使用相對路徑 # CMAKE_SOURCE_DIR: 當前cmakelists.txt的路徑 (cmake工具內置的) # android cmake 內置的 ANDROID_ABI : 當前須要編譯的cpu架構 set_target_properties(external PROPERTIES IMPORTED_LOCATION ${CMAKE_SOURCE_DIR}/src/main/jniLibs/x86/libexternal.so) #set_target_properties(external PROPERTIES LINKER_LANGUAGE CXX) # ==================引入外部 so end=================== target_link_libraries( # Specifies the target library. native-lib # Links the target library to the log library # included in the NDK. ${log-lib} # 連接第三方 so external ) 複製代碼
#include <jni.h> #include <string> #include <android/log.h> #include "extra.h" // __VA_ARGS__ 表明... 可變參數 #define LOGE(...) __android_log_print(ANDROID_LOG_ERROR,"JNI",__VA_ARGS__); extern "C"{ const char * getExternalString(); } extern "C" JNIEXPORT jstring JNICALL Java_com_wangzhen_jnitutorial_MainActivity_stringFromJNI(JNIEnv *env, jobject thiz) { // std::string hello = "Hello from new C++"; // std::string hello = getString(); // 這裏調用了第三方庫的方法 std::string hello = getExternalString(); return env->NewStringUTF(hello.c_str()); } 複製代碼
除了上面的方式還能夠給 CMake 增長一個查找 so 的 path,當咱們 target_link_libraries external 的時候就會在該路徑下找到。
#=====================引入外部 so 的第二種方式=============================== # 直接給 cmake 在添加一個查找路徑,在這個路徑下能夠找到 external # CMAKE_C_FLAGS 表明使用 c 編譯, CMAKE_CXX_FLAGS 表明 c++ # set 方法 定義一個變量 CMAKE_C_FLAGS = "${CMAKE_C_FLAGS} XXXX" # -L: 庫的查找路徑 libexternal.so #set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -L${CMAKE_SOURCE_DIR}/src/main/jniLibs/${ANDROID_ABI} ") set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -L${CMAKE_SOURCE_DIR}/src/main/jniLibs/x86") #=====================引入外部 so 的第二種方式 end=============================== 複製代碼