从安卓运行的本地代码中捕获抛出的异常

67
我目前正在开发一个跨平台程序实现的安卓部分,核心功能集成在我的应用程序中,并通过 android-ndk 实现。我发现任何在本地代码中发生的异常/崩溃最多只会偶尔报告一次。当出现错误时,我会得到以下行为之一:
  • 发生堆栈跟踪/内存转储并写入日志文件,程序消失(设备上没有指示突然应用程序不再存在的原因)。
  • 没有给出堆栈跟踪/转储或其他指示本地代码已崩溃的迹象。程序消失。
  • Java 代码崩溃并显示 NullPointerException (通常是本地代码异常的相同位置),这很痛苦。通常会导致我花费很长时间来调试为什么 Java 代码引发错误,只能发现 Java 代码没问题,本地代码错误完全被屏蔽了。
我似乎找不到任何方法来使我的代码免受本地代码中发生的错误的影响。Try/catch 语句被彻底忽略。除非我的代码被指责为罪魁祸首,否则我甚至没有机会警告用户发生了错误。
请问有人可以帮助我应对本地代码崩溃的情况吗?

单元测试,日志... 我所知道的唯一选择(但我远非万事通,请继续寻找 :) ) - Warpzit
你完全控制本地代码吗?还是只控制Java端? - Mooing Duck
仅限于本地代码的最顶层,即JNI Binder层。 - Graeme
3个回答

67

我曾经也遇到过类似的问题。在Android(一般情况下,在执行本地代码时)如果你抛出一个C++异常并且这个异常没有被捕获,虚拟机就会崩溃(如果我理解得正确的话,我认为这可能是你的问题)。我采取的解决方法是在C++中捕获任何异常,并抛出一个Java异常,而不是使用JNI。下面的代码是我的解决方案的一个简化示例。首先,你有一个JNI方法,它捕获了一个C++异常,然后在try子句中注释了Java异常。

JNIEXPORT void JNICALL Java_com_MyClass_foo (JNIEnv *env, jobject o,jstring param)
{
    try
    {
        // Your Stuff
        ...
    }
    // You can catch std::exception for more generic error handling
    catch (MyCxxException e)
    {
        throwJavaException (env, e.what());
    }
}


void throwJavaException(JNIEnv *env, const char *msg)
{
    // You can put your own exception here
    jclass c = env->FindClass("company/com/YourException");

    if (NULL == c)
    {
        //B plan: null pointer ...
        c = env->FindClass("java/lang/NullPointerException");
    }

    env->ThrowNew(c, msg);
}
请注意,在ThrowNew之后,本地方法不会自动中止。也就是说,控制流会返回到您的本地方法,并在此时挂起新异常。该异常将在您的JNI方法完成后抛出。
我希望这是你正在寻找的解决方案。

我本来希望有更全面的解决方案。但是在使用赏金之后,这似乎是保护Java代码免受本地代码崩溃的最佳方法。 - Graeme
4
虽然这个答案很好地捕获了抛出的C++异常,但它没有处理SIGNAL错误(包括C语言中的空指针异常)。这是一篇很棒的文章,在结合上述内容后,应该能够紧密地定位本地应用程序中的错误报告:https://dev59.com/RHNA5IYBdhLWcg3wH6EW#1789879 - Graeme
2
这个答案比较详细,可能更适合你的需求:https://dev59.com/TG855IYBdhLWcg3wy3sc#12014833 - Ed Burnette
为什么我们需要备选方案?如果计划失败了怎么办? - Maksim Dmitriev
@MaksimDmitriev 如果你使用自己的异常并且它不在类路径上,那么计划B就是备选方案。我已经修改了代码以使其更清晰。 - javier-sanz

8

编辑:请参见这个更优雅的答案


下面的机制基于一个C预处理器宏,我已经成功地在JNI层中实现了这个宏。

上述宏CATCH_CPP_EXCEPTION_AND_THROW_JAVA_EXCEPTION将C ++异常转换为Java异常。

请用您自己的C++异常替换mypackage::Exception。如果您没有在Java中定义相应的my.group.mypackage.Exception,则将"my/group/mypackage/Exception"替换为"java/lang/RuntimeException"

#define CATCH_CPP_EXCEPTION_AND_THROW_JAVA_EXCEPTION              \
                                                                  \
  catch (const mypackage::Exception& e)                           \
  {                                                               \
    jclass jc = env->FindClass("my/group/mypackage/Exception");   \
    if(jc) env->ThrowNew (jc, e.what());                          \
    /* if null => NoClassDefFoundError already thrown */          \
  }                                                               \
  catch (const std::bad_alloc& e)                                 \
  {                                                               \
    /* OOM exception */                                           \
    jclass jc = env->FindClass("java/lang/OutOfMemoryError");     \
    if(jc) env->ThrowNew (jc, e.what());                          \
  }                                                               \
  catch (const std::ios_base::failure& e)                         \
  {                                                               \
    /* IO exception */                                            \
    jclass jc = env->FindClass("java/io/IOException");            \
    if(jc) env->ThrowNew (jc, e.what());                          \
  }                                                               \
  catch (const std::exception& e)                                 \
  {                                                               \
    /* unknown exception */                                       \
    jclass jc = env->FindClass("java/lang/Error");                \
    if(jc) env->ThrowNew (jc, e.what());                          \
  }                                                               \
  catch (...)                                                     \
  {                                                               \
    /* Oops I missed identifying this exception! */               \
    jclass jc = env->FindClass("java/lang/Error");                \
    if(jc) env->ThrowNew (jc, "unidentified exception");          \
  }

使用上述宏定义的文件名为 Java_my_group_mypackage_example.cpp

JNIEXPORT jlong JNICALL Java_my_group_mypackage_example_function1
  (JNIEnv *env, jobject object, jlong value)
{
  try 
  {
    /* ... my processing ... */
    return jlong(result);
  }
  CATCH_CPP_EXCEPTION_AND_THROW_JAVA_EXCEPTION
  return 0;
}

JNIEXPORT jstring JNICALL Java_my_group_mypackage_example_function2
  (JNIEnv *env, jobject object, jlong value)
{
  try 
  {
    /* ... my processing ... */
    jstring jstr = env->NewStringUTF("my result");
    return  jstr;
  }
  CATCH_CPP_EXCEPTION_AND_THROW_JAVA_EXCEPTION
  return 0;
}

JNIEXPORT void JNICALL Java_my_group_mypackage_example_function3
  (JNIEnv *env, jobject object, jlong value)
{
  try 
  {
    /* ... my processing ... */
  }
  CATCH_CPP_EXCEPTION_AND_THROW_JAVA_EXCEPTION
}

仅供参考或好奇心,以下提供相应的Java代码(文件example.java)。请注意,“my-DLL-name”是上述C/C++代码编译为DLL(不带“.dll”扩展名的“my-DLL-name”)。这也可以使用Linux/Unix共享库*.so

package my.group.mypackage;

public class Example {
  static {
    System.loadLibrary("my-DLL-name");
  }

  public Example() {
    /* ... */
  }

  private native int    function1(int); //declare DLL functions
  private native String function2(int); //using the keyword
  private native void   function3(int); //'native'

  public void dosomething(int value) {
    int result = function1(value);  
    String str = function2(value);  //call your DLL functions
    function3(value);               //as any other java function
  }
}

首先,使用 javac 或您喜欢的集成开发环境或maven从example.java生成example.class。其次,使用javahexample.class生成C/C++头文件Java_my_group_mypackage_example.h


嗨@itsrajesh4uguys,我认为你的问题就在使用CATCH_CPP_EXCEPTION_AND_THROW_JAVA_EXCEPTION之前。要定位该行,您可以通过相应的代码替换CATCH_CPP_EXCEPTION_AND_THROW_JAVA_EXCEPTION(每行末尾没有\)来实现。祝你好运,干杯;-) - oHo

0

你有考虑过捕获这个异常然后将其包装成一个运行时异常,只是为了将其抛到更高的堆栈中吗?

我在SCJD中使用了类似的“hack”。通常情况下,NPE表示你的错误,但如果你确信自己没有做错任何事情,那么就创建一个详细记录的RuntimeException,解释该异常用于传递异常。然后解包它并测试是否为NPE的实例,并将其视为自己的异常进行处理。

如果这会导致错误数据,那么你别无选择,只能找到问题的根源。


你的意思是说,我应该通过抛出一个 RuntimeException 来向上传递异常吗?在调试应用程序时,如果在变量上调用 .set()(例如)并且您可以通过调试器看到此变量,并且在 .set() 调用之前直接从 Log.v() 报告,则将引发 NPE 异常。 - Graeme
我的意思是,如果你无法找到根本原因,因为它不是你自己的API抛出的。那么就在一个你完全了解的异常中包装它,比如'BubbleException',然后在堆栈的更高层次上测试这个异常并将其处理为你自己的异常。通常情况下,NPE表示某个地方为空,但如果你认为它的根源在堆栈较低的位置(也许是你正在使用的代码),那么要么放弃这段代码,要么将其抛出为你自己的RuntimeException,以免给你的API客户带来你自己无法解释的异常。 - thejartender
我的意思是本地代码不会“抛出”异常,它以完全不同的方式死亡,无法使用try/catch捕获。我认为报告的NPE是本地代码死亡的副作用,并且与崩溃没有直接关系。 - Graeme
没问题 - 谢谢你的尝试。我认为这个问题很棘手,难以确定。 - Graeme
另外,请注意前面评论中的“我认为” :) - Graeme

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接