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

·  阅读 4473

本来这篇文章想叫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 取消注册类的本地方法。类将返回到链接或注册了本地方法函数前的状态。该函数不应在常规平台相关代码中使用。相反,它可以为某些程序提供一种重新加载和重新链接本地库的途径。

总结

JNI中使用的几种常见方法暂时介绍到这里,有什么问题,也欢迎大家给我留言。 demo地址