正文
本来这篇文章想叫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实现刚才生成的头文件中的方法:
/* Header for class umeng_com_jnitech_JniInterface */
extern "C" {
/*
* 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");
}
}
此时运行即可编译.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 ||| 取消注册类的本地方法。类将返回到链接或注册了本地方法函数前的状态。该函数不应在常规平台相关代码中使用。相反,它可以为某些程序提供一种重新加载和重新链接本地库的途径。 |