2.7 JNI应用层实例分析
2.2节讲解了JNI在应用框架层的使用,那么应用层又是如何使用JNI的呢?本节将通过一个实例来演示在应用层如何配合NDK开发基于JNI的应用程序。
NDK给基于JNI的应用开发带来了极大的便利。只需要以下三步:
步骤1 在Eclipse中建立Android工程,并在项目根目录建立jni目录,然后在jni目录加入JNI层的实现代码和对应的Android.mk文件。
步骤2 将项目复制到NDK samples目录,运行ndk-build命令。NDK会自动编译出共享库,并置于armeabi目录下。
步骤3 将新生成的目录和文件从NDK中复制回Eclipse。工程目录如图2-4所示。
接下来将分步实现应用层JNI。
2.7.1 Java层分析
在Eclipse中建立Android工程,工程名为AppJni,并生成一个启动Activity: AppJniActivity。内容如下:
package com.allongriver.jni;
import android.app.Activity;
import android.os.Bundle;
import android.util.Log;
public class AppJniActivity extends Activity {
private static final String TAG = "AppJniActivity";
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
Log.d(TAG, show());
}
//声明一个Native方法,需要在JNI中实现
private native String show();
/JNI中需要调用callback方法,用来演示在JNI中如何操作Java类。为了
演示JNI函数调用过程中如何捕获Java异常,我们故意在callback函数
中抛出NullPointerException,而且没有捕获这个异常*/
private void callback(){
Log.d(TAG, "call back from native");
throw new NullPointerException();
}
/在静态代码库中加载libapp_jni.so共享库,这个库在安装
该应用程序的时候,由PackageManager从apk中解压输出到
/data/data/om.allongriver.jni/lib/libapp_jni.so */
static {
System.loadLibrary("app_jni");//不需要带共享库的前缀lib和后缀so
}
}
以上代码实现在Java层中对Native 方法show()的调用,并在调用过程中,由JNI函数回调Java层callback方法。
2.7.2 JNI层代码和异常处理
接下来看如何在JNI层实现这个Native方法。代码如下:
Jstring Java_com_allongriver_jni_AppJniActivity_show( JNIEnv env,jobject thiz )
{
/通过JNI函数GetObjectClass得到传入对象的类信息。
这里传入的对象,就是调用Native方法的那个对象/
jclass jcls = (env)->GetObjectClass(env,thiz);
//根据类信息得到callback方法的jmethodID
jmethodID jmId=(env)->GetMethodID(env,jcls,"callback","()V");
//调用callback方法
(env)->CallVoidMethod(env,thiz,jmId);
/因为在Java层的callback中抛出了未捕获的异常,所以上面的JNI函数调用必然
出现异常,这里必须检查并处理异常,否则异常将抛给Java层的callback方法
而此时Java层callback也没有捕获异常,此时,进程将死掉/
if((env)->ExceptionCheck(env))
{
(env)->ExceptionDescribe(env);
(env)->ExceptionClear(env);//清除异常
}
//处理异常后响应Java层的调用
return (env)->NewStringUTF(env, "Show message from JNI !");
}
如果把以上代码清除异常部分(如下所示)注释掉,看看会出现什么结果。
//(env)->ExceptionClear(env);//清除异常
运行程序后,logcat中的日志信息如下:
D/dalvikvm(4734): Trying to load lib //加载共享库
/data/data/com.allongriver.jni/lib/libapp_jni.so 0x4051b7f0
D/dalvikvm(4734): Added shared lib
/data/data/com.allongriver.jni/lib/libapp_jni.so 0x4051b7f0
D/dalvikvm(4734): No JNI_OnLoad found in //执行共享库中的第一个方法JNI_OnLoad
/data/data/com.allongriver.jni/lib/libapp_jni.so 0x4051b7f0, skipping init
D/AppJniActivity(4734): call back from native //JNI层发现异常
W/System.err(4734): java.lang.NullPointerException
……
D/AndroidRuntime(4734): Shutting down VM
//异常被抛给Java层,Java层继续打印出异常信息
E/AndroidRuntime(4734): FATAL EXCEPTION: main// 进程终止
在JNI编程中,一定要处理好JNI函数调用过程中可能出现的异常。
至此,读者可能会发现,应用层JNI编程跟应用框架层JNI编程没有多少相关性。难道JNI提供两套编程机制?
答案是肯定的。AppJni例子演示的是传统的JNI编程方式,符合JNI规范。但其缺点很明显,具体有三个:
1)需要遵守繁琐的JNI实现方法的命名规则。比如要严格遵守show函数的命名规则,如果出错,将无法调用到JNI层的实现方法:
Jstring Java_com_allongriver_jni_AppJniActivity_show( JNIEnv env,jobject thiz )
2)如果采用应用层的JNI使用方式,就需要在框架的Java层加入System.loadLibrary
("app_jni")这样的加载共享库的代码。而应用框架层会频繁调用,严重影响效率。
3)虚拟机在共享库中搜索定位JNI实现方法效率也受影响。Android应用框架层采用函数注册的方法回避这些问题。
本章开头Log系统的例子就是采用函数注册的方法。这是不是意味着在应用层就不能使用这种方法?答案显然是否定的。接下来把AppJni改造成函数注册。只需要借鉴Log系统JNI注册流程,在原有的app_jni中添加如下代码即可:
//这里已经可以不用遵守JNI的函数命名规则,因为我们已经做了函数映射
Jstring Java_com_allongriver_jni_AppJniActivity_show( JNIEnv env,jobject thiz )
{
// 这部分代码不需要任何改变
……
}
//下面都是为了完成函数注册添加的代码。这里是Java层方法和JNI层方法的映射
static JNINativeMethod gmethods[] = {
{"show", "()Ljava/lang/String;”,
(void)Java_com_allongriver_jni_AppJniActivity_show},
};
/
Register several native methods for one class.
/
static int registerNativeMethods(JNIEnv env, const char className,
JNINativeMethod gMethods, int numMethods)
{
jclass clazz;
clazz = (env)->FindClass(env,className);
if (clazz == NULL) {
return JNI_FALSE;
}
//调用JNIEnv提供的注册函数向虚拟机注册
if ((env)->RegisterNatives(env,clazz, gMethods, numMethods) < 0) {
return JNI_FALSE;
}
return JNI_TRUE;
}
/
Register native methods for all classes we know about.
returns JNI_TRUE on success.
/
static int registerNatives(JNIEnv env)
{
if (!registerNativeMethods(env, "com/allongriver/jni/AppJniActivity",
methods, sizeof(methods) / sizeof(methods[0]))) {
return JNI_FALSE;
}
return JNI_TRUE;
}
/虚拟机执行System.loadLibrary("app_jni")后,进入libapp_jni.so后
会首先执行这个方法,所以我们在这里做注册的动作*/
jint JNI_OnLoad(JavaVM vm, void reserved)
{
jint result = -1;
JNIEnv env = NULL;
if ( (vm)->GetEnv(vm, (void ) &env, JNI_VERSION_1_4) ) {
goto fail;
}
//最终调用(env)->RegisterNatives,这跟Log系统是一样的
if (registerNatives(env) != JNI_TRUE) {
goto fail;
}
result = JNI_VERSION_1_4;
fail:
return result;
}
至此读者可能会问,为什么同样是以函数注册方式调用JNI,在Log系统中没有执行System.loadLibrary, 也没有在JNI_OnLoad中执行注册函数呢?
那是因为系统在启动的过程中已经帮我们做了。Android启动篇会介绍这部分内容。如果应用框架层某些模块不是在系统启动过程中自动load并注册,也需要上述JNI_OnLoad步骤。读者可以参考android_media_MediaPlayer.cpp的例子。
2.8 本章小结
本章以Log系统的JNI实例为引线,贯穿了JNI技术的主要方面,让读者对JNI有足够的认识,具备深入学习框架层代码的基础。
本章首先概括了JNI在应用层和框架层中的地位;然后以Log系统为例,介绍了框架层JNI调用和注册流程,深入分析了JNIEnv的设计和实现;然后详细介绍了JNI数据类型转换、方法签名、对象操作、域和方法操作、全局引用、局部引用、弱引用以及异常处理等JNI核心内容;最后以一个应用层JNI实例演示了NDK的使用和异常处理流程。