AndroidJNI实践(2)--无需头文件的常规办法-JNI动态注册



AndroidJNI实践(2)--无需头文件的常规办法-JNI动态注册


一、环境和工具:
  Ubuntu14.04
  java version "1.7.0_95"
  IDE(Android-studio/Eclipse)
  android-ndk-r10b


二、
  1. 简介
  2. JNI 组件的入口函数
  3. 使用 registerNativeMethods 方法
  4. 测试-添加新的JNI调用方法
  5. JNI 帮助方法
  6. 参考资料

1. 简介
  Android与JNI(一)已经简单介绍了如何在 android  环境下使用 JNI 了。但是遵循 JNI 开发的基本步骤似乎有点死板,而且得到的本地函数名太丑了。
那一种方式称作JNI静态注册,还有第二种方式称作JNI动态注册,所以非常有必要在这里介绍另外一种实现方法。
2. JNI 组件的入口函数
  前一篇文章说到 static {System.loadLibrary("HelloJNI");} 会在第一次使用该类的时候加载动态库 libHelloJNI.so 。当 Android 的 VM 执行到 System.loadLibrary() 这个函数时,首先会执行 JNI_OnLoad() 这个函数。与此对应的是卸载时调用 JNI_OnUnLoad() 。
  首先调用 c 组件中的 JNI_OnLoad()  的意图包括:
  (1) 告诉 VM 此 c 组件使用那一个 JNI 版本。如果动态库中没有提供 JNI_OnLoad(),VM 默认该动态库使用最老的 JNI 1.1 版本。由于新版的 JNI 做了许多扩充,如果需要使用 JNI 的新版功能,例如 JNI 1.4的 java.nio.ByteBuffer,就必须藉由 JNI_OnLoad() 函数来告知 VM。
  (2) 另外一个作用就是初始化,例如预先申请资源等。
  现在我们先现实一个非常简单的 JNI_OnLoad(),看是不是在真的有调用到它。在 HelloJNI.c 中加入代码:
1 jint JNI_OnLoad(JavaVM* vm, void *reserved)
2 {
3     LOGD("%s called.\n", __FUNCTION__);
4
5     return JNI_VERSION_1_4;  // 注意这里要返回 JNI 的版本,否则会出错喔。
6 }
  编译:——注意这里android.mk/HelloJNI.c/Application.mk一定是jni目录下的,不然会报路径错误:
~/Code/HelloJNI_NDK_onload/jni $ ndk-build

但如果android.mk文件没写好,一般会有以下几种错误:
《1》文件夹名字
Android NDK: Could not find application project directory !    
Android NDK: Please define the NDK_PROJECT_PATH variable to point to it.    
~/Public/tools/android-ndk-r10b/build/core/build-local.mk:148: *** Android NDK: Aborting    .  Stop.
——文件夹名字修改成jni
《2》c文件中头文件找不到
~/Code/HelloJNI_NDK_onload/jni $ ndk-build
Compile thumb  : HelloJNI <= HelloJNI.c
SharedLibrary  : libHelloJNI.so
~/Code/HelloJNI_NDK_onload/obj/local/armeabi/objs/HelloJNI/HelloJNI.o: In function `JNI_OnLoad':
~/Code/HelloJNI_NDK_onload/jni/HelloJNI.c:35: undefined reference to `LOGD'
collect2: ld returned 1 exit status
make: *** [~/Code/HelloJNI_NDK_onload/obj/local/armeabi/libHelloJNI.so] Error 1
——加入头文件 #include <utils/Log.h> 之后再编译:
《3》头文件未正确包含
$ndk-build
Compile thumb  : HelloJNI <= HelloJNI.c
~/Code/HelloJNI_NDK_onload/jni/HelloJNI.c:20:23: error: utils/Log.h: No such file or directory
make: *** [~/Code/HelloJNI_NDK_onload/obj/local/armeabi/objs/HelloJNI/HelloJNI.o] Error 1
——提示找不到指定的头文件。由于我们没有把这个 jni 放到整个 android 的源码中编译,所以遇到这个错误是正常的,解决的方法是在 Android.mk 中加入源码中头文件的路径。
#预编译时,LOGD的include文件路径
LOCAL_CFLAGS  += -I~/Code/android_project/frameworks/base/include
LOCAL_CFLAGS  += -I~/Code/android_project/system/core/include
LOCAL_CFLAGS  += -I~/Code/android_project/frameworks/native/opengl/include
  再编译:
《4》文件未链接so库
$ndk-build
Compile thumb  : HelloJNI <= HelloJNI.c
SharedLibrary  : libHelloJNI.so
~/Code/HelloJNI_NDK_onload/obj/local/armeabi/objs/HelloJNI/HelloJNI.o: In function `JNI_OnLoad':
~/Code/HelloJNI_NDK_onload/jni/HelloJNI.c:36: undefined reference to `__android_log_print'
collect2: ld returned 1 exit status
make: *** [~/Code/HelloJNI_NDK_onload/obj/local/armeabi/libHelloJNI.so] Error 1
——这个很简单,只要链接 liblog.so 这个库就可以了。在 Android.mk 中添加:
LOCAL_LDLIBS  = -lc -lm -llog
  再编译:
$ndk-build
Compile thumb  : HelloJNI <= HelloJNI.c
SharedLibrary  : libHelloJNI.so
Install        : libHelloJNI.so => libs/armeabi/libHelloJNI.so
  OK,大功告成。如无意外,运行后你会在 logcat 中找到这样的打印:
/dalvikvm( 1956): Trying to load lib /data/data/com.android.hellojni/lib/libHelloJNI.so 0x41345548
D/dalvikvm( 1956): Added shared lib /data/data/com.android.hellojni/lib/libHelloJNI.so 0x41345548
D/        ( 1956): JNI_OnLoad called.

E/art: ----- class 'Lcom/android/hellojni/HelloJNIActivity;' cl=0x12c58880 -----

  这说明了在加载 JNI 的动态库时,确实调用了 JNI_OnLoad() 。调用了这个库又有什么用呢?下面为你揭晓。


3. 使用 registerNativeMethods 方法
  说了这么多,终于来重点了。先把修改后的 HelloJNI.c 列出来,然后再慢慢分析。
#include <string.h>
#include <jni.h>
//#include "com_android_hellojni_HelloJNIActivity.h"
#include <utils/Log.h>

#ifdef __cplusplus
extern "C" {
#endif

static const char *className = "com/android/hellojni/HelloJNIActivity";

/* 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
  */
//jstring JNICALL Java_com_android_hellojni_HelloJNIActivity_getStringFromJNI( JNIEnv* env,
//jobject this )
//{
//return (*env)->NewStringUTF(env, "Hello from JNI!It's Me");
//}
jstring getStringFromJNI(JNIEnv *env, jobject this) {
    return (*env)->NewStringUTF(env, "Hello form JNI! It's Me By Onload");
}

//add the second method
//1:c文件中写上实现函数;
//2:加入JNINativeMethod[]中,动态注册。
jint native_getMaxValue(JNIEnv *env, jobject this, int a, int b){
    return a > b ? a : b;
}

static JNINativeMethod gMethods[] = {
        {"getStringFromJNI", "()Ljava/lang/String;", (void *) getStringFromJNI},
        {"getMaxValue", "(II)I", (void *)native_getMaxValue},
};

// This function only registers the native methods, and is called from JNI_OnLoad
int register_location_methods(JNIEnv *env) {
    jclass clazz;
    /* look up the class */
    clazz = (*env)->FindClass(env, className);
    //clazz = env->FindClass(env, className);
    if (clazz == NULL) {
        ALOGE("Can't find class %s\n", className);
        return -1;
    }

    ALOGD("register native methods");

    /* register all the methods */
    if ((*env)->RegisterNatives(env, clazz, gMethods, sizeof(gMethods) / sizeof(gMethods[0])) !=
        JNI_OK)
        //if (env->RegisterNatives(env, clazz, gMethods, sizeof(gMethods) / sizeof(gMethods[0])) != JNI_OK)
    {
        ALOGE("Failed registering methods for %s\n", className);
        return -1;
    }

    /* fill out the rest of the ID cache */
    return 0;
}

jint JNI_OnLoad(JavaVM *vm, void *reserved) {
    JNIEnv *env = NULL;
    jint result = -1;
    ALOGD("%s:  ", __FUNCTION__);
    // for c
    if ((*vm)->GetEnv(vm, (void **) &env, JNI_VERSION_1_4) != JNI_OK) {
        // for c++
        //if( vm->GetEnv((void**) &env, JNI_VERSION_1_4) != JNI_OK) {
        ALOGE("ERROR: GetEnv failed.\n");
        return result;
    }
    if (register_location_methods(env) < 0) {
        ALOGE("ERROR: register location methods failed.\n");
        return result;
    }
    return JNI_VERSION_1_4;
}

void JNI_OnUnload(JavaVM *vm, void *reserved) {
    return;
}

#ifdef __cplusplus
}
#endif

代码1


先说一说这段代码实现了什么,他与 [1] 的结果完全一样,实现了 JAVA 中声明的 getStringFromJNI 本地方法,返回一个字符串。至于为什么不再需要以Java_com_android_hellojni_HelloJNIActivity_getStringFromJNI 命名,就要慢慢分析了。
  还是从 JNI_OnLoad() 这个函数开始说起。该函数的动作包括:获取 JNI 环境对象,登记本地方法,最后返回 JNI 版本。值得引起注意的是代码注释中,在 c/c++ 环境下编译区别,否则会编译出错。

  登记本地方法,作用是将 c/c++ 的函数映射到 JAVA 中,而在这里面起到关键作用的是结构体 JNINativeMethod 。他定义在 jni.h 中。
typedef struct {  
const char* name;     /* java 中声明的本地方法名称 */
const char* signature;  /* 描述了函数的参数和返回值 */
void*       fnPtr;    /* c/c++  的函数指针 */
} JNINativeMethod;
声明实例
static JNINativeMethod gMethods[] = {
{ "getStringFromJNI", "()Ljava/lang/String;", (void *)getStringFromJNI },
};
参数分析:
  "stringFromJNI":Java 中声明的本地方法名;
  (void *)stringFromJNI:映射对象,本地 c/c 函数,名字可以与 Java 中声明的本地方法名不一致。
  "()Ljava/lang/String;":这个应该是最难理解的,也就是结构体中的 signature 。他描述了本地方法的参数和返回值。例如
  "()V"
  "(II)V"
  "(Ljava/lang/String;Ljava/lang/String;)V"
  实际上这些字符是与函数的参数类型一一对应的。
  "()" 中的字符表示参数,后面的则代表返回值。例如"()V" 就表示void Func();
  "(II)V" 表示 void Func(int, int);
数组则以"["开始,用两个字符表示;如果参数是 Java 类,则以"L"开头,以";"结尾,中间是用"/"隔开包及类名,而其对应的 C 函数的参数则为 jobject,一个例外是 String 类,它对应的 c 类型为 jstring ; 如果 JAVA 函数位于一个嵌入类(也被称为内部类),则用$作为类名间的分隔符,例如:"Landroid/os /FileUtils$FileStatus;"。
PS:具体的每一个字符JAVA与C的对应关系以及数组等说明见《深入理解Android系统卷一》中第二章,其中有详细描述。
最终是登记所有记录在 JNINativeMethod 结构体中的 JNI 本地方法。
  使用 registerNativeMethods 方法不仅仅是为了改变那丑陋的长方法名,最重要的是可以提高效率,因为当 Java 类透过 VM 呼叫到本地函数时,通常是依靠 VM 去动态寻找动态库中的本地函数(因此它们才需要特定规则的命名格式),如果某方法需要连续呼叫很多次,则每次都要寻找一遍,所以使用 RegisterNatives 将本地函数向 VM 进行登记,可以让其更有效率的找到函数。
  registerNativeMethods 方法的另一个重要用途是,运行时动态调整本地函数与 Java 函数值之间的映射关系,只需要多次调用 registerNativeMethods 方法,并传入不同的映射表参数即可。

4. 测试---再添加一个本地方法
  为了对 registerNativeMethods有更好的理解,我在 Java 中再添加一个本地方法。

    Log.d("JNI", "The max value is:" + getMaxValue(10, 99));
}

public native int getMaxValue(int a,int b);

android工程添加代码2

在 HellocJNI.c 中添加 max 的实现方法
//add the second method
//1:c文件中写上实现函数;
//2:加入JNINativeMethod[]中,动态注册。
jint native_getMaxValue(JNIEnv *env, jobject this, int a, int b){
    return a > b ? a : b;
}

static JNINativeMethod gMethods[] = {
        {"getStringFromJNI", "()Ljava/lang/String;", (void *) getStringFromJNI},
        {"getMaxValue", "(II)I", (void *)native_getMaxValue},
};

代码3
 用 ndk 编译生成动态库。
$ndk-build
Compile thumb  : HelloJNI <= HelloJNI.c
SharedLibrary  : libHelloJNI.so
Install        : libHelloJNI.so => libs/armeabi/libHelloJNI.so

在模拟器上 run as ---> Android Application 。可以看到打印:

D/JNI     ( 2174): The max value is: 100

  证明 max 调用成功。

  通过 registerNativeMethods 这种方法,我们可以看到操作的过程中,不需要再使用 javah -jni 生成 jni 头文件。c/c++ 的函数名也可以自由取名。


5. JNI 帮助方法
  在 Android 源码中 your_path/dalvik/libnativehelper/include/nativehelper/JNIHelp.h 这个头文件提供了一些关于 JNI 的帮助函数,包括本地方法注册、异常处理等函数。但是使用这些函数需要链接动态库 libnativehelper.so 。还提供了一个计算方法映射表长度的宏定义。

1 #ifndef NELEM
2 # define NELEM(x) ((int) (sizeof(x) / sizeof((x)[0])))
3 #endif
  有了这个宏之后,我们就可以这样子写:
(*env)->RegisterNatives(env, clazz, gMethods, NELEM(gMethods));

6.参考资料
《深入理解Android系统卷一》中第二章关于JNI有详细的说明。里面有关于JNI库的加载、静态/动态注册、JNI层数据类型转换和JNIEnv等讲解。

《Android框架》中第四章JNI与NDK,其中有关于JNI的详细解析,包括一些源码等。


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值