1. 程式人生 > >手把手教你如何在Android下進行JNI開發(入門)

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

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


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

模版如下:

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) 複製程式碼

說明:

  • LOCAL_PATH := $(call my-dir) :指向當前目錄的地址,包含該.mk

  • include $(CLEAR_VARS):清理掉所有以LOCAL_開頭的內容,這句話是必須的,因為如果所有的變數都是全域性的,所有的可控的編譯檔案都需要在一個單獨的GNU中被解析並執行。

  • LOCAL_MODULE:呼叫的庫名,用來區分android.mk中的每一個模組。檔名必須是唯一的,不能有空格。注意,這裡編譯器會為你自動加上一些字首lib和字尾.so

    ,來保證檔案是一致的。

  • LOCAL_SRC_FILES:變數必須包含一個C、C++或者java原始檔的列表,這些會被編譯並聚合到一個模組中,檔案之間可以用空格或Tab鍵進行分割,換行請用"\"

  • LOCAL_LDLIBS:定義需要連結的庫。一般用於連結那些存在於系統目錄下本模組需要連結的庫(比如這裡的logcat庫)。

  • include $(BUILD_SHARED_LIBRARY):來生成一個動態庫libnative-lib.so

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常見用法

1、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);
}
複製程式碼

2、jni訪問java靜態成員變數

  • 1.使用GetObjectClassFindClass獲取呼叫物件的類

  • 2.使用GetStaticFieldID獲取欄位的ID。這裡需要傳入欄位型別的簽名描述。

  • 3.使用GetStaticIntFieldGetStaticObjectField等方法,獲取欄位的值。使用SetStaticIntFieldSetStaticObjectField等方法,設定欄位的值。

3、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);
}
複製程式碼

4、jni呼叫java靜態成員方法

  • 1.使用GetObjectClassFindClass獲取呼叫物件的類

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

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

5、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;
}
複製程式碼

6、jni引用全域性變數

  • 使用NewGlobalRef建立全域性引用,使用NewLocalRef建立區域性引用。

  • 區域性引用,通過DeleteLocalRef手動釋放物件;全域性引用,通過DeleteGlobalRef手動釋放物件。

  • 引用不主動釋放會導致記憶體洩漏。

7、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出錯!");
    }
}
複製程式碼

8、日誌列印

#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發生異常"); //日誌列印
複製程式碼

相關連線

聯絡方式

在這裡插入圖片描述