JNI技術概述
JNI是Java Native Interface的縮寫,通過使用 Java本地接口書寫程序,可以確保代碼在不同的平臺上方便移植。它允許Java代碼和其他語言寫的代碼進行交互。
其實就是java調用本地語言的技術,對于Linux和Windows而言本地語言自然是C和C++。換句話說就是java如何調用C或C++代碼,而你要把C或C++代碼給他人調用自然要編寫成動態鏈接庫了,在Windows是dll,在Linux是so。(這里我們討論Android上的so庫編寫和調用)
這個技術當初在用友工作時就接觸到了,由于當時技術的局限性,還被卡了一個星期。導致同事在外地待了一個星期。
開發環境
Eclipse+adt
android-ndk-r20(ndk的開發基礎在上一章介紹:http://www.lxweimin.com/p/c546783ad284)
靜態注冊
要對java中native關鍵字定義的方法進行注冊, 注冊方式有兩種: 靜態注冊和動態注冊, 這里我們先介紹靜態注冊
Android代碼
我們在Android中先寫一個簡單的點擊事件,并在static中加載so庫,在按鈕點擊時調用so庫中的函數
package com.example.jnitest;
import android.app.Activity;
import android.os.Bundle;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.widget.Toast;
public class MainActivity extends Activity {
static {
//加載libhello_jni.so(默認去項目目錄下的libs下查找)
System.loadLibrary("hello_jni");
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
// 測試JNI
public void testJni(View view) {
String jniString = HelloNdk();
Toast.makeText(MainActivity.this, jniString, Toast.LENGTH_LONG).show();
}
//定義一個native方法 native關鍵字說明方法的實現在so里面
public native String HelloNdk();
}
界面如下:
SO庫編寫
靜態注冊 在c代碼中我們的函數名要遵守一定的約定,不然jni無法識別函數,約定如下:
- (1)函數名:JNIEXPORT + 返回類型 + JNICALL Java_+包名 +類型 + 函數名(java 中聲明的),以下劃線連接
- (2)返回值類型,是 jni 中的數據類型,若沒有返回類型,則使用void
- (3)默認傳入兩個參數 JNIEnv* env(jvm運行環境), jobject obj(調用這個函數的Java對象)
例如:
java中函數聲明:public native String HelloNdk();
c中函數的實現:JNIEXPORT jstring JNICALL Java_com_example_jnitest_MainActivity_HelloNdk (JNIEnv * env, jobject obj)
這里你可以手動編寫,但是java提供了javah來自動從java代碼生成對應的頭文件。
- 使用cmd進入項目的根目錄,在項目的根目錄創建jni目錄
javah -classpath src -d jni com.example.jnitest.MainActivity
- classpath 表示類文件路徑
- src表示java代碼的文件夾
- d表示生成頭文件所在路徑
- jni 表示 jni 文件夾下
- com.example.jnitest.MainActivity,表示需要編譯的類文件,也就是包名+類名(也就是包含 native 方法的類文件)
生成文件內容:com_example_jnitest_MainActivity.h
/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class com_example_jnitest_MainActivity */
#ifndef _Included_com_example_jnitest_MainActivity
#define _Included_com_example_jnitest_MainActivity
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class: com_example_jnitest_MainActivity
* Method: HelloNdk
* Signature: ()Ljava/lang/String;
*/
JNIEXPORT jstring JNICALL Java_com_example_jnitest_MainActivity_HelloNdk
(JNIEnv *, jobject);
#ifdef __cplusplus
}
#endif
#endif
這里我們只做c的編寫所以去除c++的一些東西,在jni目錄下創建hello_jni.c,并拷貝代碼修改為以下:
#include <jni.h>
#include <string.h>
/* Header for class com_example_jnitest_MainActivity */
/*
* Class: com_example_jnitest_MainActivity
* Method: HelloNdk
* Signature: ()Ljava/lang/String;
*/
JNIEXPORT jstring JNICALL Java_com_example_jnitest_MainActivity_HelloNdk
(JNIEnv * env, jobject obj){
return (*env)->NewStringUTF(env, "Hello JNI!");
}
在jni.h中我們看到如下定義(在%java_home%\include\jni.h):
#include "jni_md.h"
typedef struct _jobject *jobject;
typedef jobject jclass;
typedef jobject jthrowable;
typedef jobject jstring;
typedef jobject jarray;
typedef jarray jbooleanArray;
typedef jarray jbyteArray;
typedef jarray jcharArray;
typedef jarray jshortArray;
typedef jarray jintArray;
typedef jarray jlongArray;
typedef jarray jfloatArray;
typedef jarray jdoubleArray;
typedef jarray jobjectArray;
再進入jni_md.h中看看(在%java_home%\include\win32\jni_md.h):
#define JNIEXPORT __declspec(dllexport)
#define JNIIMPORT __declspec(dllimport)
#define JNICALL __stdcall
所以我們直接就不用解釋JNIEXPORT 和 JNICALL 了
現在來討論一下jstring 返回值。
java和c是如何互通的?
其實不能互通的原因主要是數據類型的問題,jni解決了這個問題,例如那個c文件中的jstring數據類型就是java傳入的String對象,經過jni函數的轉化就能成為c的char。
對應數據類型關系如下表:
這里我們使用(env)->NewStringUTF(env, "Hello JNI!");生成了一個jstring,并返回它。
使用ndk編譯so庫
- 在jni目錄編寫實現函數對應的Android.mk(該文件的描述在上一章中)
LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
LOCAL_MODULE := hello_jni
LOCAL_SRC_FILES := hello_jni.c
include $(BUILD_SHARED_LIBRARY)
返回到項目根目錄運行ndk
ndk-build
在項目根目錄下會生成libs文件夾里面有所有平臺的so庫(這里你也可以指定平臺)
由于我的虛擬機是X86的所有它使用的是x86文件夾下的so
運行
現在所有準備工作做完我們部署項目到手機上,點擊按鈕后如下:
尾言
這里我們只對JNI的類型對應做了一些基本的說明,由于此文的專注點是如何靜態注冊jni函數所以這里不做過多的描述,將來我將用一文詳細說明jni的一些技術細節。下一章我們將對動態注冊進行說明。