Android-JNI开发系列《三》-异常处理

作者: 后厂村追寻 | 来源:发表于2020-10-03 11:31 被阅读0次

    人间观察

    做个俗人,得之坦然,失之淡然,顺其自然吧!

    假期ing,~~~,文章提前写好的,哈哈

    今天我们讲Android JNI下的异常处理,在java中有异常处理机制,在jni中也一样。

    回顾java异常

    我们知道在java中分为运行期异常和编译期异常。

    运行期异常时是程序在执行期间发生的异常,如果没有捕获可能导致程序不正常(轻者可能功能不正常,重则程序直接crash )。

    编译期异常是代码编译期间必须显示捕获的异常。throws Exception ,try{} catch{} finally{}

    今天我们不讲这个,如果不了解,可以网上找找&study

    主角-JNI异常

    在jni中native层代码执行不受jvm虚拟机的控制,因此有异常了并不会停止native函数的执行并把控制权交给异常处理程序。所以我们需要异常的检测和处理机制,一旦检查到异常时,原生native函数应该善后,比如:内存释放,返回合适的返回值,抛出异常给java层等

    在jni中一般可以分为两种,一种是在native方法调用java层方法,java层方法出现了异常(crash等)。另一种是在native方法中出现了异常(传入参数不对,程序执行非预期)对外抛出java识别的java异常(Exception类或者它的子类)。

    1.native方法调用java层方法出现异常&如何检测处理(crash)

    我们先看一下如下代码

    // JNIException.java
    public class JNIException {
        static {
            System.loadLibrary("native-lib");
        }
        public native void nativeInvokeJavaException();
        public int getStingLen() {
            String s = null;
            return s.length();
        }
    }
    

    然后我们在JNI的nativeInvokeJavaException方法里调用

    // jni_exception.cpp
    extern "C" JNIEXPORT void JNICALL
    Java_com_bj_gxz_jniapp_exception_JNIException_nativeInvokeJavaException(JNIEnv *env, jobject thiz) {
        jclass cls = env->FindClass("com/bj/gxz/jniapp/exception/JNIException");
        jmethodID getStingLenMid = env->GetMethodID(cls, "getStingLen", "()I");
        jmethodID constructMid = env->GetMethodID(cls, "<init>", "()V");
        // new一个java对象
        jobject obj = env->NewObject(cls, constructMid);
        LOG_D("getStingLen start invoke");
        env->CallIntMethod(obj, getStingLenMid);
        LOG_D("getStingLen invoke done");
    }
    

    结果:如你所想s.length()发生NPE。APP直接crash了, 输出了如下日志

    2020-10-3 10:57:11.333 7012-7012/com.bj.gxz.jniapp D/JNI: getStingLen start invoke
    2020-10-3 10:57:11.333 7012-7012/com.bj.gxz.jniapp D/JNI: getStingLen invoke done
    2020-10-3 10:57:11.334 7012-7012/com.bj.gxz.jniapp D/AndroidRuntime: Shutting down VM
    
    Caused by: java.lang.NullPointerException: Attempt to invoke virtual method 'int java.lang.String.length()' on a null object reference
            at com.bj.gxz.jniapp.exception.JNIException.getStingLen(JNIException.java:17)
            at com.bj.gxz.jniapp.exception.JNIException.nativeInvokeJavaException(Native Method)
            at com.bj.gxz.jniapp.MainActivity.onJniException(MainActivity.java:54)
            
    

    但是,但是,它打印了getStingLen invoke done这行日志,也就是说如果在native层中调用java方法的时候,当java方法发生异常(crash)的时候native方法会继续执行完成(native代码执行不受java虚拟机的控制)。继续执行后面的代码可能就不是我们的预期了(因为我们需要之前方法的结果)。那怎么处理呢?我们的APP至少不能crash吧。

    处理方法:
    在jni中提供了ExceptionCheck,ExceptionOccurred 和ExceptionDescribe和ExceptionClear方法。

    备注,里面有几个方法介绍
    官方文档

    这里就是直接说了。
    ExceptionCheckExceptionOccurred 作用一样,都是检查是否发生了异常。
    区别在于
    ExceptionCheck:有异常返回JNI_TRUE,否则返回JNI_FALSE

    ExceptionOccurred:用异常返回该异常的引用,否则返回NULL

    ExceptionDescribe:打印异常的堆栈信息
    ExceptionClear:清除异常堆栈信息

    ok,我们再改造下实现,让它不crash。

    extern "C" JNIEXPORT void JNICALL
    Java_com_bj_gxz_jniapp_exception_JNIException_nativeInvokeJavaException(JNIEnv *env, jobject thiz) {
        jclass cls = env->FindClass("com/bj/gxz/jniapp/exception/JNIException");
        jmethodID getStingLenMid = env->GetMethodID(cls, "getStingLen", "()I");
    
        jmethodID constructMid = env->GetMethodID(cls, "<init>", "()V");
        // new一个java对象
        jobject obj = env->NewObject(cls, constructMid);
    
        LOG_D("getStingLen start invoke");
        env->CallIntMethod(obj, getStingLenMid);
        LOG_D("getStingLen invoke done");
    
        // ExceptionOccurred作用和ExceptionCheck一样
        // jthrowable throwable = env->ExceptionOccurred();
    
        // 检查JNi调用是否发生了异常
        jboolean ret = env->ExceptionCheck();
        LOG_D("ExceptionCheck %d", ret);
        if (JNI_TRUE == ret) {
            // 打印Java层抛出的异常堆栈信息
            env->ExceptionDescribe();
            // 清除异常信息
            env->ExceptionClear();
            // ...这里可以处理异常发生逻辑
            return;
        }
        LOG_D("nativeInvokeJavaException exec end");
    }
    

    再次运行

    2020-10-3 11:10:16.064 8925-8925/com.bj.gxz.jniapp D/JNI: getStingLen start invoke
    2020-10-3 11:10:16.064 8925-8925/com.bj.gxz.jniapp D/JNI: getStingLen invoke done
    2020-10-3 11:10:16.064 8925-8925/com.bj.gxz.jniapp D/JNI: ExceptionCheck 1
    2020-10-3 11:10:16.065 8925-8925/com.bj.gxz.jniapp W/System.err: java.lang.NullPointerException: Attempt to invoke virtual method 'int java.lang.String.length()' on a null object reference
    2020-10-3 11:10:16.066 8925-8925/com.bj.gxz.jniapp W/System.err:     at com.bj.gxz.jniapp.exception.JNIException.getStingLen(JNIException.java:17)
    2020-10-3 11:10:16.066 8925-8925/com.bj.gxz.jniapp W/System.err:     at com.bj.gxz.jniapp.exception.JNIException.nativeInvokeJavaException(Native Method)
    2020-10-3 11:10:16.067 8925-8925/com.bj.gxz.jniapp W/System.err:     at com.bj.gxz.jniapp.MainActivity.onJniException(MainActivity.java:54)
    

    程序不crash了。这样就可以在检测到异常后做一些异常处理(比如抛异常给java层处理,下文会讲)。当然平时写代码发现后还是需要按照正常思路解决的。

    2.native方法如何抛出java类的异常

    jni.h中提供了一个ThrowNew的方法来直接对java抛出异常

    jint ThrowNew(JNIEnv *env, jclass clazz, 
    const char *message);
    
    Constructs an exception object from the specified class with the message specified by message and causes that exception to be thrown.
    

    我们demo下

    // 在java中声明一个抛出throws Exception的native方法
    public native void nativeThrowException() throws Exception;
    
    extern "C"
    JNIEXPORT void JNICALL
    Java_com_bj_gxz_jniapp_exception_JNIException_nativeThrowException(JNIEnv *env, jobject thiz) {
        jclass newExcCls = env->FindClass("java/lang/RuntimeException");
        env->ThrowNew(newExcCls, "throw exception from c++ code");
    }
    
    // 调用
        public void onJniException(View view) {
            JNIException jniException = new JNIException();
            jniException.nativeInvokeJavaException();
            try {
                jniException.nativeThrowException();
            } catch (Exception e) {
                e.printStackTrace();
                Log.e(TAG, "nativeThrowException Exception:", e);
            }
        }
    
    com.bj.gxz.jniapp E/JNI: nativeThrowException Exception:
        java.lang.RuntimeException: throw exception from c++ code
            at com.bj.gxz.jniapp.exception.JNIException.nativeThrowException(Native Method)
            at com.bj.gxz.jniapp.MainActivity.onJniException(MainActivity.java:56)
            at java.lang.reflect.Method.invoke(Native Method)
            省略不重要的信息...
    

    结果符合预期,c++代码里抛出的异常被我们捕获了。

    这样看,这两个可以结合使用,当jni中代码出现异常的时候可以通过jni.h提供的方法ThrowNew 抛到java层,由java调用者执行自己的异常处理代码逻辑。是的。

    源代码:https://github.com/ta893115871/JNIAPP

    相关文章

      网友评论

        本文标题:Android-JNI开发系列《三》-异常处理

        本文链接:https://www.haomeiwen.com/subject/ewkwuktx.html