Android JNI介绍(四)- 异常的处理

4,622 阅读4分钟

本系列文章列表:

Android JNI介绍(一)- 第一个Android JNI工程

Android JNI介绍(二)- 第一个JNI工程的详细分析

Android JNI介绍(三)- Java和Native的互相调用

Android JNI介绍(四)- 异常的处理

Android JNI介绍(五)- 函数的注册

Android JNI介绍(六)- 依赖其他库

Android JNI介绍(七)- 引用的管理

Android JNI介绍(八)- CMakeLists的使用


在上一篇文章中,我们已经了解了如何进行Java和Native的交互,本文将介绍在JNI中如何进行Java异常处理。

一、主动抛出异常

在Java中我们能看到一些类中声明的native函数会抛出异常,比如:

java.io.FileOutputStream类中的:

    private native void open0(String name, boolean append)
        throws FileNotFoundException;

android.hardware.Camera类中的:

    public native final void setPreviewSurface(Surface surface) throws IOException;
    public native final void setPreviewTexture(SurfaceTexture surfaceTexture) throws IOException;

这些函数的实现都在native层,但是其声明却包含了可能会出现的异常,那就说明native其实是可以向Java抛异常的;
JNI也可以进行异常的捕获,我们也来看一下如何在native进行异常的处理。

二、使用JNI提供的函数处理异常

  • 函数介绍

    JNI为我们提供了一些函数进行异常的处理,如下:

        // CATCH
        
        // 如果有异常出现,返回值就是异常,如果没有异常,回传NULL
        jthrowable ExceptionOccurred()
        // 内部会调用异常对应的Java类的printStackTrace()函数
        void ExceptionDescribe()   
        // 清除异常,清除异常后可以继续进行JNI操作
        void ExceptionClear()
        // 当前有没发生异常,若发生了,回传JNI_TRUE,否则回传JNI_FALSE
        jboolean ExceptionCheck()
        
        // THROW
        
        // 向Java抛出一个异常
        jint Throw(jthrowable obj)
        // 选择异常的类型,填写异常的信息进行抛出
        jint ThrowNew(jclass clazz, const char* message)
    
  • 捕获异常

    举个例子:

        jmethodID toURLID = env->GetMethodID(fileClazz,"toURL","()Ljava/net/URL;");
        jobject toURLValue = env->CallObjectMethod(fileObj, toURLID);
        // this exception may occur:
        // java.net.MalformedURLException
        jthrowable error = env->ExceptionOccurred();
        if (error != NULL) {
            // 出现了异常
        }else{
            // 没出现异常
        }
    
  • 抛出异常

    需要注意的是,这里抛出的是Throwable,也就是说,不仅可以抛出Exception,还能抛出Error。 使用方法如下:

    • Java
    public class CustomException extends Exception {
        CustomException() {
            super();
        }
    
        CustomException(String message) {
            super(message);
        }
    }
    
    public native String testExceptionNotCrash(int i) throws CustomException;
    
    • Native
    extern "C" JNIEXPORT jstring 
    JNICALL
    Java_com_wsy_jnidemo_MainActivity_testExceptionNotCrash(
            JNIEnv *env,
            jobject /* this */, jint i) {
        jstring hello = env->NewStringUTF("hello world");
        if (i > 100) {
            jclass exceptionCls = env->FindClass("com/wsy/jnidemo/CustomException");
            env->ThrowNew(exceptionCls, "i must <= 100");
            env->DeleteLocalRef(exceptionCls);
        }
        // 若出现异常,则env已不可使用一些方法,例如创建String将会crash,
        // 文档说明:https://developer.android.google.cn/training/articles/perf-jni#exceptions_1
        return hello;
    }
    

    这里虽然没出现crash,但是Java层也是无法接收到hello这个String对象的

  • 注意事项

    需要注意的是,在exception发生时,在native层已不能调用大部分的方法,例如对象创建。

    NDK官网说明如下:

    You must not call most JNI functions while an exception is pending. Your code is expected to notice the exception (via the function's return value,ExceptionCheck, or ExceptionOccurred) and return, or clear the exception and handle it. The only JNI functions that you are allowed to call while an exception is pending are:

    • DeleteGlobalRef
    • DeleteLocalRef
    • DeleteWeakGlobalRef
    • ExceptionCheck
    • ExceptionClear
    • ExceptionDescribe
    • ExceptionOccurred
    • MonitorExit
    • PopLocalFrame
    • PushLocalFrame
    • ReleaseArrayElements
    • ReleasePrimitiveArrayCritical
    • ReleaseStringChars
    • ReleaseStringCritical
    • ReleaseStringUTFChars

    例如在抛出exception后执行

    env->NewStringUTF("hello world, after exception");
    

    将导致crash,就像这样。

    但是我们刚才说到,当异常发生时,执行ExceptionDescribe会调用异常对应的Java类的printStackTrace()函数,这又是怎么做到的?明明已经出现了异常,为何还能进行和Java交互? 让我们看一看源码:

    art/runtime/jni_internal.cc

    static void ExceptionDescribe(JNIEnv* env) {
        ScopedObjectAccess soa(env);
    
        // If we have no exception to describe, pass through.
        if (!soa.Self()->GetException()) {
          return;
        }
    
        StackHandleScope<1> hs(soa.Self());
        Handle<mirror::Throwable> old_exception(
            hs.NewHandle<mirror::Throwable>(soa.Self()->GetException()));
        soa.Self()->ClearException();
        ScopedLocalRef<jthrowable> exception(env,
                                             soa.AddLocalReference<jthrowable>(old_exception.Get()));
        ScopedLocalRef<jclass> exception_class(env, env->GetObjectClass(exception.get()));
        jmethodID mid = env->GetMethodID(exception_class.get(), "printStackTrace", "()V");
        if (mid == nullptr) {
          LOG(WARNING) << "JNI WARNING: no printStackTrace()V in "
                       << mirror::Object::PrettyTypeOf(old_exception.Get());
        } else {
          env->CallVoidMethod(exception.get(), mid);
          if (soa.Self()->IsExceptionPending()) {
            LOG(WARNING) << "JNI WARNING: " << mirror::Object::PrettyTypeOf(soa.Self()->GetException())
                         << " thrown while calling printStackTrace";
            soa.Self()->ClearException();
          }
        }
        soa.Self()->SetException(old_exception.Get());
    }
    

    这里有个操作:

    1. 把Exception记下来
    2. ExceptionClear
    3. 通过JNI调用异常的printStackTrace()函数(调用过程中出了异常还是会clear,稳)
    4. 把Exception放回去

三、crash的处理

哪怕JNI提供了这么些函数,但还是crash了咋办?

NDK中有一个很方便的debug工具:addr2line,可直接根据指令地址定位代码crash位置。 addr2line的位置在:{NDK安装目录}\toolchains\{平台相关目录}\prebuilt\windows-x86_64\bin目录。 我们先在native层写个错误示范代码,再运行,可以看到日志中出现了crash信息:

crash信息
找到工程相关的动态库文件,可以看到出现crash的so文件为/lib/x86/libnative-lib.so以及指令地址为00000E61,动态库文件在工程目录的位置如下图:
so文件路径
此时使用addr2line工具进行crash位置查找,用法为:

addr2line -e so文件路径 指令地址

addr2line -e D:\android-project\study\JNIDemo\app\build\intermediates\cmake\debug\obj\x86\libnative-lib.so 00000e61

运行结果如下

运行addr2line结果

可以看到定位到的导致crash的代码是native-lib.cpp文件的33行:

crashCode



demo可直接clone:

github.com/wangshengya…

详细的JNI使用方法:

developer.android.google.cn/training/ar…