android app中使用JNI

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方法的映射部分而言,主要做了两件事情:

  1. 建立了一个表,这个表描述了Java方法和native方法的映射关系。这个表中会包含Java层中这个方法的名称,native层中相同方法的名称,参数的类型及返回值的类型等信息。对应于上面那段code中定义的gMethods这个数组。怎么没有包含这个方法所在的java class的名称信息呢?Java class name的信息当然是不可或缺的。且看这个部分完成第二件事情。
  2. 调用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<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 !

结束。

你可能感兴趣的:(android,jni,NDK)