本来这篇文章想叫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 | 获取数组大小 | ||
New<Type>Array | 创建一个指定长度的原始数据类型的数组 | ||
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 | ||
Set<PrimitiveType>ArrayRegion | 将基本类型数组的某一区域从缓冲区中复制回来的一组函数 | ||
GetFieldID | 返回类的实例(非静态)域的属性 ID。该域由其名称及签名指定。访问器函数的Get<type>Field 及 Set<type>Field系列使用域 ID 检索对象域。GetFieldID() 不能用于获取数组的长度域。应使用GetArrayLength()。 | ||
Get<type>Field | 该访问器例程系列返回对象的实例(非静态)域的值。要访问的域由通过调用GetFieldID() 而得到的域 ID 指定。 | ||
Set<type>Field | 该访问器例程系列设置对象的实例(非静态)属性的值。要访问的属性由通过调用SetFieldID() 而得到的属性 ID指定。 | ||
GetStaticFieldID GetStatic<type>Field SetStatic<type>Field | 同上,只不过是静态属性。 | ||
GetMethodID | 返回类或接口实例(非静态)方法的方法 ID。方法可在某个 clazz 的超类中定义,也可从 clazz 继承。该方法由其名称和签名决定。 GetMethodID() 可使未初始化的类初始化。要获得构造函数的方法 ID,应将<init> 作为方法名,同时将void (V) 作为返回类型。 | ||
CallVoidMethod | 同上 | ||
CallObjectMethod | 同上 | ||
CallBooleanMethod | 同上 | ||
CallByteMethod | 同上 | ||
CallCharMethod | 同上 | ||
CallShortMethod | 同上 | ||
CallIntMethod | 同上 | ||
CallLongMethod | 同上 | ||
CallFloatMethod | 同上 | ||
CallDoubleMethod | 同上 | ||
GetStaticMethodID | 调用静态方法 | ||
Call<type>Method | 同上 | ||
RegisterNatives | 向 clazz 参数指定的类注册本地方法。methods 参数将指定 JNINativeMethod 结构的数组,其中包含本地方法的名称、签名和函数指针。nMethods 参数将指定数组中的本地方法数。 | ||
UnregisterNatives | 取消注册类的本地方法。类将返回到链接或注册了本地方法函数前的状态。该函数不应在常规平台相关代码中使用。相反,它可以为某些程序提供一种重新加载和重新链接本地库的途径。 |
总结
JNI中使用的几种常见方法暂时介绍到这里,有什么问题,也欢迎大家给我留言。
demo地址
欢迎关注我的公众号: