第九章 JNI

第九章 JNI

一.JNI與NDK簡介

1、JNI(協議)

(1)定義

Java Native Interface,即 Java本地接口,相當於橋樑作用,一種協議;
即在 Java代碼 裏調用 C、C++等語言的代碼 或 C、C++代碼調用 Java 代碼(互相調用)
Android系統架構中上層(框架層+應用層)JAVA通過JNI調用底層(Linux Kernel層)C;
JNI是 Java 調用 Native 語言的一種特性,是屬於 Java 的,與 Android 無直接關係

(2)作用

實際使用中,Java需要與本地代碼(Native code)進行交互,因爲Java具備跨平臺特點,所以Java與本地代碼交互能力弱。可採用JNI特性增強Java與本地代碼交互能力

2、NDK(工具)

(1)定義

Native Development Kit,是 Android的一個工具開發包;NDK是屬於 Android 的,與Java並無直接關係

(2)作用

快速開發C、 C++的動態庫,並自動將so和應用一起打包成 APK,即可通過 NDK在 Android中 使用 JNI與本地代碼(如C、C++)交互
使Android開發的功能需在本地代碼(C/C++)實現

(3)特點

  • 運行效率高:在開發要求高性能的需求功能中,採用C/C++更加有效率,如使用本地代碼(C/C++)執行算法,能大大提高算法的執行效率
  • 代碼安全性高:java是半解釋型語言,容易被反會變後得到源代碼,而本地代碼(C/C++)不會,能提高系統安全性
  • 功能擴展性好:可方便地使用其他開發語言的開源庫,除Java開源庫還可以用(C/C++)開源庫
  • 易於代碼複用和移植
  • 用本地代碼(C/C++)開發的代碼不僅可以在安卓使用,還可以嵌入其他類型平臺使用
  • 提供了把.so和.apk打包的工具(JNI只把.so文件放到文件系統特定位置)
  • NDK提供的庫有限,僅用於處理算法效率和敏感問題
  • 提供了交叉編譯器,用於生成特定的CPU平臺動態庫

3、JNI與NDK關係

JNI是實現的目的(java與本地語言交互的接口/協議),NDK是Android中實現JNI的工具(Android工具開發包)

二.具體使用

(1)NDK集成開發流程

1、配置Android NDK環境

2、關聯Android Studio項目與NDK

a. 在Gradle的 local.properties中添加配置

ndk.dir=/Users/Carson_Ho/Library/Android/sdk/ndk-bundle

b.在Gradle的 gradle.properties中添加配置

#兼容老的Ndk
android.useDeprecatedNdk=true 

c.在Gradle的build.gradle添加ndk節點

        ndk {
            //.so文件 Linux下動態鏈接庫(同windows下dll文件),二進制文件,多用於NDK開發.用戶拿到動態庫和頭文件說明,就可以使用動態庫中function
            moduleName "hello_jni"//對應本地代碼文件,生成.so文件:lib+moduleName.so
			//abiFilters "x86","armeabi", "armeabi-v7a"//CPU類型
        }

3、創建JNI類聲明native方法

package com.sdu.chy.chytest.ndkTest

/**
 * Java調用對應的C代碼
 */

public class JNI {

    //加載JNI生成so庫
    static {
        System.loadLibrary("hello_jni");
    }

    //定義Native方法,調用C代碼對應方法
    public native String sayHello();
}

4、生成.h文件

(1)包目錄下javac生成.class類文件

ndkTest danding$ javac JNI.java

(2)外部(java)目錄下javah生成.h文件

java danding$ javah -jni com.sdu.chy.chytest.ndkTest.JNI

(3)將.h文件移到jni文件夾下

5、創建本地代碼文件

需在Android項目中調用的本地代碼hello.c

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

//類名:Java類型+本地類型 對應關係
//C函數命名格式:Java_全類名_方法名
//JNIEnv*:代表了Java環境,通過這個JNIEnv* 指針,就可以對Java端的代碼進行操作。
//jobject:代表native方法的實例(調用者),這裏是JNI.ini
JNIEXPORT jstring JNICALL Java_com_sdu_chy_chytest_ndkTest_JNI_sayHello(JNIEnv* env,jobject jobj){
    char* text = "I am from C";
    return (*env)->NewStringUTF(env,text);
}

注:

  • 如果本地代碼是C++(.cpp或者.cc),要使用extern “C” { }把本地方法括進去
  • JNIEXPORT jstring JNICALL中的JNIEXPORT 和 JNICALL不能省
  • 關於方法名Java_scut_carson_1ho_ndk_1demo_MainActivity_getFromJNI
    格式 = Java 包名 _ 類名_Java需要調用的方法名
    Java必須大寫
    對於包名,包名裏的.要改成
    ,_要改成_1。如我的包名是:scut.carson_ho.ndk_demo,則需要改成scut_carson_1ho_ndk_1demo
  • 最後,將創建好的test.cpp文件放入到工程文件目錄中的src/main/jni文件夾。若無jni文件夾,則手動創建。
  • JNI類型與Java類型對應關係介紹

6、創建Android.mk文件 & Application.mk文件

6.1)創建Android.mk文件

作用:
指定源碼編譯的配置信息,如工作目錄,編譯模塊的名稱,參與編譯的文件等
使用:
Android.mk(src/main/jni)

LOCAL_PATH := $(call my-dir)
// 設置工作目錄,而my-dir則會返回Android.mk文件所在的目錄
include $(CLEAR_VARS)
// 清除幾乎所有以LOCAL——PATH開頭的變量(不包括LOCAL_PATH)
LOCAL_MODULE    := hello_jni
// 設置模塊的名稱,即編譯出來.so文件名
// 注,要和上述步驟中build.gradle中NDK節點設置的名字相同
LOCAL_SRC_FILES := hello.c \
// 指定參與模塊編譯的C/C++源文件名
include $(BUILD_SHARED_LIBRARY)
// 指定生成的靜態庫或者共享庫在運行時依賴的共享庫模塊列表。
6.2)創建Application.mk文件

作用:配置編譯平臺相關內容
使用:
Application.mk(src/main/jni)

APP_MODULES := hello_jni
APP_ABI := all
// 最常用的APP_ABI字段:指定需要基於哪些CPU平臺的.so文件
// 常見的平臺有armeabi x86 mips,其中移動設備主要是armeabi平臺
// 默認情況下,Android平臺會生成所有平臺的.so文件,即同APP_ABI := armeabi x86 mips
// 指定CPU平臺類型後,就只會生成該平臺的.so文件,即上述語句只會生成armeabi平臺的.so文件

7、編譯上述文件,生成.so庫文件,並放入工程文件

7.1)修改build.gradle配置
        sourceSets {
            main {
                jni.srcDirs = []
                jniLibs.srcDirs = ['libs','src/main/libs']
                //生成.so文件位置'src/main/libs'
            }
        }
7.2)對JNI類執行ndk-build

在這裏插入圖片描述
編譯成功後,在src/main/會多了兩個文件夾libs & obj,其中libs下存放的是.so庫文件
在這裏插入圖片描述

8、在Android Studio項目中通過JNI調用so文件

/Users/danding/Documents/chy_workspace/app/src/main/java/com/sdu/chy/chytest/ndkTest/JniTestActivity.java

public class JniTestActivity extends AppCompatActivity {

    private TextView JniTextView;
    private Button JniScheduleBtn;

    private JniClickListener jniClickListener = new JniClickListener();

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

    public void initView(){
        JniTextView = (TextView)findViewById(R.id.jni_text_view);
        JniScheduleBtn = (Button) findViewById(R.id.jni_btn_schedule);

        JniScheduleBtn.setOnClickListener(jniClickListener);
    }

    public class JniClickListener implements View.OnClickListener{
        @Override
        public void onClick(View v) {
            switch (v.getId()){
                case R.id.jni_btn_schedule:
                    JniTextView.setText(new JNI().sayHello());//調用
                    break;
            }
        }
    }
}

點擊按鈕,調用C方法
在這裏插入圖片描述

(2)NDK開發

1、Java調用C函數

1.1)註冊JNI函數
1.靜態註冊

先由Java得到本地方法的聲明,然後再通過JNI實現該聲明方法
靜態註冊就是根據函數名來遍歷Java和JNI函數之間的關聯,而且要求JNI層函數的名字必須遵循特定的格式。
步驟1:首先在Java代碼中聲明native函數

public class JniDemo1{
       static {
             System.loadLibrary("samplelib_jni");
        }

        private native void nativeMethod();
}

步驟2:通過javah來生成native函數的.h文件

javah -d ./jni/ -classpath /Users/YOUR_NAME/Library/Android/sdk/platforms/android-21/android.jar:../../build/intermediates/classes/debug/ com.gebilaolitou.jnidemo.JniDemo1

然後就會得到一個JNI的.h文件,裏面包含這幾個native函數的聲明
觀察一下文件名以及函數名。其實JNI方法名的規範就出來了:
返回值 + Java前綴+全路徑類名+方法名+參數1JNIEnv+參數2jobject+其他參數
步驟3:編寫代碼在.h文件中實現方法

2.動態註冊

先通過JNI重載JNI_OnLoad()實現本地方法,然後直接在Java中調用本地方法。
通過RegisterNatives方法把C/C++中的方法映射到Java中的native方法,而無需遵循特定的方法命名格式。

1.2)從JNI調用C函數

步驟一:加載so庫

public class JniDemo1{
       static {
             System.loadLibrary("samplelib_jni");
        }
}

步驟二:在JNI中的實現

jint JNI_OnLoad(JavaVM* vm, void* reserved)

步驟三:在這個函數裏面去動態的註冊native方法

#include <jni.h>
#include "Log4Android.h"
#include <stdio.h>
#include <stdlib.h>

using namespace std;

#ifdef __cplusplus
extern "C" {
#endif

static const char *className = "com/gebilaolitou/jnidemo/JNIDemo2";

static void sayHello(JNIEnv *env, jobject, jlong handle) {
    LOGI("JNI", "native: say hello ###");
}

static JNINativeMethod gJni_Methods_table[] = {
    {"sayHello", "(J)V", (void*)sayHello},
};

static int jniRegisterNativeMethods(JNIEnv* env, const char* className,
    const JNINativeMethod* gMethods, int numMethods)
{
    jclass clazz;

    LOGI("JNI","Registering %s natives\n", className);
    clazz = (env)->FindClass( className);
    if (clazz == NULL) {
        LOGE("JNI","Native registration unable to find class '%s'\n", className);
        return -1;
    }

    int result = 0;
    if ((env)->RegisterNatives(clazz, gJni_Methods_table, numMethods) < 0) {
        LOGE("JNI","RegisterNatives failed for '%s'\n", className);
        result = -1;
    }

    (env)->DeleteLocalRef(clazz);
    return result;
}

jint JNI_OnLoad(JavaVM* vm, void* reserved){
    LOGI("JNI", "enter jni_onload");

    JNIEnv* env = NULL;
    jint result = -1;

    if (vm->GetEnv((void**) &env, JNI_VERSION_1_4) != JNI_OK) {
        return result;
    }

    jniRegisterNativeMethods(env, className, gJni_Methods_table, sizeof(gJni_Methods_table) / sizeof(JNINativeMethod));

    return JNI_VERSION_1_4;
}

#ifdef __cplusplus
}
#endif

注:
1)JNINativeMethod
定義
JNI允許我們提供一個函數映射表,註冊給Java虛擬機,這樣JVM就可以用函數映射表來調用相應的函數。這樣就可以不必通過函數名來查找需要調用的函數了。Java與JNI通過JNINativeMethod的結構來建立聯繫,它被定義在jni.h中
結構

typedef struct { 
    const char* name; //Java中函數名
    const char* signature; //Java中參數和返回值
    void* fnPtr; //指向C函數的函數指針
} JNINativeMethod; 

綁定
在jniRegisterNativeMethods內,通過調用RegisterNatives函數將註冊函數的Java類,以及註冊函數的數組,以及個數註冊在一起,這樣就實現了綁定。
2)JNI中的簽名
原因:
即將參數類型和返回值類型的組合。如果擁有一個該函數的簽名信息和這個函數的函數名,我們就可以順序的找到對應的Java層中的函數了。(防止Java函數重載找不到對應實現方法)
規範
(參數1類型標示;參數2類型標示;參數3類型標示…)返回值類型標示

2、C回調Java方法

(1)獲取Class對象

爲了能夠在C/C++中調用Java中的類,jni.h的頭文件專門定義了jclass類型表示Java中Class類。JNIEnv中有3個函數可以獲取jclass。

1.jclass jcl_string=env->FindClass("java/lang/String");//通過類的名稱
2.jclass GetObjectClass(jobject obj);//通過對象實例來獲取jclass,相當於Java中的getClass()函數
3.jclass getSuperClass(jclass obj);//通過jclass可以獲取其父類的jclass對象
(2)獲取屬性方法

所以爲了在C/C++獲取Java層的屬性和方法,JNI在jni.h頭文件中定義了jfieldID和jmethodID這兩種類型來分別代表Java端的屬性和方法。在訪問或者設置Java某個屬性\方法的時候,首先就要現在本地代碼中取得代表該Java類的屬性的jfieldID\jmethodID,然後才能在本地代碼中進行Java屬性的操作.
方法:一般是使用JNIEnv來進行操作
GetFieldID/GetMethodID:獲取某個屬性/某個方法
GetStaticFieldID/GetStaticMethodID:獲取某個靜態屬性/靜態方法
具體實現

jfieldID GetFieldID(JNIEnv *env, jclass clazz, const char *name, const char *sig);
jmethodID GetMethodID(JNIEnv *env, jclass clazz, const char *name, const char *sig);
jfieldID GetStaticFieldID(JNIEnv *env, jclass clazz, const char *name, const char *sig);
jmethodID GetStaticMethodID(JNIEnv *env, jclass clazz,const char *name, const char *sig);
(3)構造一個對象,並通過對象調用響應方法
jobject NewObject(jclass clazz, jmethodID methodID, ...)

三.實際場景

當出現一些用java語言無法處理的任務時,開發人員就可以利用JNI技術來完成。一般來說下面幾種情況需要用到JNI技術:
一、 開發時,需要調用java語言不支持的依賴於操作系統平臺的特性的一些功能。例如:需要調用當前的Unix系統的某個功能,而java不支持這個功能,就需要用到JNI技術來實現。
二、 開發時,爲了整合一些以前的非java語言開發的某些系統。例如,需要用到開發早期實現的一些C或C++語言開發的一些功能或系統,將這些功能整合到當前的系統或新的版本中。
三、 開發時,爲了節省程序的運行時間,必須採用一些低級或中級語言。例如爲了創建一個省時的應用,不得不採用彙編語言,然後採用java語言通過JNI技術調用這個低級語言的應用。
例如:
美圖秀秀處理圖片:用java獲取圖片文件,再用C通過顏色矩陣(RGBA)對圖片進行處理

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章