JNI 與 NDK 入門(一)

這是我參與8月更文挑戰的第7天,活動詳情查看:8月更文挑戰java

JNI

概念

JNI是Java Native Interface的簡寫,它可使Java與其餘語言(如C、C++)進行交互。android

它是Java調用Native語言的一種特性,屬於Java語言的範疇,與Android無關。程序員

爲什麼須要JNI

  • Java的源文件很是容易被反編譯,而經過Native語言生成的.so庫文件則不容易被反編譯。
  • 有時咱們使用Java時須要使用到一些庫來實現功能,但這些庫僅僅提供了一些Native語言的接口。
  • 使用Native語言編寫的代碼運行效率高,尤爲體如今音頻視頻圖片的處理等須要大量複雜運算的操做上。充分利用了硬件的性能。

因爲上述緣由,此時咱們就須要讓Java與Native語言交互。而因爲Java的特色,與Native語言的交互能力很弱。所以在此時,咱們就須要用到JNI特性加強Java與Native方法的交互能力。面試

實現的步驟

  1. 在Java中聲明Native方法(須要調用的本地方法)
  2. 經過 javac 編譯 Java源文件( 生成.class文件)
  3. 經過 javah 命令生成JNI頭文件(生成.h文件)
  4. 經過Native語言實如今Java源碼中聲明的Native方法
  5. 編譯成.so庫文件
  6. 經過Java命令執行 Java程序,最終實現Java調用本地代碼(藉助so庫文件)

NDK

概念

Native是Native Development Kit的簡寫,是Android的開發工具包,屬於Android,與Java無關係。數組

它能夠快速開發C/C++的動態庫,自動將.so和應用一塊兒打包爲APK。所以咱們能夠經過NDK來在Android開發中經過JNI與Native方法交互。markdown

使用方式

  1. 配置 Android NDK環境(在SDK Manager中下載NDK、CMake、LLDB)
  2. 建立 Android 項目,與 NDK進行關聯(建立項目時選擇C++ support)
  3. 在 Android 項目中聲明所須要調用的 Native方法
  4. 用Native語言實如今Android中聲明的Native方法
  5. 經過 ndk-bulid 命令編譯產生.so庫文件

將Android項目與NDK關聯

配置NDK路徑

local.properties中加入以下一行便可app

ndk.dir=<ndk路徑>函數

添加配置

在Gradle的 gradle.properties中加入以下一行,目的是對舊版本的NDK支持工具

android.useDeprecatedNdk=trueoop

添加ndk節點

在build.gradle中的defaultConfigandroid中加入以下的externalNativeBuild節點

apply plugin: 'com.android.application'



android {

    compileSdkVersion 27

    defaultConfig {

        applicationId "com.n0texpecterr0r.ndkdemo"

        minSdkVersion 19

        targetSdkVersion 27

        versionCode 1

        versionName "1.0"

        testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"

        externalNativeBuild {

            cmake {

                cppFlags ""

            }

        }

    }

    buildTypes {

        release {

            minifyEnabled false

            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'

        }

    }

    externalNativeBuild {

        cmake {

            path "CMakeLists.txt"

        }

    }

}



dependencies {

    implementation fileTree(dir: 'libs', include: ['*.jar'])

    implementation 'com.android.support:appcompat-v7:27.1.1'

    implementation 'com.android.support.constraint:constraint-layout:1.1.3'

    testImplementation 'junit:junit:4.12'

    androidTestImplementation 'com.android.support.test<span class="emoji emoji-sizer" style="background-image:url(/emoji-data/img-apple-64/1f3c3.png)" data-codepoints="1f3c3"></span>1.0.2'

    androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'

}
複製代碼

開發Native代碼

在Java文件中聲明native方法

咱們首先須要在Java代碼的類中經過static塊來加載咱們的Native庫。能夠經過以下代碼,其中loadLibrary的參數是在CMakeList.txt中定義的Native庫的名稱

static {

    System.loadLibrary("native-lib");

}
複製代碼

以後,咱們即可以在這個類中聲明Native方法

public native String getStringFromJNI();
複製代碼

建立CMakeList.txt

咱們還須要在src中建立一個CMakeList.txt文件,這個文件約束了Native語言源文件的編譯規則。好比下面

cmake_minimum_required(VERSION 3.4.1)



add_library(native-lib SHARED src/main/cpp/native-lib.cpp)



find_library(log-lib log)



target_link_libraries(native-lib ${log-lib})
複製代碼

add_library方法中定義了一個so庫,它的名稱是native-lib,也就是咱們在Java文件中用到的字符串,然後面則跟着這個庫對應的Native文件的路徑

find_library則是定義了一個路徑變量,通過了這個方法,log-lib這個變量中的值就是Android中log庫的路徑

target_link_libraries則是將native-lib這個庫和log庫鏈接了起來,這樣咱們就能在native-lib中使用log庫的方法。

建立Native方法文件

在前面的CMake文件中能夠看到,咱們把文件放在了src/main/cpp/,所以咱們建立cpp這個目錄,在裏面建立C++源文件native-lib.cpp。

而後, 咱們即可以開始編寫以下的代碼:

#include <jni.h>

#include <string>



extern "C"{

  JNIEXPORT jstring JNICALL

  Java_com_n0texpecterr0r_ndkdemo_MainActivity_getStringFromJNI(

      JNIEnv* env,

      jobject) {

    std::string hello = "IG牛逼";

    return env->NewStringUTF(hello.c_str());

  }

}
複製代碼

此處咱們使用的是C++語言,讓咱們來看看具體的代碼。

首先咱們引入了jni須要的jni.h,這個頭文件中聲明瞭各個jni須要用到的函數。同時咱們引入了C++中的string.h。

而後咱們看到extern "C"。爲了瞭解這裏爲何使用了extern "C",咱們首先須要知道下面的知識:

在C中,編譯時的函數簽名僅僅是包含了函數的名稱,所以不一樣參數的函數都是一樣的簽名。這也就是爲何C不支持重載。

而C++爲了支持重載,在編譯的時候函數的簽名除了包含函數的名稱,還攜帶了函數的參數及返回類型等等。

試想此時咱們有個C的函數庫要給C++調用,會由於簽名的不一樣而找不到對應的函數。所以,咱們須要使用extern "C"來告訴編譯器使用編譯C的方式來鏈接。

接下來咱們看看JNIEXPORT和JNICALL關鍵字,這兩個關鍵字是兩個宏定義,他主要的做用就是說明該函數爲JNI函數。

而jstring則對應了Java中的String類,JNI中有不少相似jstring的類來對應Java中的類,下面是Java中的類與JNI類型的對照表

咱們繼續看到函數名Java_com_n0texpecterr0r_ndkdemo_MainActivity_getStringFromJNI。其實函數名中的_至關於Java中的 . 也就是這個函數名錶明瞭java.com.n0texpecterr0r.ndkdemo.MainActivity.java中的getStringFromJNI方法,也就是咱們以前定義的native方法。

格式大概以下:

Java_包名_類名_須要調用的方法名

其中,Java必須大寫,包名裏的.要改爲__要改爲_1

接下來咱們看到這個函數的兩個參數:

  • JNIEnv* env:表明了JVM的環境,Native方法能夠經過這個指針來調用Java代碼
  • jobject obj:它就至關於定義了這個JNI方法的類 (MainActivity) 的this引用

而後能夠看到後面咱們建立了一個string hello,以後經過env->NewStringUTF(hello.c_str())方法建立了一個jstring類型的變量並返回。

在Java代碼中調用native方法

接着,咱們即可以在MainActivty中像調用Java方法同樣調用這個native方法

TextView tv = findViewById(R.id.sample_text);

tv.setText(getStringFromJNI());
複製代碼

咱們嘗試運行,能夠看到,咱們成功用C++構建了一個字符串並返回給Java調用:

image.png

CMake

咱們在NDK開發中使用CMake的語法來編寫簡單的代碼描述編譯的過程,因爲這篇文章是講NDK的,因此關於CMake的語法就再也不贅述了。。。若是想要了解CMake語法能夠學習這本書《CMake Practice

JNI與Java代碼交互

方法簽名

概念

在咱們JNI層調用一個方法時,須要傳遞一個參數——方法簽名。

爲何要使用方法簽名呢?由於在Java中的方法是能夠重載的,兩個方法可能名稱相同而參數不一樣。爲了區分調用的方法,就引入了方法簽名的概念。

簽名規則

對於基本類型的參數,每一個類型對應了一個不一樣的字母:

  • boolean Z
  • byte B
  • char C
  • short S
  • int I
  • long J
  • float F
  • double D
  • void V

對於類,則使用 L+類名 的方式,其中(.)用(/)代替,最後加上分號

好比 java.lang.String就是 Ljava/lang/String;

對於數組,則在前面加 [ ,而後加類型的簽名,幾維數組就加幾個。

好比 int[]對應的就是[I , boolean[][]對應的則是[[Z,而java.lang.String[]就是[Ljava/lang/String;

打印方法簽名

咱們能夠經過 javap -s 命令來打印方法的簽名。

例子

好比下面的方法

public native String getMessage();



public native String getMessage(String id,long i);
複製代碼

對應的方法簽名分別爲:

()Ljava/lang/String;

(Ljava/long/String;J)Ljava/lang/String;
複製代碼

能夠看到,前面括號中表示的是方法的參數列表,後面表示的則是返回值。

今天就先肝這麼多,明天再說

公衆號:程序員喵大人(專一於Android各種學習筆記、面試題以及IT類資訊的分享。)

相關文章
相關標籤/搜索