Android JNI基础介绍

     JNI全称Java Native Interface ,主要目的是用于实现与其他语言交互而使用,我们遇到比较多应该是C/C++的使用,透过java调用C/C++中的函数,反过来也可以从C/C++回调java中的方法,实现双向交互。当然JNI也存在着许多的缺点,一旦程序使用了JNI的技术,那么也将失去了平台的可移植性,使用不当容易造成内存泄露。那为什么还要使用JNI?我的理解:

      1. C/C++已经有许多优秀的开发库,如opencv、ffmpeg等;

      2. 在处理图像数据等复杂的大量计算下,C/C++的运行效率比Java高的多;

      3. .jar文件容易被反编译,.so文件不易被反编译;

 

一、JNI的基本类型

C/C++JNIJavabit
typedef unsigned charjbooleanboolean8
typedef signed charjbytebyte8
typedef unsigned shortjcharchar16
typedef shortjshortshort16
typedef longjlonglong64
typedef floatjfloatfloat32
typedef doublejdoubledouble64
typedef intjintint32

 

 二、JNI数组类型及其它

JNIJAVA
jobjectObject
jclassClass
jstringString
jobjectArrayObject[]
jbyteArraybyte[]
jcharArraychar[]
jshortArrayshort[]
jintArrayint[]
jlongArraylong[]
jfloatArrayfloat[]
jdoubleArraydouble[]
jbooleanArrayboolean[]
jthrowableThrowable

 

三、JNI中的重要的数据结构JavaVM、JNIEnv

        在C中JavaVM的数据结构如下:

typedef const struct JNIInvokeInterface* JavaVM;

struct JNIInvokeInterface {
    void*       reserved0;
    void*       reserved1;
    void*       reserved2;

    jint        (*DestroyJavaVM)(JavaVM*);
    jint        (*AttachCurrentThread)(JavaVM*, JNIEnv**, void*);
    jint        (*DetachCurrentThread)(JavaVM*);
    jint        (*GetEnv)(JavaVM*, void**, jint);
    jint        (*AttachCurrentThreadAsDaemon)(JavaVM*, JNIEnv**, void*);
};

        在C++中定义的JavaVM其实也多少的区别,仅仅是在JNIInvokeInterface外包一层包裹作为其中的一个元素,调用同样的函数全都转到调用JNIInvokeInterface中,那么接下的JNIEnv也一样道理。JNIInvokeInterface结构体中,主要声明了几个JNI使用的关键函数,如GetEnv,AttchCurrentThread,值得注意的是JNIEnv是一个线程相关变量,就是说在不同的线程当中的JNIEnv都是不同,相互独立的,而JavaVM则是描述JVM的变量,一个JVM进程中仅有一个JavaVM,所以我们可以将JavaVM作为全局变量使用,当我们需要使用JNIEnv重JNI中开启的线程回调我们Java程序方法时,我们则需要使用JavaVM获取当前的环境变量JNIEnv,调用AttachCurrentThread函数添加到当前线程中。

         在C中JNIEnv的数据结构如下:

typedef const struct JNINativeInterface* JNIEnv;

struct JNINativeInterface {   //这里仅列出常用部分,每一种类型都有相应的定义
    ...
    jclass               (*FindClass)(JNIEnv*, const char*);

    jobject              (*NewGlobalRef)(JNIEnv*, jobject);
    void                (*DeleteGlobalRef)(JNIEnv*, jobject);
    void                (*DeleteLocalRef)(JNIEnv*, jobject);
    jobject              (*NewLocalRef)(JNIEnv*, jobject);
    jobject              (*NewObject)(JNIEnv*, jclass, jmethodID, ...);

    jstring               (*NewStringUTF)(JNIEnv*, const char*);
    const char*          (*GetStringUTFChars)(JNIEnv*, jstring, jboolean*);
    void                (*ReleaseStringUTFChars)(JNIEnv*, jstring, const char*);

    jintArray            (*NewIntArray)(JNIEnv*, jsize);
    void                (*ReleaseIntArrayElements)(JNIEnv*, jintArray, jint*, jint);

    jfieldID              (*GetFieldID)(JNIEnv*, jclass, const char*, const char*);
    jint                 (*GetIntField)(JNIEnv*, jobject, jfieldID);
    void                (*SetIntField)(JNIEnv*, jobject, jfieldID, jint);

    jfieldID          (*GetStaticFieldID)(JNIEnv*, jclass, const char*, const char*);
    jint                 (*GetStaticIntField)(JNIEnv*, jobject, jfieldID);
    void                (*SetStaticIntField)(JNIEnv*, jobject, jfieldID, jint);

    jmethodID       (*GetMethodID)(JNIEnv*, jclass, const char*, const char*); 
    jint                 (*CallIntMethod)(JNIEnv*, jobject, jmethodID, ...);  

    jmethodID       (*GetStaticMethodID)(JNIEnv*, jclass, const char*, const char*);
    jint                 (*CallStaticIntMethod)(JNIEnv*, jclass, jmethodID, ...);

    jint                 (*GetJavaVM)(JNIEnv*, JavaVM**);
    jint             (*RegisterNatives)(JNIEnv*, jclass, const JNINativeMethod*,jint);
    jint                 (*UnregisterNatives)(JNIEnv*, jclass);
    ...//JNI对异常处理部分后边介绍
}

 最后补充两个关键的函数声明与一个数据结构:

JNIEXPORT jnit JNICALL JNI_OnLoad(JavaVM* vm, void* reserved);

JNIEXPORT jnit JNICALL JNI_OnUnload(JavaVM* vm, void* reserved);

其中JNI_OnLoad是在链接库加载完成时调用,JNI_OnUnLoad在链接库被移除时调用。

          在C中JNINativeMethod的数据结构如下:

typedef struct {
    const char* name;        //java端的方法名
    const char* signature;     //java端的方法名的签名,后边会详细叙述
    void*       fnPtr;       //jni端的函数指针
} JNINativeMethod;

 

四、Java如何调用JNI函数

        1. 静态注册

定义一个Java类,需要使用JNI的函数必须声明为native类型,如public native String display();

package org.penguin.demo
public class Test{
    static{ System.loadLibrary("test");}   // 加载库文件 libtest.so
    public native String display();
}

创建jni目录,定义c/c++文件实现display函数,如定义文件test.cpp

#include <jni.h> //这个是必须的

#ifdef __cplusplus__
extern "c" {
#endif

jstring Java_org_penguin_demo_Test_display(JNIEnv *env , jobject thiz)
{
    return env->NewStringUTF("test");
    // C语言  return (*env)->NewStringUTF(env, test);
}

#ifdef __cplusplus__
}
#endif

定义编译脚本Android.mk

LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
LOCAL_MODULE := test
LOCAL_SRC_FILES := \
    test.cpp 
include $(BUILD_SHARED_LIBRARY)

         当我们调用System.loadLibrary("test");后,调用display函数,那么它将会找一个叫Java_[全限定类名,其中"/"转为"_"的格式],我们可以看到这种方式,函数名字非常的长,那么动态注册的方式。

        2. 动态注册

        这里我们只需要将test.cpp文件内容修改一下即可

#include <jni.h> //这个是必须的
#include<stddef.h>
#ifdef __cplusplus__
extern "C" {
#endif

jstring  jni_display(JNIEnv *env , jobject thiz)
{
    return env->NewStringUTF("test");
}
//方法签名 “(参数类型签名1参数类型签名2...参数类型签名n)返回值类型签名”
static JNINativeMethod gMethods[] = 
{
    { "display" , "()Ljava/lang/String;" , (void*)jni_display },
}

static int registerNativeMethods(JNIEnv *env ,const char * className,
        JNINativeMethod* getMethods , int numMethods)
{
    jclass clazz;
    clazz = env->FindClass(className);
    if(clazz == NULL)return JNI_FALSE;
    if( env->RegisterNatives(clazz , getMethos , numMethos ) < 0 )
    {
        return JNI_FALSE;
    }
    return JNI_TRUE;
}

jint JNI_OnLoad(JavaVM *vm , void * reserved)
{
    jint result = JNI_ERR;
    JNIEnv *env = NULL;
    if( vm->GetEnv( (void**)&env , JNI_VERSION_1_4 ) != JNI_OK ) return result;
    if( registerNativeMethods( env , "org/penguin/demo/Test" , gMethods , 
            sizeof(gMethods ) / sizeof(gMethods [0])) != JNI_TRUE ) { 
        return result; 
    }
    return JNI_VERSION_1_4;
}

#ifdef __cplusplus__
}
#endif

          当我们Java端调用System.loadLibray("test"); 后,将会回调JNI_OnLoad函数,我们就可以在此处注册函数,或是初始化某些东西,我们可以通过调用JNIEnv中的函数RegisterNatives来注册函数Jni函数映射。

         3. 关于方法描述的签名规则

                 3.1 格式:(参数类型签名1参数类型签名2...参数类型签名n)返回值类型签名,例:在Java中有这么一个方法String display(String name, int what){...},那么其相应的方法签名如:(Ljava/lang/String;I)Ljava/lang/String;

                3.2 关于FastJNI:方法签名增加“!”区分,!(Ljava/lang/String;I)Ljava/lang/String;,需要注意的是FastJNI的函数里的处理不应依赖其它方法,处理不当容易引起ART死锁现象。

Java 类型

签名

Java 类型

签名

boolean

Z

String

Ljava/lang/String;

byte

B

Object

Ljava/lang/Object;

char

C

int[]

[I

long

J

String

[Ljava/lang/String;

float

F

Object[]

[java/lang/Object;

double

D

int[][]

[[I

short

S

Object[][]

[[Ljava/lang/Object;

int

I

L全限定类名;

数组

[数据类型签名

 

 

       注:还可以通过javap -s -p xxx.class 获取方法签名

 

五、JNI如何回调Java函的方法

       在JNI中有个数据结构JNIEnv的环境变量,它可以帮我们调用Java层的方法,具体操作如下

       1. 同样我们Java中定义一个类为Test.java

package org.penguin.demo
public class Test {
    static { System.loadLibrary("test"); }
    public native void init();
    public void onCompleted(){ }
}

        2.  同样在jni目录下定义test.cpp实现native方法

#include <jni.h>
#include<stddef.h>
#ifdef __cplusplus__
extren "C" {
#endif

void Java_org_penguin_demo_Test_init(JNIEnv *env , jobject thiz)
{    
    jclass clazz = env->FindClass("org/penguin/demo/Test"); //查找Test这个类
    if( clazz == NULL ) return;
    //查找这个方法
    jmethodID onCompletedID = env->GetMethodID(clazz , "onCompleted", "()V");
    if( jmethodID == NULL) return;
    env->CallVoidMethod( thiz, onCompletedID );
    // 这里是不是觉得和反射很像
}

#ifdef __cplusplus__
}
#endif

            在这里当我调用在java端调用init后,将会init()---> Java_org_penguin_demo_Test_init(..)-->onCompleted()

 

六、如何在JNI上打印LOG

        在第三方apk开发中与直接在android源码环境下的开发库是存在着差异的,在编译器中使用NDK是没有utils/Log.h这么的一个头文件的,那么在编译中如何打印LOG,在三方apk开发中可以导入android/log.h这个头文件。

        在jni目录下的Android.mk中添加LOCAL_LDLIBS := -llog

LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
LOCAL_MODULE := test
LOCAL_SRC_FILES := test.cpp
LOCAL_LDLIBS := -llog
include $(BUILD_SHARED_LIBRARY)

        在jni目录下新建文件test_log.h

#include <android/log.h>
#define LOG_TAG
#define LOGD(...)  __android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, __VA_ARGS__)
#define LOGW(...)  __android_log_print(ANDROID_LOG_WARN, LOG_TAG, __VA_ARGS__)
#define LOGI(...)  __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__)
#define LOGE(...)  __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__)
#define LOGF(...)  __android_log_print(ANDROID_LOG_FATAL, LOG_TAG, __VA_ARGS__)

        test.cpp 中调用,这里的Java类就沿用前面模块的吧

#include <jni.h>
#include <test_log.h>
#define LOG_TAG “Penguin”
#ifdef __cplusplus__
extren “C” {
#endif
namespace {

void Java_org_penguin_demo_Test_init(JNIEnv *env , jobject thiz)
{
    LOGW(“Test init fail ! %s%s” , “jni” , “warn” );
}

}
#ifdef __cplusplus__
}
#endif

        这时运行程序,我们adb logcat -s Penguin就成捕捉到LOG了。

 

七、如何在JNI中持有Java对象

        有些时候,我们在jni层需要开启线程做耗时的操作,这时候我们又怎么知道这个操作跑完了,这时候我们就需要通过回调Java层的方法来告诉Java层,程序已经准备好了。

       在Java层同样有类Test.java如下:

package org.penguin.demo;

import android.util.Log;

public class Test {

    static{
        System.loadLibrary("test");
    }
	
    public native void init();
    public void jniCallback(){
        Log.d("Penguin","callback");
    }
    public void onCompleted(){
        Log.d("Penguin","onCompleted");
    }
	
}

       在JNI层的test.cpp如下:

#include <jni.h>
#include <stddef.h>  //定义NULL的头文件
#include <pthread.h> //linux线程的头文件
#include <unistd.h>  //sleep的头文件
#include <test_log.h>//沿用前边定义的

#define LOG_TAG "Penguin"

#ifdef __cplusplus
extern "C" {
#endif
namespace android {

static struct fields_t
{
    jmethodID mCallback;    //保存回调方法的id
    jclass mClassTest;       //保存被回调的类
    jobject globalWeakRef;   //保存Java传入对象
} fields;

static JavaVM * gJavaVM;     //保存Java虚拟机

void *native_exec(void *arg)   //线程的方法体
{
    LOGW("native_exec");
    JNIEnv *env;
    //gJavaVM->GetEnv((void**)&env,JNI_VERSION_1_4);  //这里不能用这个方法
    gJavaVM->AttachCurrentThread(&env,NULL);         //将线程添加到Java环境中
    if(env == NULL)return NULL;

    int i = 0;
    while(i < 10){
        env->CallVoidMethod(fields.globalWeakRef,fields.mCallback);
        sleep(5);   //每隔5s回调一次
        i++;
    }
    gJavaVM->DetachCurrentThread();
}
void Java_org_penguin_demo_Test_init(JNIEnv *env , jobject thiz)
{
    env->GetJavaVM(&gJavaVM); //保存JavaVM,在三方apk开发才这么做,android
    //源码环境下可以直接通过AndroidRuntime::getJNIEnv()来取得可用的env

    fields.mClassTest = env->FindClass("org/penguin/demo/Test");

    if(fields.mClassTest == NULL)return;

    jmethodID onCompletedID =        
        env->GetMethodID(fields.mClassTest,"onCompleted","()V");

    if(onCompletedID == NULL)return;

    LOGW("CallVoidMethod");
    env->CallVoidMethod(thiz,onCompletedID);
    
    //获取和保存要回调的Java方法
    fields.mCallback = env->GetMethodID(fields.mClassTest,"jniCallback","()V");

    if(fields.mCallback == NULL)return;

    fields.globalWeakRef = env->NewGlobalRef(thiz);//保存传入的java对象

    LOGW("pthread_create");
    pthread_t thid;     //android源码环境下直接AndroidRuntime::createJavaThread(..);
    pthread_create(&thid,NULL,native_exec,NULL);  //启动一个线程
}

}
#ifdef __cplusplus
}
#endif

       为什么在jni中的线程里不能直接JavaVM->GetEnv((void**)&env,JNI_VERSION_1_4);

       因为JNIEnv是一个与线程相关的变量,就是说每一个不同的线程中JNIEnv都不同,相互独立的,而JavaVM描述的是虚拟机,一个虚拟机进程中仅有一个JavaVM,值得注意的是在android里,每一个应用进程都创建了一个自己的虚拟机,所以这个JavaVM可以在一个进程里全局使用,还有一点是JNIEnv不在任意线程中使用,必须是java线程,或者是让已有的native线程通过调用AttachCurrentThread添加到java环境中。

       这里建议传入的java对象是个弱引用fields.globalWeakRef = env->NewGlobalRef(thiz); ,如init函数改为init(Test test);相应的JNI函数里的处理thiz改为test这个jobject对象。

       在android源码环境下有更好封装,android_runtime/AndroidRuntime.h就直接通过AndroidRuntime::getJNIEnv()取得可用的JNIEnv指针,也可以直接创建一个Java线程,通过AndroidRuntime::createJavaThread(...)这个方法。

 

八、在Java中持久化保存JNI的指针地址

       一般的,我们会将JNI中创建的持久引用保存到Java层中,通过在Java类中声明一个成员变量,然后在JNI层查找到该变量,往变量注入JNI引用地址,需要用到时,重新在JNI层通过获取回这个变量的值进行类型强转来取回之前的JNI引用,这是一个在android系统上的一个惯用技巧。

      在Java部分的Test类中

package org.penguin.demo;
public class Test {
    static {
        System.loadLibrary(“test”);
    }
    private long nNativeContext;
    public native void init();
    public native void start();
}

      在JNI部分的test.cpp这么定义

#include <jni.h>
#include <stddef.h>
#include <test_log.h>

#define LOG_TAG "Penguin"

#ifdef __cplusplus
extern "C" {
#endif
namespace android {

static jfieldID nNativeContext;  //用于保留Java的变量

class Test {
public:
void jniStart()
{
    LOGW("jniStart");
}
};

void Java_org_penguin_demo_Test_init(JNIEnv *env , jobject thiz)
{
    LOGW("init");
    Test *test = new Test();
    jclass clazz = env->FindClass("org/penguin/demo/Test");

    //取得Java层中nNativeContext成员变量的ID
    nNativeContext = env->GetFieldID(clazz,"nNativeContext","J");
    //将JNI Test对象地址写入到nNativeContext
    env->SetLongField(thiz,nNativeContext,(jlong)test);
}

void Java_org_penguin_demo_Test_start(JNIEnv *env , jobject thiz)
{
    LOGW("start");
    //从Java层中取回刚被初始化的test对象
    Test *test = (Test*)env->GetLongField(thiz,nNativeContext);
    test->jniStart();
}

}
#ifdef __cplusplus
}
#endif

      还有一点值得注意的是,为什么我要使用long类型而不用int型来保存JNI的指针,在32位处理器中long和int同占32位,而在64位处理器long型占64位,int型占32位,在64位的机器中保存指针就要使用长整型来保存,这时候就不是用int型了。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值