JNI提供了一种机制,使得在Java 代码中可以使用 C/C++ 的本地层代码,这种使用主要是指在 Java 代码中调用 C/C++ 代码。这种机制为我们开启了一扇门,一扇将Java 代码与广阔的 C/C++ 本地层连接起来的门。
基于 android-ndk-r8d 提供的sample 程序——android-ndk-r8d/samples/hello-jni 的代码,我们来看一下,如何在 android 应用开发中使用 JNI。
首先来看在 Java 代码中需要做些什么事情:
package com.example.hellojni;
import android.app.Activity;
import android.widget.TextView;
import android.os.Bundle;
public class HelloJni extends Activity
{
/** Called when the activity is first created. */
@Override
public void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
/* Create a TextView and set its content.
* the text is retrieved by calling a native
* function.
*/
TextView tv = new TextView(this);
tv.setText( stringFromJNI() );
setContentView(tv);
}
/* A native method that is implemented by the
* 'hello-jni' native library, which is packaged
* with this application.
*/
public native String stringFromJNI();
/* This is another native method declaration that is *not*
* implemented by 'hello-jni'. This is simply to show that
* you can declare as many native methods in your Java code
* as you want, their implementation is searched in the
* currently loaded native libraries only the first time
* you call them.
*
* Trying to call this function will result in a
* java.lang.UnsatisfiedLinkError exception !
*/
public native String unimplementedStringFromJNI();
/* this is used to load the 'hello-jni' library on application
* startup. The library has already been unpacked into
* /data/data/com.example.hellojni/lib/libhello-jni.so at
* installation time by the package manager.
*/
static {
System.loadLibrary("hello-jni");
}
}
上面那些带 native 修饰符的方法,就是在 Java 代码中调用,实现却在本地层的方法。native 修饰符告诉Java 编译器,暂时不需要去链接这些方法。
那Java的机制到底要如何去调用到那个在本地层实现的Java method的呢?可以看到最后的那个static的block,里面有调用到System.loadLibrary(),这个动作完成的事情,其实就是把实现了java方法的shared library加载进来。java的机制会在这个shared library中找到那个java方法的实现。
可是,JVM是怎么样知道java方法和本底层函数的对应关系的呢?或者说,在Java Code中,调用了那个native修饰的家伙,要如何去找到它的实现呢?C/C++中可没有java的string那种东西。建立Java方法到native方法的映射,也就是告诉Java层,它所调用的那些native方法在本底层对应于哪个实际的C/C++方法,有两种方法。一种是通过给C/C++的函数按照某个特定的规则来命名实现的。比如前面的那个public native String stringFromJNI()方法,Java的机制是会知道它的实现应该是在C/C++中的Java_com_example_hellojni_HelloJni_stringFromJNI() 这个函数的。看看这个函数名,是有多么的冗长啊,这个函数名中要包含对应的Java 方法的package name、class name及method name的信息嘛。可以看到这个方法名的构成,Java开头,然后是下划线,紧接着是下划线分割的package name,然后跟着的是下划线和class name,最后是下划线和方法名。
建立Java方法到native方法的映射还有另外的一种让人感觉更为舒服的方法,那就是JNI_OnLoad机制。一个shared library,如果他里面有一个JNI_OnLoad(JavaVM* vm, void*)函数,那么当这个shared library被load起来的时候,它首先就会被调用一次。然后在这个JNI_OnLoad()函数中,就可以通过JNI提供的一些函数,来显式地注册Java 方法到native方法的映射关系了。我们修改ndk的sample code,来使用JNI_OnLoad这种机制(同时我们想要加一个限制,那就是只能基于"jni.h"提供的方法来实现,尽管在libandroid_android.so中已经存在一些非常好用的设施了,以使得我们的JNI code可以用ndk提供的设施轻松的build过)。如下是前面那个 stringFromJNI()的实现代码:
#include
#include
#include
#include
#include "JniDebug.h"
#include "JniHelper.h"
/* This is a trivial JNI example where we use a native method
* to return a new VM String. See the corresponding Java source
* file located at:
*
* apps/samples/hello-jni/project/src/com/example/hellojni/HelloJni.java
*/
//extern "C" {
// JNIEXPORT jstring JNICALL Java_com_example_hellojni_HelloJni_stringFromJNI(JNIEnv * env, jobject thiz );
//};
//
//JNIEXPORT jstring
//JNICALL Java_com_example_hellojni_HelloJni_stringFromJNI( JNIEnv* env,
// jobject thiz )
//{
// const char *str = "Hello from JNI !";
// JniDebug(str);
// return env->NewStringUTF(str);
//}
static jstring HelloJni_stringFromJNI( JNIEnv* env, jobject thiz )
{
JniDebug("from HelloJni_stringFromJNI");
const char *str = "Hello from JNI !";
JniDebug(str);
return env->NewStringUTF(str);
}
static JNINativeMethod gMethods[] = {
NATIVE_METHOD(HelloJni, stringFromJNI, "()Ljava/lang/String;"),
};
void register_com_example_hellojni(JNIEnv* env) {
jniRegisterNativeMethods(env, "com/example/hellojni/HelloJni", gMethods, NELEM(gMethods));
}
int JNI_OnLoad(JavaVM* vm, void*) {
JNIEnv* env;
if (vm->GetEnv(reinterpret_cast(&env), JNI_VERSION_1_6) != JNI_OK) {
abort();
}
register_com_example_hellojni(env);
JniDebug("JNI_OnLoad in hello-jni");
return JNI_VERSION_1_6;
}
来看这段code,就建立Java方法到native方法的映射部分而言,主要做了两件事情:
- 建立了一个表,这个表描述了Java方法和native方法的映射关系。这个表中会包含Java层中这个方法的名称,native层中相同方法的名称,参数的类型及返回值的类型等信息。对应于上面那段code中定义的gMethods这个数组。怎么没有包含这个方法所在的java class的名称信息呢?Java class name的信息当然是不可或缺的。且看这个部分完成第二件事情。
- 调用JNI的方法把第一步中建立的那个表注册进系统,对应于上面那个对于jniRegisterNativeMethods()函数的调用。这个地方实际上是需要提供Java class name信息的啦。
实际上在此处,不管是上面的第一步,还是第二步,我们都没有直接使用"jni.h"中提供的那些函数或结构。而是参照android系统中libcore/luni/src/main/native/下面的那些实现,定义了一个宏来更加方便的建立Java 方法到native方法的映射表。同时参照android系统中libnativehelper的实现,对"jni.h"中直接提供的那些方法进行了一点点的增强。接下来来看这个部分的Code。首先是头文件JniHelper.h:
#include "jni.h"
#ifdef __cplusplus
extern "C" {
#endif
/*
* Register one or more native methods with a particular class.
* "className" looks like "java/lang/String". Aborts on failure.
* TODO: fix all callers and change the return type to void.
*/
int jniRegisterNativeMethods(C_JNIEnv* env, const char* className, const JNINativeMethod* gMethods, int numMethods);
#ifdef __cplusplus
}
#endif
/*
* For C++ code, we provide inlines that map to the C functions. g++ always
* inlines these, even on non-optimized builds.
*/
#if defined(__cplusplus)
inline int jniRegisterNativeMethods(JNIEnv* env, const char* className, const JNINativeMethod* gMethods, int numMethods) {
return jniRegisterNativeMethods(&env->functions, className, gMethods, numMethods);
}
#endif
#define NATIVE_METHOD(className, functionName, signature) \
{ #functionName, signature, reinterpret_cast(className ## _ ## functionName) }
# define NELEM(x) ((int) (sizeof(x) / sizeof((x)[0])))
接下来是JniHelper.cpp 文件:
#include "JniDebug.h"
#include "JniHelper.h"
#include
/**
* Equivalent to ScopedLocalRef, but for C_JNIEnv instead. (And slightly more powerful.)
*/
template
class scoped_local_ref {
public:
scoped_local_ref(C_JNIEnv* env, T localRef = NULL)
: mEnv(env), mLocalRef(localRef)
{
}
~scoped_local_ref() {
reset();
}
void reset(T localRef = NULL) {
if (mLocalRef != NULL) {
(*mEnv)->DeleteLocalRef(reinterpret_cast(mEnv), mLocalRef);
mLocalRef = localRef;
}
}
T get() const {
return mLocalRef;
}
private:
C_JNIEnv* mEnv;
T mLocalRef;
// Disallow copy and assignment.
scoped_local_ref(const scoped_local_ref&);
void operator=(const scoped_local_ref&);
};
static jclass findClass(C_JNIEnv* env, const char* className) {
JNIEnv* e = reinterpret_cast(env);
return (*env)->FindClass(e, className);
}
extern "C" int jniRegisterNativeMethods(C_JNIEnv* env, const char* className,
const JNINativeMethod* gMethods, int numMethods)
{
JNIEnv* e = reinterpret_cast(env);
JniDebug("Registering %s natives", className);
scoped_local_ref c(env, findClass(env, className));
if (c.get() == NULL) {
JniDebug("Native registration unable to find class '%s', aborting", className);
abort();
}
if ((*env)->RegisterNatives(e, c.get(), gMethods, numMethods) < 0) {
JniDebug("RegisterNatives failed for '%s', aborting", className);
abort();
}
return 0;
}
在Java 方法的native实现的那个文件中,我们看到有一些打log的函数。这个地方也顺便看一下在做NDK开发时打log的方法,首先是头文件:
void JniDebug(const char format[], ...);
这个头文件,没什么可说的。接着来看这个打log的函数的实现:
#include "stdio.h"
static const size_t kBufferSize = 256;
#define LOG_TAG "hello-jni"
#include
#include "JniDebug.h"
void JniDebug(const char format[], ...) {
va_list args;
va_start(args, format);
__android_log_vprint(ANDROID_LOG_DEBUG, LOG_TAG, format, args);
va_end(args);
}
看上去与我们平常用的那些printf函数也真心没有太大的区别。要打log,自然不能忘了要在jni的Android.mk文件中,建立对于liblog的依赖。像下面这样:
LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
ANDROID_SOURCE_TOP=$(HOME)/android/Data/android_src/JellyBean/
LOCAL_MODULE := hello-jni
LOCAL_SRC_FILES := hello-jni.cpp \
JniHelper.cpp\
JniDebug.cpp
LOCAL_LDLIBS := -llog
include $(BUILD_SHARED_LIBRARY)
接下来再来看一看我们上面的那个hello-jni程序运行的结果。它打出来的log:
03-02 08:44:23.362: D/hello-jni(922): Registering com/example/hellojni/HelloJni natives
03-02 08:44:23.362: D/hello-jni(922): JNI_OnLoad in hello-jni
03-02 08:44:23.512: D/hello-jni(922): from HelloJni_stringFromJNI
03-02 08:44:23.512: D/hello-jni(922): Hello from JNI !
结束。