文章目錄
第九章 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)對圖片進行處理