JNI提供了一种机制,以使得在Java Code中可以使用C/C++的本地层Code。这种机制实际上为我们开启了一扇门,一扇将Java Code和广阔的C/C++本地层连接起来的门。
基于android-ndk-r8d提供的sample程序,android-ndk-r8d/samples/hello-jni的code,我们来看一下,要如何在android app中使用JNI,从而在Java code中调用C/C++的功能。
首先来看在Java code中都需要做些什么事情:
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修饰的method,它们就是在Java code中调用,但实际的实现code却在本地层的方法。native这个修饰大概是会告诉java编译器,暂时不需要去链接这些method吧。
那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 <string.h> #include <jni.h> #include <stdlib.h> #include <android/log.h> #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<void**>(&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方法的映射部分而言,主要做了两件事情:
实际上在此处,不管是上面的第一步,还是第二步,我们都没有直接使用"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<void*>(className ## _ ## functionName) } # define NELEM(x) ((int) (sizeof(x) / sizeof((x)[0])))
接下来是JniHelper.cpp 文件:
#include "JniDebug.h" #include "JniHelper.h" #include <stdlib.h> /** * Equivalent to ScopedLocalRef, but for C_JNIEnv instead. (And slightly more powerful.) */ template<typename T> 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<JNIEnv*>(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<JNIEnv*>(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<JNIEnv*>(env); JniDebug("Registering %s natives", className); scoped_local_ref<jclass> 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 <android/log.h> #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 !
结束。