手把手教你如何在Android下進行JNI開發(入門)

在進行Android開發的過程當中,咱們一定會遇到視頻圖像處理、高強度密集運算、特殊算法等場景,這時咱們就不得不須要去接觸一些C/C++代碼,進行JNI開發。下面我將從Android.mk和CMake這兩種方式教你們如何進行開發。文章結尾將給出演示的項目代碼,若是你能耐心地仔細看完,相信你必定能掌握如何在Android下進行JNI開發。java


使用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

模版以下: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

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的同級目錄下生成libsobj文件夾,存放的是編譯好的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 ...

}

複製代碼

點擊查看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常見用法

一、jni訪問java非靜態成員變量

  • 1.使用GetObjectClassFindClass獲取調用對象的類

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

  • 3.使用GetIntFieldGetObjectField等方法,獲取字段的值。使用SetIntFieldSetObjectField等方法,設置字段的值。

注意:即便字段是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);
}
複製代碼

二、jni訪問java靜態成員變量

  • 1.使用GetObjectClassFindClass獲取調用對象的類

  • 2.使用GetStaticFieldID獲取字段的ID。這裏須要傳入字段類型的簽名描述。

  • 3.使用GetStaticIntFieldGetStaticObjectField等方法,獲取字段的值。使用SetStaticIntFieldSetStaticObjectField等方法,設置字段的值。

三、jni調用java非靜態成員方法

  • 1.使用GetObjectClassFindClass獲取調用對象的類

  • 2.使用GetMethodID獲取方法的ID。這裏須要傳入方法的簽名描述。

  • 3.使用CallVoidMethod執行無返回值的方法,使用CallIntMethodCallBooleanMethod等執行有返回值的方法。

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);
}
複製代碼

四、jni調用java靜態成員方法

  • 1.使用GetObjectClassFindClass獲取調用對象的類

  • 2.使用GetStaticMethodID獲取方法的ID。這裏須要傳入方法的簽名描述。

  • 3.使用CallStaticVoidMethod執行無返回值的方法,使用CallStaticIntMethodCallStaticBooleanMethod等執行有返回值的方法。

五、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");
    //找到構造方法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;
}
複製代碼

六、jni引用全局變量

  • 使用NewGlobalRef建立全局引用,使用NewLocalRef建立局部引用。

  • 局部引用,經過DeleteLocalRef手動釋放對象;全局引用,經過DeleteGlobalRef手動釋放對象。

  • 引用不主動釋放會致使內存泄漏。

七、jni異常處理

  • 使用ExceptionOccurred進行異常的檢測。注意,這裏只能檢測java異常。

  • 使用ExceptionClear進行異常的清除。

  • 使用ThrowNew來上拋異常。

注意,ExceptionOccurredExceptionClear通常是成對出現的,相似於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發生異常"); //日誌打印
複製代碼

相關連接

聯繫方式

在這裏插入圖片描述
相關文章
相關標籤/搜索