一、簡介
先說JNI(Java Native Interface)吧,有過不同語言間通信經歷的一般都知道,它允許Java代碼和其他語言(尤其C/C++)寫的代碼進行交互,只要遵守調用約定即可。首先看下JNI調用C/C++的過程,注意寫程序時自下而上,調用時自上而下。
可 見步驟非常的多,很麻煩,使用JNI調用.dll/.so共享庫都能體會到這個痛苦的過程。如果已有一個編譯好的.dll/.so文件,如果使用JNI技 術調用,我們首先需要使用C語言另外寫一個.dll/.so共享庫,使用SUN規(guī)定的數據結構替代C語言的數據結構,調用已有的 dll/so中公布的函 數。然后再在Java中載入這個庫dll/so,最后編寫Java native函數作為鏈接庫中函數的代理。經過這些繁瑣的步驟才能在Java中調用 本地代碼。因此,很少有Java程序員愿意編寫調用dll/.so庫中原生函數的java程序。這也使Java語言在客戶端上乏善可陳,可以說JNI是 Java的一大弱點!
那么JNA是什么呢?
JNA(Java Native Access)框架是一個開源的Java框架,是SUN公司主導開發(fā)的,建立在經典的JNI的基礎之上的一個框架。使用JNI調用共享類庫(.dll/.so文件)是非常麻煩的事情,既需要編寫java代碼,又要編寫C語言的代理方法,這其中需要很多數據類型的轉換,是讓人非常頭痛。JNA框架就是為了解決這些問題和繁瑣的事情而開發(fā)的,它提供一組Java工具類用于在運行期動態(tài)訪問系統本地共享類庫而不需要編寫任何Native/JNI代碼。開發(fā)人員只要在一個java接口中描述目標native library的函數與結構,JNA將自動實現Java接口到native function的映射,大大降低了Java調用本體共享庫的開發(fā)難度。JNA與.NET平臺上的P/Invoke機制一樣簡單和方便。
之所以說它是JNI的替 代者,是因為JNA大大簡化了調用本地方法的過程,使用很方便,基本上不需要脫離Java環(huán)境就可以完成。
如果要和上圖做個比較,那么JNA調用C/C++的過程大致如下:
可以看到步驟減少了很多,最重要的是我們不需要重寫我們的動態(tài)鏈接庫文件,而是有直接調用的API,大大簡化了我們的工作量。
JNA只需要我們寫Java代碼而不用寫JNI或本地代碼。功能相對于Windows的Platform/Invoke和Python的ctypes。
二、原理
JNA使用一個小型的JNI庫插樁程序來動態(tài)調用本地代碼。開發(fā)者使用Java接口描述目標本地庫的功能和結構,這使得它很容易利用本機平臺的功能,而不會產生多平臺配置和生成JNI代碼的高開銷。這樣的性能、準確性和易用性顯然受到很大的重視。
此外,JNA包括一個已與許多本地函數映射的平臺庫,以及一組簡化本地訪問的公用接口。
注意:
JNA是建立在JNI技術基礎之上的一個Java類庫,它使您可以方便地使用java直接訪問動態(tài)鏈接庫中的函數。
原來使用JNI,你必須手工用C寫一個動態(tài)鏈接庫,在C語言中映射Java的數據類型。
JNA中,它提供了一個動態(tài)的C語言編寫的轉發(fā)器,可以自動實現Java和C的數據類型映射,你不再需要編寫C動態(tài)鏈接庫。
也許這也意味著,使用JNA技術比使用JNI技術調用動態(tài)鏈接庫會有些微的性能損失。但總體影響不大,因為JNA也避免了JNI的一些平臺配置的開銷。
三、相關jna.jar包和.so庫文件的下載
JNA的項目是放在Github上面的,目前最新版本是4.4.0,已有打包好的jar文件可供下載。
我這里使用的是4.2.1版本的,并提供了可以直接使用的jar包和.so庫文件,JNA開發(fā)的jna.jar以及.so庫文件,
四、配置環(huán)境,編譯sayhello.so庫文件
1、在app下面建立一個jni文件夾,添加庫函數文件sayhello.c:
#include "sayhello.h"
int sayHello(){
printf("Hello World!");
return 1;
}
以及頭文件sayhello.h:
#include <stdio.h>
int sayHello();
2、其他就是配置我們編譯C/C++代碼的環(huán)境了,如果不懂可以參考這篇文章:NDK學習筆記<一> 初步認識JNI|成功搭建NDK開發(fā)環(huán)境
3、成功配置我們的C/C++編譯環(huán)境后,在lib目錄下面成功編譯出我們需要的.so文件。然后把jna.jar和相關的.so文件添加到項目中
4、接著我們在src目錄下面添加jniLibs文件夾:把JNA的libjnidispatch.so.so庫文件和我們生成的libsayhello.so庫文件全部添加合并到jniLibs
文件夾下面,并配置我們的build.gradle文件,添加jniLibs.srcDirs = ['src/main/jniLibs']:
android {
compileSdkVersion 25
buildToolsVersion "26.0.1"
defaultConfig {
applicationId "com.afinalstone.androidstudy.myjna_01"
minSdkVersion 16
targetSdkVersion 25
versionCode 1
versionName "1.0"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
sourceSets {
main {
jniLibs.srcDirs = ['src/main/jniLibs']
}
}
}
dependencies {
compile fileTree(include: ['*.jar'], dir: 'libs')
compile 'com.android.support:appcompat-v7:25.3.1'
compile 'com.android.support.constraint:constraint-layout:1.0.2'
}
五、創(chuàng)建Library,成功實現Java調用C/C++函數代碼庫
創(chuàng)建一個Clibrary對象:
package com.afinalstone.androidstudy.myjna_01;
import com.sun.jna.Library;
import com.sun.jna.Native;
import com.sun.jna.Pointer;
import com.sun.jna.ptr.IntByReference;
import com.sun.jna.ptr.PointerByReference;
//繼承Library,用于加載庫文件
public interface Clibrary extends Library {
//加載libhello.so鏈接庫
Clibrary INSTANTCE = (Clibrary) Native.loadLibrary("sayhello", Clibrary.class);
//此方法為鏈接庫中的方法
int sayHello();
}
1、定義接口對象。
在上面的代碼中,我們定義了一個接口,繼承自Library 或StdCallLibrary,默認的是繼承Library ,
如果動態(tài)鏈接庫里的函數是以stdcall方式輸出的,那么就繼承StdCallLibrary,比如眾所周知的kernel32庫。
2、接口內部定義
接口內部需要一個公共靜態(tài)常量:INSTANCE,通過這個常量,就可以獲得這個接口的實例,從而使用接口的方法,也就是調用外部dll/so的函數。
該常量通過Native.loadLibrary()這個API函數獲得,該函數有2個參數:
第一個參數是動態(tài)鏈接庫dll/so的名稱,但不帶.dll或.so這樣的后綴,這符合JNI的規(guī)范,因為帶了后綴名就不可以跨操作系統平臺了。搜索動態(tài)鏈 接庫路徑的順序是:先從當前類的當前文件夾找,如果沒有找到,再在工程當前文件夾下面找win32/win64文件夾,找到后搜索對應的dll文件,如果 找不到再到WINDOWS下面去搜索,再找不到就會拋異常了。比如上例中printf函數在Windows平臺下所在的dll庫名稱是msvcrt,而在 其它平臺如Linux下的so庫名稱是c。
第二個參數是本接口的Class類型。JNA通過這個Class類型,根據指定的.dll/.so文件,動態(tài)創(chuàng)建接口的實例。該實例由JNA通過反射自動生成。
然后在MainActivity中調用sayHello方法:
接口中只需要定義你要用到的函數或者公共變量,不需要的可以不定義,注意參數和返回值的類型,應該和鏈接庫中的函數類型保持一致。
定義好接口后,就可以使用接口中的函數即相應dll/so中的函數了
六、調用C++函數代碼
定義好接口后,就可以使用接口中的函數即相應函數庫中的函數了,這里我們在MainActivity中響應點擊事件調用C++函數代碼,并打印出結果:
package com.afinalstone.androidstudy.myjna_01;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.util.Log;
import android.view.View;
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
//按鈕點擊事件
public void onClick(View view){
int result = Clibrary.INSTANTCE.sayHello();
Log.d("MainActivity","sayHello的放回結果:"+result);
}
}
到這里位置,如果成功運行項目,整個項目結構以及結果基本是這樣的:
七、避免錯誤的建議:
1.要注意我們生成的庫文件都是lib**.so,但是我們在加載.so庫文件的時候是不需要lib前綴的。
2.要檢查build.gradle中是否指定了.so庫文件的地址,在這里我之所以吧所有的.so庫文件都放在了jniLibs文件夾中,
是因為我在build.gradle中指定了 jniLibs.srcDirs = ['src/main/jniLibs']
3.可以解壓成功編譯的apk,查看該apk的lib目錄下面是否有l(wèi)ibsayhello.so和libjnidispatch.so兩個庫文件。
4.項目中偶時候需要用到Java調用c++函數代碼,但是始終出錯,主要錯誤原因是undefined symbol,找不到c++ 方法。
需要我們使用extern "C" 給C++代碼做標記,否則無法找到。
#include <stdlib.h>
#include <iostream>
using namespace std;
extern "C"
{
void test() {
cout << "TEST" << endl;
}
int addTest(int a,int b)
{
int c = a + b ;
return c ;
}
}
項目地址:傳送門