通常在以下几种情况下考虑使用JNI:
环境变量设置
» echo $PATH
~/Mac_tras/ndk/android-ndk-r10d
jni/Android.mk
LOCAL_PATH := $(call my-dir) //表示当前目录
include $(CLEAR_VARS) //清除全局变量
LOCAL_MODULE := hello-jni //编译的目标对象,系统将会生成 'libhello-jni.so'文件,供java文件调用
LOCAL_SRC_FILES := hello-jni.c //编译的源文件,系统将根据该文件来生成目标对象
LOCAL_LDLIBS := -lz //第三方库如 -llog
include $(BUILD_EXECUTABLE) //elf $(BUILD_SHARED_LIBRARY)动态库hello-jni.so,$(BUILD_STATIC_LIBRARY)静态库hello-jni.a,$(BUILD_EXECUTABLE)是可执行文件hello-jni
jni/Application.mk
APP_ABI := armeabi x86 //目标架构如 arm/x86...
c/c++文件需和 Android.mk 一起放在 jni 目录下,编译
» ndk-build
[armeabi] Compile thumb : hello <= hello.c
[armeabi] Executable : hello
[armeabi] Install : hello => libs/armeabi/hello
运行
» adb push libs/armabi/hello /data/local/tmp
» adb shell /data/local/tmp/hello
java使用 native 方法基本流程:
将需要本地实现的 java 方法加上 native 申明
public native String stringFromJNI();
在本地代码中实现 native 方法
jstring
Java_com_example_hellojni_HelloJni_stringFromJNI( JNIEnv* env,
jobject thiz )
{
#if defined(__arm__)
#define ABI "armeabi"
#elif defined(__i386__)
#define ABI "x86"
#else
#define ABI "unknown"
#endif
return (*env)->NewStringUTF(env, "Hello from JNI ! Compiled with ABI " ABI "."); //在C中必须先对env和vm间接寻址(得到的内容仍然是一个指针),在调用方法时要将env或vm传入作为第一个参数.
}
在 java 类中加载上述动态链接库
static {
System.loadLibrary("hello-jni");
}
本地代码库的加载系统方法System.loadLibrary在static静态代码块中实现的,这是因为静态代码块只会在Java类加载时被调用,并且只会被调用一次。
在Java代码中通过System.loadLibrary方法来加载本地代码库,当本地代码动态库被JVM加载时,JVM会自动调用本地代码中的JNI_OnLoad函数
关键结构体JNIEnv&JavaVM
JNIEnv是一个与线程相关的变量,不同线程的JNIEnv彼此独立。JavaVM是虚拟机在JNI层的代表,在一个虚拟机进程中只有一个JavaVM,因此该进程的所有线程都可以使用这个JavaVM。当后台线程需要调用JNI native时,在native库中使用全局变量保存JavaVM尤为重要,这样使得后台线程能通过JavaVM获得JNIEnv。
Java语言的执行环境是Java虚拟机(JVM),JVM其实是主机环境中的一个进程,每个JVM虚拟机进程在本地环境中都有一个JavaVM结构体,该结构体在创建Java虚拟机时被返回,在JNI中创建JVM的函数为JNI_CreateJavaVM。
在JVM进程中,JavaVM是全局唯一的,而JNIEnv则是按线程分配。
JNI_CreateJavaVM(JavaVM **pvm, void **penv, void*args); //AndroidRuntime的成员函数startVm调用另外一个函数JNI_CreateJavaVM来创建以及初始化一个Dalvik虚拟机实例
每一个Dalvik虚拟机实例还有一个JNI环境列表
JNIEnv* dvmCreateJNIEnv(Thread* self) //Dalvik虚拟机启动过程中,会调用dvmCreateJNIEnv方法创建JNIEnv
JNI_CreateJavaVM主要完成以下四件事情。
为当前进程创建一个Dalvik虚拟机实例,即一个JavaVMExt对象。
为当前线程创建和初始化一个JNI环境,即一个JNIEnvExt对象,这是通过调用函数dvmCreateJNIEnv来完成的。
将参数vm_args所描述的Dalvik虚拟机启动选项拷贝到变量argv所描述的一个字符串数组中去,并且调用函数dvmStartup来初始化前面所创建的Dalvik虚拟机实例。
调用函数dvmChangeStatus将当前线程的状态设置为正在执行NATIVE代码,并且将面所创建和初始化好的JavaVMExt对象和JNIEnvExt对象通过输出参数p_vm和p_env返回给调用者
jni.h
struct _JNIEnv;
struct _JavaVM;
typedef const struct JNINativeInterface* C_JNIEnv;
#if defined(__cplusplus)
typedef _JNIEnv JNIEnv;
typedef _JavaVM JavaVM;
#else
typedef const struct JNINativeInterface* JNIEnv;
typedef const struct JNIInvokeInterface* JavaVM;
#endif
在C和C++中JavaVM的定义有所不同,在C中JavaVM是JNIInvokeInterface类型指针,而在C++中又对JNIInvokeInterface进行了一次封装,比C中少了一个参数,这也是为什么JNI代码更推荐用C++来编写的原因。
结构体存储大多都指针函数
(函数表结构)
jstring (*NewStringUTF)(JNIEnv*, const char*);
这里是一些常见的数据结构的定义,可以看出这里预处理命令#ifdef __cplusplus
,如果jni是C++实现的话,相关变量其实是一些class的指针.在C里面则更加简单,全部都是”void*“的typedef.
JNIEnv* env
实际上等价于声明 const struct JNINativeInterface** env
,因此要调用JNINativeInterface结构体内的函数指针就必须先对env间接寻址.
(env)的类型是const struct JNINativeInterface(指向JNINativeInterface结构体的指针),这时候可以用这个指针调用结构体的成员函数指针,(env)-> GetMethodID(env, jclass, const char, const char*)
关键函数
/* * Prototypes for functions exported by loadable shared libs. These are * called by JNI, not provided by JNI. */
JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void* reserved);
JNIEXPORT void JNI_OnUnload(JavaVM* vm, void* reserved);
native中 logcat
单元头部加上
#include <android/log.h>
#define LOG_TAG "I'm tag"
log.h
声明了函数int __android_log_print(int prio, const char *tag, const char *fmt, ...)
链接器指定__android_log_print
函数所在的库文件liblog.so
.在Android.mk文件中加上一行LOCAL_LDLIBS := -llog
在native函数中可以用如下语句输出了
__android_log_print(ANDROID_LOG_DEBUG,LOG_TAG,"name=%s,age=%d",”卢斌晖”,28);
第一个参数ANDROID_LOG_DEBUG是枚举常量,它在log.h中定义.
typedef enum android_LogPriority
{
ANDROID_LOG_UNKNOWN = 0,
ANDROID_LOG_DEFAULT, /* only for SetMinPriority() */
ANDROID_LOG_VERBOSE,
ANDROID_LOG_DEBUG,
ANDROID_LOG_INFO,
ANDROID_LOG_WARN,
ANDROID_LOG_ERROR,
ANDROID_LOG_FATAL,
ANDROID_LOG_SILENT, /* only for SetMinPriority(); must be last */
} android_LogPriority;
JNI访问Java成员
在JNI中通过下面的函数来获得Java运行环境中的类。
jclass FindClass(const char *name);
name:类全名,包含包名,其实包名间隔符用“/”代替“.”
为了在C/C++中表示Java的属性和方法,JNI在jni.h头文件中定义了jfieldID和jmethodID类型来分别代表Java对象的属性和方法。
使用JNIEnv提供的JNI方法,我们就可以获得属性和方法相对应的jfieldID和jmethodID
jni.h
// 根据属性签名返回 clazz类中的该属性ID
jfieldID GetFieldID(jclass clazz, const char*name, const char *sig);
// 如果获得静态属性ID,则调用下面的函数
jfieldID GetStaticFieldID(jclass clazz, constchar *name, const char *sig);
// 根据方法签名返回clazz类中该方法ID
jmethodID GetMethodID(jclass clazz, const char*name, const char *sig);
// 如果是静态方法,则调用下面的函数实现
jmethodID GetStaticMethodID(jclass clazz, constchar *name, const char *sig);
可以看到这四个方法的参数列表都是一模一样的,下面来分析下每个参数的含义:
JNI类型签名 : 可以参照 smali 语法
取得了代表属性和静态属性的jfieldID,就可以使用JNIEnv中提供的方法来获取和设置属性/静态属性
jclass
jclass代表JAVA中的java.lang.Class.
jni.h中jclass的定义如下
#ifdef __cplusplus
/*Reference types, in C++*/
class _jobject {};
class _jclass : public _jobject {}; /*_jclass继承_jobject*/
typedef _jclass* jclass;
#else
/*Reference types, in C.*/
typedef void* jobject;
typedef jobject jclass;
#endif
在C中jclass代表类型void,在C++中代表类型_jclass.因此jclass是指针,我们能够在log中输出jclass变量值
__android_log_print(ANDROID_LOG_DEBUG,"native函数中输出","地址=%p",jclass变量);
每次JAVA调用native都必须重新获得jclass,上次调用native所得到的jclass在下次调用native时再使用是无效的.下面是C代码
static jclass StudentClazz; //全局变量
jint JNI_OnLoad(JavaVM* vm,void* reserved)
{
JNIEnv* env = NULL;
jint result=-1;
if( (*vm)->GetEnv(vm,(void**)&env , JNI_VERSION_1_4) != JNI_OK)
return result;
StudentClazz=(*env)->FindClass(env,"com/example/hellojni/HelloJni$Student"); //初始化
return JNI_VERSION_1_4;
}
JNIEXPORT void JNICALL jobjectProcess(JNIEnv *env, jobject instance,jobject student,jobject flag)
{
/*StudentClazz=(*env)->FindClass(env,"com/example/hellojni/HelloJni$Student");*/
__android_log_print(ANDROID_LOG_DEBUG,"在jobjectProcess中输出","StudentClazz=%p",StudentClazz);
nameFieldId=(*env)->GetFieldID(env,StudentClazz,"name","Ljava/lang/String;");
jstring name=(jstring)((*env)->GetObjectField(env,student,nameFieldId));
}
下面是Activity的代码
static
{
System.loadLibrary("hello-jni");
}
public native void jobjectProcess(Student student,Integer flag);
public static class Student{/*省略的代码*/}
protected void onResume()
{
jobjectProcess(new Student(),new Integer(20));
super.onResume();
}
上面的C代码在JNI_OnLoad函数中对StudentClazz初始化,在jobjectProcess函数内没有再对StudentClazz赋值。此时运行程序会出错并在LogCat输出如下信息:
DEBUG/在jobjectProcess中输出(8494): StudentClazz=0x44c0a8f0
WARN/dalvikvm(8286): JNI WARNING: 0x44c0a8f0 is not a valid JNI reference
WARN/dalvikvm(8286): in Lcom/example/hellojni/HelloJni;.jobjectProcess (Lcom/example/hellojni/HelloJni$Student;Ljava/lang/Integer;)V (GetFieldID)
提示StudentClazz所指向的地址(0x44c0a8f0)不是合法的JNI引用。如果把jobjectProcess函数的第一行注释解除掉,再次给StudentClazz赋值程序便正常执行。
其实不管在哪个native函数内得到的StudentClazz值都是相同的,但每次native调用还是必须执行一次FindClass重新给StudentClazz赋值。
native的char*和JAVA的String相互转换
首先确保C/C++源文件的字符编码是UTF-8与JAVA的class文件字符编码保持一致
JNI提供了jstring来引用JAVA的String类型变量,如果native函数需要返回 String或者接受String类型参数就必须使用到jstring
而C/C++用char引用字符串起始地址,当native函数接到jstring后要转换为char所指向的字符串才能处理
http://blog.csdn.net/freechao/article/details/7692239
Android JNI机制
Dalvik虚拟机的启动过程分析
原文地址: http://www.codefrom.com/c/85