专栏名称: 唤之
目录
相关文章推荐
OSC开源社区  ·  升级到Svelte ... ·  2 天前  
程序猿  ·  “我真的受够了Ubuntu!” ·  昨天  
程序员的那些事  ·  成人玩偶 + ... ·  2 天前  
OSC开源社区  ·  李彦宏:DeepSeek让我们明白要将最优秀 ... ·  3 天前  
51好读  ›  专栏  ›  唤之

JNI入门这篇文章就够了(含demo)

唤之  · 掘金  · 程序员  · 2017-12-08 07:29

正文

本来这篇文章想叫JNI使用详解或者使用全解的,但是想了想,这篇文章的内容应该只算基础教学。所以改成这个名字,既成为了标题党,也算是客观。

准备工作

这篇文章直接进入正题,所谓的ndk下载工程创建我就不多说了,如果有疑问的可以参考我之前的一篇文章 Android Studio中jni的使用 。 在app的build.gradle中:

 defaultConfig {
        applicationId "umeng.testjni"
        minSdkVersion 15
        targetSdkVersion 24
        versionCode 1
        versionName "1.0"
        ndk {
            moduleName "JniTest"
            ldLibs "log", "z", "m"
            abiFilters "armeabi", "armeabi-v7a", "x86"
        }
    }

其中moduleName是生成的.so文件的名字,如果设置成JniTest,生成的.so文件会是libJniTest.so,ldLibs是依赖的库。 打开gradle.properties文件,增加配置:

android.useDeprecatedNdk = true

local.properties增加ndk的配置路径:

ndk.dir=/Users/xxxx/xxxx/sdk/android-ndk-r10e
sdk.dir=/Users/xxxxx/Library/Android/sdk

代码工作

工程中新建一个接口类 JniInterface :

public class JniInterface {
    static {
        System.loadLibrary("JniTest");
    }
  public static native String sayHello();
}

待会我们会一个一个的在这个类中添加方法。 其中 System.loadLibrary("JniTest");是加载.so文件,sayHello是c++的方法名字。 这时打开命令行,切到当前应用工程的目录下(严格来说不是工程目录下,而是java代码目录下,即app/src/main/java),输入如下命令:

javah -jni xxxx.com.jnitech.JniInterface

其中 xxxx.com.jnitech.JniInterface是我们刚刚编写的文件,这时会在对应的路径下生成一个 xxxx.com.jnitech.JniInterface.h文件

在main目录下建立jni文件夹,新建main.c实现刚才生成的头文件中的方法:

#include <jni.h>
/* Header for class umeng_com_jnitech_JniInterface */
#include <stddef.h>
#ifndef _Included_umeng_com_jnitech_JniInterface
#define _Included_umeng_com_jnitech_JniInterface
#ifdef __cplusplus
extern "C" {
#endif
/*
 * Class:     umeng_com_jnitech_JniInterface
 * Method:    sayHello
 * Signature: ()Ljava/lang/String;
 */
JNIEXPORT jstring JNICALL Java_umeng_com_jnitech_JniInterface_sayHello
        (JNIEnv *env, jclass object){
    return (*env)->NewStringUTF(env,"hello umeng");
}

#ifdef __cplusplus
}

#endif
#endif

此时运行即可编译.so文件,在build/intermediates/ndk目录下可以找到对应文件。 如果文章就这样结束了,大家一定觉得很水,所以这仅仅是一个开始。

调用C的方法

上面的例子是一个在Java中调用C中字符串的方法。下面要实现一个Java调用C方法的例子。 找到头文件 umeng_com_jnitech_JniInterface.h :

JNIEXPORT jint JNICALL Java_umeng_com_jnitech_JniInterface_sum
        (JNIEnv *, jclass,jint,jint);

添加了这个声明之后,需要去.c文件中进行实现:

JNIEXPORT jint JNICALL Java_umeng_com_jnitech_JniInterface_sum
        (JNIEnv *env, jclass object, jint a, jint b){
    return (a+b);
}

这个方法可以看出是传入两个int值,做一个加法,再将值返回的操作。 在Java中也需要声明一下:

 public static native int sum(int a,int b);

然后调用即可:

                Toast.makeText(MainActivity.this,"3+4="+JniInterface.sum(3,4),Toast.LENGTH_SHORT).show();

这里要做一下详细说明,java中的int对应到C中就是jint,这是一个原始类型的转化问题,除此还有:

Java类型 本地类型(JNI) 描述
boolean(布尔型) jboolean 无符号8个比特
byte(字节型) jbyte 有符号8个比特
char(字符型) jchar 无符号16个比特
short(短整型) jshort 有符号16个比特
int(整型) jint 有符号32个比特
long(长整型) jlong 有符号64个比特
float(浮点型) jfloat 32个比特
double(双精度浮点型) jdouble 64个比特
void(空型) void N/A

数组操作

上面的方法是传入两个int值,如果是数组如何操作,这里注意,传入的类型要是基础数据类型,要想传入一个ArrayList肯定是不可以的。所以我们就用最基础的String数组。 头文件增加方法:

JNIEXPORT jstring JNICALL Java_umeng_com_jnitech_JniInterface_testArray
        (JNIEnv *, jclass,jobjectArray,jobjectArray);

实现:

JNIEXPORT jstring JNICALL Java_umeng_com_jnitech_JniInterface_testArray
        (JNIEnv *env, jclass object, jobjectArray a, jobjectArray b){
    jsize count1 = (*env)->GetArrayLength(env,a);
    jsize count2 = (*env)->GetArrayLength(env,b);
    jstring aa = (jstring) (*env)->GetObjectArrayElement(env,a, count1-1);
    jstring bb = (jstring) (*env)->GetObjectArrayElement(env,b, count2-1);


    return (*env)->NewStringUTF(env,"数组收到");
}

增加java的接口

    public static native String testArray(String[] a,String[] b);

C调用Java中的String类型

上面提到了如何从Java中调用C的String,如果C需要调用Java的类型该如何处理呢? 如在Java中有如下方法:

    public  String helloNoStatic(){
        return "这个字符串来自java非静态,穿越c,展示在这里";
    }

C中应该如何调用呢?答案是反射,反射的方法与Java反射的方法类似。 头文件:

JNIEXPORT jstring JNICALL Java_umeng_com_jnitech_JniInterface_callNonStaticJavaString
        (JNIEnv *, jclass);

实现:

JNIEXPORT jstring JNICALL Java_umeng_com_jnitech_JniInterface_callNonStaticJavaString
        (JNIEnv *env, jclass object){
    jclass dpclazz = (*env)->FindClass(env,"umeng/com/jnitech/JniInterface");
    if(dpclazz==0){
        return;
    }
    jmethodID method1 = (*env)->GetMethodID(env,dpclazz,"helloNoStatic","()Ljava/lang/String;");
    if(method1==0){
        return;
    }
    jstring result =  (jstring)(*env)->CallObjectMethod(env,object,method1);
    return result;

}

如果是一个静态方法:

    public static String hello(){
        return "这个字符串来自java静态,穿越c,展示在这里";
    }

该如何实现呢? 与静态类似,只是C中调用的方法不同:

JNIEXPORT jstring JNICALL Java_umeng_com_jnitech_JniInterface_callJavaStringSum
        (JNIEnv *env, jclass object,jstring a,jstring b){
    jclass dpclazz = (*env)->FindClass(env,"umeng/com/jnitech/JniInterface");
    if(dpclazz==0){
        return;
    }
    jmethodID method1 = (*env)->GetStaticMethodID(env,dpclazz,"sumStringCallBackJava","(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;");
    if(method1==0){
        return;
    }
    jstring result =  (jstring)(*env)->CallStaticObjectMethod(env,object,method1,a,b);
//    return (*env)->NewStringUTF(env,"hello umeng");
    return result;

}

看过代码的同学,可能会疑问 "()Ljava/lang/String; 这是什么意思,这是域描述符,这里详细介绍一下: 括号内为调用方法的参数,括号后面的是返回值。 域描述符对应如下:

JAVA语言
Z boolean
B byte
C char
S short
I int
J long
F float
D double
String Ljava/lang/String;
int[ ] [I
float[ ] [F
String[ ] [Ljava/lang/String;
Object[ ] [Ljava/lang/Object;
int [ ][ ] [[I
float[ ][ ] [[F

C中调用Java相加的方法

Java中:

    public static int sumCallBackJava(int a,int b){
        Log.e("xxxxxx","a+b="+a+b);
        return a+b;
    }

头文件:

JNIEXPORT jstring JNICALL Java_umeng_com_jnitech_JniInterface_sumCallBack
        (JNIEnv *, jclass,jint,jint);

实现:

JNIEXPORT jint JNICALL Java_umeng_com_jnitech_JniInterface_sumCallBack
        (JNIEnv *env, jclass object, jint a, jint b){
    jclass dpclazz = (*env)->FindClass(env,"umeng/com/jnitech/JniInterface");
    if(dpclazz==0){
        return 0;
    }
    jmethodID method1 = (*env)->GetStaticMethodID(env,dpclazz,"sumCallBackJava","(II)I");
    if(method1==0){
        return 0;
    }
    jint result =  (*env)->CallStaticIntMethod(env,object,method1,a,b);
//    return (*env)->NewStringUTF(env,"hello umeng");
    return result;
}

JNI函数操作查询

|函数|Java |本地类型|说明| | --- | --- | --- | --- | |GetBooleanArrayElements |jbooleanArray |jboolean| ReleaseBooleanArrayElements 释放| |GetByteArrayElements| jbyteArray |jbyte| ReleaseByteArrayElements 释放| |GetCharArrayElements |jcharArray| jchar| ReleaseShortArrayElements 释放 |GetShortArrayElements| jshortArray| jshort |ReleaseBooleanArrayElements 释放| |GetIntArrayElements |jintArray| jint |ReleaseIntArrayElements 释放| |GetLongArrayElements| jlongArray| jlong |ReleaseLongArrayElements 释放| |GetFloatArrayElements |jfloatArray| jfloat |ReleaseFloatArrayElements 释放| |GetDoubleArrayElements| jdoubleArray| jdouble |ReleaseDoubleArrayElement 释放| |GetObjectArrayElement |自定义对象 |object || |SetObjectArrayElement |自定义对象 |object || |GetArrayLength ||| 获取数组大小| |NewArray ||| 创建一个指定长度的原始数据类型的数组| |GetPrimitiveArrayCritical ||| 得到指向原始数据类型内容的指针,该方法可能使垃圾回收不能执行,该方法可能返回数组的拷贝,因此必须释放此资源。| |ReleasePrimitiveArrayCritical ||| 释放指向原始数据类型内容的指针,该方法可能使垃圾回收不能执行,该方法可能返回数组的拷贝,因此必须释放此资源。| |NewStringUTF ||| jstring类型的方法转换| |GetStringUTFChars ||| jstring类型的方法转换| |DefineClass ||| 从原始类数据的缓冲区中加载类| |FindClass 该函数用于加载本地定义的类。它将搜索由CLASSPATH 环境变量为具有指定名称的类所指定的目录和 zip文件| |GetObjectClass ||| 通过对象获取这个类。该函数比较简单,唯一注意的是对象不能为NULL,否则获取的class肯定返回也为NULL| |GetSuperclass ||| 获取父类或者说超类 。 如果 clazz 代表类class而非类 object,则该函数返回由 clazz 所指定的类的超类。 如果 clazz指定类 object 或代表某个接口,则该函数返回NULL| |IsAssignableFrom ||| 确定 clazz1 的对象是否可安全地强制转换为clazz2| |Throw |||抛出 java.lang.Throwable 对象| |ThrowNew ||| 利用指定类的消息(由 message 指定)构造异常对象并抛出该异常| |ExceptionOccurred ||| 确定是否某个异常正被抛出。在平台相关代码调用 ExceptionClear() 或 Java 代码处理该异常前,异常将始终保持抛出状态| |ExceptionDescribe ||| 将异常及堆栈的回溯输出到系统错误报告信道(例如 stderr)。该例程可便利调试操作| |ExceptionClear ||| 清除当前抛出的任何异常。如果当前无异常,则此例程不产生任何效果| |FatalError ||| 抛出致命错误并且不希望虚拟机进行修复。该函数无返回值| |NewGlobalRef ||| 创建 obj 参数所引用对象的新全局引用。obj 参数既可以是全局引用,也可以是局部引用。全局引用通过调用DeleteGlobalRef() 来显式撤消。| |DeleteGlobalRef ||| 删除 globalRef 所指向的全局引用| |DeleteLocalRef ||| 删除 localRef所指向的局部引用| |AllocObject ||| 分配新 Java 对象而不调用该对象的任何构造函数。返回该对象的引用。clazz 参数务必不要引用数组类。| |getObjectClass ||| 返回对象的类| |IsSameObject ||| 测试两个引用是否引用同一 Java 对象| |NewString ||| 利用 Unicode 字符数组构造新的 java.lang.String 对象| |GetStringLength ||| 返回 Java 字符串的长度(Unicode 字符数)| |GetStringChars ||| 返回指向字符串的 Unicode 字符数组的指针。该指针在调用 ReleaseStringchars() 前一直有效| |ReleaseStringChars ||| 通知虚拟机平台相关代码无需再访问 chars。参数chars 是一个指针,可通过 GetStringChars() 从 string 获得| |NewStringUTF ||| 利用 UTF-8 字符数组构造新 java.lang.String 对象| |GetStringUTFLength ||| 以字节为单位返回字符串的 UTF-8 长度| |GetStringUTFChars ||| 返回指向字符串的 UTF-8 字符数组的指针。该数组在被ReleaseStringUTFChars() 释放前将一直有效| |ReleaseStringUTFChars ||| 通知虚拟机平台相关代码无需再访问 utf。utf 参数是一个指针,可利用 GetStringUTFChars() 获得| |NewObjectArray ||| 构造新的数组,它将保存类 elementClass 中的对象。所有元素初始值均设为 initialElement| |SetArrayRegion ||| 将基本类型数组的某一区域从缓冲区中复制回来的一组函数| |GetFieldID ||| 返回类的实例(非静态)域的属性 ID。该域由其名称及签名指定。访问器函数的GetField 及 SetField系列使用域 ID 检索对象域。GetFieldID() 不能用于获取数组的长度域。应使用GetArrayLength()。| |GetField ||| 该访问器例程系列返回对象的实例(非静态)域的值。要访问的域由通过调用GetFieldID() 而得到的域 ID 指定。 | |SetField ||| 该访问器例程系列设置对象的实例(非静态)属性的值。要访问的属性由通过调用SetFieldID() 而得到的属性 ID指定。| |GetStaticFieldID GetStaticField SetStaticField ||| 同上,只不过是静态属性。| |GetMethodID ||| 返回类或接口实例(非静态)方法的方法 ID。方法可在某个 clazz 的超类中定义,也可从 clazz 继承。该方法由其名称和签名决定。 GetMethodID() 可使未初始化的类初始化。要获得构造函数的方法 ID,应将 作为方法名,同时将void (V) 作为返回类型。| |CallVoidMethod ||| 同上| |CallObjectMethod ||| 同上 | |CallBooleanMethod ||| 同上 | |CallByteMethod ||| 同上| |CallCharMethod ||| 同上| |CallShortMethod ||| 同上 | |CallIntMethod ||| 同上| |CallLongMethod ||| 同上| |CallFloatMethod ||| 同上 | |CallDoubleMethod ||| 同上 | |GetStaticMethodID ||| 调用静态方法| |CallMethod ||| 同上 | |RegisterNatives ||| 向 clazz 参数指定的类注册本地方法。methods 参数将指定 JNINativeMethod 结构的数组,其中包含本地方法的名称、签名和函数指针。nMethods 参数将指定数组中的本地方法数。| |UnregisterNatives ||| 取消注册类的本地方法。类将返回到链接或注册了本地方法函数前的状态。该函数不应在常规平台相关代码中使用。相反,它可以为某些程序提供一种重新加载和重新链接本地库的途径。 |







请到「今天看啥」查看全文