一、JNIEnv与JavaVM
1. JNIEnv 概念 : 是一个线程相关的结构体, 该结构体代表了 Java 在本线程的运行环境 ;
2. JNIEnv 与 JavaVM :
JavaVM : JavaVM 是 Java虚拟机在 JNI 层的代表, JNI 全局只有一个;
JNIEnv : JavaVM 在线程中的代表, 每个线程都有一个, JNI 中可能有很多个 JNIEnv;
3. JNIEnv 作用 :
调用 Java 函数 : JNIEnv 代表 Java 运行环境, 可以使用 JNIEnv 调用 Java 中的代码;
操作 Java 对象 : Java 对象传入 JNI 层就是 Jobject 对象, 需要使用 JNIEnv 来操作这个 Java 对象;
4. JNIEnv 体系结构
线程相关 : JNIEnv 是线程相关的, 即 在 每个线程中 都有一个 JNIEnv 指针, 每个JNIEnv 都是线程专有的, 其它线程不
能使用本线程中的 JNIEnv, 线程 A 不能调用 线程 B 的 JNIEnv;
JNIEnv 不能跨线程 : 当前线程有效 : JNIEnv 只在当前线程有效, JNIEnv 不能在 线程之间进行传递, 在同一个线程中,
多次调用 JNI层方法, 传入的 JNIEnv 是相同的;
本地方法匹配多JNIEnv : 在 Java 层定义的本地方法, 可以在不同的线程调用, 因此 可以接受不同的 JNIEnv;
JNIEnv 结构 : 由上面的代码可以得出, JNIEnv 是一个指针, 指向一个线程相关的结构, 线程相关结构指向 JNI 函数指针
数组, 这个数组中存放了大量的 JNI 函数指针, 这些指针指向了具体的 JNI 函数;
注意:JNIEnv只在当前线程中有效。本地方法不能将JNIEnv从一个线程传递到另一个线程中。相同的 Java 线程中对本地
方法多次调用时,传递给该本地方法的JNIEnv是相同的。但是,一个本地方法可被不同的 Java 线程所调用,因此可以接受
不同的 JNIEnv。
二、Application.mk:
1. Application.mk目的是描述在你的应用程序中所需要的模块(即静态库或动态库)。Application.mk文件通常被放置在 $PROJECT/jni/Application.mk
下,$PROJECT指的是您的项目。
2. 该文件内变量的作用:
2.1 APP_PROJECT_PATH:这个变量是强制性的,并且会给出应用程序工程的根目录的一个绝对路径。这是用来复制或者安装一个没有任何版本限制的JNI库,从而给APK生成工具一个详细的路径。
2.2 APP_MODULES:这个变量是可选的,如果没有定义,这个模块名字被定义在Android.mk文件中的LOCAL_MODULE 中。NDK将由在Android.mk中声明的默认的模块编译,并且包含所有的子文件(makefile文件), NDK会自动计算模块的依赖。如果APP_MODULES定义了,它必须是一个空格分隔的模块列表( 注意:NDK在R4开始改变了这个变量的行为,在此之前: 在Application.mk中,该变量是强制的必须明确列出所有需要的模块)。
2.3 APP_OPTIM:这个变量是可选的,用来定义“release”或"debug"。在编译您的应用程序模块的时候,可以用来改变优先级。“release”模式是默认的,并且会生成高度优化的二进制代码。"debug"模式生成的是未优化的二进制代码,但可以检测出很多的BUG,可以用于调试。注意:如果你的应用程序是可调试的(即: 如果你的清单文件在它的<application>标签中把android:debuggable属性设为true),默认将是debug而非release。把APP_OPTIM设置为release可以覆写它。注意:可以调试release和debug版二进制,但release版构建倾向于在调试会话中提供较少信息:一些变量被优化并且不能被检测,代码重新排序可能致使代码步进变得困难,堆栈跟踪可能不可靠,等等。
2.4 APP_CFLAGS : 这个变量是可选的, 一个C编译器开关集合,在编译任意模块的任意C或C++源代码时传递。它可以用于改变一个给定的应用程序需要依赖的模块的构建,而不是修改它自身的Android.mk文件。
2.5 APP_BUILD_SCRIPT : 默认,NDK构建系统将在 $(APP_PROJECT_PATH)/jni下寻找一个名为 Android.mk 的文件。即,对于这个文件 $(APP_PROJECT_PATH)/jni/Android.mk。如果你想重载这个行为,你可以定义APP_BUILD_SCRIPT指向一个不同的构建脚本。一个非绝对路径将总是被解析为相对于NDK顶级目录的路径。
2.6 APP_ABI : 默认情况下,NDK的编译系统根据 "armeabi" ABI生成机器代码。可以使用APP_ABI 来选择一个不同的ABI。比如:为了在ARMv7的设备上支持硬件FPU指令。可以使用 APP_ABI := armeabi-v7a或者为了支持IA-32指令集,可以使用 APP_ABI := x86或者为了同时支持这三种,可以使用 APP_ABI := armeabi armeabi-v7a x86。
2.7 APP_STL :默认,NDK构建系统提供由Android系统给出的最小C++运行时库(/system/lib/libstdc++.so)的C++头文件。然而,NDK带有另一个C++实现,你可以在你自己的应用程序中使用或链接它。定义APP_STL以选择它们其中的一个:
APP_STL := stlport_static --> static STLport library
APP_STL := stlport_shared --> shared STLport library
APP_STL := system --> default C++ runtime library
三、 Android.mk
1. Android.mk的作用:
Android.mk 用来向编译系统描述你的源代码。具体来说:该文件是GNU Makefile的一小部分,会被
编译系统解析一次或多次。你可以在每一个Android.mk 文件中定义一个或多个模块,你也可以在几个模块
中使用同一个源代码文件。每个模块属下列类型之一:
a. APK程序,一般的Android程序,编译打包生成apk文件;
b. JAVA库,java类库,编译打包生成jar文件;
c. C\C++应用程序,可执行的C\C++应用程序;
d. C\C++静态库,编译生成C\C++静态库,并打包成.a文件;
e. C\C++共享库, 编译生成共享库(动态链接库),并打包成.so, 有且只有共享库才能被安装/复制到
您的应用软件(APK)包中。
以下面代码为例:
LOCAL_PATH: = $(call my-dir)
JNI_PATH: =$(LOCAL_PATH)
include $(CLEAR_VARS)
# This is the target being built.
LOCAL_MODULE:= liblike
LOCAL_SRC_FILES := liblike.a
#include $(PREBUILT_STATIC_LIBRARY)
#include $(CLEAR_VARS)
# This is the target being built.
LOCAL_MODULE: = libhellojni
LOCAL_C_INCLUDES : = $(JNI_PATH) /include \
$(JNI_PATH) /test/inc \
$(JNI_PATH) /test/ble/inc \
$(JNI_PATH) /testlib/inc
#LOCAL_C_INCLUDES := $(JNI_PATH)/include
# All of the source files that we will compile.
LOCAL_SRC_FILES: = native_func.cpp \
test/src/test1.cpp \
test/src/test2.cpp \
test/src/test3.c
#LOCAL_C_INCLUDES += system/core/include/cutils
# All of the shared libraries we link against.
LOCAL_SHARED_LIBRARIES : = liblog libcutils libutils
#LOCAL_STATIC_LIBRARIES := liblike
LOCAL_PROGUARD_ENABLED : = disabled
# Also need the JNI headers.
LOCAL_C_INCLUDES + = \
$(JNI_H_INCLUDE)
LOCAL_PRELINK_MODULE : = false
LOCAL_LDLIBS : = -llog -lz -lm
include $(BUILD_SHARED_LIBRARY)
(1). LOCAL_PATH := $(call my-dir) ,一个Android.mk file首先必须定义好LOCAL_PATH变量。它用于在开发树中查找源文件。在这个例子中,宏函数'my-dir', 由编译系统提供,用于返回当前路径(即包含Android.mk 文件的目录)。
(2). include $(CLEAR_VARS),CLEAR_VARS由编译系统提供((可以在 android 安装目录的
/build/core/config.mk 文件看到其定义,为 CLEAR_VARS:= $(BUILD_SYSTEM)/clear_vars.mk)),指定让GNU MAKEFILE为你清除许多LOCAL_XXX变量(例如 LOCAL_MODULE, LOCAL_SRC_FILES, LOCAL_STATIC_LIBRARIES, 等等...),除LOCAL_PATH 。这是必要的,因为所有的编译控制文件都在同一个GNU MAKE执行环境中,所有的变量都是全局的。
(3). LOCAL_MODULE:= liblike 、LOCAL_SRC_FILES := liblike.a 和 LOCAL_STATIC_LIBRARIES := libshlib 其中LOCAL_MODULE变量必须定义,以标识你在Android.mk文件中描述的每个模块,名称必须是唯一的,而且不包含任何空格。 LOCAL_SRC_FILES := liblike.a 用于引用由C或者C++封装.a的库文件且该库文件的所在目录为jni的根目录。LOCAL_STATIC_LIBRARIES := liblike ,表示该模块需要使用哪些静态库,以便在编译时进行链接 。
(4). LOCAL_MODULE:= libhellojni ,LOCAL_MODULE变量必须定义,以标识你在Android.mk文件中描述的每个模块。名称必须是唯一的,而且不包含任何空格。注意编译系统会自动产生合适的前缀和后缀,换句话说,一个被命名为'like'的共享库模块,将会生成'liblike.so'文件(也可以直接已libxxx命名好)。
(5). LOCAL_SRC_FILES := test/src/test1.cpp \ ,LOCAL_SRC_FILES变量必须包含需要编译的.c文件或.cpp文件的C或C++源代码文件 。
(6). LOCAL_C_INCLUDES : = $(JNI_PATH) /include \ 表示头文件的搜索路径。默认的头文件的搜索路径是LOCAL_PATH目录。注意"\"后面不能为空格,但可以为换行符。
(7). TARGET_ARCH:目标 CPU平台的名字;TARGET_PLATFORM:Android.mk 解析的时候,目标 Android 平台的名字;TARGET_ARCH_ABI暂时只支持两个值:armeabi 和 armeabi-v7a。
(8). LOCAL_SHARED_LIBRARIES: 表示模块在运行时要依赖的共享库(动态库),在链接时就需要,以便在生成文件时嵌入其相应的信息。
(9). LOCAL_STATIC_LIBRARIES: 表示该模块需要使用哪些静态库,以便在编译时进行链接。
(10). LOCAL_SHARED_LIBRARIES: 表示模块在运行时要依赖的共享库(动态库),在链接时就需要,以便在生成文件时嵌入其相应的信息。注意:它不会附加列出的模块到编译图,也就是仍然需要在Application.mk 中把它们添加到程序要求的模块中。
(11). LOCAL_LDLIBS: 编译模块时要使用的附加的链接器选项。这对于使用‘-l’前缀传递指定库的名字是有用的。例如,LOCAL_LDLIBS := -lz表示告诉链接器生成的模块要在加载时刻链接到/system/lib/libz.so可查看 docs/STABLE-APIS.TXT 获取使用 NDK发行版能链接到的开放的系统库列表。
(12). LOCAL_ARM_MODE: 默认情况下, arm目标二进制会以 thumb 的形式生成(16 位),你可以通过设置这个变量为 arm如果你希望你的 module 是以 32 位指令的形式。
'arm' (32-bit instructions) mode. E.g.:
LOCAL_ARM_MODE := arm
注意:可以在编译的时候告诉系统针对某个源码文件进行特定的类型的编译
比如,LOCAL_SRC_FILES := foo.c bar.c.arm 这样就告诉系统总是将 bar.c 以arm的模式编译。
(13). LOCAL_C_INCLUDES + = \ $(JNI_H_INCLUDE) 表示增加jni的头文件
(14). include $(BUILD_SHARED_LIBRARY ):表示编译一个共享库。
(15). LOCAL_CFLAGS: 可选的编译器选项,在编译 C 代码文件的时候使用。注意:不要在 Android.mk 中改变 optimization/debugging 级别,只要在 Application.mk 中指定合适的信息,就会自动地为你处理这个问题,在调试期间,会让 NDK自动生成有用的数据文件。
(16). LOCAL_CXXFLAGS: 与 LOCAL_CFLAGS同理,针对 C++源文件。
(17). LOCAL_CPPFLAGS: 与 LOCAL_CFLAGS同理,但是对 C 和 C++ source files都适用。
(18). LOCAL_ALLOW_UNDEFINED_SYMBOLS: 默认情况下, 在试图编译一个共享库时,任何未定义的引用将导致一个“未定义的符号”错误。这对于在源代码文件中捕捉错误会有很大的帮助。然而,如果因为某些原因,需要不启动这项检查,可把这个变量设为'true'。注意相应的共享库可能在运行时加载失败。(这个一般尽量不要去设为 true)。
(19). LOCAL_MODULE_PATH 和 LOCAL_UNSTRIPPED_PATH
在 Android.mk 文件中, 还可以用LOCAL_MODULE_PATH 和LOCAL_UNSTRIPPED_PATH指定最后的目标安装路径.不同的文件系统路径用以下的宏进行选择:
TARGET_ROOT_OUT:表示根文件系统。
TARGET_OUT:表示 system文件系统。
TARGET_OUT_DATA:表示 data文件系统。
用法如:LOCAL_MODULE_PATH :=$(TARGET_ROOT_OUT)
至于LOCAL_MODULE_PATH 和LOCAL_UNSTRIPPED_PATH的区别,暂时还不清楚。
2. GNU Make‘功能’宏
GNU Make‘功能’宏,必须通过使用'$(call )'来调用,调用他们将返回文本化的信息。
(1). my-dir:返回当前 Android.mk 所在的目录的路径,相对于 NDK 编译系统的顶层。这是有用的,
在 Android.mk 文件的开头如此定义: LOCAL_PATH := $(call my-dir)
(2). all-subdir-makefiles: 返回一个位于当前'my-dir'路径的子目录中的所有Android.mk的列表。
例如,看下面的目录层次:
sources/foo/Android.mk
sources/foo/lib1/Android.mk
sources/foo/lib2/Android.mk
如果 sources/foo/Android.mk 包含一行:include $(call all-subdir-makefiles),那么它就会自动包含 sources/foo/lib1/Android.mk 和 sources/foo/lib2/Android.mk。这项功能用于向编译系统提供深层次嵌套的代码目录层次。注意,在默认情况下,NDK 将会只搜索在 sources/*/Android.mk 中的文件。
(3). this-makefile: 返回当前Makefile 的路径(即这个函数调用的地方)
(4). parent-makefile: 返回调用树中父 Makefile 路径。即包含当前Makefile的Makefile 路径。
(5). grand-parent-makefile:返回调用树中父Makefile的父Makefile的路径
3. Android.mk 使用模板
在一个 Android.mk 中可以生成多个APK应用程序,JAVA库,C\C++可执行程序,C\C++动态库和
C\C++静态库。
(1). 编译JAVA库模板
LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
# Build all java files in the java subdirectory
LOCAL_SRC_FILES := $(call all-subdir-java-files)
# Any libraries that this library depends on
LOCAL_JAVA_LIBRARIES := android.test.runner
# The name of the jar file to create
LOCAL_MODULE := sample
# Build a static jar file.
include $(BUILD_STATIC_JAVA_LIBRARY)
注:LOCAL_JAVA_LIBRARIES := android.test.runner表示生成的JAVA库的jar文件名
(2). 编译C/C++应用程序模板如下:
LOCAL_PATH := $(call my-dir)
#include $(CLEAR_VARS)
LOCAL_SRC_FILES := main.c
LOCAL_MODULE := test_exe
#LOCAL_C_INCLUDES :=
#LOCAL_STATIC_LIBRARIES :=
#LOCAL_SHARED_LIBRARIES :=
include $(BUILD_EXECUTABLE)
注:‘:=’是赋值的意思,'+='是追加的意思,‘$’表示引用某变量的值
LOCAL_SRC_FILES中加入源文件路径,LOCAL_C_INCLUDES中加入需要的头文件搜索路径
LOCAL_STATIC_LIBRARIES 加入所需要链接的静态库(*.a)的名称,
LOCAL_SHARED_LIBRARIES 中加入所需要链接的动态库(*.so)的名称,
LOCAL_MODULE表示模块最终的名称,BUILD_EXECUTABLE 表示以一个可执行程序的方式
进行编译。
(3). 编译C\C++静态库
LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
LOCAL_SRC_FILES := \
helloworld.c
LOCAL_MODULE:= libtest_static
#LOCAL_C_INCLUDES :=
#LOCAL_STATIC_LIBRARIES :=
#LOCAL_SHARED_LIBRARIES :=
include $(BUILD_STATIC_LIBRARY)
和上面相似,BUILD_STATIC_LIBRARY 表示编译一个静态库。
(4). 编译C\C++动态库的模板
LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
LOCAL_SRC_FILES := helloworld.c
LOCAL_MODULE := libtest_shared
TARGET_PRELINK_MODULES := false
#LOCAL_C_INCLUDES :=
#LOCAL_STATIC_LIBRARIES :=
#LOCAL_SHARED_LIBRARIES :=
include $(BUILD_SHARED_LIBRARY)
和上面相似,BUILD_SHARED_LIBRARY 表示编译一个共享库。
以上三者的生成结果分别在如下目录中,generic 依具体 target 会变:
out/target/product/generic/obj/JAVA_LIBRARIES
out/target/product/generic/obj/EXECUTABLE
out/target/product/generic/obj/STATIC_LIBRARY
out/target/product/generic/obj/SHARED_LIBRARY
每个模块的目标文件夹分别为:
a. APK程序:XXX_intermediates
b. JAVA库程序:XXX_intermediates
这里的XXX
c. C\C++可执行程序:XXX_intermediates
d. C\C++静态库: XXX_static_intermediates
e. C\C++动态库: XXX_shared_intermediates
四、 使用RegisterNatives方法注册手动注册native方法:
1. 在对应jni文件中,创建以JNIEXPORT + 返回类型(以j开头的类型或者void) + JNICALL为修饰符的方
法且第一个和第二个参数的类型分别为JNIEnv *和jobject:(以在jni文件创建的方法为例)
JNIEXPORT jint JNICALL native_connect(JNIEnv *env, jobject this, jshort value){
}
JNIEXPORT jint JNICALL native_pisConnect(JNIEnv *env, jclass thiz,
jint type, jstring infor, jstring pwd, jstring host){
}
public static void sendMessage(int id,byte[] data){
}
2. 创建一个java,专门用于调用jni接口:
public class MyInterface{
static{
System.loadLibrary(“hellojni”);//so库名称去掉开头的lib
}
public static native int connect(short value);
public static native int pisConnect(int type, String infor, String pwd,String host);
}
3. 创建JNINativeMethod数组(用于实现java文件中和jni文件中方法的一一对应):
JNINativeMethod methods = { "connect", "(S)I", (void*) native_connect }, { "pisProcSet","()V", (void*) native_proc_set },
{ "pisConnect","(ILjava/lang/String;Ljava/lang/String;
Ljava/lang/String;)I", (void*) native_pisConnect }}
4. 使用RegisterNatives方法注册native方法
a. 使用步骤
//创建java类(调用jni接口的类)的指针
const char* className = "com/ryan/jniutil/MyInterface";
jclass clazz;
clazz = env->FindClass(className);
env->RegisterNatives(clazz, methods, sizeof(methods) / sizeof(methods[0]));
RegisterNatives方法的参数和返回值:
第一个参数: 根据jni自有的方法获取java中使用jni接口的类;
第二个参数: JNINativeMethod类型的数组;
第三个参数: 数组的长度。
返回值: 如果返回值小于0,则native方法注册失败;反之,注册成功;
b. 所有的native方法的注册是在JNI_OnLoad进行的,所以在JNI_OnLoad方法中通过env-
>RegisterNatives实现native方法注册
五、 JNI_OnLoad方法的使用
1. 作用:
当共享库第一次被加载的时候会被回调。该方法用于注册native方法,便于在java类中使用,
JNI_OnLoad函数JNI规范定义的;该方法中可以进行一些初始化工作,比如注册函数映射表,缓存一些变
量等,最后返回当前环境所支持的JNI环境。
2. 实例:
jint JNI_OnLoad(JavaVM* vm, void* reserved){
JNIEnv* env = NULL;
jint result = -1;
if (vm->GetEnv((void**) &env, JNI_VERSION_1_4) != JNI_OK) {
goto bail;
}
const char* className = "com/ryan/jniutil/MyInterface";
jclass clazz;
clazz = env->FindClass(className);
env->RegisterNatives(clazz, methods, sizeof(methods) / sizeof(methods[0]));
result = JNI_VERSION_1_4;
bail:
return result;
}
六、 根据获取的ID,来取得和设置属性,以及调用方法
1. 获得、设置属性和静态属性
取得了代表属性和静态属性的jfieldID,就可以使用JNIEnv中提供的方法来获取和设置属性/静态属性。
获取属性/静态属性的形式:
Get<Type>Field GetStatic<Type>Field。
设置属性/静态属性的形式:
Set<Type>Field SetStatic<Type>Field。
取得成员属性:
jobject GetObjectField(jobjectobj, jfieldID fieldID);
jboolean GetBooleanField(jobjectobj, jfieldID fieldID);
jbyte GetByteField(jobjectobj, jfieldID fieldID);
取得静态属性:
jobject GetStaticObjectField(jclassclazz, jfieldID fieldID);
jboolean GetStaticBooleanField(jclassclazz, jfieldID fieldID);
jbyte GetStaticByteField(jclassclazz, jfieldID fieldID);
Get方法的第一个参数代表要获取的属性所属对象或jclass对象,第二个参数即属性ID。
设置成员属性:
void SetObjectField(jobjectobj, jfieldID fieldID, jobject val);
void SetBooleanField(jobjectobj, jfieldID fieldID,jboolean val);
void SetByteField(jobjectobj, jfieldID fieldID, jbyte val);
设置静态属性:
void SetStaticObjectField(jobjectobj, jfieldID fieldID, jobject val);
void SetStaticBooleanField(jobjectobj, jfieldID fieldID,jboolean val);
void SetStaticByteField(jobjectobj, jfieldID fieldID, jbyte val);
Set方法的第一个参数代表要设置的属性所属的对象或jclass对象,第二个参数即属性ID,第三个参数代表要设置的值。
2. 调用方法
取得了代表方法和静态方法的jmethodID,就可以用在JNIEnv中提供的方法来调用方法和静态方法。
调用普通方法:
Call<Type>Method(jobject obj, jmethodID methodID,...);
Call<Type>MethodV(jobject obj, jmethodID methodID,va_listargs);
Call<Type>tMethodA(jobject obj, jmethodID methodID,constjvalue *args);
调用静态方法:
CallStatic<Type>Method(jclass clazz, jmethodID methodID,...);
CallStatic<Type>MethodV(jclass clazz, jmethodID methodID,va_listargs);
CallStatic<Type>tMethodA(jclass clazz, jmethodID methodID,constjvalue *args);
上面的Type这个方法的返回值类型,如Int,Char,Byte等等。
第一个参数代表调用的这个方法所属于的对象,或者这个静态方法所属的类。
第二个参数代表jmethodID。
后面的参数,就代表这个方法的参数列表了。
七、 本地创建Java对象
1. 本地代码创建Java对象
JNIEnv提供了下面几个方法来创建一个Java对象:
jobject NewObject(jclass clazz, jmethodID methodID,...);
jobject NewObjectV(jclass clazz, jmethodIDmethodID,va_list args);
jobject NewObjectA(jclass clazz, jmethodID methodID,const jvalue *args) ;
本地创建Java对象的函数和前面本地调用Java方法很类似:
第一个参数jclass class 代表的你要创建哪个类的对象;
第二个参数jmethodID methodID 代表你要使用哪个构造方法ID来创建这个对象。
只要有jclass和jmethodID ,我们就可以在本地方法创建这个Java类的对象。由于Java的构造方法的特点,方法名与类名一样,并且
没有返回值,所以对于获得构造方法的ID的方法 env->GetMethodID(clazz,method_name ,sig)中的第二个参数是固定为类名,第
三个参数和要调用的构造方法有关,默认的Java构造方法没有返回值,没有参数。例如:
jclassclazz=env->FindClass("java/util/Date"); //取得java.util.Date类的jclass对象
jmethodID id_date=env->GetMethodID(clazz,"Date","()V"); //取得某一个构造方法的jmethodID
jmethodID constr = env->GetMethodID (clazz , "<init>" , "()V" ); //取得默认的构造方法的jmethodID
jobject date=env->NewObject(clazz,id_date); //调用NewObject方法创建java.util.Date对象
2. 本地方法对Java字符串的操作
在 Java中,字符串String对象是Unicoode(UTF-16)编码,每个字符不论是中文还是英文还是符号,一个字符总是占用两个字
节。在 C/C++中一个字符是一个字节, C/C++中的宽字符是两个字节的。所以Java通过JNI接口可以将Java的字符串转换到C/C++的
宽字符串(wchar_t*),或是传回一个 UTF-8的字符串(char*)到C/C++,反过来,C/C++可以通过一个宽字符串,或是一个UTF-8
编码的字符串创建一个Java端的 String对象。
例:
在Java端有一个字符串 String str="abcde";,在本地方法中取得它并且输出:
void native_string_operation (JNIEnv * env, jobject obj)
{
//取得该字符串的jfieldID
jfieldID id_string = env->GetFieldID(env->GetObjectClass(obj), "str", "Ljava/lang/String;");
jstring string = (jstring)(env->GetObjectField(obj, id_string)); //取得该字符串,强转为jstring类型。
printf("%s",string);
}
由上面的代码可知,从java端取得的String属性或者是方法返回值的String对象,对应在JNI中都是jstring类型,它并不是C/C++中的
字符串。所以,我们需要对取得的 jstring类型的字符串进行一系列的转换,才能使用。
JNIEnv提供了一系列的方法来操作字符串:
const jchar *GetStringChars(jstring str, jboolean*isCopy) //将一个jstring对象,转换为(UTF-16)编码的宽字符串(jchar*)。
const char *GetStringUTFChars(jstring str,jboolean *isCopy)//将一个jstring对象,转换为(UTF-8)编码的字符串(char*)。
这两个函数的参数中,第一个参数传入一个指向Java 中String对象的jstring引用。第二个参数传入的是一个jboolean的指针,其值可
以为NULL、JNI_TRUE、JNI_FLASE。如果为JNI_TRUE则表示开辟内存,然后把Java中的String拷贝到这个内存中,然后返回指向这
个内存地址的指针。如果为JNI_FALSE,则直接返回指向Java中String的内存指针。这时不要改变这个内存中的内容,这将破坏String
在Java中始终是常量的规则。如果是NULL,则表示不关心是否拷贝字符串。使用这两个函数取得的字符,在不适用的时候,要分别对
应的使用下面两个函数来释放内存:
RealeaseStringChars(jstring jstr, const jchar*str)
RealeaseStringUTFChars(jstring jstr, constchar* str)
//第一个参数指定一个jstring变量,即要释放的本地字符串的资源
//第二个参数就是要释放的本地字符串
3. 创建Java String对象
jstring NewString(const jchar *unicode, jsize len) // 根据传入的宽字符串创建一个Java String对象
jstring NewStringUTF(const char *utf) // 根据传入的UTF-8字符串创建一个Java String对象
4. 返回Java String对象的字符串长度
jsize GetStringLength(jstring jstr) //返回一个java String对象的字符串长度
jsize GetStringUTFLength(jstring jstr) //返回一个java String对象经过UTF-8编码后的字符串长度
八、 Java数组在本地代码中的处理
1. 通过GetFieldID获取一个Java数组变量的ID,然后用GetObjectFiled取得该数组变量到本地方法,返回值为jobject,然后我们可
以强制转换为j<Type>Array类型。
typedef jarray jbooleanArray;
typedef jarray jbyteArray;
typedef jarray jcharArray;
typedef jarray jshortArray;
typedef jarray jintArray;
typedef jarray jlongArray;
typedef jarray jfloatArray;
typedef jarray jdoubleArray;
typedef jarray jobjectArray;
j<Type>Array类型是JNI定义的一个对象类型,并不是C/C++的数组,如int[]数组,double[]数组等等。所以我们要j<Type>Array
类型转换为C/C++中的数组来操作。JNIEnv定义了一系列的方法来把一个j<Type>Array类型转换为C/C++数组或把C/C++数组转换
为j<Type>Array。
jsize GetArrayLength(jarray array) // 获得数组的长度
jobjectArray NewObjectArray(jsize len, jclass clazz, jobject init) // 创建对象数组,指定其大小,init值可以为NULL
jobject GetObjectArrayElement(jobjectArray array, jsize index) // 获得数组的指定元素
void SetObjectArrayElement(jobjectArray array, jsize index,jobject val) // 设置数组元素
2. 创建数组:
jbooleanArray NewBooleanArray(jsize len) // 创建Boolean数组,指定其大小
jbyteArray NewByteArray(jsize len) //下面的都类似,创建对应类型的数组,并指定大小
jcharArray NewCharArray(jsize len)
jshortArray NewShortArray(jsize len)
jintArray NewIntArray(jsize len)
jlongArray NewLongArray(jsize len)
jfloatArray NewFloatArray(jsize len)
jdoubleArray NewDoubleArray(jsize len)
3. 获得指定类型数组的元素:
jboolean * GetBooleanArrayElements(jbooleanArray array,jboolean *isCopy)
jbyte * GetByteArrayElements(jbyteArray array, jboolean *isCopy)
jchar * GetCharArrayElements(jcharArray array, jboolean *isCopy)
jshort * GetShortArrayElements(jshortArray array, jboolean *isCopy)
jint * GetIntArrayElements(jintArray array, jboolean *isCopy)
jlong * GetLongArrayElements(jlongArray array, jboolean *isCopy)
jfloat * GetFloatArrayElements(jfloatArray array, jboolean *isCopy)
jdouble * GetDoubleArrayElements(jdoubleArray array ,jboolean *isCopy)
4. 释放指定数组:
void ReleaseBooleanArrayElements(jbooleanArrayarray, jboolean *elems, jint mode)
void ReleaseByteArrayElements(jbyteArray array, jbyte *elems, jint mode)
void ReleaseCharArrayElements(jcharArray array, jchar *elems, jint mode)
void ReleaseShortArrayElements(jshortArray array, jshort *elems, jint mode)
void ReleaseIntArrayElements(jintArray array, jint *elems, jint mode)
void ReleaseLongArrayElements(jlongArray array, jlong *elems, jint mode)
void ReleaseFloatArrayElements(jfloatArray array, jfloat *elems, jint mode)
void ReleaseDoubleArrayElements(jdoubleArray array, jdouble *elems, jint mode)
void * GetPrimitiveArrayCritical(jarray array, jboolean *isCopy)
void ReleasePrimitiveArrayCritical(jarray array, void* carray, jint mode)
5. 获取数组的某段数据(把数组中的元素存放到buf指针指向的地址,从而获取指向array数组的buf指针):
void GetBooleanArrayRegion(jbooleanArray array, jsize start, jsize len, jboolean *buf)
void GetByteArrayRegion(jbyteArray array, jsize start, jsize len, jbyte *buf)
void GetCharArrayRegion(jcharArray array, jsize start, jsize len, jchar *buf)
void GetShortArrayRegion(jshortArray array, jsize start, jsize len, jshort *buf)
void GetIntArrayRegion(jintArray array, jsize start, jsize len, jint *buf)
void GetLongArrayRegion(jlongArray array, jsize start, jsize len, jlong *buf)
void GetFloatArrayRegion(jfloatArray array, jsize start, jsize len, jfloat *buf)
void GetDoubleArrayRegion(jdoubleArray array, jsize start, jsize len, jdouble *buf)
6. 设置数组元素(通过指针buf把指向的元素放到array):
void SetBooleanArrayRegion(jbooleanArray array, jsizestart, jsize len,const jboolean *buf)
void SetByteArrayRegion(jbyteArray array, jsize start,jsize len,const jbyte *buf)
void SetCharArrayRegion(jcharArray array, jsize start,jsize len,const jchar *buf)
void SetShortArrayRegion(jshortArray array, jsizestart, jsize len,const jshort *buf)
void SetIntArrayRegion(jintArray array, jsize start,jsize len,const jint *buf)
void SetLongArrayRegion(jlongArray array, jsize start,jsize len,const jlong *buf)
void SetFloatArrayRegion(jfloatArray array, jsizestart, jsize len,const jfloat *buf)
void SetDoubleArrayRegion(jdoubleArray array, jsizestart, jsize len,const jdouble *buf)
7. 调用父类方法:
CallNonvirtual<TYPE>Method 在JNI中定义的CallNonvirtual<TYPE>Method就能够对子类对象调用父类方法的功能。如果想要调用一个对象的父类方法,而不是子类的这个方法的话,就
可以使用CallNonvirtual<TYPE>Method
例:
package video1;
public class Father {
public void function(){
System.out.println("Father:function");
}
}
package video1;
public class Child extends Father {
public void function() {
System.out.println("Child:function");
}
}
package video1;
import java.util.Date;
public class TestNative {
public native void sayHello();
public Father p = new Child();
public static void main(String[] args) {
System.loadLibrary("nativeCode");//Java类 中加载DLL,然后调用声明的native方法
TestNative tst=new TestNative();
tst.sayHello();
}
}
- C/C++代码
#include "video1_TestNative.h"
#include<iostream>
using namespace std;
JNIEXPORT void JNICALL Java_cn_itcast_TestNative_sayHello (JNIEnv *env, jobject obj){
jfieldID id_p = env->GetFieldID (clazz_TestNative,"p","Lvideo1/Father;");
jobject p = env->GetObjectField(obj,id_p);//取得属性
jclass clazz_Father = env->FindClass ("video1/Father");//找到Father类
jmethodID id_Father_function = env->GetMethodID(clazz_Father,"function","()V");//获取Father类里面方法的ID
//调用方法,取得的是子类方法
env->CallVoidMethod(p,id_Father_function);
//相当于如下java代码
//Father p = tst.p;
//p.function();
env->CallNonvirtualVoidMethod(p,clazz_Father,id_Father_function);//调用父类方法
}
8. 上面是JNIEnv提供给本地代码调用的数组操作函数,大致可以分为下面几类:
a. 获取数组的长度:
jsize GetArrayLength(jarray array);
b. 对象类型数组的操作:
jobjectArray NewObjectArray(jsize len, jclass clazz,jobject init) // 创建
jobject GetObjectArrayElement(jobjectArray array, jsize index) // 获得元素
void SetObjectArrayElement(jobjectArray array, jsize index,jobject val) // 设置元素
JNI没有提供直接把Java的对象类型数组(Object[ ])直接转到C++中的jobject[ ]数组的函数。而是直接通过
Get/SetObjectArrayElement这样的函数来对Java的Object[ ]数组进行操作
c. 对基本数据类型数组的操作:
基本数据类型数组的操作方法比较多,大致可以分为如下几类:
Get<Type>ArrayElements/Realease<Type>ArrayElements;
Get<Type>ArrayElements(<Type>Array arr, jboolean*isCopied);
这 类函数可以把Java基本类型的数组转换到C/C++中的数组。有两种处理方式,一是拷贝一份传回本地代码,另一种是把指向
Java数组的指针直接传回到 本地代码,处理完本地化的数组后,通过Realease<Type>ArrayElements来释放数组。处理方式由
Get方法的第二个参 数isCopied来决定。
Realease<Type>ArrayElements(<Type>Arrayarr,<Type>* array, jint mode)用这个函数可以选择将如何处理Java和
C/C++本地数组:
其第三个参数mode可以取下面的值:
0:对Java的数组进行更新并释放C/C++的数组
JNI_COMMIT:对Java的数组进行更新但是不释放C/C++的数组
JNI_ABORT:对Java的数组不进行更新,释放C/C++的数组
例如:
Test.java
public class Test {
privateint [] arrays=new int[]{1,2,3,4,5};
publicnative void show();
static{
System.loadLibrary("NativeTest");
}
publicstatic void main(String[] args) {
newTest().show();
}
}
本地方法:
void native_test_show(JNIEnv * env, jobject obj)
{
jfieldIDid_arrsys=env->GetFieldID(env->GetObjectClass(obj),"arrays","[I");
jintArrayarr=(jintArray)(env->GetObjectField(obj, id_arrsys));
jint*int_arr=env->GetIntArrayElements(arr,NULL);
jsizelen=env->GetArrayLength(arr);
for(inti=0; i<len; i++)
{
cout<<int_arr[i]<<endl;
}
env->ReleaseIntArrayElements(arr,int_arr,JNI_ABORT);
}
九、局部引用与全局引用
1. JNI中的引用变量
Java 代码与本地代码里在进行参数传递与返回值复制的时候,要注意数据类型的匹配。对于int, char等基本类型直接进行拷贝即
可,对于Java中的对象类型,通过传递引用实现。VM保证所有的Java对象正确的传递给了本地代码,并且维持这些引用,因此这些对
象不会被Java的gc(垃圾收集器)回收。因此,本地代码必须有一种方式来通知VM本地代码不再使用这些Java对象,让gc来回收这
些对象。
JNI将传递给本地代码的对象分为两种:局部引用和全局引用。
局部引用:只在上层Java调用本地代码的函数内有效,当本地方法返回时,局部引用自动回收。
全局引用:只有显示通知VM时,全局引用才会被回收,否则一直有效,Java的gc不会释放该引用的对象。
默认的话,传递给本地代码的引用是局部引用。所有的JNI函数的返回值都是局部引用。
jstring MyNewString(JNIEnv *env, jchar *chars, jint len)
{
static jclassstringClass = NULL; //static 不能保存一个局部引用
jmethodID cid;
jcharArrayelemArr;
jstringresult;
if(stringClass == NULL) {
stringClass = (*env)->FindClass(env, "java/lang/String"); // 局部引用
if(stringClass == NULL) {
return NULL; /* exception thrown */
}
}
/* It iswrong to use the cached stringClass here,
because itmay be invalid. */
cid =(*env)->GetMethodID(env, stringClass, "<init>","([C)V");
...
elemArr =(*env)->NewCharArray(env, len);
...
result =(*env)->NewObject(env, stringClass, cid, elemArr);
(*env)->DeleteLocalRef(env, elemArr);
return result;
}
2. 手动释放局部引用情况
虽然局部引用会在本地代码执行之后自动释放,但是有下列情况时,要手动释放:本地代码访问一个很大的Java对象时,在使
用该对象后,本地代码要去执行比较复杂耗时的运算时,由于本地代码还没有返回,Java收集器无法释放该本地引用的对象,这时,应
该手动释放掉该引用对象。
/* A native method implementation */
JNIEXPORT void JNICALL func(JNIEnv *env, jobject this)
{
lref =... /* a large Java object*/
... /* last use of lref */
(*env)->DeleteLocalRef(env, lref);
lengthyComputation(); /* maytake some time */
return; /* all local refs are freed */
}
这个情形的实质,就是允许程序在native方法执行期间,java的垃圾回收机制有机会回收native代码不在访问的对象。本地代码创建了大量局部引用,这可能会导致JNI局部引用表溢出,此时有必要及时地删除那些不再被使用的局部引用。比如:在本地代码里创建一个很大的对象数组。
for (i = 0; i < len; i++) {
jstring jstr= (*env)->GetObjectArrayElement(env, arr, i);
... /*process jstr */
(*env)->DeleteLocalRef(env, jstr);
}
在上述循环中,每次都有可能创建一个巨大的字符串数组。在每个迭代之后,native代码需要显示地释放指向字符串元素的局部引用。
创建的工具函数,它会被未知的代码调用,在工具函数里使用完的引用要及时释放。不返回的本地函数。例如,一个可能进入无限事件分发的循环中的方法。此时在循环中释放局部引用,是至关重要的,这样才能不会无限期地累积,进而导致内存泄露。
局部引用只在创建它们的线程里有效,本地代码不能将局部引用在多线程间传递。一个线程想要调用另一个线程创建的局部引用是不被允许的。将一个局部引用保存到全局变量中,然后在其它线程中使用它,这是一种错误的编程。
3. 全局引用
在一个本地方法被多次调用时,可以使用一个全局引用跨越它们。一个全局引用可以跨越多个线程,并且在被程序员手动释放之前,一直有效。和局部引用一样,全局引用保证了所引用的对象不会被垃圾回收。
JNI允许程序员通过局部引用来创建全局引用, 全局引用只能由NewGlobalRef函数创建。下面是一个使用全局引用例子:
jstring MyNewString(JNIEnv *env, jchar *chars, jint len)
{
static jclassstringClass = NULL;
...
if(stringClass == NULL) {
jclasslocalRefCls =
(*env)->FindClass(env, "java/lang/String");
if(localRefCls == NULL) {
return NULL;
}
/* Createa global reference */
stringClass = (*env)->NewGlobalRef(env, localRefCls);
/* Thelocal reference is no longer useful */
(*env)->DeleteLocalRef(env, localRefCls);
/* Is theglobal reference created successfully? */
if(stringClass == NULL) {
return NULL; /* out of memory exception thrown */
}
}
...
}
4. 释放全局引用
在native代码不再需要访问一个全局引用的时候,应该调用DeleteGlobalRef来释放它。如果调用这个函数失败,Java VM将不会
回收对应的对象。
5. JNIEnv的使用:
a. JNIEnv指针的获取,以及连接当前线程:
JNIEnv *env = NULL;
int env_status = g_JavaVM->GetEnv((void **)&env , JNI_VERSION_1_4); //其中 g_JavaVM 是JavaVM结构体指针
jint attachResult = g_JavaVM->AttachCurrentThread(&env ,NULL);
b. 释放当前线程:
g_JavaVM->DetachCurrentThread() //其中 g_JavaVM 是JavaVM结构体指针
十、本地C代码中创建Java对象及本地JNI对象的保存
1. Android中Bitmap对象的创建
通 常在JVM里创建Java的对象就是创建Java类的实例,再调用Java类的构造方法。而有时Java的对象需要在本地代码里创建。以Android中 的Bitmap的构建为例,Bitmap中并没有Java对象创建的代码及外部能访问的构造方法,所以它的实例化是在JNI的c中实现的。BitmapFactory.java中提供了得到Bitmap的方法,时序简化为:
BitmapFactory.java->BitmapFactory.cpp -> GraphicsJNI::createBitmap() [graphics.cpp]
GraphicsJNI::createBitmap()[graphics.cpp]的实现:
jobjectGraphicsJNI::createBitmap(JNIEnv* env, SkBitmap*bitmap, bool isMutable, jbyteArray ninepatch, int density)
{
SkASSERT(bitmap != NULL);
SkASSERT(NULL!= bitmap->pixelRef());
jobject obj=env->AllocObject(gBitmap_class);
if (obj) {
env->CallVoidMethod(obj,gBitmap_constructorMethodID, (jint)bitmap,isMutable, ninepatch, density);
if(hasException(env)) {
obj =NULL;
}
}
return obj;
}
而gBitmap_class的得到是通过:
jclass c=env->FindClass("android/graphics/Bitmap");
gBitmap_class =(jclass)env->NewGlobalRef(c);
//gBitmap_constructorMethodID是Bitmap的构造方法(方法名用”<init>”)的jmethodID:
gBitmap_constructorMethodID=env->GetMethodID(gBitmap_class, "<init>", "(IZ[BI)V");
总结一下,c中如何访问Java对象的属性:
a. 通过JNIEnv::FindClass()找到对应的jclass;
b. 通过JNIEnv::GetMethodID()找到类的构造方法的jfieldID;
c. 通过JNIEnv::AllocObject创建该类的对象;
d. 通过JNIEnv::CallVoidMethod()调用Java对象的构造方法。
2. 本地JNI对象保存在Java环境中
C代码中某次被调用时生成的对象,在其他函数调用时是不可见的,虽然可以设置全局变量但那不是好的解决方式,Android中通常是在Java域中定义一个int型的变量,在本地代码生成对象的地方,与这个Java域的变量关联,在别的使用到的地方,再从这个变量中取值。
以JNICameraContext为例来说明:
JNICameraContext是android_hardware_camera.cpp中定义的类型,并会在本地代码中生成对象并与Java中android.hardware.Camera的mNativeContext关联。
在注册native函数之前,C中就已经把Java域中的属性的jfieldID得到了。通过下列方法:
jclass clazz =env->FindClass("android/hardware/Camera ");
jfieldID field = env->GetFieldID(clazz, "mNativeContext","I");
如果执行成功,把field保存到fileds.context成员变量中。
生成cpp对象时,通过JNIEnv::SetIntField()设置为Java对象的属性
static void android_hardware_Camera_native_setup(JNIEnv*env, jobject thiz, jobjectweak_this, jintcameraId)
{
// …
sp<JNICameraContext>context = new JNICameraContext(env, weak_this,clazz, camera);
// …
// 该处通过context.get()得到context对象的地址,保存到了Java中的mNativeContext属性里
env->SetIntField(thiz,fields.context, (int)context.get());
}
而要使用时,又通过JNIEnv::GetIntField()获取Java对象的属性,并转化为JNICameraContext类型:
JNICameraContext* context=reinterpret_cast<JNICameraContext*>(env->GetIntField(thiz,fields.context));
if (context!= NULL) {
// …
}
总结一下,c++中生成的对象如何保存和使用:
a. 通过JNIEnv::FindClass()找到对应的jclass;
b. 通过JNIEnv::GetFieldID()找到类中属性的jfieldID;
c. 某个调用过程中,生成cpp对象时,通过JNIEnv::SetIntField()设置为Java对象的属性;
d. 另外的调用过程中,通过JNIEnv::GetIntField()获取Java对象的属性,再转化为真实的对象类型。
本文章中的一些内容来自于:
http://www.cnblogs.com/yaozhongxiao/archive/2012/03/06/2381586.html
http://www.cnblogs.com/hnrainll/archive/2012/12/18/2822711.html
http://blog.csdn.net/banketree/article/details/40535325
注:jni的语法规则可参考:
共同学习,写下你的评论
评论加载中...
作者其他优质文章