手把手教你如何在Android下進(jìn)行JNI開發(fā)(入門)

在進(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)前目錄的地址,包含該.mk

  • include $(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的同級目錄下生成libsobj文件夾,存放的是編譯好的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就行了:

image

在向?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。
image

支持C++的項(xiàng)目目錄

image
  • 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++方法體。
image
  • 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
image

至此完成了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引用類型如下:

image

在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 ...

}

點(diǎn)擊查看JNI接口

簽名描述

基礎(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下。

image

JNI常見用法

1、jni訪問java非靜態(tài)成員變量

  • 1.使用GetObjectClass、FindClass獲取調(diào)用對象的類

  • 2.使用GetFieldID獲取字段的ID。這里需要傳入字段類型的簽名描述。

  • 3.使用GetIntFieldGetObjectField等方法,獲取字段的值。使用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.使用GetObjectClassFindClass獲取調(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來上拋異常。

注意,ExceptionOccurredExceptionClear一般是成對出現(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ā)生異常"); //日志打印

相關(guān)連接

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容