在進(jìn)行Android開(kāi)發(fā)的過(guò)程中昌渤,我們必定會(huì)遇到視頻圖像處理、高強(qiáng)度密集運(yùn)算憔四、特殊算法等場(chǎng)景膀息,這時(shí)我們就不得不需要去接觸一些C/C++代碼,進(jìn)行JNI開(kāi)發(fā)了赵。下面我將從Android.mk和CMake這兩種方式教大家如何進(jìn)行開(kāi)發(fā)潜支。文章結(jié)尾將給出演示的項(xiàng)目代碼,如果你能耐心地仔細(xì)看完柿汛,相信你一定能掌握如何在Android下進(jìn)行JNI開(kāi)發(fā)冗酿。
使用Android.mk進(jìn)行JNI開(kāi)發(fā)
1.編寫(xiě)native接口和C/C++代碼
定義native接口
package com.xuexiang.jnidemo;
public class JNIApi {
public native String stringFromJNI();
}
編寫(xiě)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.編寫(xiě)Android.mk
模版如下:
LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
LOCAL_MODULE := native-lib
LOCAL_SRC_FILES := native-lib.cpp
## 導(dǎo)入logcat日志庫(kù)
LOCAL_LDLIBS := -L$(SYSROOT)/usr/lib -llog
include $(BUILD_SHARED_LIBRARY)
說(shuō)明:
LOCAL_PATH := $(call my-dir)
:指向當(dāng)前目錄的地址,包含該.mkinclude $(CLEAR_VARS)
:清理掉所有以LOCAL_開(kāi)頭的內(nèi)容,這句話是必須的络断,因?yàn)槿绻械淖兞慷际侨值牟锰妫械目煽氐木幾g文件都需要在一個(gè)單獨(dú)的GNU中被解析并執(zhí)行。LOCAL_MODULE
:調(diào)用的庫(kù)名貌笨,用來(lái)區(qū)分android.mk中的每一個(gè)模塊弱判。文件名必須是唯一的,不能有空格锥惋。注意昌腰,這里編譯器會(huì)為你自動(dòng)加上一些前綴lib和后綴.so,來(lái)保證文件是一致的膀跌。LOCAL_SRC_FILES
:變量必須包含一個(gè)C遭商、C++或者java源文件的列表,這些會(huì)被編譯并聚合到一個(gè)模塊中,文件之間可以用空格或Tab鍵進(jìn)行分割,換行請(qǐng)用"\"LOCAL_LDLIBS
:定義需要鏈接的庫(kù)捅伤。一般用于鏈接那些存在于系統(tǒng)目錄下本模塊需要鏈接的庫(kù)(比如這里的logcat庫(kù))劫流。include $(BUILD_SHARED_LIBRARY)
:來(lái)生成一個(gè)動(dòng)態(tài)庫(kù)libnative-lib.so
3.編寫(xiě)Application.mk
# APP_ABI := armeabi armeabi-v7a arm64-v8a x86
APP_ABI := all
APP_OPTIM := release
## 引用靜態(tài)庫(kù)
APP_STL := stlport_static
#NDK_TOOLCHAIN_VERSION=4.8
#APP_PLATFORM := android-14
說(shuō)明:
APP_ABI
:定義編譯so文件的CPU型號(hào)徒仓,all為所有類(lèi)型懂讯。也可以指定特定類(lèi)型的CPU型號(hào)摄咆,直接使用空格隔開(kāi)岖沛。APP_OPTIM
:優(yōu)化選項(xiàng)姊途,非必填许布。其值可以為'release'或'debug'.此變量用來(lái)修改優(yōu)先等級(jí).默認(rèn)情況下為release.在release模式下聂使,將編譯生成被優(yōu)化了的二進(jìn)制的機(jī)器碼趣席,而debug模塊用來(lái)生成便于調(diào)試的未被優(yōu)化的二進(jìn)制機(jī)器碼粮彤。APP_STL
:選擇支持的C++標(biāo)準(zhǔn)庫(kù)根穷。在默認(rèn)情況下,NDK通過(guò)Androoid自帶的最小化的C++運(yùn)行庫(kù)(system/lib/libstdc++.so)來(lái)提供標(biāo)準(zhǔn)C++頭文件.然而导坟,NDK提供了可供選擇的C++實(shí)現(xiàn)屿良,你可以通過(guò)此變量來(lái)選擇使用哪個(gè)或鏈接到你的程序。
APP_STL := stlport_static --> static STLport library
APP_STL := stlport_shared --> shared STLport library
APP_STL := system --> default C++ runtime library
比如惫周,這里我們使用到了#include <string>
尘惧,就需要設(shè)置stlport_static
4.設(shè)置項(xiàng)目根目錄的local.properties文件
因?yàn)锳ndroid Studio 2.2以后推薦使用CMake進(jìn)行JNI開(kāi)發(fā),因此需要修改一下參數(shù)進(jìn)行兼容递递。
android.useDeprecatedNdk=true
5.編譯C/C++代碼生成so文件
cd 到j(luò)ni(存放Android.mk的目錄)下喷橙,執(zhí)行ndk-build
即可啥么。
執(zhí)行成功后,將會(huì)在jni的同級(jí)目錄下生成libs
和obj
文件夾贰逾,存放的是編譯好的so文件悬荣。
6.在模塊的build.gradle中設(shè)置so文件路徑
sourceSets {
main {
jni.srcDirs = []
jniLibs.srcDirs = ['src/main/libs']
}
}
至此完成了Android.mk的設(shè)置,下面我們就可以愉快地進(jìn)行jni開(kāi)發(fā)了疙剑!
上面介紹的Android.mk都可以在Eclispe和Android Studio下進(jìn)行編譯開(kāi)發(fā)氯迂,可以說(shuō)是一種比較傳統(tǒng)的做法。下面我將介紹Android Studio著重推薦的CMake方式進(jìn)行JNI開(kāi)發(fā)言缤。
使用CMake進(jìn)行JNI開(kāi)發(fā)
開(kāi)發(fā)環(huán)境
JNI:Java Native Interface(Java 本地編程接口)嚼蚀,一套編程規(guī)范,它提供了若干的 API 實(shí)現(xiàn)了 Java 和其他語(yǔ)言的通信(主要是 C/C++)管挟。Java 可以通過(guò) JNI 調(diào)用本地的 C/C++ 代碼轿曙,本地的 C/C++ 代碼也可以調(diào)用 java 代碼。Java 通過(guò) C/C++ 使用本地的代碼的一個(gè)關(guān)鍵性原因在于 C/C++ 代碼的高效性哮独。
在 Android Studio 下拳芙,進(jìn)行JNI的開(kāi)發(fā),需要準(zhǔn)備以下內(nèi)容:
Android Studio 2.2以上皮璧。
NDK:這套工具集允許為 Android 使用 C 和 C++ 代碼舟扎。
CMake:一款外部構(gòu)建工具,可與 Gradle 搭配使用來(lái)構(gòu)建原生庫(kù)悴务。如果只計(jì)劃使用 ndk-build睹限,則不需要此組件。
LLDB:一種調(diào)試程序讯檐,Android Studio 使用它來(lái)調(diào)試原生代碼羡疗。
創(chuàng)建支持C++的項(xiàng)目
新建支持C++的項(xiàng)目
在新建項(xiàng)目時(shí),勾上Include C++ support
就行了:
在向?qū)У?Customize C++ Support 部分别洪,有下列自定義項(xiàng)目可供選擇:
- C++ Standard:使用下拉列表選擇使用哪種 C++ 標(biāo)準(zhǔn)叨恨。選擇 Toolchain Default 會(huì)使用默認(rèn)的 CMake 設(shè)置。
- Exceptions Support:如果希望啟用對(duì) C++ 異常處理的支持挖垛,請(qǐng)選中此復(fù)選框痒钝。如果啟用此復(fù)選框,Android Studio 會(huì)將 -fexceptions 標(biāo)志添加到模塊級(jí) build.gradle文件的 cppFlags中痢毒,Gradle 會(huì)將其傳遞到 CMake送矩。
- Runtime Type Information Support:如果希望支持 RTTI,請(qǐng)選中此復(fù)選框哪替。如果啟用此復(fù)選框栋荸,Android Studio 會(huì)將 -frtti 標(biāo)志添加到模塊級(jí) build.gradle文件的 cppFlags中,Gradle 會(huì)將其傳遞到 CMake。
支持C++的項(xiàng)目目錄
src/main/cpp
下存放的我們編寫(xiě)供JNI調(diào)用的C++源碼晌块。CMakeLists.txt
文件是CMake的配置文件,通常他包含的內(nèi)容如下:
# TODO 設(shè)置構(gòu)建本機(jī)庫(kù)文件所需的 CMake的最小版本
cmake_minimum_required(VERSION 3.4.1)
# TODO 添加自己寫(xiě)的 C/C++源文件
add_library( native-lib
SHARED
src/main/cpp/native-lib.cpp )
# TODO 依賴(lài) NDK中的庫(kù)
find_library( log-lib
log )
# TODO 將目標(biāo)庫(kù)與 NDK中的庫(kù)進(jìn)行連接
target_link_libraries( native-lib
${log-lib} )
build.gradle的配置
android {
...
defaultConfig {
...
externalNativeBuild {
cmake {
// 默認(rèn)是 “ cppFlags "" ”
// 如果要修改 Customize C++ Support 部分爱沟,可在這里加入
cppFlags "-frtti -fexceptions"
}
}
ndk {
// abiFiliter: ABI 過(guò)濾器(application binary interface,應(yīng)用二進(jìn)制接口)
// Android 支持的 CPU 架構(gòu)
abiFilters 'armeabi-v7a','arm64-v8a','x86','x86_64'//, 'armeabi' 不支持了
}
}
buildTypes {
...
}
externalNativeBuild {
cmake {
path "CMakeLists.txt"
}
}
}
注意事項(xiàng)
- 1.在使用JNI前摸袁,需要加載so庫(kù)
static {
System.loadLibrary("native-lib");
}
- 2.快速生成C++代碼:先在java中定義native方法钥顽,然后使用
Alt + Enter
快捷鍵自動(dòng)生成C++方法體义屏。
3.CPP 資源文件夾下面的文件和文件夾不能重名靠汁,不然 System.loadLibrary() 時(shí)找不到,會(huì)報(bào)錯(cuò):java.lang.UnsatisfiedLinkError: Native method not found.
4.在定義庫(kù)的名字時(shí)闽铐,不要加前綴 lib 和后綴 .so蝶怔,不然會(huì)報(bào)錯(cuò):java.lang.UnsatisfiedLinkError: Couldn’t load xxx : findLibrary【findLibrary returned null錯(cuò)誤.
5.新建 C/C++ 源代碼文件,要添加到 CMakeLists.txt 文件中兄墅。
# 增加c++源代碼
add_library( # library的名稱(chēng).
native-lib
# 標(biāo)志庫(kù)共享.
SHARED
# C++源碼文件的相對(duì)路徑.
src/main/cpp/native-lib.cpp )
# 將目標(biāo)庫(kù)與 NDK中的庫(kù)進(jìn)行連接
target_link_libraries( # 目標(biāo)library的名稱(chēng).
native-lib
${log-lib} )
- 6.引入第三方 .so文件踢星,要添加到 CMakeLists.txt 文件中。
# TODO 添加第三方庫(kù)
# TODO add_library(libavcodec-57
# TODO 原先生成的.so文件在編譯后會(huì)自動(dòng)添加上前綴lib和后綴.so隙咸,
# TODO 在定義庫(kù)的名字時(shí)沐悦,不要加前綴lib和后綴 .so,
# TODO 不然會(huì)報(bào)錯(cuò):java.lang.UnsatisfiedLinkError: Couldn't load xxx : findLibrary returned null
add_library(avcodec-57
# TODO STATIC表示靜態(tài)的.a的庫(kù)五督,SHARED表示.so的庫(kù)
SHARED
IMPORTED)
set_target_properties(avcodec-57
PROPERTIES IMPORTED_LOCATION
# TODO ${CMAKE_SOURCE_DIR}:表示 CMakeLists.txt的當(dāng)前文件夾路徑
# TODO ${ANDROID_ABI}:編譯時(shí)會(huì)自動(dòng)根據(jù) CPU架構(gòu)去選擇相應(yīng)的庫(kù)
# 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 路徑指向上面會(huì)編譯出錯(cuò)(無(wú)法在jniLibs中引入)充包,指向下面的路徑就沒(méi)問(wèn)題
include_directories( src/main/cpp/ffmpeg/include )
- 8.C++ library編譯生成的so文件副签,在
build/intermediates/cmake
下
至此完成了CMake的設(shè)置,下面我們就可以愉快地進(jìn)行jni開(kāi)發(fā)了基矮!
講完了兩種進(jìn)行JNI開(kāi)發(fā)的姿勢(shì)后淆储,下面我們來(lái)簡(jiǎn)單講講JNI的基礎(chǔ)語(yǔ)法。
JNI基礎(chǔ)語(yǔ)法
基礎(chǔ)類(lèi)型
Java類(lèi)型 | native類(lèi)型 | 描述 |
---|---|---|
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 |
引用類(lèi)型
JNI為不同的java對(duì)象提供了不同的引用類(lèi)型家浇,JNI引用類(lèi)型如下:
在c里面本砰,所有JNI引用類(lèi)型其實(shí)都是jobject。
Native方法參數(shù)
- JNI接口指針是native方法的第一個(gè)參數(shù)钢悲,JNI接口指針的類(lèi)型是JNIEnv点额。
- 第二個(gè)參數(shù)取決于native method是否靜態(tài)方法,如果是非靜態(tài)方法譬巫,那么第二個(gè)參數(shù)是對(duì)對(duì)象的引用咖楣,如果是靜態(tài)方法,則第二個(gè)參數(shù)是對(duì)它的class類(lèi)的引用
- 剩下的參數(shù)跟Java方法參數(shù)一一對(duì)應(yīng)
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 ...
}
簽名描述
基礎(chǔ)數(shù)據(jù)類(lèi)型
Java類(lèi)型 | 簽名描述 |
---|---|
boolean | Z |
byte | B |
char | C |
short | S |
int | I |
long | J |
float | F |
double | D |
void |
引用數(shù)據(jù)類(lèi)型
(以L
開(kāi)頭芦昔,以;
結(jié)束诱贿,中間對(duì)應(yīng)的是該類(lèi)型的完整路徑)
String : Ljava/lang/String;
Object : Ljava/lang/Object;
自定義類(lèi)型 Area : Lcom/xuexiang/jnidemo/Area;
數(shù)組
(在類(lèi)型前面添加[
,幾維數(shù)組就在前面添加幾個(gè)[
)
int [] :[I
Long[][] : [[J
Object[][][] : [[[Ljava/lang/Object
使用命令查看
javap -s <java類(lèi)的class文件路徑>
class文件存在于 build->intermediates->classes
下。
JNI常見(jiàn)用法
1、jni訪問(wèn)java非靜態(tài)成員變量
1.使用
GetObjectClass
珠十、FindClass
獲取調(diào)用對(duì)象的類(lèi)2.使用
GetFieldID
獲取字段的ID料扰。這里需要傳入字段類(lèi)型的簽名描述。3.使用
GetIntField
焙蹭、GetObjectField
等方法晒杈,獲取字段的值。使用SetIntField
孔厉、SetObjectField
等方法拯钻,設(shè)置字段的值。
注意:即使字段是private
也照樣可以正常訪問(wèn)撰豺。
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訪問(wèn)java靜態(tài)成員變量
1.使用
GetObjectClass
、FindClass
獲取調(diào)用對(duì)象的類(lèi)2.使用
GetStaticFieldID
獲取字段的ID污桦。這里需要傳入字段類(lèi)型的簽名描述亩歹。3.使用
GetStaticIntField
、GetStaticObjectField
等方法凡橱,獲取字段的值小作。使用SetStaticIntField
、SetStaticObjectField
等方法稼钩,設(shè)置字段的值顾稀。
3、jni調(diào)用java非靜態(tài)成員方法
1.使用
GetObjectClass
变抽、FindClass
獲取調(diào)用對(duì)象的類(lèi)2.使用
GetMethodID
獲取方法的ID础拨。這里需要傳入方法的簽名描述。3.使用
CallVoidMethod
執(zhí)行無(wú)返回值的方法绍载,使用CallIntMethod
诡宗、CallBooleanMethod
等執(zhí)行有返回值的方法。
extern "C"
JNIEXPORT void JNICALL
Java_com_xuexiang_jnidemo_JNIApi_testCallParamMethod(JNIEnv *env, jobject instance) {
//回調(diào)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調(diào)用java靜態(tài)成員方法
1.使用
GetObjectClass
塔沃、FindClass
獲取調(diào)用對(duì)象的類(lèi)2.使用
GetStaticMethodID
獲取方法的ID。這里需要傳入方法的簽名描述阳谍。3.使用
CallStaticVoidMethod
執(zhí)行無(wú)返回值的方法蛀柴,使用CallStaticIntMethod
、CallStaticBooleanMethod
等執(zhí)行有返回值的方法矫夯。
5鸽疾、jni調(diào)用java構(gòu)造方法
1.使用
FindClass
獲取需要構(gòu)造的類(lèi)2.使用
GetMethodID
獲取構(gòu)造方法的ID。方法名為<init>
, 這里需要傳入方法的簽名描述训貌。3.使用
NewObject
執(zhí)行創(chuàng)建對(duì)象制肮。
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");
//找到構(gòu)造方法jmethodID public Area(int width, int height)
jmethodID j_constructor_methoid = env->GetMethodID(j_class, "<init>", "(II)V");
//初始化java類(lèi)構(gòu)造方法 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");
//調(diào)用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
創(chuàng)建全局引用,使用NewLocalRef
創(chuàng)建局部引用。局部引用豺鼻,通過(guò)DeleteLocalRef手動(dòng)釋放對(duì)象;全局引用综液,通過(guò)DeleteGlobalRef手動(dòng)釋放對(duì)象。
引用不主動(dòng)釋放會(huì)導(dǎo)致內(nèi)存泄漏儒飒。
7谬莹、jni異常處理
使用
ExceptionOccurred
進(jìn)行異常的檢測(cè)。注意桩了,這里只能檢測(cè)java異常附帽。使用
ExceptionClear
進(jìn)行異常的清除。使用
ThrowNew
來(lái)上拋異常圣猎。
注意士葫,ExceptionOccurred
和ExceptionClear
一般是成對(duì)出現(xiàn)的乞而,類(lèi)似于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;");
//檢測(cè)是否發(fā)生Java異常
jthrowable exception = env->ExceptionOccurred();
if (exception != NULL) {
LOGE("jni發(fā)生異常");
//jni清空異常信息
env->ExceptionClear(); //需要和ExceptionOccurred方法成對(duì)出現(xiàn)
throwException(env, "native出錯(cuò)!");
}
}
8爪模、日志打印
#include <android/log.h> //引用android log
//定義日志打印的方法
#define TAG "CMake-JNI" // 這個(gè)是自定義的LOG的標(biāo)識(shí)
#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG,TAG ,__VA_ARGS__) // 定義LOGD類(lèi)型
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO,TAG ,__VA_ARGS__) // 定義LOGI類(lèi)型
#define LOGW(...) __android_log_print(ANDROID_LOG_WARN,TAG ,__VA_ARGS__) // 定義LOGW類(lèi)型
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR,TAG ,__VA_ARGS__) // 定義LOGE類(lèi)型
#define LOGF(...) __android_log_print(ANDROID_LOG_FATAL,TAG ,__VA_ARGS__) // 定義LOGF類(lèi)型
LOGE("jni發(fā)生異常"); //日志打印