JNI 编程

JNI编程

JNI是一种本地编程接口。它允许运行在JAVA虚拟机中的JAVA代码和用其他编程语言,诸如C语言、C++、汇编,写的应用和库之间的交互操作。

第一个JNI程序

#引入jni头文件支持
include_directories("C:/Program Files/Java/jdk1.8.0_171/include")
# windows:win32  Mac:darwin
include_directories("C:/Program Files/Java/jdk1.8.0_171/include/win32")
# 修改生成动态库
add_library(lsn7jni SHARED lsn7_jni.cpp)

最终你的动态库会生成在下面位置( 可以修改)

dll目录.png

然后打开一个Java IDE (AS/ECLIPSE/文本文档 都行)

例子:在AS的java单元测试下进行编写(此时使用的是电脑的java环境)

 @Test
public void addition_isCorrect() {
        assertEquals(4, 2 + 2);
        //加载指定动态库 注意:当前我的电脑系统是64位
        System.load("xxxx/lsn7jni.dll");
        //调用jni方法
        System.out.println(test(1,"22",3.1f));
}
native String test(int i,String j,float k);

然后在VS中编写

// lsn6_jni.cpp: 定义应用程序的入口点。
//
#include "jni.h"
//c++中需要以c的方式编译
extern "C"
//JNIEnv: 由Jvm传入与线程相关的变量。定义了JNI系统操作、java交互等方法。
//jobject: 表示当前调用对象,即 this , 如果是静态的native方法,则获得jclass
JNIEXPORT jstring JNICALL Java_com_dongnao_jniTest_ExampleUnitTest_test
(JNIEnv *env, jobject, jint i, jstring j, jfloat k) {
    // 获得c字符串  
    // 开闭内存x,拷贝java字符串到x中 返回指向x的指针
    // 参数2 isCopy:
    //提供一个boolean(int)指针,用于接收jvm传给我们的字符串是否是拷贝的。
    //通常,我们不关心这个,一般传个NULL就可以
    const char* str = env->GetStringUTFChars(j, JNI_FALSE);
    char returnStr[100];
    //格式化字符串
    sprintf(returnStr,"C++ string:%d,%s,%f",i,str,k);
    //释放掉内存 x
    env->ReleaseStringUTFChars(j,str);
    // 返回java字符串
    return  env->NewStringUTF(returnStr);
}

现在第一个jni程序就已经能够运行了,在AS中运行单元测试进行jni方法的测试

对于不熟悉的同学可以,使用javah获得方法该如何声明。javah是JDK中提供的工具(Java环境变量不用说了吧?)

javah -o [输出文件名] [全限定名]

D:\Lance\ndk\lsn7_jni\JniTest\app\src\test\java> javah -o ExampleUnitTest.h com.dongnao.jniTest.ExampleUnitTest

JNI数据类型

JNIEXPORT 和 JNICALL,定义在jni_md.h头文件中。

JNIEXPORT:

​ Windows 中,定义为__declspec(dllexport)。因为Windows编译 dll 动态库规定,如果动态库中的函数要被外部调用,需要在函数声明中添加此标识,表示将该函数导出在外部可以调用。

​ 在 Linux/Unix/Mac os/Android 这种 Like Unix系统中,定义为__attribute__ ((visibility ("default")))

​ GCC 有个visibility属性, 启用这个属性gcc -fvisibility=xx:

  1. 当-fvisibility=hidden时

    动态库中的函数默认是被隐藏的即 hidden. 除非显示声明为__attribute__((visibility("default"))).

  2. 当-fvisibility=default时

    动态库中的函数默认是可见的.除非显示声明为__attribute__((visibility("hidden"))).

JNICALL:

​ 在类Unix中无定义,在Windows中定义为:_stdcall ,一种函数调用约定

类Unix系统中这两个宏可以省略不加。

Java类型 本地类型 描述
boolean jboolean C/C++8位整型
byte jbyte C/C++带符号的8位整型
char jchar C/C++无符号的16位整型
short jshort C/C++带符号的16位整型
int jint C/C++带符号的32位整型
long jlong C/C++带符号的64位整型
float jfloat C/C++32位浮点型
double jdouble C/C++64位浮点型
Object jobject 任何Java对象,或者没有对应java类型的对象
Class jclass Class对象
String jstring 字符串对象
Object[] jobjectArray 任何对象的数组
boolean[] jbooleanArray 布尔型数组
byte[] jbyteArray 比特型数组
char[] jcharArray 字符型数组
short[] jshortArray 短整型数组
int[] jintArray 整型数组
long[] jlongArray 长整型数组
float[] jfloatArray 浮点型数组
double[] jdoubleArray 双浮点型数组
C/C++中获取java的数组时:

extern "C"
JNIEXPORT jstring JNICALL
Java_com_dongnao_jnitest_MainActivity_test(JNIEnv *env, jobject instance, jobjectArray a_,jintArray b_) {
    //1、 获得字符串数组
    //获得数组长度
    int32_t str_length = env->GetArrayLength(a_);
    LOGE("字符串 数组长度:%d",str_length);
    //获得字符串数组的数据
    for (int i = 0; i < str_length; ++i) {
        jstring str = static_cast<jstring>(env->GetObjectArrayElement(a_, i));
        const char* c_str =  env->GetStringUTFChars(str, 0);
        LOGE("字符串有:%s",c_str);
        //使用完释放
        env->ReleaseStringUTFChars(str,c_str);
    }
    //2、获得基本数据类型数组
    int32_t int_length = env->GetArrayLength(b_);
    LOGE("int 数组长度:%d",int_length);
    //对应的有 GetBoolean 、GetFloat等
    jint *b = env->GetIntArrayElements(b_, 0);
    for (int i = 0; i < int_length; i++) {
        LOGE("int 数据有:%d",b[i]);
    }
    env->ReleaseIntArrayElements(b_, b, 0);
    return env->NewStringUTF("222");
}

C/C++反射Java

反射方法

在C/C++中反射创建Java的对象,调用Java的方法

package com.dongnao.jnitest;
import android.util.Log;

public class Helper {
    private static final String TAG = "Helper";
    //private和public 对jni开发来说没任何区别 都能反射调用
    public void instanceMethod(String a,int b,boolean c){
        Log.e(TAG,"instanceMethod a=" +a +" b="+b+" c="+c );
    }

    public static void staticMethod(String a,int b,boolean c){
        Log.e(TAG,"staticMethod a=" +a +" b="+b+" c="+c);
    }
}

extern "C"
JNIEXPORT void JNICALL
Java_com_dongnao_jnitest_MainActivity_invokeHelper(JNIEnv *env, jobject instance) {
    jclass clazz = env->FindClass("com/dongnao/jnitest/Helper");
    //获得具体的静态方法 参数3:签名(下方说明)
    //如果不会填 可以使用javap
    jmethodID staticMethod = env->GetStaticMethodID(clazz,"staticMethod","(Ljava/lang/String;IZ)V");
    //调用静态方法
    jstring staticStr= env->NewStringUTF("C++调用静态方法");
    env->CallStaticVoidMethod(clazz,staticMethod,staticStr,1,1);

    //获得构造方法 <init>:构造方法写法
    jmethodID constructMethod = env->GetMethodID(clazz,"<init>","()V");
    //创建对象
    jobject  helper = env->NewObject(clazz,constructMethod);
    jmethodID instanceMethod = env->GetMethodID(clazz,"instanceMethod","(Ljava/lang/String;IZ)V");
    jstring instanceStr= env->NewStringUTF("C++调用实例方法");
    env->CallVoidMethod(helper,instanceMethod,instanceStr,2,0);

    //释放
    env->DeleteLocalRef(clazz);
    env->DeleteLocalRef(staticStr);
    env->DeleteLocalRef(instanceStr);
    env->DeleteLocalRef(helper);
}

基本数据类型的签名采用一系列大写字母来表示, 如下表所示:

Java类型 签名
boolean Z
short S
float F
byte B
int I
double D
char C
long J
void V
引用类型 L + 全限定名 + ;
数组 [+类型签名

可以使用javap来获取反射方法时的签名

#cd 进入 class所在的目录 执行: javap -s 全限定名,查看输出的 descriptor
D:\Lance\ndk\lsn7_jni\JniTest\app\build\intermediates\classes\debug>javap -s com.dongnao.jnitest.Helper
Compiled from "Helper.java"
public class com.dongnao.jnitest.Helper {
  public com.dongnao.jnitest.Helper();
    descriptor: ()V

  public void instanceMethod(java.lang.String, int, boolean);
    descriptor: (Ljava/lang/String;IZ)V

  public static void staticMethod(java.lang.String, int, boolean);
    descriptor: (Ljava/lang/String;IZ)V
}

反射属性

int a = 10;
static String b = "java字符串";
private static final String TAG = "Helper";

public void testReflect() {
        Log.e(TAG,"修改前 : a = " +a +" b="+b);
        reflectHelper();
        Log.e(TAG,"修改后 : a = " +a +" b="+b);
}
public  native void  reflectHelper();


extern "C"
JNIEXPORT void JNICALL
Java_com_dongnao_jnitest_Helper_reflectHelper(JNIEnv *env, jobject instance) {

    //instance 就是 helper
    jclass clazz = env->GetObjectClass(instance);
    //获得int a的标示
    jfieldID a = env->GetFieldID(clazz,"a","I");
    int avalue = env->GetIntField(instance,a);
    LOGE("获得java属性a:%d",avalue);
    //修改属性值
    env->SetIntField(instance,a,100);

    jfieldID b = env->GetStaticFieldID(clazz,"b","Ljava.lang.String;");
    //获取值
    jstring bstr = static_cast<jstring>(env->GetStaticObjectField(clazz, b));
    const char* bc_str = env->GetStringUTFChars(bstr,0);
    LOGE("获得java属性b:%s",bc_str);

    //修改
    jstring new_str = env->NewStringUTF("C++字符串");
    env->SetStaticObjectField(clazz,b,new_str);

    env->ReleaseStringUTFChars(bstr,bc_str);
    env->DeleteLocalRef(new_str);
    env->DeleteLocalRef(clazz);
}

JNI引用

在 JNI 规范中定义了三种引用:局部引用(Local Reference)、全局引用(Global Reference)、弱全局引用(Weak Global Reference)。

局部引用

大多数JNI函数会创建局部引用。NewObject/FindClass/NewStringUTF 等等都是局部引用。

局部引用只有在创建它的本地方法返回前有效,本地方法返回后,局部引用会被自动释放。
因此无法跨线程、跨方法使用。

extern "C"
JNIEXPORT jstring JNICALL
xxx(JNIEnv *env, jobject instance) {
    //错误
    //不能在本地方法中把局部引用存储在静态变量中缓存起来供下一次调用时使用。
    // 第二次执行 str依然有值,但是其引用的 “C++字符串” 已经被释放
    static jstring str;
    if(str == NULL){
         str = env->NewStringUTF("C++字符串");
    }
    return str;
}

释放一个局部引用有两种方式:
1、本地方法执行完毕后VM自动释放;
2、通过DeleteLocalRef手动释放;

VM会自动释放局部引用,为什么还需要手动释放呢?
因为局部引用会阻止它所引用的对象被GC回收。

为什么需要手动释放局部引用.png

全局引用

全局引用可以跨方法、跨线程使用,直到它被手动释放才会失效 。
由 NewGlobalRef 函数创建

extern "C"
JNIEXPORT jstring JNICALL
Java_com_dongnao_jnitest_MainActivity_test1(JNIEnv *env, jobject instance) {
    //正确
    static jstring globalStr;
    if(globalStr == NULL){
        jstring str = env->NewStringUTF("C++字符串");
        //删除全局引用调用  DeleteGlobalRef
        globalStr = static_cast<jstring>(env->NewGlobalRef(str));
        //可以释放,因为有了一个全局引用使用str,局部str也不会使用了
        env->DeleteLocalRef(str);
    }
    return globalStr;
}

弱引用

与全局引用类似,弱引用可以跨方法、线程使用。与全局引用不同的是,弱引用不会阻止GC回收它所指向的VM内部的对象 。

在对Class进行弱引用是非常合适(FindClass),因为Class一般直到程序进程结束才会卸载。

在使用弱引用时,必须先检查缓存过的弱引用是指向活动的对象,还是指向一个已经被GC的对象

extern "C"
JNIEXPORT jclass JNICALL
Java_com_dongnao_jnitest_MainActivity_test1(JNIEnv *env, jobject instance) {
    static jclass globalClazz = NULL;
    //对于弱引用 如果引用的对象被回收返回 true,否则为false
    //对于局部和全局引用则判断是否引用java的null对象
    jboolean isEqual = env->IsSameObject(globalClazz, NULL);
    if (globalClazz == NULL || isEqual) {
        jclass clazz = env->GetObjectClass(instance);
        //删除使用 DeleteWeakGlobalRef
        globalClazz = static_cast<jclass>(env->NewWeakGlobalRef(clazz));
        env->DeleteLocalRef(clazz);
    }
    return globalClazz;
}

JNI_OnLoad

调用System.loadLibrary()函数时, 内部就会去查找so中的 JNI_OnLoad 函数,如果存在此函数则调用。

JNI_OnLoad会:

告诉 VM 此 native 组件使用的 JNI 版本。

​ 对应了Java版本,android中只支持JNI_VERSION_1_2 、JNI_VERSION_1_4、JNI_VERSION_1_6

​ 在JDK1.8有 JNI_VERSION_1_8。

jint JNI_OnLoad(JavaVM* vm, void* reserved){
    // 2、4、6都可以
    return JNI_VERSION_1_4;
}

动态注册

在此之前我们一直在jni中使用的 Java_PACKAGENAME_CLASSNAME_METHODNAME 来进行与java方法的匹配,这种方式我们称之为静态注册。

而动态注册则意味着方法名可以不用这么长了,在android aosp源码中就大量的使用了动态注册的形式

//Java:
native void dynamicNative();
native String dynamicNative(int i);

//C++:
void  dynamicNative1(JNIEnv *env, jobject jobj){
    LOGE("dynamicNative1 动态注册");
}
jstring  dynamicNative2(JNIEnv *env, jobject jobj,jint i){
    return env->NewStringUTF("我是动态注册的dynamicNative2方法");
}

//需要动态注册的方法数组
static const JNINativeMethod mMethods[] = {
        {"dynamicNative","()V", (void *)dynamicNative1},
        {"dynamicNative", "(I)Ljava/lang/String;", (jstring *)dynamicNative2}

};
//需要动态注册native方法的类名
static const char* mClassName = "com/dongnao/jnitest/MainActivity";
jint JNI_OnLoad(JavaVM* vm, void* reserved){
    JNIEnv* env = NULL;
    //获得 JniEnv
    int r = vm->GetEnv((void**) &env, JNI_VERSION_1_4);
    if( r != JNI_OK){
        return -1;
    }
    jclass mainActivityCls = env->FindClass( mClassName);
    // 注册 如果小于0则注册失败
    r = env->RegisterNatives(mainActivityCls,mMethods,2);
    if(r  != JNI_OK )
    {
        return -1;
    }
    return JNI_VERSION_1_4;
}

native线程调用Java

native调用java需要使用JNIEnv这个结构体,而JNIEnv是由Jvm传入与线程相关的变量。

但是可以通过JavaVM的AttachCurrentThread方法来获取到当前线程中的JNIEnv指针。

JavaVM* _vm = 0;
jobject  _instance = 0;
jint JNI_OnLoad(JavaVM* vm, void* reserved){
    _vm = vm;
    return JNI_VERSION_1_4;
}
void *task(void *args){
    JNIEnv *env;
    //将本地当前线程附加到jvm,并获得jnienv
    //成功则返回0
    _vm->AttachCurrentThread(&env,0);
    
    jclass clazz = env->GetObjectClass(_instance);

    //获得具体的静态方法 参数3:方法签名
    //如果不会填 可以使用javap
    jmethodID staticMethod = env->GetStaticMethodID(clazz,"staticMethod","(Ljava/lang/String;IZ)V");
    //调用静态方法
    jstring staticStr= env->NewStringUTF("C++调用静态方法");
    env->CallStaticVoidMethod(clazz,staticMethod,staticStr,1,1);

    //获得构造方法
    jmethodID constructMethod = env->GetMethodID(clazz,"<init>","()V");
    //创建对象
    jobject  helper = env->NewObject(clazz,constructMethod);
    jmethodID instanceMethod = env->GetMethodID(clazz,"instanceMethod","(Ljava/lang/String;IZ)V");
    jstring instanceStr= env->NewStringUTF("C++调用实例方法");
    env->CallVoidMethod(helper,instanceMethod,instanceStr,2,0);

    //释放
    env->DeleteLocalRef(clazz);
    env->DeleteLocalRef(staticStr);
    env->DeleteLocalRef(instanceStr);
    env->DeleteLocalRef(helper);
    //分离
    _vm->DetachCurrentThread();
    return 0;
}

//Helper 类方法
extern "C"
JNIEXPORT void JNICALL
Java_com_dongnao_jnitest_Helper_nativeThread(JNIEnv *env, jobject instance) {
    pthread_t  pid;
    //这里注意释放
    _instance = env->NewGlobalRef(instance);
   pthread_create(&pid,0,task,0);
}

原创文章,转载请联系作者哦~~

欢迎进群一起交流学习:
Android开发交流群群聊二维码.png
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,080评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,422评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,630评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,554评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,662评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,856评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,014评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,752评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,212评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,541评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,687评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,347评论 4 331
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,973评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,777评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,006评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,406评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,576评论 2 349

推荐阅读更多精彩内容