手把手教你如何在Android下進行JNI開發(入門)
在進行Android開發的過程中,我們必定會遇到視訊影象處理、高強度密集運算、特殊演算法等場景,這時我們就不得不需要去接觸一些C/C++程式碼,進行JNI開發。下面我將從Android.mk和CMake這兩種方式教大家如何進行開發。文章結尾將給出演示的專案程式碼,如果你能耐心地仔細看完,相信你一定能掌握如何在Android下進行JNI開發。
使用Android.mk進行JNI開發
1.編寫native介面和C/C++程式碼
定義native介面
package com.xuexiang.jnidemo; public class JNIApi { public native String stringFromJNI(); }
編寫C/C++程式碼
extern "C" JNIEXPORT jstring JNICALL Java_com_xuexiang_jnidemo_JNIApi_stringFromJNI( JNIEnv *env, jobject /* this */) { std::string hello = "Hello from C++"; return env->NewStringUTF(hello.c_str()); }
2.編寫Android.mk
模版如下:
LOCAL_PATH := $(call my-dir) include $(CLEAR_VARS) LOCAL_MODULE := native-lib LOCAL_SRC_FILES := native-lib.cpp ## 匯入logcat日誌庫 LOCAL_LDLIBS := -L$(SYSROOT)/usr/lib -llog include $(BUILD_SHARED_LIBRARY)
說明:
-
LOCAL_PATH := $(call my-dir)
:指向當前目錄的地址,包含該.mk -
include $(CLEAR_VARS)
:清理掉所有以LOCAL_開頭的內容,這句話是必須的,因為如果所有的變數都是全域性的,所有的可控的編譯檔案都需要在一個單獨的GNU中被解析並執行。 -
LOCAL_MODULE
:呼叫的庫名,用來區分android.mk中的每一個模組。檔名必須是唯一的,不能有空格。注意,這裡編譯器會為你自動加上一些字首lib和字尾.so,來保證檔案是一致的。 -
LOCAL_SRC_FILES
:變數必須包含一個C、C++或者java原始檔的列表,這些會被編譯並聚合到一個模組中,檔案之間可以用空格或Tab鍵進行分割,換行請用”\” -
LOCAL_LDLIBS
:定義需要連結的庫。一般用於連結那些存在於系統目錄下本模組需要連結的庫(比如這裡的logcat庫)。 -
include $(BUILD_SHARED_LIBRARY)
:來生成一個動態庫libnative-lib.so
3.編寫Application.mk
# APP_ABI := armeabi armeabi-v7a arm64-v8a x86 APP_ABI := all APP_OPTIM := release ## 引用靜態庫 APP_STL := stlport_static #NDK_TOOLCHAIN_VERSION=4.8 #APP_PLATFORM := android-14
說明:
-
APP_ABI
:定義編譯so檔案的CPU型號,all為所有型別。也可以指定特定型別的CPU型號,直接使用空格隔開。 -
APP_OPTIM
:優化選項,非必填。其值可以為’release’或’debug’.此變數用來修改優先等級.預設情況下為release.在release模式下,將編譯生成被優化了的二進位制的機器碼,而debug模組用來生成便於除錯的未被優化的二進位制機器碼。 -
APP_STL
:選擇支援的C++標準庫。在預設情況下,NDK通過Androoid自帶的最小化的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
比如,這裡我們使用到了 #include <string>
,就需要設定 stlport_static
4.設定專案根目錄的local.properties檔案
因為Android Studio 2.2以後推薦使用CMake進行JNI開發,因此需要修改一下引數進行相容。
android.useDeprecatedNdk=true
5.編譯C/C++程式碼生成so檔案
cd 到jni(存放Android.mk的目錄)下,執行 ndk-build
即可。
執行成功後,將會在jni的同級目錄下生成 libs
和 obj
資料夾,存放的是編譯好的so檔案。
6.在模組的build.gradle中設定so檔案路徑
sourceSets { main { jni.srcDirs = [] jniLibs.srcDirs = ['src/main/libs'] } }
至此完成了Android.mk的設定,下面我們就可以愉快地進行jni開發了!
上面介紹的Android.mk都可以在Eclispe和Android Studio下進行編譯開發,可以說是一種比較傳統的做法。下面我將介紹Android Studio著重推薦的CMake方式進行JNI開發。
使用CMake進行JNI開發
開發環境
JNI:Java Native Interface(Java 本地程式設計介面),一套程式設計規範,它提供了若干的 API 實現了 Java 和其他語言的通訊(主要是 C/C++)。Java 可以通過 JNI 呼叫本地的 C/C++ 程式碼,本地的 C/C++ 程式碼也可以呼叫 java 程式碼。Java 通過 C/C++ 使用本地的程式碼的一個關鍵性原因在於 C/C++ 程式碼的高效性。
在 Android Studio 下,進行JNI的開發,需要準備以下內容:
-
Android Studio 2.2以上。
-
NDK:這套工具集允許為 Android 使用 C 和 C++ 程式碼。
-
CMake:一款外部構建工具,可與 Gradle 搭配使用來構建原生庫。如果只計劃使用 ndk-build,則不需要此元件。
-
LLDB:一種除錯程式,Android Studio 使用它來除錯原生程式碼。
建立支援C++的專案
新建支援C++的專案
在新建專案時,勾上 Include C++ support
就行了:
在嚮導的 Customize C++ Support 部分,有下列自定義專案可供選擇:
- C++ Standard:使用下拉列表選擇使用哪種 C++ 標準。選擇 Toolchain Default 會使用預設的 CMake 設定。
- Exceptions Support:如果希望啟用對 C++ 異常處理的支援,請選中此複選框。如果啟用此複選框,Android Studio 會將 -fexceptions 標誌新增到模組級 build.gradle檔案的 cppFlags中,Gradle 會將其傳遞到 CMake。
- Runtime Type Information Support:如果希望支援 RTTI,請選中此複選框。如果啟用此複選框,Android Studio 會將 -frtti 標誌新增到模組級 build.gradle檔案的 cppFlags中,Gradle 會將其傳遞到 CMake。
支援C++的專案目錄
-
src/main/cpp
下存放的我們編寫供JNI呼叫的C++原始碼。 -
CMakeLists.txt
檔案是CMake的配置檔案,通常他包含的內容如下:
# TODO 設定構建本機庫檔案所需的 CMake的最小版本 cmake_minimum_required(VERSION 3.4.1) # TODO 新增自己寫的 C/C++原始檔 add_library( native-lib SHARED src/main/cpp/native-lib.cpp ) # TODO 依賴 NDK中的庫 find_library( log-lib log ) # TODO 將目標庫與 NDK中的庫進行連線 target_link_libraries( native-lib ${log-lib} )
build.gradle的配置
android { ... defaultConfig { ... externalNativeBuild { cmake { // 預設是 “ cppFlags "" ” // 如果要修改 Customize C++ Support 部分,可在這裡加入 cppFlags "-frtti -fexceptions" } } ndk { // abiFiliter: ABI 過濾器(application binary interface,應用二進位制介面) // Android 支援的 CPU 架構 abiFilters 'armeabi-v7a','arm64-v8a','x86','x86_64'//, 'armeabi' 不支援了 } } buildTypes { ... } externalNativeBuild { cmake { path "CMakeLists.txt" } } }
注意事項
- 1.在使用JNI前,需要載入so庫
static { System.loadLibrary("native-lib"); }
- 2.快速生成C++程式碼:先在java中定義native方法,然後使用
Alt + Enter
快捷鍵自動生成C++方法體。
-
3.CPP 資原始檔夾下面的檔案和資料夾不能重名,不然 System.loadLibrary() 時找不到,會報錯:java.lang.UnsatisfiedLinkError: Native method not found.
-
4.在定義庫的名字時,不要加字首 lib 和字尾 .so,不然會報錯:java.lang.UnsatisfiedLinkError: Couldn’t load xxx : findLibrary【findLibrary returned null錯誤.
-
5.新建 C/C++ 原始碼檔案,要新增到 CMakeLists.txt 檔案中。
# 增加c++原始碼 add_library( # library的名稱. native-lib # 標誌庫共享. SHARED # C++原始碼檔案的相對路徑. src/main/cpp/native-lib.cpp ) # 將目標庫與 NDK中的庫進行連線 target_link_libraries( # 目標library的名稱. native-lib ${log-lib} )
- 6.引入第三方 .so檔案,要新增到 CMakeLists.txt 檔案中。
# TODO 新增第三方庫 # TODO add_library(libavcodec-57 # TODO 原先生成的.so檔案在編譯後會自動新增上字首lib和字尾.so, # TODO在定義庫的名字時,不要加字首lib和字尾 .so, # TODO不然會報錯:java.lang.UnsatisfiedLinkError: Couldn't load xxx : findLibrary returned null add_library(avcodec-57 # TODO STATIC表示靜態的.a的庫,SHARED表示.so的庫 SHARED IMPORTED) set_target_properties(avcodec-57 PROPERTIES IMPORTED_LOCATION # TODO ${CMAKE_SOURCE_DIR}:表示 CMakeLists.txt的當前資料夾路徑 # TODO ${ANDROID_ABI}:編譯時會自動根據 CPU架構去選擇相應的庫 # TODO ABI資料夾上面不要再分層,直接就 jniLibs/${ANDROID_ABI}/ # TODO ${CMAKE_SOURCE_DIR}/src/main/jniLibs/ffmpeg/${ANDROID_ABI}/libavcodec-57.so ${CMAKE_SOURCE_DIR}/src/main/jniLibs/${ANDROID_ABI}/libavcodec-57.so)
- 7.引入第三方 .h 資料夾,也要新增到 CMakeLists.txt 檔案中
# TODO include_directories( src/main/jniLibs/${ANDROID_ABI}/include ) # TODO 路徑指向上面會編譯出錯(無法在jniLibs中引入),指向下面的路徑就沒問題 include_directories( src/main/cpp/ffmpeg/include )
- 8.C++ library編譯生成的so檔案,在
build/intermediates/cmake
下
至此完成了CMake的設定,下面我們就可以愉快地進行jni開發了!
講完了兩種進行JNI開發的姿勢後,下面我們來簡單講講JNI的基礎語法。
JNI基礎語法
基礎型別
Java型別 | native型別 | 描述 |
---|---|---|
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 |
引用型別
JNI為不同的java物件提供了不同的引用型別,JNI引用型別如下:
在c裡面,所有JNI引用型別其實都是jobject。
Native方法引數
- JNI介面指標是native方法的第一個引數,JNI介面指標的型別是JNIEnv。
- 第二個引數取決於native method是否靜態方法,如果是非靜態方法,那麼第二個引數是對物件的引用,如果是靜態方法,則第二個引數是對它的class類的引用
- 剩下的引數跟Java方法引數一一對應
extern "C" /* specify the C calling convention */ jdouble Java_pkg_Cls_f__ILjava_lang_String_2 ( JNIEnv *env,/* interface pointer */ jobject obj,/* "this" pointer */ jint i,/* argument #1 */ jstring s)/* argument #2 */ { const char *str = env->GetStringUTFChars(s, 0); ... env->ReleaseStringUTFChars(s, str); return ... }
ACE.md" rel="nofollow,noindex" target="_blank">點選檢視JNI介面
簽名描述
基礎資料型別
Java型別 | 簽名描述 |
---|---|
boolean | Z |
byte | B |
char | C |
short | S |
int | I |
long | J |
float | F |
double | D |
void |
引用資料型別
(以 L
開頭,以 ;
結束,中間對應的是該型別的完整路徑)
String : Ljava/lang/String; Object : Ljava/lang/Object; 自定義型別 Area : Lcom/xuexiang/jnidemo/Area;
陣列
(在型別前面新增 [
,幾維陣列就在前面新增幾個 [
)
int [] :[I Long[][]: [[J Object[][][] : [[[Ljava/lang/Object
使用命令檢視
javap -s <java類的class檔案路徑>
class檔案存在於 build->intermediates->classes
下。
JNI常見用法
1、jni訪問java非靜態成員變數
-
1.使用
GetObjectClass
、FindClass
獲取呼叫物件的類 -
2.使用
GetFieldID
獲取欄位的ID。這裡需要傳入欄位型別的簽名描述。 -
3.使用
GetIntField
、GetObjectField
等方法,獲取欄位的值。使用SetIntField
、SetObjectField
等方法,設定欄位的值。
注意:即使欄位是 private
也照樣可以正常訪問。
extern "C" JNIEXPORT void JNICALL Java_com_xuexiang_jnidemo_JNIApi_testCallNoStaticField(JNIEnv *env, jobject instance) { //獲取jclass jclass j_class = env->GetObjectClass(instance); //獲取jfieldID jfieldID j_fid = env->GetFieldID(j_class, "noStaticField", "I"); //獲取java成員變數int值 jint j_int = env->GetIntField(instance, j_fid); LOGI("noStaticField==%d", j_int);//noStaticField==0 //Set<Type>Field修改noStaticKeyValue的值改為666 env->SetIntField(instance, j_fid, 666); }
2、jni訪問java靜態成員變數
-
1.使用
GetObjectClass
、FindClass
獲取呼叫物件的類 -
2.使用
GetStaticFieldID
獲取欄位的ID。這裡需要傳入欄位型別的簽名描述。 -
3.使用
GetStaticIntField
、GetStaticObjectField
等方法,獲取欄位的值。使用SetStaticIntField
、SetStaticObjectField
等方法,設定欄位的值。
3、jni呼叫java非靜態成員方法
-
1.使用
GetObjectClass
、FindClass
獲取呼叫物件的類 -
2.使用
GetMethodID
獲取方法的ID。這裡需要傳入方法的簽名描述。 -
3.使用
CallVoidMethod
執行無返回值的方法,使用CallIntMethod
、CallBooleanMethod
等執行有返回值的方法。
extern "C" JNIEXPORT void JNICALL Java_com_xuexiang_jnidemo_JNIApi_testCallParamMethod(JNIEnv *env, jobject instance) { //回撥JNIApi中的noParamMethod jclass clazz = env->FindClass("com/xuexiang/jnidemo/JNIApi"); if (clazz == NULL) { printf("find class Error"); return; } jmethodID id = env->GetMethodID(clazz, "paramMethod", "(I)V"); if (id == NULL) { printf("find method Error"); return; } env->CallVoidMethod(instance, id, ++number); }
4、jni呼叫java靜態成員方法
-
1.使用
GetObjectClass
、FindClass
獲取呼叫物件的類 -
2.使用
GetStaticMethodID
獲取方法的ID。這裡需要傳入方法的簽名描述。 -
3.使用
CallStaticVoidMethod
執行無返回值的方法,使用CallStaticIntMethod
、CallStaticBooleanMethod
等執行有返回值的方法。
5、jni呼叫java構造方法
-
1.使用
FindClass
獲取需要構造的類 -
2.使用
GetMethodID
獲取構造方法的ID。方法名為<init>
, 這裡需要傳入方法的簽名描述。 -
3.使用
NewObject
執行建立物件。
extern "C" JNIEXPORT jint JNICALL Java_com_xuexiang_jnidemo_JNIApi_testCallConstructorMethod(JNIEnv *env, jobject instance) { //獲取jclass jclass j_class = env->FindClass("com/xuexiang/jnidemo/Area"); //找到構造方法jmethodIDpublic Area(int width, int height) jmethodID j_constructor_methoid = env->GetMethodID(j_class, "<init>", "(II)V"); //初始化java類構造方法public Area(int width, int height) jobject j_Area_obj = env->NewObject(j_class, j_constructor_methoid, 2, 10); //找到getArea()jmethodID jmethodID j_getArea_methoid = env->GetMethodID(j_class, "getArea", "()I"); //呼叫java中的public int getArea() 獲取面積 jint j_area = env->CallIntMethod(j_Area_obj, j_getArea_methoid); LOGI("面積==%d", j_area);//面積==20 return j_area; }
6、jni引用全域性變數
-
使用
NewGlobalRef
建立全域性引用,使用NewLocalRef
建立區域性引用。 -
區域性引用,通過DeleteLocalRef手動釋放物件;全域性引用,通過DeleteGlobalRef手動釋放物件。
-
引用不主動釋放會導致記憶體洩漏。
7、jni異常處理
-
使用
ExceptionOccurred
進行異常的檢測。注意,這裡只能檢測java異常。 -
使用
ExceptionClear
進行異常的清除。 -
使用
ThrowNew
來上拋異常。
注意, ExceptionOccurred
和 ExceptionClear
一般是成對出現的,類似於java的try-catch。
//上拋java異常 void throwException(JNIEnv *env, const char *message) { jclass newExcCls = env->FindClass("java/lang/Exception"); env->ThrowNew(newExcCls, message); } extern "C" JNIEXPORT void JNICALL Java_com_xuexiang_jnidemo_JNIApi_jniTryCatchException(JNIEnv *env, jobject instance) { //獲取jclass jclass j_class = env->GetObjectClass(instance); //獲取jfieldID jfieldID j_fid = env->GetFieldID(j_class, "method", "Ljava/lang/String666;"); //檢測是否發生Java異常 jthrowable exception = env->ExceptionOccurred(); if (exception != NULL) { LOGE("jni發生異常"); //jni清空異常資訊 env->ExceptionClear(); //需要和ExceptionOccurred方法成對出現 throwException(env, "native出錯!"); } }
8、日誌列印
#include <android/log.h> //引用android log //定義日誌列印的方法 #define TAG "CMake-JNI" // 這個是自定義的LOG的標識 #define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG,TAG ,__VA_ARGS__) // 定義LOGD型別 #define LOGI(...) __android_log_print(ANDROID_LOG_INFO,TAG ,__VA_ARGS__) // 定義LOGI型別 #define LOGW(...) __android_log_print(ANDROID_LOG_WARN,TAG ,__VA_ARGS__) // 定義LOGW型別 #define LOGE(...) __android_log_print(ANDROID_LOG_ERROR,TAG ,__VA_ARGS__) // 定義LOGE型別 #define LOGF(...) __android_log_print(ANDROID_LOG_FATAL,TAG ,__VA_ARGS__) // 定義LOGF型別 LOGE("jni發生異常"); //日誌列印