前面两篇熟悉了AndroidStudio中的Jni开发流程,今天开始初步学习jni的相关知识。
先来看一下本地函数的组成:
JNIEXPORT jint JNICALL Java_com_pngfi_jnidemo_CUseJava_callAdd
(JNIEnv *env, jobject this)
- JNIEXPORT和JNICALL这两个宏(被定义在jni.h)确保这个函数在本地库外可见,并且C编译器会进行正确的调用转换
- 方法名:Java_全类名_方法名
- 第一个参数查阅jni.h可以知道
typedef const struct JNINativeInterface* JNIEnv;
而JNINativeInterface是一个结构体,里面定义许多jni相关的方法。在下面的示例中我们有用到,(*env)也就是结构体JNINativeInterface的指针
- 第二个参数根据本地方法是一个静态方法还是实例方法而有所不同。本地方法是一个静态方法时,第二个参数代表本地方法所在的类;本地方法是一个实例方法时,第二个参数代表本地方法所在的对象。
基本调用,包括字符串和数组
新建Module,在其中新建一个类JavaUseC,这个类提供三个方法,如下:
package com.pngfi.jnidemo;
/**
* Created by pngfi on 2016/3/28.
*/
public class JavaUseC {
/**
*加法
* @param x
* @param
* @return
*/
public native int sum(int x,int y);
/**
* 给字符串后添加一个串
* @param str
* @return
*/
public native String strAppend(String str);
/**
*改变数组元素
* @param arr
* @return
*/
public native int[] changeValue(int[] arr);
}
这个三个方法都是本地方法。
进入该类包目录之上,执行javah -jni 全类名,其中-jni可省略,即可生成对应头文件。
新建jni文件夹,在其中新建.c文件,引入刚刚生成的头文件,并把其中方法声明拷入.c文件中。
实现所有的方法:
#include "com_pngfi_jnidemo_JavaUseC.h"
#include <android/log.h>
#include <stdio.h>
#include <string.h>
#define TAG "clog"
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO,TAG,__VA_ARGS__)
#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG,TAG,__VA_ARGS__)
/*
* Class: com_pngfi_jnidemo_JavaUseC
* Method: sum
* Signature: (II)I
*/
JNIEXPORT jint JNICALL Java_com_pngfi_jnidemo_JavaUseC_sum
(JNIEnv *env, jobject obj, jint x, jint y) {
LOGI("%d",x);
//直接返回
return x + y;
}
/*
* Class: com_pngfi_jnidemo_JavaUseC
* Method: strAppend
* Signature: (Ljava/lang/String;)Ljava/lang/String;
*/
JNIEXPORT jstring JNICALL Java_com_pngfi_jnidemo_JavaUseC_strAppend
(JNIEnv *env, jobject obj, jstring str) {
//把java语言中的字符串转换成c语言中的char数组
char *s=(*env)->GetStringUTFChars(env,str,NULL);
if (s == NULL) {
return NULL; //OutOfMemoryError抛出
}
//用c代码操作
strcat(s," hello");
//释放
(*env)->ReleaseStringUTFChars(env,str,s);
//再把char*转换成java中的字符串
return (*env)->NewStringUTF(env,s);
}
/*
* Class: com_pngfi_jnidemo_JavaUseC
* Method: changeValue
* Signature: ([I)[I
*/
JNIEXPORT jintArray JNICALL Java_com_pngfi_jnidemo_JavaUseC_changeValue
(JNIEnv *env, jobject obj, jintArray jarry) {
JNIEXPORT jintArray JNICALL Java_com_pngfi_jnidemo_JavaUseC_changeValue
(JNIEnv *env, jobject obj, jintArray jarry) {
int len=(*env)->GetArrayLength(env,jarry);
int *arrry=(*env)->GetIntArrayElements(env,jarry,0);
if (arrry==NULL){
return NULL;
}
int i;
for (i=0;i<len;i++){
*(arrry+i)+=10;
LOGI("%d\n",*(arrry+i));
}
//要释放指针
(*env)->ReleaseIntArrayElements(env,jarry,arrry,0);
return jarry;
}
第一个方法没什么好说的,我们来看第二方法,因为我们不能在c代码中直接操作jstring,必须通过jni函数将其转换成c/c++中的字符串,通过GetStringUTFChars方法来实现。不要忘记检查GetStringUTFChars,因为JVM需要为新诞生的UTF-8字符串分配内存,这个操作有可能因为内存太少而失败。失败时,GetStringUTFChars会返回NULL,并抛出一个OutOfMemoryError异常。这些JNI抛出的异常与JAVA中的异常是不同的。一个由JNI抛出的未决的异常不会改变程序执行流,因此,我们需要一个显示的return语句来跳过C函数中的剩余语句。Java_com_pngfi_jnidemo_JavaUseC_strAppend函数返回后,异常会在java代码中strAppend方法的调用者中抛出。
从GetStringUTFChars中获取的UTF-8字符串在本地代码中使用完毕后,要使用ReleaseStringUTFChars告诉JVM这个UTF-8字符串不会被使用了,因为这个UTF-8字符串占用的内存会被回收。
第三个int型数组的也类似字符串。
配置bulid.gradle文件,并在java代码中引入库,即可使用函数。
在c代码中回调java
感觉在c代码中回调java,类似于java中的反射。
首先新建一个java类,如下
package com.pngfi.jnidemo;
/**
* Created by pngfi on 2016/3/29.
*/
public class CUseJava {
public int add(int x,int y){
return x+y;
}
public native int callAdd();
}
生成头文件,新建c源代码,实现如下
//
// Created by pngfi on 2016/3/29.
//
#include "com_pngfi_jnidemo_CUseJava.h"
JNIEXPORT jint JNICALL Java_com_pngfi_jnidemo_CUseJava_callAdd
(JNIEnv *env, jobject this){
//通过全类名获得需要的类
jclass clazz=(*env)->FindClass(env,"com/pngfi/jnidemo/CUseJava");
//后两个参数是方法名字和签名,签名通过对.class文件执行 javap -s 全类名 获得
jint mId=(*env)->GetMethodID(env,clazz,"add","(II)I");
//CallIntMethod中的Int对应调用方法的返回值,我们add方法返回int
jint res=(*env)->CallIntMethod(env,this,mId,5,6);
return res;
}
其中方法签名的获得需要先编译生成.class即字节码文件,然后进入如下图所示的目录下,执行javap -s 全类名
其中jnidemo是我的module名字。
关于CallIntMethod方法,如果你要调用的方法返回值是Void那么就使用CallVoidMethod,以此类推。还有该方法中依然使用了可变参数,前面三个参数是固定的,后面继续添加的即为我们要调用的java方法的参数。