Android JNI的使用方法


1、JNI是什么

JNI是Java Native Interface的縮寫,它提供若干的API實現Java與其他語言之間的通信。而Android Framework由基於Java語言的的Java層與基於C/C++語言的C/C++層組成,每個層中的功能模塊都是以有相應的語言編寫,並且兩層中的大部分模塊有着千絲萬縷的聯系。而在兩層之間充當連接橋梁這一角色的就是JNI,它允許Java代碼和C/C++編寫的應用程序與庫之間進行交互;通常在以下幾種情況下使用JNI

1、注重處理速度,C/C++的處理速度要優於Java語言

2、硬件控制,硬件驅動程序通常使用C語言編寫,而要是Java層能夠控制硬件,需要用到JNI

3、C/C++代碼的復用,一些好的C/C++模塊可以被多處復用

2、在Java中調用C庫函數

下面以一個例子來說明在Java代碼中調用C庫函數的流程

1、編寫Java代碼

class HelloJNI {
    
    /*聲明本地方法,該函數在C庫中實現*/
    native void printHello();
    native void printString(String str);
    /*在靜態塊中加載C庫,可以保證在main方法前加載完成*/
    static { System.loadLibrary("./hellojni"); }
    public static void main(String args[])
    {   
        HelloJNI myJNI = new HelloJNI();    
        /*調用C庫中實現的函數*/
        myJNI.printHello();
        myJNI.printString("Hello world from printstring func");
    }   
}

在上述代碼中使用native關鍵字聲明本地方法,告訴Java編譯器,此函數由其他語言編寫;在靜態塊中加載hellojni庫,該庫由C語言實現,

如果是在Linux系統下則會加載libhellojni.so,如果在Windows系統下則會加載hellojni.dll;(本文以Linux系統為測試環境)

2、編譯Java代碼

javac HelloJNI.java

編譯Java代碼很簡單,只要配置好JDK就可以完成編譯,需要注意的是此時編譯通過,但如果運行的話,由於沒有實現本地函數,所以會拋出找不到函數的異常

3、生成C頭文件

當Java調用本地函數printHello或者printString時並非直接映射到C語言的printHello或者printString函數,而是有一套自己的映射方法,使用如下命令即可生成C函數的頭文件
javap HelloJni
執行完成后生成HelloJni.h如下
/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class HelloJNI */
#ifndef _Included_HelloJNI
#define _Included_HelloJNI
#ifdef __cplusplus
extern "C" {
#endif
/*
 * Class:     HelloJNI
 * Method:    printHello
 * Signature: ()V
 */
JNIEXPORT void JNICALL Java_HelloJNI_printHello(JNIEnv *, jobject);
/*
 * Class:     HelloJNI
 * Method:    printString
 * Signature: (Ljava/lang/String;)V
 */
JNIEXPORT void JNICALL Java_HelloJNI_printString(JNIEnv *, jobject, jstring);
#ifdef __cplusplus
}
#endif
#endif
可以看到生成的函數原型並非與Java代碼調用的函數一致,函數有JNIEXPORT和JNICALL兩個關鍵字聲明,這兩個關鍵詞是必須的,有了他們JNI才能正常調用函數;而通過觀察函數名稱,我們可以知道其命名方式是"Java_類名_本地方法名"; 再看參數,可知JNIEnv*和jobject是本地函數的共同參數,第一個參數是JNI接口的直接,用來調用JNI提供的基本函數集;第二個參數中保存着調用本地方法的對象的一個引用,上例中的jobject中保存的對象myJNI的引用,其他的參數根據Java代碼的本地方法的調用生成的

4、編寫C/C++代碼
把上一步驟生成的HelloJni.h頭文件include進來,實現其聲明的函數即可,編寫hellojni.c如下

#include "HelloJNI.h"
#include <stdio.h>
/*
 * Class:     HelloJNI
 * Method:    printHello
 * Signature: ()V
 */
JNIEXPORT void JNICALL Java_HelloJNI_printHello(JNIEnv *env, jobject obj)
{
    printf("Hello World!\n");
    return;
}
/*
 * Class:     HelloJNI
 * Method:    printString
 * Signature: (Ljava/lang/String;)V
 */
JNIEXPORT void JNICALL Java_HelloJNI_printString(JNIEnv *env, jobject obj, jstring string)
{
    /*JNI提供的基本函數集,將jstring轉化成char *類型*/
    const char *str = (*env)->GetStringUTFChars(env, string, 0);
    printf("%s! \n" ,str);
    return ;
}
 

5、生成C動態鏈接庫

gcc -fPIC -shared -o libhellojni.so hellojni.c -I$JAVA_HOME/include

其中JAVA_HOME已經配置到環境變量中,表示JDK安裝的目錄,需要指定其中的include目錄使用jni.h頭文件

6、運行Java程序

此時執行java HelloJni會提供找不到hellojni庫,這是由於在加載C庫的時候在默認目錄中沒有找到libhellojni.so庫,只需將該庫復制到/usr/lib/下再次執行

xlzh@cmos:~/code/jni/simpleJNI$ java HelloJNI 
Hello World!
Hello world from printstring func!

3、調用JNI函數

上圖來自<Android框架揭秘>

由上圖可知此示例程序有JniFuncMain類、JniTest類和libjnifunc.so(linux系統)組成,此示例有Java和C代碼混合而成。

JniFuncMain類:

public class JniFuncMain
{
    private static int staticIntField = 300;
    /*加載libjnifunc.so庫*/
    static { System.loadLibrary("jnifunc"); } 
    /*使用static關鍵字聲明本地方法,再C庫中實現*/
    public static native JniTest createJniObject();
    public static void main(String[] args) {
        System.out.println("[Java] createJniObject() call native method");    
        /*調用C庫的createJniObject,得到JniTest對象,注意不是用new*/
        JniTest jniObj = createJniObject();
        /*利用JniTest對象調用JniTest中的方法*/
        jniObj.callTest();
    }
}

此例中與上例不同的是本地方法返回了一個JniTest類的對象的引用,這樣就可以在JniFuncMain類中調用JniTest類的方法。

JniTest類

class JniTest {
    private int intField;
    public JniTest(int num)
    {   
        intField = num; 
        System.out.println("[Java] call JniTest: intFiled" + intField);
    }   
    public int callByNative(int num)
    {   
        System.out.println("[Java] JniTest 對象的 callByNative(" + num + ")調用");  
        return num;
    }   
    public void callTest()
    {   
        System.out.println("[Java] JniTest對象的callTest() 方法調用: intField = " + intField);  
    }   
}

JniTest類提供兩個方法供JniFuncMain類和C庫函數調用

JniFuncMain.h

/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class JniFuncMain */
#ifndef _Included_JniFuncMain
#define _Included_JniFuncMain
#ifdef __cplusplus
extern "C" {
#endif
/*
 * Class:     JniFuncMain
 * Method:    createJniObject
 * Signature: ()LJniTest;
 */
JNIEXPORT jobject JNICALL Java_JniFuncMain_createJniObject
  (JNIEnv *, jclass);
#ifdef __cplusplus
}
#endif
#endif

使用javah JniFuncMain生成頭文件,需要注意的是第二個參數是jclass,而不是jobject,這是由於該本地方法在JniFuncMain類中聲明的是static方法,所以第二個參數表示的該類的應用,而不需要對象的引用

jnifunc.cpp

#include "JniFuncMain.h"
JNIEXPORT jobject JNICALL Java_JniFuncMain_createJniObject(JNIEnv *env, jclass clazz)
{
    jclass targetClass;
    jmethodID mid;
    jobject newObject;
    jstring hellostr;
    jfieldID fid;
    jint staticIntField;
    jint result;
    /*獲取JniFuncMain類的staticField變量值*/
    fid = env->GetStaticFieldID(clazz, "staticIntField", "I");
    staticIntField = env->GetStaticIntField(clazz, fid);
    printf("[CPP] 獲取 JniFuncMain類的staticIntField 值\n");
    printf("    JniFuncMain.staticIntField = %d\n", staticIntField);
    /*查找生成對象的類*/
    targetClass = env->FindClass("JniTest");
    /*查找構造方法*/
    mid = env->GetMethodID(targetClass, "<init>", "(I)V");
    /*生成JniTest對象*/
    printf("[CPP] JniTest 對象生成 \n");
    newObject = env->NewObject(targetClass, mid, 100);
    /*調用對象的方法*/
    mid = env->GetMethodID(targetClass, "callByNative", "(I)I");
    result = env->CallIntMethod(newObject, mid, 200);
    /*設置JniObject對象的intField值*/
    fid = env->GetFieldID(targetClass, "intField", "I");
    printf("[CPP] 設置JniTest對象的intField值為200\n");
    env->SetIntField(newObject, fid, result);
    /*返回對象引用*/
    return newObject;
}
如果想在C代碼中訪問Java中的成員變量,就需要獲取相應成員變量的ID值,成員變量的ID值保存在jfieldID類型的變量中;獲取成員變量ID的JNI本地方法有兩個,分別是

/* 獲取Java中的靜態成員變量ID
 * env      : JNI接口指針
 * clazz    : 包含成員變量的類的jclass
 * name     : 成員變量名稱
 * signature: 成員變量簽名
 */
jfield GetStaticFieldID(JNIEnv *env, jclass clazz, const char *name, const char *signature);
/*獲取Java中的普通成員變量ID*/
jfield GetFieldID(JNIEnv *env, jclass clazz, const char *name, const char *signature);
上述示例中要訪問Java類中的靜態成員變量,所以需要使用GetStaticFieldID方法,其他參數很簡單,直接使用即可,而對於變量的簽名,則需要借助Java反編譯器javap命令,如下所示

xlzh@cmos:~/code/jni/middleJNI$ javap -s -p JniFuncMain
Compiled from "JniFuncMain.java"
public class JniFuncMain {
  private static int staticIntField;
    Signature: I
  public JniFuncMain();
    Signature: ()V
  public static native JniTest createJniObject();
    Signature: ()LJniTest;
  public static void main(java.lang.String[]);
    Signature: ([Ljava/lang/String;)V
  static {};
    Signature: ()V
}

可以看到,staticIntField的簽名是I,將I傳入第四個參數即可,其他函數中用到簽名的時候可用同樣的方法獲取

OK,我們得到了成員變量的ID,那么如何通過成員變量的ID來獲取或者設置成員變量的值呢?就需要用到以下幾個JNI函數

/*
 * 獲取Java類中靜態成員變量的值
 * <jnitype> jobject,jboolean,jbyte,jchar, jshort, jint, jlong, jfloat, jdouble
 * <type>    Object ,Boolean ,Byte,Char  , Short , Int,  Long , Float , Double
 * env:      JNI接口指針
 * jcalss:   包含成員變量的類
 * jfieldID: 成員變量ID
 */
<jnitype> GetStatic<type>Field(JNIEnv *env, jcalss jclazz, jfieldID fieldID)
/*
 * 獲取Java類的對象中普通成員變量的值
 */
<jnitype> Get<type>Field(JNIEnv *env, jobject obj, jfieldID fieldID)
/*
 * 設置Java類中靜態成員變量的值
 */
<jnitype> SetStatic<type>Field(JNIEnv *env, jcalss clazz, jfieldID fieldID, <type> value)
/*
 * 設置Java類的對象中普通成員變量的值
 */
<jnitype> Set<type>Field(JNIEnv *env, jobject obj,  jfieldID fieldID, <type> value)
 
與成員變量類似, 獲取和調用類中方法的JNI函數原型如下
/*獲取Java類靜態方法的ID*/
jmethod GetStaticMethodID(JNIEnv *env, jclass clazz, const char *name, const char *signature)
/*獲取Java類的對象中普通方法的ID*/
jmethod GetMethodID(JNIEnv *env, jclass clazz, const char *name, const char *signature)
/*調用Java類中的靜態方法*/
 <jnitype> CallStatic<type>Method(JNIEnv *env, jclass clazz, jmethod methodID, ...)
/*調用Java類的對象中的普通方法*/
 <jnitype> Call<type>Method(JNIEnv *env, jobject obj, jmethod methodID, ...)
如何獲取Java類的對象呢?以示例中獲取JniTest類的對象代碼例,分為三步
1、獲取JniTest的類
2、獲取JniTest類的構造方法ID
3、通過構造方法的ID調用和JniTest類使用NeoObject方法生成對象
對比上例中獲取JniTest的對象流程,可以很清楚的進行對照

4、在C代碼中運行Java類

Java類編譯的字節碼需要在Java虛擬機上運行,那么在C/C++中運行Java類自然也需要加載Java虛擬機;JNI為我們提供了一套Invocation API,它允許本地代碼在自身內存區域內加載Java虛擬機,同樣我們以實例的方式進行講解

InvocationApiTest.java

public class InvocationApiTest {
    public static void main(String[] args) {
        System.out.println(args[]0);
    }
}

invocationApi.c

#include <jni.h>
int main(void)
{
    JNIEnv *env;
    JavaVM *vm;
    JavaVMInitArgs vm_args;
    JavaVMOption options[1];
    jint res;
    jclass cls;
    jmethodID mid;
    jstring jstr;
    jclass stringClass;
    jobjectArray args;
    /*加載虛擬機選項*/
    options[0].optionString = "-Djava.class.path=.";
    vm_args.version = JNI_VERSION_1_6;
    vm_args.options = options;
    vm_args.nOptions = 1;
    vm_args.ignoreUnrecognized = JNI_TRUE;
    /*生成虛擬機*/
    res = JNI_CreateJavaVM(&vm, (void**)&env, &vm_args);
    /*查找並加載類*/
    cls = (*env)->FindClass(env, "InvocationApiTest");
    /*獲取main()方法的ID*/
    mid = (*env)->GetStaticMethodID(env, cls, "main", "([Ljava/lang/String;)V");
    /*生成字符串對象*/
    jstr = (*env)->NewStringUTF(env, "Hello Invocation API!!");
    stringClass = (*env)->FindClass(env, "java/lang/String");
    args = (*env)->NewObjectArray(env, 1, stringClass, jstr);
    
    /*調用main()方法*/
    (*env)->CallStaticVoidMethod(env, cls, mid, args);
    /*銷毀虛擬機*/
    (*vm)->DestroyJavaVM(vm);
}
編譯允許結果如下
xlzh@cmos:~/code/jni/superJNI$ javac InvocationApiTest.java 
xlzh@cmos:~/code/jni/superJNI$ sudo echo "/usr/lib/jvm/java-1.7.0-openjdk-amd64/jre/lib/amd64/jamvm" >> /etc/ld.so.conf
xlzh@cmos:~/code/jni/superJNI$ sudo ldconfig
xlzh@cmos:~/code/jni/superJNI$ gcc -o a.out invocationApi.c -I$JAVA_HOME/include -L$JAVA_HOME/jre/lib/amd64/jamvm/ -ljvm
xlzh@cmos:~/code/jni/superJNI$ ./a.out 
Hello Invocation API!!
xlzh@cmos:~/code/jni/superJNI$ 

 




免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM