【進階解密】JNI初學(xué)篇(一)-靜態(tài)注冊

一、JNI簡介

??Android系統(tǒng)按語言來劃分的話由兩個世界組成,分別是\color{red}{Java和Native}。為什么要這樣劃分呢?Android系統(tǒng)全部由Java開發(fā)不行嗎?除了性能的原因外,更多的原因是,Java誕生前,有多程序代碼都是由Native語言寫的,因此重復(fù)利用這些Native語言編寫的庫十分必要,況且Native擁有更好的性能。這樣就產(chǎn)生一個問題,如何將Java世界的代碼和Native代碼連接起來呢?這就需要一個橋梁,也就是JNI。

jni關(guān)系圖.png

??JNIJava Native Interface的縮寫,譯為java本地接口,是java與其他語言通信的橋梁。當(dāng)java語言無法處理的時候,就可以使用JNI技術(shù)來完成,主要有以下幾種情況需要使用JNI技術(shù)。
需要調(diào)用Java語言不支持的依賴于操作系統(tǒng)平臺特性的一些功能。例如:需要調(diào)用當(dāng)前的UNIX系統(tǒng)的某個功能,而Java不支持這個功能,就需要用到JNI技術(shù)來實現(xiàn)
為了整合以前的非Java語言開發(fā)的系統(tǒng)。例如早期使用C/C++實現(xiàn)的功能或系統(tǒng),通過JNI可以直接整合到項目,而無需再使用Java語言重復(fù)造輪子
為了提升運行效率,節(jié)約程序運行時間。例如:游戲,音視頻編解碼、圖形繪制等,使用C/C++開發(fā)會運行更快

二、手動實現(xiàn)JNI demo(靜態(tài)注冊)

??Native方法注冊分為靜態(tài)注冊動態(tài)注冊,先對兩者進行簡要說明:

靜態(tài)注冊

原理根據(jù)函數(shù)名來建立 java 方法與 JNI 函數(shù)的一一對應(yīng)關(guān)系

實現(xiàn)流程
編寫 java 代碼;
利用 javah 指令生成對應(yīng)的 .h 文件;
對 .h 中的聲明進行實現(xiàn);

缺點
編寫不方便,JNI 方法名字必須遵循規(guī)則且名字很長;
編寫過程步驟多,不方便;
程序運行效率低,因為初次調(diào)用native函數(shù)時需要根據(jù)根據(jù)函數(shù)名在JNI層中搜索對應(yīng)的本地函數(shù),
然后建立對應(yīng)關(guān)系,這個過程比較耗時;

動態(tài)注冊

原理
利用 RegisterNatives 方法來注冊 java 方法與 JNI 函數(shù)的一一對應(yīng)關(guān)系;

實現(xiàn)流程
1、java中定義Native方法
2、編寫C/C++文件,實現(xiàn)jni接口方法,以及JNI_OnLoad方法
3、編譯so庫
4、java加載so庫,調(diào)用native方法

優(yōu)點:
靈活性高, 更改類名,包名或方法時, 只需對更改模塊進行少量修改, 效率高
缺點
對新手來說稍微有點難理解, 同時會由于搞錯簽名, 方法, 導(dǎo)致注冊失敗

下面分別對這兩種注冊方法進行講解。

??本篇我們先講一下如何靜態(tài)注冊Native,并實現(xiàn)我們的JNI demo

1、首先我們得創(chuàng)建一個Native方法吧,所以新建類JNITest.java,并創(chuàng)建Native方法getName

public class JNITest {
    static {
        System.loadLibrary("mgjnitest");
    }

    public static native String getName();
}

2、第二步,我們需要手動使用Javac 命令生產(chǎn).h頭文件:jni_com_cj_constom_jnitest_JNITest.h


C:\workspace\MyPracticeApp\jnitest\src\main\java\jni\com\cj\constom\jnitest>javac JNITest.java

C:\workspace\MyPracticeApp\jnitest\src\main\java\jni\com\cj\constom\jnitest>cd ../../../../../

C:\workspace\MyPracticeApp\jnitest\src\main\java>javah -jni jni.com.cj.constom.jnitest.JNITest

C:\workspace\MyPracticeApp\jnitest\src\main\java>


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

#ifndef _Included_jni_com_cj_constom_jnitest_JNITest
#define _Included_jni_com_cj_constom_jnitest_JNITest
#ifdef __cplusplus
extern "C" {
#endif
/*
 * Class:     jni_com_cj_constom_jnitest_JNITest
 * Method:    getName
 * Signature: ()Ljava/lang/String;
 */
JNIEXPORT jstring JNICALL Java_jni_com_cj_constom_jnitest_JNITest_getName
  (JNIEnv *, jclass);

#ifdef __cplusplus
}
#endif
#endif

3、根據(jù).h頭文件,創(chuàng)建對應(yīng)的.c文件jni_com_cj_constom_jnitest_JNITest.c,用于Native層的具體實現(xiàn)

#include <jni.h>
#include <string.h>
jstring Java_jni_com_cj_constom_jnitest_JNITest_getName(JNIEnv *env,jclass type){
 return (*env)->NewStringUTF(env, "Hello from JNI !");
}

備注:jstring Java_jni_com_cj_constom_jnitest_JNITest_getName(JNIEnv *env,jclass type)方法中,和java層的方法定義有兩點差異:
1、返回值不同于java中的String,而是jString,這個是jni所定義的數(shù)據(jù)類型別名,可以參考下方“java、C/C++,jni數(shù)據(jù)類型參照表”和“引用類型JNI參照表”
2、另外方法名稱由java層的getName變?yōu)榱薐ava_jni_com_cj_constom_jnitest_JNITest_getName,JAVA開頭指的是從JAVA層來調(diào)用JNI方法的,jni_com_cj_constom_jnitest_JNITest_getName則指的是 包名+類名+方法名

java、C/C++,jni數(shù)據(jù)類型參照表.png

引用類型jni參照表.png

4、將.h文件和.c文件存放到同一個目錄,這里我們建立一個jni目錄

image.png

5、在上一步創(chuàng)建的jni目錄中,創(chuàng)建NDK編譯規(guī)則Android.mk

LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
LOCAL_MODULE    := mgjnitest
LOCAL_SRC_FILES := jni_com_cj_constom_jnitest_JNITest.c
include $(BUILD_SHARED_LIBRARY)

Android.mk文件參數(shù)規(guī)則說明如下:

# 此變量表示源文件在開發(fā)樹中的位置
# 在這里,構(gòu)建系統(tǒng)提供的宏函數(shù) my-dir 將返回當(dāng)前目錄(包含 Android.mk 文件本身的目錄)的路徑
LOCAL_PATH := $(call my-dir)

# 清除除了LOCAL_PATH之外的所有LOCAL_XXX變量
# 這個清理動作是必須的,因為所有的編譯控制文件由同一個GNU Make解析和執(zhí)行,其變量是全局的。所以清理后才能避免相互影響
include $(CLEAR_VARS)

# 表示Android.mk中的每一個模塊,名字必須唯一且不包含空格
# 構(gòu)建系統(tǒng)在生成最終共享庫文件時,會將正確的前綴和后綴自動添加到您分配給 LOCAL_MODULE 的名稱
LOCAL_MODULE := hello-jni

# 此可選變量可讓您覆蓋構(gòu)建系統(tǒng)默認用于其生成的文件的名稱
# 例如,如果 LOCAL_MODULE 的名稱為 foo,您可以強制系統(tǒng)將它生成的文件命名為 libnewfoo
LOCAL_MODULE_FILENAME := libhello-jni

# 枚舉源文件,以空格分隔多個文件
# LOCAL_SRC_FILES 變量必須包含要構(gòu)建到模塊中的 C 和/或 C++ 源文件列表
LOCAL_SRC_FILES =: src/hello-jni.cpp \
                   src/hello-jnicallback.cpp

# 此變量用于存儲當(dāng)前模塊依賴的靜態(tài)庫模塊列表
# 如果當(dāng)前模塊是共享庫或可執(zhí)行文件,此變量將強制這些庫鏈接到生成的二進制文件
# 如果當(dāng)前模塊是靜態(tài)庫,此變量只是指示,依賴當(dāng)前模塊的模塊也會依賴列出的庫
LOCAL_STATIC_LIBRARIES := world-jni

# 此變量包含在構(gòu)建共享庫或可執(zhí)行文件時要使用的其他鏈接器標(biāo)志列表
# 它可讓您使用 -l 前綴傳遞特定系統(tǒng)庫的名稱
# 例如,以下示例 -lz 指示鏈接器生成在加載時鏈接到 /system/lib/libz.so 的模塊
LOCAL_LDLIBS := -llog -lz -lm -landroid

# 設(shè)置頭文件的include目錄
LOCAL_C_INCLUDES := $(LOCAL_PATH)//include

# 指向 GNU Makefile 腳本,用于收集您自最近 include 后在 LOCAL_XXX 變量中定義的所有信息
# 此腳本確定要構(gòu)建的內(nèi)容及其操作方法
# BUILD_STATIC_LIBRARY: 編譯為靜態(tài)庫,靜態(tài)庫變量導(dǎo)致構(gòu)建系統(tǒng)生成擴展名為 .a 的庫
# BUILD_SHARED_LIBRARY: 編譯為動態(tài)庫,共享庫變量導(dǎo)致構(gòu)建系統(tǒng)生成具有 .so 擴展名的庫文件
# BUILD_EXECUTABLE: 編譯為Native C可執(zhí)行程序
# BUILD_PREBUILT: 該模塊已經(jīng)預(yù)先編譯,指向預(yù)建共享庫的單一路徑,例如 foo/libfoo.so
include $(BUILD_SHARED_LIBRARY)

6、在項目根目錄創(chuàng)建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

Application.mk的參數(shù)規(guī)則如下:

# 此變量用于存儲應(yīng)用項目根目錄的絕對路徑
# 構(gòu)建系統(tǒng)使用此信息將生成的 JNI 共享庫的簡縮版放入 APK 生成工具已知的特定位置
# 如果將 Application.mk 文件放在 $NDK/apps/<myapp>/ 下,則必須定義此變量
# 如果將其放在 $PROJECT/jni/ 下,則此變量可選
# APP_PROJECT_PATH

# 將此可選變量定義為 release 或 debug,在構(gòu)建應(yīng)用的模塊時可使用它來更改優(yōu)化級別
# 發(fā)行模式是默認模式,可生成高度優(yōu)化的二進制文件。調(diào)試模式會生成未優(yōu)化的二進制文件,更容易調(diào)試
# 請注意,您可以調(diào)試發(fā)行或調(diào)試二進制文件。但發(fā)行二進制文件在調(diào)試時提供的信息較少
# 例如,構(gòu)建系統(tǒng)會選擇某些合適的變量,您無需檢查它們
# 此外,代碼重新排序可能增大單步調(diào)試代碼的難度;堆疊追蹤可能不可靠
# 在應(yīng)用清單的 <application> 標(biāo)記中聲明 android:debuggable 將導(dǎo)致此變量默認使用 debug而非 release
# 將 APP_OPTIM 設(shè)置為 release 可替換此默認值
# APP_OPTIM

# 指定機器指令集
# APP_ABI := all
APP_ABI := armeabi-v7a arm64-v8a

# 目標(biāo) Android 平臺的名稱
APP_PLATFORM := android-26

7、使用NDK工具對.c文件進行編譯,生成對應(yīng)的so庫

C:\workspace\MyPracticeApp\jnitest>cd jni

C:\workspace\MyPracticeApp\jnitest\jni>ndk-build
Android NDK: APP_PLATFORM not set. Defaulting to minimum supported version android-16.
[arm64-v8a] Compile        : mgjnitest <= jni_com_cj_constom_jnitest_JNITest.c
[arm64-v8a] SharedLibrary  : libmgjnitest.so
[arm64-v8a] Install        : libmgjnitest.so => libs/arm64-v8a/libmgjnitest.so
[armeabi-v7a] Compile thumb  : mgjnitest <= jni_com_cj_constom_jnitest_JNITest.c
[armeabi-v7a] SharedLibrary  : libmgjnitest.so
[armeabi-v7a] Install        : libmgjnitest.so => libs/armeabi-v7a/libmgjnitest.so
[x86] Compile        : mgjnitest <= jni_com_cj_constom_jnitest_JNITest.c
[x86] SharedLibrary  : libmgjnitest.so
[x86] Install        : libmgjnitest.so => libs/x86/libmgjnitest.so
[x86_64] Compile        : mgjnitest <= jni_com_cj_constom_jnitest_JNITest.c
[x86_64] SharedLibrary  : libmgjnitest.so
[x86_64] Install        : libmgjnitest.so => libs/x86_64/libmgjnitest.so

image.png

8、配置jnitest module中的build.gradle文件,對

android {
    compileSdkVersion 28



    defaultConfig {
        minSdkVersion 15
        targetSdkVersion 28
        versionCode 1
        versionName "1.0"

        testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
        ndk {//打包apk時所集成的so庫類型
            abiFilters "armeabi-v7a"
        }


    }

    externalNativeBuild{//配置ndk編譯路徑,自動化ndk編譯,從而無需第七步的手動ndk-build命令編譯
        ndkBuild{
            path "jni/Android.mk"
        }
    }

    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }

    sourceSets {//指定jnilib目錄路徑
        main {
            jni.srcDirs = []
            jniLibs.srcDirs = ['libs']
        }
    }


}

9、開始從Java層調(diào)用Native層的方法,回到我們最初創(chuàng)建的JNITest.java文件,我們在static初始化中加載Native so,然后我們就可以在Activity中調(diào)用getName方法了

public class JNITest {
    static {
        System.loadLibrary("mgjnitest");
    }

    public static native String getName();
}

MainActivity.java中的按鈕點擊事件,通過JNITest.getName()獲取到我們.c文件中返回的"Hello from JNI !"字符串

public void getJNIValue(View view) {
        String test = JNITest.getName();
        Toast.makeText(getApplicationContext(), "value=" + test, Toast.LENGTH_SHORT).show();
    }

運行效果圖如下,第一個JNI demo成功!
demo github地址為:
https://github.com/MRCHENJUN/JNItest/tree/master/MyPracticeApp

image.png

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

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

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