音视频直播技术Android开发经验谈Android开发

「音视频直播技术」JNI编程常见问题

2017-09-10  本文已影响228人  音视频直播技术专家

前言

本文是JNI编程注意事项的第二篇文章。在上篇中讲解了 JavaVM/JNIEnv, Threads, jclass/jfieldID/jmethodID 以及 Local/Global 引用。今天我们继续讲解余下的部分。

Native 库

我们可以使用System.loadLibrary将共享库导入进来。引入Native代码的最好方法如下:

如果用C++编写,JNI_OnLoad函数应该看起来像下面的样子:

jint JNI_OnLoad(JavaVM* vm, void* reserved)
{
    JNIEnv* env;
    if (vm->GetEnv(reinterpret_cast<void**>(&env), JNI_VERSION_1_6) != JNI_OK) {
        return -1;
    }

    // Get jclass with env->FindClass.
    // Register methods with env->RegisterNatives.

    return JNI_VERSION_1_6;
}

你也可以用共享库的完整路径名调用System.load函数而不是System.loadLibrary。对于Andrioid应用来说, 您可能会发现从上下文对象获取应用程序的私有数据存储区域的完整路径非常方便。

上面的方法是推荐方法,但不是唯一的方法。其实,可以不需要显式注册JNI方法,也不需要提供JNI_OnLoad函数。您可以使用以特定方式命名的Native方法。但这种方式很不好,因为如果方法签名是错的,直到第一次它被使用时你才知道它出错了。

另一个关于JNI_OnLoad需要注意的事项:任何FindClass操作,都应该在加载共享库的类加载器上下文中调用。通常,FindClass使用与解释栈顶端方法相关联的加载器,如果没有(因为线程刚刚绑定),它将使用“系统”类加载器。这使JNI_OnLoad成为查找和缓存类对象引用的最好地方。

UTF-8 和 UTF-16 符字串

Java编程语言使用UTF-16编码。为了方便,JNI提供了与UTF-8一起使用的方法。但这种UTF-8是修改过的UTF-8编码方式。这种方式对于C代码是有用的,因为它将\u0000编码为0xc0 0x80而不是0x00。好处是,您可以依靠拥有C风格的零终止字符串。坏处是,您不能将任意的UTF-8数据传递给JNI,并希望它能正常工作。

如果可能,通常使用UTF-16字符串操作更快。在Android当前版本中,使用GetStringChars函数不需要拷贝其内容(它的内容是UTF-8编码),但使用GetStringUTFChars则需要分配和转换为UTF-8。请注意,UTF-16字符串不是以零终止的,\u0000被认为是正常数据,所以你需要自己保存字符串长度以及jchar指针。

不要忘记释放你获得的字符串。字符串函数返回jchar *或jbyte *,它们是C样式的指向原始数据的指针,而不是本地引用。它们被保证有效,直到调用Release,这意味着当native方法返回时它们不会自动释放。

传递给NewStringUTF的数据必须使用修改过的UTF-8格式。常见的错误是从文件或网络流读取字符数据,并将其传递给NewStringUTF,而不对其进行过滤。除非你知道数据是7位ASCII,否则你需要去掉高ASCII字符或将它们转换成适当的UTF-8格式。

如果不这样做,UTF-16转换可能不会是您期望结果的。扩展的JNI检查将扫描字符串并警告您它是无效数据,但它们不会捕获所有内容。

原始数组

JNI提供了访问数组对象内容的功能,虽然对象数组必须一次访问一个条目,但是可以直接读取和写入原始数组,就像它们在C中被声明一样。

使接口尽可能高效,除非受到VM实现的限制,Get<PrimitiveType>ArrayElements系列调用允许运行时返回指向实际元素的指针,或分配一些内存并复制他们。无论哪种方式,返回的原始指针都将保证是有效的,直到发出相应的Release调用(这意味着,如果数据未被复制,数组中的对象是固定的,并且不能被重新定位)。你必须释放你获得的每个数组,此外,如果Get调用失败,您必须确保代码不会释放这个空指针。

您可以通过传递isCopy参数是否是NULL来确定数据是否被复制了。但这种方式基本没什么用。

Release函数的mode参数有三种值。运行时的行为依赖于返回的是实际数据的指针还是其副本:

检查isCopy标志的原因之一,是在更改数组后知道是否需要使用JNI_COMMIT参数调用Release。如果在更改数组和执行代码之间进行交替,你可以什么都不做。检查标志的第二个原因,是有效地处理JNI_ABORT。例如,您可能需要得到一个数组,修改它,并将其传递给其他函数,然后丢弃更改。如果您知道JNI正在为您制作新的副本,则无需创建另一个“可编辑的”副本。如果JNI传给你的是原始的数据,那么你需要自己做拷贝。

常见的错误,是认为如果 *isCopy为false,则可以跳过Release调用。如果没有分配复制缓冲区,则原始内存必须被固定,并且不能被垃圾收集器移动。另请注意,JNI_COMMIT标志不会释放数组,您需要再次使用不同的标志调用Release。

Region Calls

拷贝数据时有一种替代方法,例如,使用Ge<Type>ArrayElements和GetStringChars,这两个函数非常有用。

    jbyte* data = env->GetByteArrayElements(array, NULL);
    if (data != NULL) {
        memcpy(buffer, data, len);
        env->ReleaseByteArrayElements(array, data, JNI_ABORT);
    }

上面的代码首先获得数组,将len字节元素复制出来,然后释放数组。根据实现方式,Get要么是获得地址,要么是复制数组内容。代码复制数据(可能是第二次),然后调用Release;在这种情况下,JNI_ABORT确保没有第三副本的机会。

下在的代码可以更简单地完成同样的事情:

env->GetByteArrayRegion(array, 0, len, buffer);

这种方式有几种优势:

类似地,您可以使用Set<Type>ArrayRegion调用将数据复制到数组中,并使用GetStringRegion或GetStringUTFRegion从字符串中复制字符。

异常

当异常待处理时,不能调用大多数JNI函数。您的代码应该会注意到异常(通过函数的返回值,ExceptionCheck或ExceptionOccurred)并返回,或者清除异常并处理它。
当异常挂起时,您允许调用的JNI函数有:

许多JNI调用可能会引发异常,但通常会提供更简单的检查失败的方法。例如,如果NewString返回非NULL值,则不需要检查异常。但是,如果调用方法(使用像CallObjectMethod这样的函数),则必须始终检查异常,因为如果抛出异常,返回值将无效。

注意,被解释的代码抛出的异常不能解开本机堆栈帧,因为Android不支持C++异常。JNI Throw和ThrowNew指令在当前线程中设置了一个异常指针。返回到本地代码管理后,异常将被注意到和处理。

本地代码可以通过调用ExceptionCheck或ExceptionOccurred“捕获”异常,并用ExceptionClear清除它。像往常一样,抛弃异常而不处理它们可能会导致问题。

没有用于操作Throwable对象的内置函数,所以如果你想得到异常字符串,你需要找到Throwable类,查找getMessage的方法ID "()java/lang/String;",并且如果结果是非空的,则使用GetStringUTFChars获取可以传递给printf(3)或等同物的信息。

扩展检查

JNI几乎没有错误检查,错误通常会导致崩溃。Android提供了一种称为CheckJNI的模式,在调用标准实现之前,将JavaVM和JNIEnv函数表指针切换到执行扩展系列检查的函数表。

扩展检查包括:

(方法和字段的辅助功能仍未被检查:访问限制不适用于Native代码。)

有几种启用CheckJNI的方法:
如是你使用的是模拟器,CheckJNI默认是打开的。
如果拥有root权限的设备,你可以使用下面的一系列命令重启 Runtime 并开启 CheckJNI:

adb shell stop
adb shell setprop dalvik.vm.checkjni true
adb shell start

在这些情况下,当 Runtime 启动时,在 logcat 输出中可以看到如下信息:

D AndroidRuntime: CheckJNI is ON

如果你是一台普通设备,你可以使用下面的命令

adb shell setprop debug.checkjni 1

这不会影响已经运行的应用程序,但从该点启动的任何应用程序将启用CheckJNI。(将属性更改为任何其他值或重新启动将会再次禁用CheckJNI。)在这种情况下,你能在下次应用程序启动时在logcat输出中看到下面的信息:

D Late-enabling CheckJNI

您还可以在应用程序的manifest中设置android:debuggable属性,以便为您的应用程序启用CheckJNI。请注意,Android构建工具会自动为某些构建类型执行此操作。

常见问题

FAQ: 为什么会出现 UnsatisfiedLinkError?

在处理Native代码时,看到这样的失败并不罕见:

java.lang.UnsatisfiedLinkError: Library foo not found

在某些情况下这意味着,库没有发现。其它情况是说库存在,但不能由 dlopen 打开。失败的具体信息在异常的信息中可以找到。

您可能遇到“库未找到”异常的常见原因:

另一类UnsatisfiedLinkError故障类似于:

java.lang.UnsatisfiedLinkError: myfunc
        at Foo.myfunc(Native Method)
        at Foo.main(Foo.java:10)

在 logcat 中你将看到:

W/dalvikvm(  880): No implementation found for native LFoo;.myfunc ()V

这意味着在运行时无法成功找到匹配的方法,一些常见的原因是:

使用javah自动生成JNI头可能有助于避免一些问题。

FAQ: 为什么FindClass找不到我的类?

这个建议大多数情况下同样适用于使用GetMethodID或GetStaticMethodID无法找到方法,或无法找到GetFieldID或GetStaticFieldID字段)。

确保类名字符串格式正确。JNI类名以包名开头,并以斜杠分隔,如java/lang/String。如果您正在查找数组类,则需要从适当数量的方括号开始,并且还必须用'L'和';'包装类,所以String的一维数组将是[Ljava/lang/String;。如果你正在查找一个内部类,请使用'$'而不是'.'。一般来说,在.class文件中使用javap是查找类的内部名称的好方法。

如果您使用混淆器,请确保混淆器没有抽出您的类。如果您的类/方法/字段仅用于JNI,则可能会发生这种情况。

如果类名称正确,您可能会遇到类加载器问题。FindClass想要在与你的代码相关联的类加载器中启动类搜索。它检查调用堆栈,看起来像下面这样:

Foo.myfunc(Native Method)
Foo.main(Foo.java:10)

最上面的方法是Foo.myfunc。 FindClass找到与Foo类关联的ClassLoader对象并使用它。

这种做法通常都是没问题的。但如果您自己创建一个线程,可能会遇到麻烦(可能通过调用pthread_create然后使用AttachCurrentThread连接)。现在您的应用程序没有堆栈帧。如果你从这个线程调用FindClass,JavaVM将在“系统”类加载器中启动,而不是与您的应用程序相关联的加载器,因此尝试查找应用程序特定的类将失败。

有几种方法可以解决这个问题:

FAQ: 在Native代码间如何共享原始数据?

您可能会发现自己需要在从托管和本地代码之间访问大量原始数据缓冲区的情况。通常的例子包括操作位图或声音样本。有两种基本方法:
您可以将数据存储在byte[]中。这样从托管代码访问非常快。但是,在本地方面您无法保证不复制数据就可访问数据。在某些实现中,GetByteArrayElements和GetPrimitiveArrayCritical将返回实际指向托管堆中原始数据的指针,但另一方面,它将在本机堆上分配一个缓冲区并复制数据。

另一种方法是将数据存储在直接字节缓冲区中。这些可以使用java.nio.ByteBuffer.allocateDirect或JNI NewDirectByteBuffer函数创建。与常规字节缓冲区不同,存储不会在托管堆上分配,并且可以直接从本地代码访问(使用GetDirectBufferAddress获取地址)。根据实现直接字节缓冲访问的方式,从托管代码访问数据可能非常慢。

选择哪个使用取决于两个因素:

  1. 大多数数据访问是由Java或C / C ++编写的代码发生的?
  2. 如果数据最终被传递给系统API,那么它应该是什么形式的?(例如,如果数据最终被传递给byte[]的函数,那么在直接ByteBuffer中进行处理可能是不明智的。)

如果基于上面的两点仍然判断不出来,请使用直接字节缓冲区。JNI直接构建对它们的支持,并且在将来的版本中性能会得到改善。

小结

本文首先介绍了JNI加载动态库的常用规则,然后讲了使用UTF-8需要注意的事项。仅接着介绍了访问原始数组,区块调用,异常等要注意的点,最后对编写JNI程序常见的问题给出了问题的原因和解决办法。

希望本篇文章对您有所帮助,并继续关注我,谢谢!

上一篇下一篇

猜你喜欢

热点阅读