什么是JNI
- JNI是Java Native Interface的简称,即Java本地开发接口
- JNI是一个协议,用来沟通Java代码和本地代码(C/C++)
为什么使用JNI
- Java语言没有的功能(很少)
- 代码已经有现成的C/C++版本,不想重写(一般情况)
- Java太慢了(怎么慢?)
- 跨平台实现(Mac, Windows, Android, iOS)
- 特殊的功能性语言(Fortran的数学计算
使用JNI的注意事项
- 只提供了C/C++的支持,其他语言需要额外的步骤
- 不安全,特别是内存管理,以及Java对象的引用计数
- 不可移值,不同的CPU架构需要重新编译(arm, mips, x86)
如何使用JNI-基础知识
- Android SDK(Java)
- C/C++语言
- NDK(Android.mk, ndk-build)
如何使用JNI-C/C++语言基础
- 指针的使用
- 内存管理:malloc, free, new, delete
- 变量类型:int, float, double, char, …
- C++导出C函数:extern “C” { }
如何使用JNI-基本过程
- 下载NDK,支持Windows、Linux和Mac OS X;
- 解压NDK到本地磁盘,设置好PATH;
- 新建Android工程,在工程目录下新建jni文件夹(文件夹名称必须是jni);
- 在jni文件夹下建立Android.mk文件(Android.mk文件的编写方法见后文);
- 在jni文件夹下编写好C/C++代码文件;
- 命令行切换到当前目录,运行ndk-build,等待编译完成,编译完成后so文件在libs/armeabi文件夹下;
- 在eclipse里运行Android工程,so文件会打包到apk里
- TIPS:ADT插件安装选择Native Development,在工程的右键菜单有Android Tools/Add Native Support,集成了CDT的C/C++ IDE环境,在Java编译前执行ndk-build
如何使用JNI-JNI语法
- HelloJni例子(NDK_DIR/samples/hello-jni)
- HelloJni.java
- HelloJni.c
- Android.mk
如何使用JNI-HelloJni.java
public class HelloJni extends Activity
{
// ...
public native String stringFromJNI(); // 声明native
static {
// 在libs/armeabi下加载libhello-jni.so
System.loadLibrary("hello-jni");
// 指定路径加载libhello-jni.so
// System.load("path/libhello-jni.so");
}
}
- 声明native函数,不用在Java中实现,建议native函数为private
- 加载jni so文件,有以下两种方式,按需选择一种即可
- 调用System.loadLibrary默认在libs/armeabi下加载so文件,名称与Android.mk里的LOCAL_MODULE一样
- 调用System.load,指定so文件的完整路径,必须写完整so文件名
如何使用JNI-HelloJni.c
#include <string.h>
#include <jni.h>
jstring
Java_com_example_hellojni_HelloJni_stringFromJNI( JNIEnv* env,
jobject thiz )
{
// C的写法
return (*env)->NewStringUTF(env, "Hello from JNI !");
// C++的写法
// return env->NewStringUTF("Hello form JNI !");
}
- 函数名规则:Java_包名_类名_Java函数名( JNIEnv* env, jobject thiz, 其他参数)
- 函数名可以用javah自动生成,命令行 $ cd bin $ javah -classpath . -jni com.example.hellojni.HelloJni
- 产生对应包名的.h文件,实现.h的函数
- JNIEnv* env:JNI环境的指针,JVM当前线程的句柄,JNI函数调用入口
- jobject thiz:调用JNI函数当前对象的引用,相当于this指针
如何使用JNI-类型对应表
Java Type | Native Type | Size in bits |
boolean | jboolean | 8, unsigned |
byte | jbyte | 8 |
char | jchar | 16, unsigned |
short | jshort | 16 |
int | jint | 32 |
long | jlong | 64 |
float | jfloat | 32 |
double | jdouble | 64 |
void | void | N/a |
byte [] | jbyteArray | 8*n |
class Object | jobject | 32 |
如何使用JNI-字符串
//访问jstring
char *str = (*env)->GetStringUTFChars(env,prompt,0);
/* this maps into regular C char* */
printf(“%s”, str); /* now it is ok to print */
(*env)->ReleaseStringUTFChars(env, prompt, str);
/* must release String to avoid memory leaks */
//返回jstring
char buf[128]; /* allocate memory for local char* in C */
scanf(“%s”, buf); /* read into char* from stdio */
return( (*env)->NewStringUTF(env, buf));
/* construct and return the Java String */
如何使用JNI-数组
jsize len = (*env)->GetArrayLength(env,arr);
jint *body = (*env)->GetIntArrayElements(env,arr,0);
for (i=0;i<len;++i){
sum += body[i];
}
(*env)->ReleaseIntArrayElements(env,arr,body,0);
/* very important – copies back to java array if copy had to be made */
- 其他类型(float, byte, double)的调用方式类似:
- Get<type>ArrayElements
- Release<type>ArrayElements
- 注意: 这些Get函数会拷贝数组,如果不需要内存拷贝,使用:Get/Set<type>ArrayRegion functions
Get Function | Release Function | Array Type |
GetBooleanArrayElements | ReleaseBooleanArrayElements | boolean |
GetByteArrayElements | ReleaseByteArrayElements | byte |
GetShortArrayElements | ReleaseShortArrayElements | short |
GetIntArrayElements | ReleaseIntArrayElements | int |
GetLongArrayElements | ReleaseLongArrayElements | long |
GetFloatArrayElements | ReleaseFloatArrayElements | float |
GetDoubleArrayElements | ReleaseDoubleArrayElements | double |
GetObjectArrayElements | ReleaseObjectArrayElements | object |
如何使用JNI-调用Java函数
jclass cls = (*env)->GetObjectClass(env, thiz);
jmethodID methodId = (*env)->GetMethodID(env, cls, "getAout", "()I");
bool use_opensles = (*env)->CallIntMethod(env, thiz, methodId) == AOUT_OPENSLES;
// ...
(*env)->DeleteLocalRef(env, cls);
- GetObjectClass获取得到调用JNI函数当前对象的类,如果不是当前对象的类,使用FindClass(JNIEnv *env, const char* className)
- GetMethodID获取调用JNI函数当前对象的类函数,getAout为Java函数名,()I为输入参数和返回值签名,签名的规则见后文
- CallIntMethod调用Java对象的getAout函数,返回值是int类型,无返回值函数调用CallVoidMethod,其他类型返回值依此类推,静态函数调用CallStaticTypeMethod
- DeleteLocalRef删除引用,cls使用完后,记得删除引用计数
//JNI回调函数的调用Java函数的方法如下
JavaVM *myVm;
jint JNI_OnLoad(JavaVM *vm, void *reserved) {
myVm = vm; // Keep a reference on the Java VM.
return JNI_VERSION_1_2;
}
static void CallbackFunc() {
JNIEnv *env;
if ((*myVm)->AttachCurrentThread(myVm, &env, NULL) < 0)
return;
// ...
(*myVm)->DetachCurrentThread(myVm);
}
- 在JNI_OnLoad里保存JavaVM
- 调用AttachCurrentThread获得JNIEnv,使用JNIEnv调用Java函数
- 调用DetachCurrentThread解绑
如何使用JNI-Java函数变量签名
Java Type | Native Type | Size in bits |
boolean | jboolean | Z |
byte | jbyte | B |
char | jchar | C |
short | jshort | S |
int | jint | I |
long | jlong | L |
float | jfloat | F |
double | jdouble | D |
void | void | V |
String | jstring | Ljava/lang/String; |
如何使用JNI-加入Log
#include <android/log.h>
#define LOG_TAG "System.out”
#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, __VA_ARGS__)
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__)
LOGI("info\n");
LOGD("debug\n");
LOGD(“The string is %s”, p_str);
- Android.mk文件增加LOCAL_LDLIBS += -llog
- 在LogCat里可以看到JNI输出的日志信息
如何使用JNI-Android.mk
LOCAL_PATH := $(call my-dir) // ①
include $(CLEAR_VARS) // ②
LOCAL_MODULE := hello-jni // ③
LOCAL_SRC_FILES := hello-jni.c // ④
include $(BUILD_SHARED_LIBRARY) // ⑤
① LOCAL_PATH用于指定源代码的目录,my-dir宏函数是编译环境提供,表示当前文件夹
② CLEAR_VARS由编译系统提供,用于清除一些LOCAL_XXX的值(如:LOCAL_MODULE, LOCAL_SRC_FILES, LOCAL_STATIC_LIBRARIES …),不清除LOCAL_PATH
③ LOCAL_MODULE定义模块名称标识,名称必须唯一,不能包含空格,编译系统会编译生成lib<modulename>.so或lib<modulename>.a文件
④ LOCAL_SRC_FILES指定要编译的.c和.cpp文件,多个文件以空格分隔,换行连接符为 \
⑤ 编译成共享库,生成so文件,还可以是BUILD_STATIC_LIBRARY,编译静态库,即a文件,.a文件必须链接到so文件里
- LOCAL_LDLIBS := -llog –lz 加载liblog.a静态库和libz.a静态库,这两个静态库由NDK提供,也可以指定搜索路径加载编译好的静态库,使用-Lpath_to_lib/xxx.a
- LOCAL_STATIC_LIBRARIES += hello-jni表示编译依赖的静态库,在项目里的其他Android.mk文件定义
- LOCAL_C_INCLUDES += include指定include路径
- LOCAL_CFLAGS += -DANDROID=1定义ANDROID宏的值为1
- 多个共享库都需要调用loadLibrary加载,包括依赖的so库,Android不会自动寻找依赖库
内存管理
- 全局引用
- NewGlobalRef
- jobject NewGlobalRef(JNIEnv *env, jobject obj);
- 创建一个新的obj全局引用,调用DeleteGlobalRef()释放全局引用
- 局部引用
- 每一个传入到native函数的参数和返回值都是本地引用,其值只有在当前线程的native函数调用期间有效,当native函数返回,引用失效。适用于所以jobject派生类,包括jclass, jstring, jarray等。
- 判断两个object引用是否一样,不能使用==,要使用函数IsSameObject。
- 不能过度分配局部引用,当创建了大量的局部引用的适合,必须调用DeleteLocalRef手动释放局部引用,Android支持最多512个局部引用。
- jfieldID和jmethodID不是引用对象。
本文小结
其实jni教程在现在的网络已经泛滥了,所以至于开发环境那些就懒得做了,结合之前公司技术分享的PPT写了这个还算详细的小结。
现在的JNI只是教别人怎么调底层的方法,但是在调用后毕竟要有回馈结果到界面给用户,打算之后会结合源码把JNI讲得更深入点,同时会用回调的逻辑。