在進(jìn)行Android開發(fā)的過程中,我們必定會(huì)遇到視頻圖像處理、高強(qiáng)度密集運(yùn)算、特殊算法等場景,這時(shí)我們就不得不需要去接觸一些C/C++代碼,進(jìn)行JNI開發(fā)。下面我將從Android.mk和CMake這兩種方式教大家如何進(jìn)行開發(fā)。文章結(jié)尾將給出演示的項(xiàng)目代碼,如果你能耐心地仔細(xì)看完,相信你一定能掌握如何在Android下進(jìn)行JNI開發(fā)。
使用Android.mk進(jìn)行JNI開發(fā)
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
## 導(dǎo)入logcat日志庫
LOCAL_LDLIBS := -L$(SYSROOT)/usr/lib -llog
include $(BUILD_SHARED_LIBRARY)
說明:
LOCAL_PATH := $(call my-dir):指向當(dāng)前目錄的地址,包含該.mkinclude $(CLEAR_VARS):清理掉所有以LOCAL_開頭的內(nèi)容,這句話是必須的,因?yàn)槿绻械淖兞慷际侨值模械目煽氐木幾g文件都需要在一個(gè)單獨(dú)的GNU中被解析并執(zhí)行。LOCAL_MODULE:調(diào)用的庫名,用來區(qū)分android.mk中的每一個(gè)模塊。文件名必須是唯一的,不能有空格。注意,這里編譯器會(huì)為你自動(dòng)加上一些前綴lib和后綴.so,來保證文件是一致的。LOCAL_SRC_FILES:變量必須包含一個(gè)C、C++或者java源文件的列表,這些會(huì)被編譯并聚合到一個(gè)模塊中,文件之間可以用空格或Tab鍵進(jìn)行分割,換行請用"\"LOCAL_LDLIBS:定義需要鏈接的庫。一般用于鏈接那些存在于系統(tǒng)目錄下本模塊需要鏈接的庫(比如這里的logcat庫)。include $(BUILD_SHARED_LIBRARY):來生成一個(gè)動(dòng)態(tài)庫libnative-lib.so
3.編寫Application.mk
# APP_ABI := armeabi armeabi-v7a arm64-v8a x86
APP_ABI := all
APP_OPTIM := release
## 引用靜態(tài)庫
APP_STL := stlport_static
#NDK_TOOLCHAIN_VERSION=4.8
#APP_PLATFORM := android-14
說明:
APP_ABI:定義編譯so文件的CPU型號,all為所有類型。也可以指定特定類型的CPU型號,直接使用空格隔開。APP_OPTIM:優(yōu)化選項(xiàng),非必填。其值可以為'release'或'debug'.此變量用來修改優(yōu)先等級.默認(rèn)情況下為release.在release模式下,將編譯生成被優(yōu)化了的二進(jìn)制的機(jī)器碼,而debug模塊用來生成便于調(diào)試的未被優(yōu)化的二進(jìn)制機(jī)器碼。APP_STL:選擇支持的C++標(biāo)準(zhǔn)庫。在默認(rèn)情況下,NDK通過Androoid自帶的最小化的C++運(yùn)行庫(system/lib/libstdc++.so)來提供標(biāo)準(zhǔn)C++頭文件.然而,NDK提供了可供選擇的C++實(shí)現(xiàn),你可以通過此變量來選擇使用哪個(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開發(fā),因此需要修改一下參數(shù)進(jìn)行兼容。
android.useDeprecatedNdk=true
5.編譯C/C++代碼生成so文件
cd 到j(luò)ni(存放Android.mk的目錄)下,執(zhí)行ndk-build即可。
執(zhí)行成功后,將會(huì)在jni的同級目錄下生成libs和obj文件夾,存放的是編譯好的so文件。
6.在模塊的build.gradle中設(shè)置so文件路徑
sourceSets {
main {
jni.srcDirs = []
jniLibs.srcDirs = ['src/main/libs']
}
}
至此完成了Android.mk的設(shè)置,下面我們就可以愉快地進(jìn)行jni開發(fā)了!
上面介紹的Android.mk都可以在Eclispe和Android Studio下進(jìn)行編譯開發(fā),可以說是一種比較傳統(tǒng)的做法。下面我將介紹Android Studio著重推薦的CMake方式進(jìn)行JNI開發(fā)。
使用CMake進(jìn)行JNI開發(fā)
開發(fā)環(huán)境
JNI:Java Native Interface(Java 本地編程接口),一套編程規(guī)范,它提供了若干的 API 實(shí)現(xiàn)了 Java 和其他語言的通信(主要是 C/C++)。Java 可以通過 JNI 調(diào)用本地的 C/C++ 代碼,本地的 C/C++ 代碼也可以調(diào)用 java 代碼。Java 通過 C/C++ 使用本地的代碼的一個(gè)關(guān)鍵性原因在于 C/C++ 代碼的高效性。
在 Android Studio 下,進(jìn)行JNI的開發(fā),需要準(zhǔn)備以下內(nèi)容:
Android Studio 2.2以上。
NDK:這套工具集允許為 Android 使用 C 和 C++ 代碼。
CMake:一款外部構(gòu)建工具,可與 Gradle 搭配使用來構(gòu)建原生庫。如果只計(jì)劃使用 ndk-build,則不需要此組件。
LLDB:一種調(diào)試程序,Android Studio 使用它來調(diào)試原生代碼。
image
創(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:如果希望啟用對 C++ 異常處理的支持,請選中此復(fù)選框。如果啟用此復(fù)選框,Android Studio 會(huì)將 -fexceptions 標(biāo)志添加到模塊級 build.gradle文件的 cppFlags中,Gradle 會(huì)將其傳遞到 CMake。
- Runtime Type Information Support:如果希望支持 RTTI,請選中此復(fù)選框。如果啟用此復(fù)選框,Android Studio 會(huì)將 -frtti 標(biāo)志添加到模塊級 build.gradle文件的 cppFlags中,Gradle 會(huì)將其傳遞到 CMake。
支持C++的項(xiàng)目目錄
src/main/cpp下存放的我們編寫供JNI調(diào)用的C++源碼。CMakeLists.txt文件是CMake的配置文件,通常他包含的內(nèi)容如下:
# TODO 設(shè)置構(gòu)建本機(jī)庫文件所需的 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 將目標(biāo)庫與 NDK中的庫進(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 過濾器(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庫
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.在定義庫的名字時(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的名稱.
native-lib
# 標(biāo)志庫共享.
SHARED
# C++源碼文件的相對路徑.
src/main/cpp/native-lib.cpp )
# 將目標(biāo)庫與 NDK中的庫進(jìn)行連接
target_link_libraries( # 目標(biāo)library的名稱.
native-lib
${log-lib} )
- 6.引入第三方 .so文件,要添加到 CMakeLists.txt 文件中。
# TODO 添加第三方庫
# TODO add_library(libavcodec-57
# TODO 原先生成的.so文件在編譯后會(huì)自動(dòng)添加上前綴lib和后綴.so,
# TODO 在定義庫的名字時(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的庫,SHARED表示.so的庫
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)的庫
# 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ò)(無法在jniLibs中引入),指向下面的路徑就沒問題
include_directories( src/main/cpp/ffmpeg/include )
- 8.C++ library編譯生成的so文件,在
build/intermediates/cmake下
至此完成了CMake的設(shè)置,下面我們就可以愉快地進(jìn)行jni開發(fā)了!
講完了兩種進(jìn)行JNI開發(fā)的姿勢后,下面我們來簡單講講JNI的基礎(chǔ)語法。
JNI基礎(chǔ)語法
基礎(chǔ)類型
| 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引用類型其實(shí)都是jobject。
Native方法參數(shù)
- JNI接口指針是native方法的第一個(gè)參數(shù),JNI接口指針的類型是JNIEnv。
- 第二個(gè)參數(shù)取決于native method是否靜態(tài)方法,如果是非靜態(tài)方法,那么第二個(gè)參數(shù)是對對象的引用,如果是靜態(tài)方法,則第二個(gè)參數(shù)是對它的class類的引用
- 剩下的參數(shù)跟Java方法參數(shù)一一對應(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ù)類型
| Java類型 | 簽名描述 |
|---|---|
| boolean | Z |
| byte | B |
| char | C |
| short | S |
| int | I |
| long | J |
| float | F |
| double | D |
| void |
引用數(shù)據(jù)類型
(以L開頭,以;結(jié)束,中間對應(yīng)的是該類型的完整路徑)
String : Ljava/lang/String;
Object : Ljava/lang/Object;
自定義類型 Area : Lcom/xuexiang/jnidemo/Area;
數(shù)組
(在類型前面添加[,幾維數(shù)組就在前面添加幾個(gè)[)
int [] :[I
Long[][] : [[J
Object[][][] : [[[Ljava/lang/Object
使用命令查看
javap -s <java類的class文件路徑>
class文件存在于 build->intermediates->classes下。
JNI常見用法
1、jni訪問java非靜態(tài)成員變量
1.使用
GetObjectClass、FindClass獲取調(diào)用對象的類2.使用
GetFieldID獲取字段的ID。這里需要傳入字段類型的簽名描述。3.使用
GetIntField、GetObjectField等方法,獲取字段的值。使用SetIntField、SetObjectField等方法,設(shè)置字段的值。
注意:即使字段是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靜態(tài)成員變量
1.使用
GetObjectClass、FindClass獲取調(diào)用對象的類2.使用
GetStaticFieldID獲取字段的ID。這里需要傳入字段類型的簽名描述。3.使用
GetStaticIntField、GetStaticObjectField等方法,獲取字段的值。使用SetStaticIntField、SetStaticObjectField等方法,設(shè)置字段的值。
3、jni調(diào)用java非靜態(tài)成員方法
1.使用
GetObjectClass、FindClass獲取調(diào)用對象的類2.使用
GetMethodID獲取方法的ID。這里需要傳入方法的簽名描述。3.使用
CallVoidMethod執(zhí)行無返回值的方法,使用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)用對象的類2.使用
GetStaticMethodID獲取方法的ID。這里需要傳入方法的簽名描述。3.使用
CallStaticVoidMethod執(zhí)行無返回值的方法,使用CallStaticIntMethod、CallStaticBooleanMethod等執(zhí)行有返回值的方法。
5、jni調(diào)用java構(gòu)造方法
1.使用
FindClass獲取需要構(gòu)造的類2.使用
GetMethodID獲取構(gòu)造方法的ID。方法名為<init>, 這里需要傳入方法的簽名描述。3.使用
NewObject執(zhí)行創(chuàng)建對象。
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類構(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)建局部引用。局部引用,通過DeleteLocalRef手動(dòng)釋放對象;全局引用,通過DeleteGlobalRef手動(dòng)釋放對象。
引用不主動(dòng)釋放會(huì)導(dǎo)致內(nèi)存泄漏。
7、jni異常處理
使用
ExceptionOccurred進(jìn)行異常的檢測。注意,這里只能檢測java異常。使用
ExceptionClear進(jìn)行異常的清除。使用
ThrowNew來上拋異常。
注意,ExceptionOccurred和ExceptionClear一般是成對出現(xiàn)的,類似于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;");
//檢測是否發(fā)生Java異常
jthrowable exception = env->ExceptionOccurred();
if (exception != NULL) {
LOGE("jni發(fā)生異常");
//jni清空異常信息
env->ExceptionClear(); //需要和ExceptionOccurred方法成對出現(xiàn)
throwException(env, "native出錯(cuò)!");
}
}
8、日志打印
#include <android/log.h> //引用android log
//定義日志打印的方法
#define TAG "CMake-JNI" // 這個(gè)是自定義的LOG的標(biāo)識
#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發(fā)生異常"); //日志打印