在進行Android開發的過程當中,咱們一定會遇到視頻圖像處理、高強度密集運算、特殊算法等場景,這時咱們就不得不須要去接觸一些C/C++代碼,進行JNI開發。下面我將從Android.mk和CMake這兩種方式教你們如何進行開發。文章結尾將給出演示的項目代碼,若是你能耐心地仔細看完,相信你必定能掌握如何在Android下進行JNI開發。java
package com.xuexiang.jnidemo;
public class JNIApi {
public native String stringFromJNI();
}
複製代碼
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());
}
複製代碼
模版以下:android
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)
複製代碼
說明:c++
LOCAL_PATH := $(call my-dir)
:指向當前目錄的地址,包含該.mkgit
include $(CLEAR_VARS)
:清理掉全部以LOCAL_開頭的內容,這句話是必須的,由於若是全部的變量都是全局的,全部的可控的編譯文件都須要在一個單獨的GNU中被解析並執行。github
LOCAL_MODULE
:調用的庫名,用來區分android.mk中的每個模塊。文件名必須是惟一的,不能有空格。注意,這裏編譯器會爲你自動加上一些前綴lib和後綴.so,來保證文件是一致的。算法
LOCAL_SRC_FILES
:變量必須包含一個C、C++或者java源文件的列表,這些會被編譯並聚合到一個模塊中,文件之間能夠用空格或Tab鍵進行分割,換行請用"\"編程
LOCAL_LDLIBS
:定義須要連接的庫。通常用於連接那些存在於系統目錄下本模塊須要連接的庫(好比這裏的logcat庫)。數組
include $(BUILD_SHARED_LIBRARY)
:來生成一個動態庫libnative-lib.sobash
# 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
由於Android Studio 2.2之後推薦使用CMake進行JNI開發,所以須要修改一下參數進行兼容。
android.useDeprecatedNdk=true
複製代碼
cd 到jni(存放Android.mk的目錄)下,執行ndk-build
便可。
執行成功後,將會在jni的同級目錄下生成libs
和obj
文件夾,存放的是編譯好的so文件。
sourceSets {
main {
jni.srcDirs = []
jniLibs.srcDirs = ['src/main/libs']
}
}
複製代碼
至此完成了Android.mk的設置,下面咱們就能夠愉快地進行jni開發了!
上面介紹的Android.mk均可以在Eclispe和Android Studio下進行編譯開發,能夠說是一種比較傳統的作法。下面我將介紹Android Studio着重推薦的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 使用它來調試原生代碼。
在新建項目時,勾上Include C++ support
就好了:
在嚮導的 Customize C++ Support 部分,有下列自定義項目可供選擇:
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} )
複製代碼
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"
}
}
}
複製代碼
static {
System.loadLibrary("native-lib");
}
複製代碼
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} )
複製代碼
# 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)
複製代碼
# TODO include_directories( src/main/jniLibs/${ANDROID_ABI}/include )
# TODO 路徑指向上面會編譯出錯(沒法在jniLibs中引入),指向下面的路徑就沒問題
include_directories( src/main/cpp/ffmpeg/include )
複製代碼
build/intermediates/cmake
下至此完成了CMake的設置,下面咱們就能夠愉快地進行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。
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 ...
}
複製代碼
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
下。
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);
}
複製代碼
1.使用GetObjectClass
、FindClass
獲取調用對象的類
2.使用GetStaticFieldID
獲取字段的ID。這裏須要傳入字段類型的簽名描述。
3.使用GetStaticIntField
、GetStaticObjectField
等方法,獲取字段的值。使用SetStaticIntField
、SetStaticObjectField
等方法,設置字段的值。
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);
}
複製代碼
1.使用GetObjectClass
、FindClass
獲取調用對象的類
2.使用GetStaticMethodID
獲取方法的ID。這裏須要傳入方法的簽名描述。
3.使用CallStaticVoidMethod
執行無返回值的方法,使用CallStaticIntMethod
、CallStaticBooleanMethod
等執行有返回值的方法。
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");
//找到構造方法jmethodID public 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;
}
複製代碼
使用NewGlobalRef
建立全局引用,使用NewLocalRef
建立局部引用。
局部引用,經過DeleteLocalRef手動釋放對象;全局引用,經過DeleteGlobalRef手動釋放對象。
引用不主動釋放會致使內存泄漏。
使用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出錯!");
}
}
複製代碼
#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發生異常"); //日誌打印
複製代碼