1. 程式人生 > >Android Studio開發JNI示例

Android Studio開發JNI示例

JNI和NDK介紹

JNI(Java Native Interface),是方便Java呼叫C、C++等Native程式碼所封裝的一層介面,相當於一座橋樑。通過JNI可以操作一些Java無法完成的與系統相關的特性,尤其在影象和視訊處理中大量用到。

NDK(Native Development Kit)是Google提供的一套工具,其中一個特性是提供了交叉編譯,即C或者C++不是跨平臺的,但通過NDK配置生成的動態庫卻可以相容各個平臺。比如C在Windows平臺編譯後生成.exe檔案,那麼原始碼通過NDK編譯後可以生成在安卓手機上執行的二進位制檔案.so

在AS中使用ndk-build開發JNI示例

Android Studio2.2之前對於JNI開發的支援不是很好,開發一般使用Eclipse+外掛編寫本地動態庫。後面Google官方全面增強了對JNI的支援,包括內建NDK。

1.在AS中新建一個專案

2.宣告一個native方法

package com.mercury.jnidemo;

public class JNITest {

    public native static String getStrFromJNI();

}

3.通過javah命令生成標頭檔案

在AS的Terminal中,先進入要呼叫原生代碼的類所在的目錄,也就是在專案中的具體路徑,比如這裡是cd app\src\main\java

。然後通過javah命令生成該類的標頭檔案,注意包名+類名.這裡是javah -jni com.mercury.jnidemo.JNITest,生成標頭檔案com_mercury_jnidemo_JNITest.h

實際專案最終可以不包含此標頭檔案,不熟悉C的語法的開發人員,藉助於該標頭檔案可以知道JNI的相關語法:

/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class com_mercury_jnidemo_JNITest */

#ifndef _Included_com_mercury_jnidemo_JNITest
#define _Included_com_mercury_jnidemo_JNITest #ifdef __cplusplus extern "C" { #endif /* * Class: com_mercury_jnidemo_JNITest * Method: getStrFromJNI * Signature: ()Ljava/lang/String; */ JNIEXPORT jstring JNICALL Java_com_mercury_jnidemo_JNITest_getStrFromJNI (JNIEnv *, jclass); #ifdef __cplusplus } #endif #endif

首先引入jni.h,裡面包含了很多巨集定義及呼叫本地方法的結構體。重點是方法名的格式。這裡的JNIEXPORT和JNICALL都是jni.h中所定義的巨集。JNIEnv *表示一個指向JNI環境的指標,可通過它來訪問JNI提供的介面方法。jclass也是jni.h中定義好的,型別是jobject,實際上是一個不確定型別的指標,這裡用來接收Java中的this。實際編寫中一般只要遵循Java_包名_類名_方法名就好了。

4.實現JNI方法

像上面的標頭檔案只是定義了方法,並沒有實現,就像一個介面一樣。這裡就用C寫一個簡單的無參的JNI方法。
先建立一個jni目錄,我直接在src的父目錄下建立的,也可以在其他目錄建立,因為最終只需要編譯好的動態庫。在jni目錄下建立Android.mk和demo.c檔案。

AndroidStudio開發JNI示例_1.png

Android.mk是一個makefile配置檔案,安卓大量採用makefile進行自動化編譯。LOCAL_MODULE定義的名稱就是編譯好的so庫名稱,比如這裡是jni-demo最終生成的動態庫名稱就叫libjni-demo.so。 LOCAL_SRC_FILES表示參與編譯的原始檔名稱,這裡就是demo.c

LOCAL_PATH := $(call my-dir)

include $(CLEAR_VARS)

LOCAL_MODULE := jni-demo
LOCAL_SRC_FILES := demo.c

include $(BUILD_SHARED_LIBRARY)

這裡的demo.c實現了一個很簡單的方法,返回String型別。

#include<jni.h>

jstring Java_com_mercury_jnidemo_JNITest_getStrFromJNI(JNIEnv *env,jobject thiz){
    return (*env)->NewStringUTF(env,"I am Str from jni libs!");
}

這時候NDK編譯生成的動態庫會有四個CPU平臺:arm64-v8a、armeabi-v7a、x86、x86_64。如果建立Application.mk就可以指定要生成的CPU平臺,語法也很簡單:

APP_ABI := all

這樣就會生成各個CPU平臺下的動態庫。

5.使用ndk-build程式設計生成.so庫

切回到jni目錄的父目錄下,在Terminal中執行ndk-build指令,就可以在和jni目錄同級生成一個libs資料夾,裡面存放相對應的平臺的.so庫。同時生成的還有一箇中間臨時的obj資料夾,和jni資料夾可以一起刪除。
需要注意,使用NDK一定要先在build.gradle下要配置ndk-build的相關路徑,這樣在編寫原生代碼時才會有相關的提示功能,並且可以關聯到相關的標頭檔案

externalNativeBuild {
        ndkBuild {
            path 'jni/Android.mk'
        }
    }

還有一點,網上很多資料都在build.gradle中加入以下程式碼:

sourceSets{
        main{
            jniLibs.srcDirs=['libs']
        }
    }

這樣就指定了目標.so庫的存放位置。但在實際使用中,就算不指定,執行時仍然可以載入正確的.so庫檔案,並且如果新增該程式碼後有時會報出以下錯誤:

 Error:Execution failed for task ':usejava:transformNativeLibsWithMergeJniLibsForDebug'.
	> More than one file was found with OS independent path 'lib/x86/libjni-calljava.so'
	> 

6.載入.so庫並呼叫方法

在類初始化的時候要載入該.so庫,一般會寫在靜態程式碼塊裡。名稱就是前面的LOCAL_MODULE。

    static {
        System.loadLibrary("jni-demo");
    }


需要注意的是如果是有參的JNI方法,那麼直接在引數列表裡補充在jni.h預先typedef好的資料型別就可以了。
AndroidStudio開發JNI示例_jni-1.gif

JNI呼叫Java

不同於JNI呼叫C,JNI呼叫Java的過程不是單獨存在的。而是編寫native方法,Java先通過JNI呼叫該方法,在方法內部再去回撥類中對應的Java方法。步驟有些類似於Java中的反射。這裡寫定義三個點選事件,三個Native方法,三種Java的方法型別,根據相關的Log判斷是否成功。

public class MainActivity extends AppCompatActivity {

    public static final String TAG = "MainActivity";

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
    }

    static {
        System.loadLibrary("jni-calljava");
    }

    public void noParamMethod() {
        Log.i(TAG, "無參的Java方法被呼叫了");
    }

    public void paramMethod(int number) {
        Log.i(TAG, "有參的Java方法被呼叫了" + number + "次");
    }

    public static void staticMethod() {
        Log.i(TAG, "靜態的Java方法被呼叫了");
    }

    public void click1(View view) {
        test1();
    }

    public void click2(View view) {
        test2();
    }

    public void click3(View view) {
        test3();
    }

    public native void test1();

    public native void test2();

    public native void test3();

}

1.呼叫Java無參方法

  • JNI呼叫本地方法,根據類名找到類,注意類名用"/"分隔。
  • 找到類後,根據方法名找到方法。該函式GetMethodID最後一個形參是該形參列表的簽名。不同於Java,C中是通過簽名標識去找方法。
  • 獲取方法的簽名:首先定位到該類的位元組碼檔案所在的父目錄,一般在module\build\intermediates\classes\debug>,通過javap -s com.mercury.usejava.MainActivity獲取整個類所有的內部型別簽名。無參方法test1()的簽名是()V
  • 通過JNIEnv物件的CallVoidMethod來完成方法的回撥,最後一個形參是可變引數。
JNIEXPORT void JNICALL Java_com_mercury_usejava_MainActivity_test1
  (JNIEnv * env, jobject obj){
       //回撥MainActivity中的noParamMethod
    jclass clazz = (*env)->FindClass(env, "com/mercury/usejava/MainActivity");
    if (clazz == NULL) {
        printf("find class Error");
        return;
    }
    jmethodID id = (*env)->GetMethodID(env, clazz, "noParamMethod", "()V");
    if (id == NULL) {
        printf("find method Error");
    }
    (*env)->CallVoidMethod(env, obj, id);
  }

2.呼叫Java有參方法

類似於無參方法,只是引數簽名和可變引數的不同

3.呼叫Java靜態方法

注意獲取方法名的方法是GetStaticMethodID,呼叫方法的函式名是CallStaticVoidMethod,並且由於是靜態方法,不應該傳入jobject引數,而直接是jclass.

JNIEXPORT void JNICALL Java_com_mercury_usejava_MainActivity_test3
  (JNIEnv * env, jobject obj){
    jclass clazz = (*env)->FindClass(env, "com/mercury/usejava/MainActivity");
    if (clazz == NULL) {
        printf("find class Error");
        return;
    }
    jmethodID id = (*env)->GetStaticMethodID(env, clazz, "staticMethod", "()V");
    if (id == NULL) {
        printf("find method Error");
    }

    (*env)->CallStaticVoidMethod(env, clazz, id);
  }

相應日誌

AndroidStudio開發JNI示例_jni-2.gif

使用CMake開發JNI

CMake是一個跨平臺的安裝(編譯)工具,通過編寫CMakeLists.txt,可以生成對應的makefile或project檔案,再呼叫底層的編譯。AS 2.2之後工具中增加了對CMake的支援,官方也推薦用CMake+CMakeLists.txt的方式,代替ndk-build+Android.mk+Application.mk的方式去構建JNI專案.

1.建立使用CMake構建的專案

開始前AS要先在SDK Manager中安裝SDK Tools->CMake
AndroidStudio開發JNI示例_2.png
只要勾選Include C++ Support。其中會提示配置C++支援的功能.
AndroidStudio開發JNI示例_3.png
一般預設就可以了,各個選項的具體含義:

  • C++ Standard:指定編譯庫的環境。
  • Exception Support:當前專案支援C++異常處理
  • Runtime Type Information Support:除異常處理外,還支援動態轉型別(dynamic casting) 、模組整合、以及物件I/O

2.工程的目錄結構

AndroidStudio開發JNI示例_4.png
建立好的工程主Module下直接就有.externalNativeBuild,多出一個CMakeLists.txt,相當於以前的配置檔案。並且在src/main目錄下多了一個cpp資料夾,裡面存放的是C++檔案,相當於以前的jni資料夾。這個是工程建立後AS生成的示例JNI方法,返回了一個字串。後面開發JNI就可以按照這個目錄結構。

相應的,build.gradle下也增加了一些配置。

android {
    ...
    defaultConfig {
        ...
        externalNativeBuild {
            cmake {
                cppFlags "-std=c++14 -frtti -fexceptions"
            }
        }
    }
    buildTypes {
        ...
    }
    externalNativeBuild {
        cmake {
            path "CMakeLists.txt"
        }
    }
}

defaultConfig中的externalNativeBuild各項屬性和前面建立專案時的選項配置有關,外部的externalNativeBuild則定義了CMakeLists.txt的存放路徑。
如果只是在自己的專案中使用,CMake的方式在打包APK的時候會自動將cpp檔案編譯成so檔案拷貝進去。如果要提供給外部使用時,Make Project,之後在libs目錄下就可以看到生成的對應配置的相關CPU平臺的.so檔案。

CMakeLists.txt

CMakeLists.txt可以自定義命令、查詢檔案、標頭檔案包含、設定變數,具體可見 官方文件。專案預設生成的CMakeLists.txt核心內容如下:


# 編譯本地庫時我們需要的最小的cmake版本
cmake_minimum_required(VERSION 3.4.1)

# 相當於Android.mk
add_library( # Sets the name of the library.設定編譯生成本地庫的名字
             native-lib

             # Sets the library as a shared library.庫的型別
             SHARED

             # Provides a relative path to your source file(s).編譯檔案的路徑
             src/main/cpp/native-lib.cpp )

# 新增一些我們在編譯我們的本地庫的時候需要依賴的一些庫,這裡是用來打log的庫
find_library( # Sets the name of the path variable.
              log-lib

              # Specifies the name of the NDK library that
              # you want CMake to locate.
              log )

# 關聯自己生成的庫和一些第三方庫或者系統庫
target_link_libraries( # Specifies the target library.
                       native-lib

                       # Links the target library to the log library
                       # included in the NDK.
                       ${log-lib} )

使用CMakeLists.txt同樣可以指定so庫的輸出路徑,但一定要在add_library之前設定,否則不會生效:


set(CMAKE_LIBRARY_OUTPUT_DIRECTORY 
	${PROJECT_SOURCE_DIR}/libs/${ANDROID_ABI}) #指定路徑
#生成的so庫在和CMakeLists.txt同級目錄下的libs資料夾下

如果想要配置so庫的目標CPU平臺,可以在build.gradle中設定

android {
    ...
    defaultConfig {
        ...
        ndk{
            abiFilters "x86","armeabi","armeabi-v7a"
        }
    }
	...
  
}

需要注意的是,如果是多次使用add_library,則會生成多個so庫。如果想將多個本地檔案編譯到一個so庫中,只要最後一個引數新增多個C/C++檔案的相對路徑就可以

用C語言實現字串加密

Java中實現字串加密的一種比較簡單的方法是異或,將字串轉換為字元陣列,遍歷對其中的每個字元用金鑰(可以是字元)進行一次異或運算,生成新的字串。如果用JNI和C實現,大致步驟如下(jstring是要加密的字串):

1 獲取jstring的長度

2 動態開闢一個跟data長度一樣的char*

3 將 jstring型別轉換為char陣列(用char*接收)

4 遍歷char陣列,進行異或運算

5 將char*轉換為jstring型別返回

6 釋放動態開闢的堆記憶體空間

效果圖
AndroidStudio開發JNI示例_jni-3.gif

我是用的是5.0的模擬器,有時會閃退,檢視系統日誌,會報出一下錯誤:

JNI DETECTED ERROR IN APPLICATION: input is not valid Modified UTF-8

網上查了一下,JNI在呼叫NewStringUTF方法時,遇到不認識的字元就會退出,因為虛擬機器dalvik/vm/CheckJni.cpp裡面的checkUTFString會對字元型別進行檢查。替代方案是在開始轉換前,先檢查char*中是否含有非UTF-8字元,有的話返回空字串。完整程式碼如下:

#include<jni.h>
#include <stdlib.h>

jboolean checkUtfBytes(const char* bytes, const char** errorKind) ;

jstring Java_com_mercury_cmakedemo_MainActivity_encryptStr
        (JNIEnv *env, jobject object, jstring data){
    if(data==NULL){
        return (*env)->NewStringUTF(env, "");
    }
    jsize len = (*env)->GetStringLength(env, data);
    char *buffer = (char *) malloc(len * sizeof(char));
    (*env)->GetStringUTFRegion(env, data, 0, len, buffer);
    int i=0;
    for (; i <len ; i++) {
        buffer[i] = (char) (buffer[i] ^ 2);
    }

    const char *errorKind = NULL;
    checkUtfBytes(buffer, &errorKind);
    free(buffer);
    if (errorKind == NULL) {
        return (*env)->NewStringUTF(env, buffer);
    } else {
        return (*env)->NewStringUTF(env, "");
    }
}

//把char*和errorKind傳入,如果errorKind不為NULL說明含有非utf-8字元,做相應處理
jboolean checkUtfBytes(const char* bytes, const char** errorKind) {
    while (*bytes != '\0') {
        jboolean utf8 = *(bytes++);
        // Switch on the high four bits.
        switch (utf8 >> 4) {
            case 0x00:
            case 0x01:
            case 0x02:
            case 0x03:
            case 0x04:
            case 0x05:
            case 0x06:
            case 0x07:
                // Bit pattern 0xxx. No need for any extra bytes.
                break;
            case 0x08:
            case 0x09:
            case 0x0a:
            case 0x0b:
            case 0x0f:
                /*
                 * Bit pattern 10xx or 1111, which are illegal start bytes.
                 * Note: 1111 is valid for normal UTF-8, but not the
                 * modified UTF-8 used here.
                 */
                *errorKind = "start";
                return utf8;
            case 0x0e:
                // Bit pattern 1110, so there are two additional bytes.
                utf8 = *(bytes++);
                if ((utf8 & 0xc0) != 0x80) {
                    *errorKind = "continuation";
                    return utf8;
                }
                // Fall through to take care of the final byte.
            case 0x0c:
            case 0x0d:
                // Bit pattern 110x, so there is one additional byte.
                utf8 = *(bytes++);
                if ((utf8 & 0xc0) != 0x80) {
                    *errorKind = "continuation";
                    return utf8;
                }
                break;
        }
    }
    return 0;
}