一. java调用c方法
1. 创建一个类,声明需要的native方法,方法的实现由后面实现的c方法实现
// HelloJni.java
package com.diysoul.jni;
public class HelloJni {
public HelloJni() {
}
public native String getHelloString();
public native String get(int i);
public native String upper(String s);
static {
System.loadLibrary("HelloJni");
}
}
2. 自动生成头文件 java本地方法对应的c代码的方法名 javah + 全类名
控制台切换到包src目录(项目src,1.6及以下版本为class即项目inclasses)
输入完整的类名(eclipse中选中类,右键-Copy Qualified Name,即可复制完整类名)
如 E:AndroidWorkspaceHelloJnisrc>javah com.diysoul.jni.HelloJni
将会生成对应的头文件 com_diysoul_jni_HelloJni.h
3. 实现该方法.
创建一个jni目录,将.h文件移动到此目录,创建一个对应的c文件,在里面实现java代码中声明的native方法.
方法名:Java_包名(.替换为_)_java代码中的方法名
参数:JNIEnv* pJNIEnv, jobject jobj, 其中jobj对象表示java代码中native方法所在的类的对象
// jni/com_diysoul_jni_HelloJni.c
#include <stdlib.h>
#include <stdio.h>
#include "com_diysoul_jni_HelloJni.h"
#include "DebugMsg.h"
#include "StrUtil.h"
JNIEXPORT jstring JNICALL Java_com_diysoul_jni_HelloJni_getHelloString(JNIEnv* pJNIEnv,
jobject obj) {
return (*pJNIEnv)->NewStringUTF(pJNIEnv, "Hello,Jni..!");
}
JNIEXPORT jstring JNICALL Java_com_diysoul_jni_HelloJni_get(JNIEnv* pJNIEnv, jobject obj,
jint i) {
LogD("input is %d
", i);
if (i == 1) {
return (*pJNIEnv)->NewStringUTF(pJNIEnv, "Haha it's number 1");
} else {
return (*pJNIEnv)->NewStringUTF(pJNIEnv, "Unknown");
}
}
JNIEXPORT jstring JNICALL Java_com_diysoul_jni_HelloJni_upper(JNIEnv* pJNIEnv,
jobject obj, jstring s) {
jstring result;
char* cstr = Jstring2CStr(pJNIEnv, s);
int i = 0;
for (; i < strlen(cstr); i++) {
if (cstr[i] >= 'a' && cstr[i] <= 'z') {
cstr[i] += ('A' - 'a');
}
}
result = (*pJNIEnv)->NewStringUTF(pJNIEnv, cstr);
free(cstr);
return result;
}
JNIEnv,jstring等数据类型都在android-ndkplatformsandroid-8arch-armusrincludejni.h中定义,因此需要包含头文件,#include <jni.h>
4. 创建mk文件,拷贝一份Android.mk文件到c代码所在的目录,文件内容如下
LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
# 编译后的库名
LOCAL_MODULE := HelloJni
# 需要编译的源文件
LOCAL_SRC_FILES := StrUtil.c com_diysoul_jni_HelloJni.c
# 需要引入的库
# liblog.so是android提供的支持c打印的库 android/log.h
LOCAL_LDLIBS += -llog
include $(BUILD_SHARED_LIBRARY)
6. 把C代码打包成函数库
Windows下使用Cygwin工具打包.
切换到工程所在的目录 /cygdrive/e/AndroidWorkspace/HelloJni
执行命令ndk-build
将会得到类似如下的输出:
$ ndk-build
Android NDK: WARNING: APP_PLATFORM android-21 is larger than android:minSdkVersion 8 in ./AndroidManifest.xml
[armeabi] Compile thumb : HelloJni <= com_diysoul_jni_HelloJni.c
[armeabi] SharedLibrary : libHelloJni.so
[armeabi] Install : libHelloJni.so => libs/armeabi/libHelloJni.so
表示生成了一个名为libHelloJni.so的库文件,注意引用时要去掉前面的lib及.so
在eclipse中刷新一下可以看到libs下有此so文件
成功后,会产生一个临时文件夹obj,再次编译时,应先其删除
7. 在Java代码中引出库文件
static {
System.loadLibrary("HelloJni");
}
直接调用java中的Native方法即可调用c函数方法来执行
HelloJni jni = new HelloJni();
String string = jni.getHelloString();
Log.d(TAG, string);
二. C代码中调用Java方法. 这里的C代码仍然是需要Java调用的代码,在C代码中利用JNIEnv的反射方法来实现调用java代码
C代码中通过以下步骤实现:
1. 获取jclass
jni.h头文件中专门定义了jclass类型来表示Java中的Class类
JNIEnv类中有如下几个简单的函数可以取得jclass
jclass FindClass(const char* clsName) 根据类名来查找一个类,完整类名
jclass GetObjectClass(jobject obj) 根据一个对象,获取该对象的类
jclass GetSuperClass(jclass obj) 获取一个类的父类
FindClass 会在classpath系统环境变量下寻找类,需要传入完整的类名,注意包与包之间是用"/"而不是"."来分割
2. 本地代码访问Java类中的属性与方法
JNI在jni.h头文件中定义了jfieldID,jmethodID类表示Java端的属性和方法
JNIEnv获取相应的fieldID和jmethodID的方法:
GetFieldID/GetMethodID
GetStaticFieldID/GetStaticMethodID
GetMethodID也可以取得构造函数的jmethodID。创建Java对象时调用指定的构造函数。
以下是GetMethodID的声明:
jmethodID (JNICALL *GetMethodID)(JNIEnv *env, jclass clazz, const char *name, const char *sig);
name表示方法名,sig为方法签名
方法签名是对函数参数和返回值的描述,对同一个函数,在java中允许重载,这个时候就需要这个sig来进行区分
类型 相应的签名
boolean Z
byte B
char C
short S
int I
long J
float F
double D
void V
object L用/分隔包的完整类名: Ljava/lang/String;
Array [签名 [I [Ljava/lang/Object;
Method (参数1类型签名 参数2类型签名···)返回值类型签名
方法签名举例
void f1() ()V
int f2(int, long) (IJ)I
boolean f3(int[]) ([I)B
double f4(String, int) (Ljava/lang/String;I)D
void f5(int, String [], char) (I[Ljava/lang/String;C)V
使用javap命令来产生签名,在控制台下执行javap命令
javap -s -p [full class Name]
-s 表示输出签名信息
-p 同-private,输出包括private访问权限的成员信息
如:查看String类的方法签名,Javap -s java/lang/String
查看自定义类的方法签名,需要切换到class文件所在的目录再执行命令.
3. 本地方法通过调用CallVoidMethod来调用返回值为void的实例方法
除了CallVoidMethod这个函数以外,JNI也支持对返回值为其它类型的方法的调用。
如果你调用的方法返回值类型为int,你的本地方法会使用CallIntMethod。
类似地,你可以调用CallObjectMethod来调用返回值为java.lang.String、数组等对象类型的方法。
你也可以使用Call<Type>Method系列的函数来调用接口方法
4. 其它常用的方法
jstring (*NewStringUTF)(JNIEnv*, const char*);
将一个c中的char*数组(函数第2个参数传入)转换为一个jstring对象.
GetXXXArrayElements 与 ReleaseXXXArrayElements函数, 存取与释放Java简单类型的数组
XXX代表了数组的类型
函数 Java 数组类型 本地类型
GetBooleanArrayElements jbooleanArray jboolean
GetByteArrayElements jbyteArray jbyte
GetCharArrayElements jcharArray jchar
GetShortArrayElements jshortArray jshort
GetIntArrayElements jintArray jint
GetLongArrayElements jlongArray jlong
GetFloatArrayElements jfloatArray jfloat
GetDoubleArrayElements jdoubleArray jdouble
以jboolean为例:
jboolean* (*GetBooleanArrayElements)(JNIEnv* env, jbooleanArray array, jboolean* isCopy);
参数:env:JNI 接口指针; array:Java 字符串对象; isCopy:指向布尔值的指针
返回值:返回指向数组元素的指针,如果操作失败,则为 NULL。
void (*ReleaseBooleanArrayElements)(JNIEnv* env, jbooleanArray array, jboolean* elems, jint mode);
参数:env:JNI 接口指针; array:Java 数组对象; elems:指向数组元素的指针; mode:释放模式
jsize (*GetArrayLength)(JNIEnv*, jarray);
取得数组的大小
示例,将jstring对象转换为c中的char*
// StrUtil.c
#include <stdio.h>
#include <string.h>
#include <jni.h>
char* Jstring2CStr(JNIEnv* env, jstring jstr) {
char* rtn = NULL;
jclass clsstring = (*env)->FindClass(env, "java/lang/String");
jstring strencode = (*env)->NewStringUTF(env, "GB2312");
jmethodID mid = (*env)->GetMethodID(env, clsstring, "getBytes",
"(Ljava/lang/String;)[B");
jbyteArray barr = (jbyteArray)(*env)->CallObjectMethod(env, jstr, mid,
strencode);
jsize alen = (*env)->GetArrayLength(env, barr);
jbyte* ba = (*env)->GetByteArrayElements(env, barr, JNI_FALSE);
if (alen > 0) {
rtn = (char*) malloc(alen + 1); //" "
memcpy(rtn, ba, alen);
rtn[alen] = 0;
}
(*env)->ReleaseByteArrayElements(env, barr, ba, 0);
return rtn;
}
三. 常见错误
1 java.lang.UnsatisfiedLinkError -> c代码方法名不符合jni规范,尽量使用javah来生成
2 程序运行后异常退出,没有日志打印 -> c代码有运行错误
3 在cygwin中交叉编译时报错 -> c代码有编译错误,比如函数或类型未声明,少符号等等
4 在java层调用native方法时会出现java.lang.UnsatisfiedLinkError这个异常.
原因在c或cpp文件的第一行, 如果用c实现的话, 只需要#include <jni.h>即可; 但是如果用c++实现, 那么必须要#include你刚刚生成的.h文件(javah自动生成的), 而不是jni.h.
四. 常用的c头文件
1.jni.h 声明所有和JNI相关的类型和接口.jni路径为 android-ndk-r10eplatformsandroid-21arch-armusrinclude
2.log.h 声明android提供的c打印的函数接口.需要在Android.mk中导入库liblog.so
即添加以下代码LOCAL_LDLIBS += -llog
log.h 路径为 android-ndk-r10eplatformsandroid-21arch-armusrincludeandroid
五. jni中c或c++使用的区别
jni.h定义的c/c++的函数是不一样的,使用不当也会报编译错误.如
c文件中,const char* (*GetStringUTFChars)(JNIEnv*, jstring, jboolean*);
对应的c++文件的方法为,const char* GetStringUTFChars(jstring string, jboolean* isCopy)
调用时,c:(*env)->GetStringUTFChars(env, tagObj, NULL);
c++:env->GetStringUTFChars(tagObj, NULL);
在Android.mk中添加c/c++文件如下,
LOCAL_SRC_FILES := diysoul_utilities_jni_JLog.c diysoul_utilities_jni_SystemCommand.c
LOCAL_SRC_FILES := diysoul_utilities_jni_JLog.cpp diysoul_utilities_jni_SystemCommand.cpp
// DebugMsg.h
#ifndef _DEBUG_MSG_ #define _DEBUG_MSG_ #include <android/log.h> // 需要库liblog.so支持 Android.mk中增加一行 // LOCAL_LDLIBS += -llog #define LOG_TAG "debug" #define Log(...) __android_log_print(ANDROID_LOG_DEFAULT, LOG_TAG, __VA_ARGS__); #define LogV(...) __android_log_print(ANDROID_LOG_VERBOSE, LOG_TAG, __VA_ARGS__); #define LogD(...) __android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, __VA_ARGS__); #define LogI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__); #define LogW(...) __android_log_print(ANDROID_LOG_WARN, LOG_TAG, __VA_ARGS__); #define LogE(...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__); #define LogF(...) __android_log_print(ANDROID_LOG_FATAL, LOG_TAG, __VA_ARGS__); #define LogS(...) __android_log_print(ANDROID_LOG_SILENT, LOG_TAG, __VA_ARGS__); #endif // _DEBUG_MSG_