Android与JNI(二)
软件版本:
ubuntu10.04
java version "1.6.0_30-ea"
eclipse
android-ndk-r5b
目录:
1. 简介
2. JNI 组件的入口函数
3. 使用 registerNativeMethods 方法
4. 测试
5. JNI 帮助方法
6. 参考资料
1. 简介
已经简单介绍了如何在 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.c 中加入代码:
1 jint JNI_OnLoad(JavaVM* vm, void *reserved) 2 { 3 LOGD("%s called. ", __FUNCTION__); 4 5 return JNI_VERSION_1_4; // 注意这里要返回 JNI 的版本,否则会出错喔。 6 }
编译:
$ndk-build Compile thumb : HelloJNI <= HelloJNI.c 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:35: undefined reference to `LOGD' collect2: ld returned 1 exit status make: *** [/home/eddy/workspace/HelloJNI/obj/local/armeabi/libHelloJNI.so] Error 1
加入头文件 #include <utils/Log.h> 之后再编译:
$ndk-build Compile thumb : HelloJNI <= HelloJNI.c /home/eddy/workspace/HelloJNI/jni/HelloJNI.c:20:23: error: utils/Log.h: No such file or directory make: *** [/home/eddy/workspace/HelloJNI/obj/local/armeabi/objs/HelloJNI/HelloJNI.o] Error 1
提示找不到指定的头文件。由于我们没有把这个 jni 放到整个 android 的源码中编译,所以遇到这个错误是正常的,解决的方法是在 Android.mk 中加入源码中头文件的路径。
LOCAL_CFLAGS += -Iyour_path/android4.0/include/frameworks/base/include LOCAL_CFLAGS += -Iyour_path/android4.0/include/system/core/include LOCAL_CFLAGS += -Iyour_path/android4.0/include/frameworks/base/opengl/include LOCAL_CFLAGS += -Iyour_path/android4.0/include/system/core/include
再编译:
$ndk-build Compile thumb : HelloJNI <= HelloJNI.c 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 += -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.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.c 列出来,然后再慢慢分析。
1 /* 2 * Copyright (C) 2009 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 * 16 */ 17 #include <string.h> 18 #include <jni.h> 19 #include "com_example_hellojni_HelloJNI.h" 20 #include <utils/Log.h> 21 22 #ifdef __cplusplus 23 extern "C" { 24 #endif 25 26 static const char* className = "com/example/hellojni/HelloJNI"; 27 28 /* This is a trivial JNI example where we use a native method 29 * to return a new VM String. See the corresponding Java source 30 * file located at: 31 * 32 * apps/samples/hello-jni/project/src/com/example/HelloJni/HelloJni.java 33 */ 34 //jstring Java_com_example_hellojni_HelloJNI_stringFromJNI(JNIEnv *env, jobject this) 35 //{ 36 // return (*env)->NewStringUTF(env, "Hello from JNI !"); 37 // //return "Hello from Jni !"; 38 //} 39 jstring stringFromJNI(JNIEnv* env, jobject this) 40 { 41 return (*env)->NewStringUTF(env, "Hello form JNI!"); 42 } 43 44 static JNINativeMethod gMethods[] = { 45 { "stringFromJNI", "()Ljava/lang/String;", (void *)stringFromJNI }, 46 }; 47 48 // This function only registers the native methods, and is called from JNI_OnLoad 49 int register_location_methods(JNIEnv *env) 50 { 51 jclass clazz; 52 53 /* look up the class */ 54 clazz = (*env)->FindClass(env, className ); 55 //clazz = env->FindClass(env, className); 56 if (clazz == NULL) { 57 LOGE("Can't find class %s ", className); 58 return -1; 59 } 60 61 LOGD("register native methods"); 62 63 /* register all the methods */ 64 if ((*env)->RegisterNatives(env, clazz, gMethods, sizeof(gMethods) / sizeof(gMethods[0])) != JNI_OK) 65 //if (env->RegisterNatives(env, clazz, gMethods, sizeof(gMethods) / sizeof(gMethods[0])) != JNI_OK) 66 { 67 LOGE("Failed registering methods for %s ", className); 68 return -1; 69 } 70 71 /* fill out the rest of the ID cache */ 72 return 0; 73 } 74 75 jint JNI_OnLoad(JavaVM* vm, void *reserved) 76 { 77 JNIEnv* env = NULL; 78 jint result = -1; 79 80 LOGD("%s: +", __FUNCTION__); 81 82 // for c 83 if ((*vm)->GetEnv(vm, (void**) &env, JNI_VERSION_1_4) != JNI_OK) { 84 // for c++ 85 //if( vm->GetEnv((void**) &env, JNI_VERSION_1_4) != JNI_OK) { 86 LOGE("ERROR: GetEnv failed. "); 87 return result; 88 } 89 90 if( register_location_methods(env) < 0 ) 91 { 92 LOGE("ERROR: register location methods failed. "); 93 return result; 94 } 95 96 return JNI_VERSION_1_4; 97 } 98 99 100 101 void JNI_OnUnload(JavaVM* vm, void *reserved) 102 { 103 return; 104 } 105 106 #ifdef __cplusplus 107 } 108 #endif
先说一说这段代码实现了什么,他与 [1] 的结果完全一样,实现了 JAVA 中声明的 stringFromJNI 本地方法,返回一个字符串。至于为什么不再需要以Java_com_example_hellojni_HelloJNI_stringFromJNI 命名,就要慢慢分析了。
还是从 JNI_OnLoad() 这个函数开始说起。该函数的动作包括:获取 JNI 环境对象,登记本地方法,最后返回 JNI 版本。值得引起注意的是第83和85行,在 c 环境下编译,使用第83行,c++ 环境下,使用第85行,否则会编译出错。
登记本地方法,作用是将 c/c++ 的函数映射到 JAVA 中,而在这里面起到关键作用的是结构体 JNINativeMethod 。他定义在 jni.h 中。
1 typedef struct { 2 const char* name; /* java 中声明的本地方法名称 */ 3 const char* signature; /* 描述了函数的参数和返回值 */ 4 void* fnPtr; /* c/c++的函数指针 */ 5 } JNINativeMethod;
声明实例就是第44到46行。
1 static JNINativeMethod gMethods[] = { 2 { "stringFromJNI", "()Ljava/lang/String;", (void *)stringFromJNI }, 3 };
参数分析:
"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;"。
最终是通过第64行代码登记所有记录在 JNINativeMethod 结构体中的 JNI 本地方法。
使用 registerNativeMethods 方法不仅仅是为了改变那丑陋的长方法名,最重要的是可以提高效率,因为当 Java 类透过 VM 呼叫到本地函数时,通常是依靠 VM 去动态寻找动态库中的本地函数(因此它们才需要特定规则的命名格式),如果某方法需要连续呼叫很多次,则每次都要寻找一遍,所以使用 RegisterNatives 将本地函数向 VM 进行登记,可以让其更有效率的找到函数。
registerNativeMethods 方法的另一个重要用途是,运行时动态调整本地函数与 Java 函数值之间的映射关系,只需要多次调用 registerNativeMethods 方法,并传入不同的映射表参数即可。
4. 测试
为了对 registerNativeMethods有更好的理解,我在 Java 中再添加一个本地方法。
1 package com.example.hellojni; 2 3 import android.os.Bundle; 4 import android.app.Activity; 5 import android.util.Log; 6 import android.view.Menu; 7 import android.view.MenuItem; 8 import android.widget.TextView; 9 import android.support.v4.app.NavUtils; 10 11 public class HelloJNI extends Activity { 12 13 @Override 14 public void onCreate(Bundle savedInstanceState) { 15 super.onCreate(savedInstanceState); 16 setContentView(R.layout.hello_jni); 17 getActionBar().setDisplayHomeAsUpEnabled(true); 18 19 /* Create a TextView and set its content. 20 * the text is retrieved by calling a native 21 * function. 22 */ 23 TextView tv = new TextView(this); 24 tv.setText( stringFromJNI() ); 25 setContentView(tv); 26 27 Log.d("JNI", "max = " + max(10, 100)); 28 } 29 30 @Override 31 public boolean onCreateOptionsMenu(Menu menu) { 32 getMenuInflater().inflate(R.menu.hello_jni, menu); 33 return true; 34 } 35 36 37 @Override 38 public boolean onOptionsItemSelected(MenuItem item) { 39 switch (item.getItemId()) { 40 case android.R.id.home: 41 NavUtils.navigateUpFromSameTask(this); 42 return true; 43 } 44 return super.onOptionsItemSelected(item); 45 } 46 47 /* A native method that is implemented by the 48 * 'HelloJNI' native library, which is packaged 49 * with this application. 50 */ 51 public native String stringFromJNI(); 52 53 /* This is another native method declaration that is *not* 54 * implemented by 'HelloJNI'. This is simply to show that 55 * you can declare as many native methods in your Java code 56 * as you want, their implementation is searched in the 57 * currently loaded native libraries only the first time 58 * you call them. 59 * 60 * Trying to call this function will result in a 61 * java.lang.UnsatisfiedLinkError exception ! 62 */ 63 public native String unimplementedStringFromJNI(); 64 65 public native int max(int a,int b); 66 67 /* this is used to load the 'HelloJNI' library on application 68 * startup. The library has already been unpacked into 69 * /data/data/com.example.HelloJni/lib/libHelloJNI.so at 70 * installation time by the package manager. 71 */ 72 static { 73 System.loadLibrary("HelloJNI"); 74 } 75 }
在 HellocJNI.c 中添加 max 的实现方法。
1 int native_max(JNIEnv* env, jobject this, int a, int b) 2 { 3 return (a > b ? a:b); 4 } 5 6 static JNINativeMethod gMethods[] = { 7 { "stringFromJNI", "()Ljava/lang/String;", (void *)stringFromJNI }, 8 { "max", "(II)I", (void *)native_max }, 9 };
用 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 。还提供了一个计算方法映射表长度的宏定义。
1 #ifndef NELEM 2 # define NELEM(x) ((int) (sizeof(x) / sizeof((x)[0]))) 3 #endif
有了这个宏之后,我们就可以这样子写:
1 (*env)->RegisterNatives(env, clazz, gMethods, NELEM(gMethods));
6. 参考资料
[1]. Android与JNI(一)
[2]. Android JNI知识简介
[3]. Android中JNI编程的那些事儿
[4]. Android 动态注册JNI