Android与JNI(二) ---- Java调用C++ 动态调用

时间:2022-04-03 15:51:04

目录:

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

1. 简介

  Android与JNI(一)已经简单介绍了如何在 android  环境下使用 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.cpp 中加入代码:

 #include <jni.h>
#include <android/log.h> #define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG,"jnitest",__VA_ARGS__)
jint JNI_OnLoad(JavaVM* vm, void *reserved)
{
LOGD("JNI_OnLoad called");
return JNI_VERSION_1_4;
}

编译:

 
$ndk-build
Compile++ thumb: hellojni <= hellojni.cpp
SharedLibrary : libHelloJNI.so
/home/eddy/workspace/HelloJNI/obj/local/armeabi/objs/HelloJNI/HelloJNI.o: In function `JNI_OnLoad':
/home/eddy/workspace/HelloJNI/jni/HelloJNI.c:36: undefined reference to `__android_log_print'
collect2: ld returned 1 exit status
make: *** [/home/eddy/workspace/HelloJNI/obj/local/armeabi/libHelloJNI.so] Error 1
 

  出错了。是链接出错,这个很简单,只要链接 liblog.so 这个库就可以了。在 Android.mk 中添加:

LOCAL_LDLIBS := -llog

  再编译:

[armeabi] Compile++ thumb: hellojni <= hellojni.cpp
[armeabi] StaticLibrary : libstdc++.a
[armeabi] SharedLibrary : libhellojni.so
[armeabi] Install : libhellojni.so => libs/armeabi/libhellojni.so

**** Build Finished ****

  OK,大功告成。如无意外,运行后你会在 logcat 中找到这样的打印:

/dalvikvm( 1956): Trying to load lib /data/data/com.example.hellojni/lib/libHelloJNI.so 0x41345548
D/dalvikvm( 1956): Added shared lib /data/data/com.example.hellojni/lib/libHelloJNI.so 0x41345548
D/ ( 1956): JNI_OnLoad called.

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

3. 使用 registerNativeMethods 方法

  说了这么多,终于来重点了。先把修改后的 hellojni.cpp 列出来,然后再慢慢分析。

#include <string.h>
#include <jni.h>
#include <stdlib.h>
#include <stdio.h>
#include <android/log.h>
#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG,"jni",__VA_ARGS__) #ifdef __cplusplus
extern "C" {
#endif static const char* className = "com/example/hellojni/MainActivity";
JNIEXPORT jstring JNICALL stringFromJNI(JNIEnv* env, jclass clazz)
{
//return env->NewStringUTF(env, "Hello form JNI!");
return env->NewStringUTF("hello world returned.");
} static JNINativeMethod gMethods[] = {
{ "stringFromJNI", "()Ljava/lang/String;", (void *)stringFromJNI },
}; // This function only registers the native methods, and is called from JNI_OnLoad
JNIEXPORT int JNICALL register_location_methods(JNIEnv *env)
{
jclass clazz; /* look up the class */
clazz = env->FindClass(className );
//clazz = env->FindClass(env, className);
if (clazz == NULL) {
LOGD("Can't find class %s\n", className);
return -;
} LOGD("register native methods"); /* register all the methods */
//if ((*env)->RegisterNatives(env, clazz, gMethods, sizeof(gMethods) / sizeof(gMethods[0])) != JNI_OK)
if (env->RegisterNatives(clazz, gMethods, sizeof(gMethods) / sizeof(gMethods[])) != JNI_OK)
{
LOGD("Failed registering methods for %s\n", className);
return -;
} /* fill out the rest of the ID cache */
return ;
} jint JNI_OnLoad(JavaVM* vm, void *reserved)
{
JNIEnv* env = NULL;
jint result = -; LOGD("%s: +has loaded", __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) {
LOGD("ERROR: GetEnv failed.\n");
return result;
} if( register_location_methods(env) < )
{
LOGD("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] 的结果完全一样,实现了 JAVA 中声明的 stringFromJNI 本地方法,返回一个字符串。至于为什么不再需要以Java_com_example_hellojni_HelloJNI_stringFromJNI 命名,就要慢慢分析了。

  还是从 JNI_OnLoad() 这个函数开始说起。该函数的动作包括:获取 JNI 环境对象,登记本地方法,最后返回 JNI 版本。

值得引起注意的是:

(*env)->NewStringUTF(env, "Hello from JNI !");

这一行,这是c的写法,而我的是cpp程序,需要改写成:

env->NewStringUTF( "Hello from JNI !");

否则会编译出错。

可以参考这个链接:http://blog.csdn.net/kandyer/article/details/9097051

  登记本地方法,作用是将 c/c++ 的函数映射到 JAVA 中,而在这里面起到关键作用的是结构体 JNINativeMethod 。他定义在 jni.h 中。

 typedef struct {
const char* name;     /* java 中声明的本地方法名称 */
const char* signature;  /* 描述了函数的参数和返回值 */
void* fnPtr;    /* c/c++的函数指针 */
} JNINativeMethod;

声明实例

 static JNINativeMethod gMethods[] = {
{ "stringFromJNI", "()Ljava/lang/String;", (void *)stringFromJNI },
};

参数分析:

  "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类型     C类型
  V      void         void
  Z      jboolean     boolean
  I       jint         int
  J       jlong        long
  D      jdouble       double
  F      jfloat            float
  B      jbyte            byte
  C      jchar           char
  S      jshort          short
  数组则以"["开始,用两个字符表示
  [I     jintArray       int[]
  [F     jfloatArray     float[]
  [B     jbyteArray     byte[]
  [C    jcharArray      char[]
  [S    jshortArray      short[]
  [D    jdoubleArray    double[]
  [J     jlongArray      long[]
  [Z    jbooleanArray    boolean[]
  上面的都是基本类型,如果参数是 Java 类,则以"L"开头,以";"结尾,中间是用"/"隔开包及类名,而其对应的 C 函数的参数则为 jobject,一个例外是 String 类,它对应的 c 类型为 jstring 。
  Ljava/lang/String;     String     jstring
  Ljava/net/Socket;      Socket    jobject
  如果 JAVA 函数位于一个嵌入类(也被称为内部类),则用$作为类名间的分隔符,例如:"Landroid/os /FileUtils$FileStatus;"。

  最终是通过登记所有记录在 JNINativeMethod 结构体中的 JNI 本地方法。

  使用 registerNativeMethods 方法不仅仅是为了改变那丑陋的长方法名,最重要的是可以提高效率,因为当 Java 类透过 VM 呼叫到本地函数时,通常是依靠 VM 去动态寻找动态库中的本地函数(因此它们才需要特定规则的命名格式),如果某方法需要连续呼叫很多次,则每次都要寻找一遍,所以使用 RegisterNatives 将本地函数向 VM 进行登记,可以让其更有效率的找到函数。

  registerNativeMethods 方法的另一个重要用途是,运行时动态调整本地函数与 Java 函数值之间的映射关系,只需要多次调用 registerNativeMethods 方法,并传入不同的映射表参数即可。

4. 测试

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

package com.example.hellojni;

import android.os.Bundle;
import android.app.Activity;
import android.util.Log;
import android.view.Menu;
import android.widget.TextView; public class MainActivity extends Activity { @Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
TextView tv = new TextView(this);
tv.setText( stringFromJNI() );
setContentView(tv); Log.d("JNI", "max = " + max(10, 100));
} @Override
public boolean onCreateOptionsMenu(Menu menu) {
// Inflate the menu; this adds items to the action bar if it is present.
getMenuInflater().inflate(R.menu.main, menu);
return true;
} public native String stringFromJNI();
public native int max(int a,int b);
static {
System.loadLibrary("hellojni");
}
}

在 hellojni.cpp 中添加 max 的实现方法。

  int native_max(JNIEnv* env, jclass clazz, int a, int b)
{
return (a > b ? a:b);
} static JNINativeMethod gMethods[] = {
{ "stringFromJNI", "()Ljava/lang/String;", (void *)stringFromJNI },
{ "max", "(II)I", (void *)native_max },
};

用 ndk 编译生成动态库。

$ndk-build
Compile thumb : HelloJNI <= HelloJNI.c
SharedLibrary : libHelloJNI.so
Install : libHelloJNI.so => libs/armeabi/libHelloJNI.so

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

D/dalvikvm( 2174): Trying to load lib /data/data/com.example.hellojni/lib/libHelloJNI.so 0x413488a8
D/dalvikvm( 2174): Added shared lib /data/data/com.example.hellojni/lib/libHelloJNI.so 0x413488a8
D/ ( 2174): JNI_OnLoad: +
D/ ( 2174): register native methods
D/JNI ( 2174): max = 100

  证明 max 调用成功。

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

5. JNI 帮助方法

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

#ifndef NELEM
# define NELEM(x) ((int) (sizeof(x) / sizeof((x)[0])))
#endif

  有了这个宏之后,我们就可以这样子写:

 (*env)->RegisterNatives(env, clazz, gMethods, NELEM(gMethods));

  6. 参考资料

  [1]. Android与JNI(一)
  [2]. Android JNI知识简介
  [3]. Android中JNI编程的那些事儿  [4]. Android 动态注册JNI

参考链接:http://www.cnblogs.com/eddy-he/archive/2012/08/09/2629974.html

最后附上工程源码:http://files.cnblogs.com/chuanwei-zhang/hellojni-dynamic.zip