NDK

Android NDK 是一组使您能将 C 或 C++(“原生代码”)嵌入到 Android 应用中的工具。

  • 在平台之间移植其应用。
  • 重复使用现有库,或者提供其自己的库供重复使用。
  • 在某些情况下提高性能,特别是像游戏这种计算密集型应用。

原生共享库:NDK 从 C/C++ 源代码编译这些库或 .so 文件。

原生静态库:NDK 也可编译静态库或 .a 文件,而您可将静态库关联到其他库。

Java 原生接口 (JNI):JNI 是 Java 和 C++ 组件用以互相通信的接口。

应用二进制接口 (ABI):ABI 可以非常精确地定义应用的机器代码在运行时应该如何与系统交互。NDK 根据这些定义编译 .so 文件。不同的 ABI 对应不同的架构:NDK 为 32 位 ARM、AArch64、x86 及 x86-64 提供 ABI 支持。

JNI

JNI是Java Native Interface。 它定义了Android从托管代码(用Java或Kotlin编程语言编写)编译的字节码的方式,以与本机代码(用C / C ++编写)进行交互。 JNI是供应商中立的,支持从动态共享库加载代码,虽然繁琐但有时相当有效。

要浏览全局JNI引用并查看创建和删除全局JNI引用的位置,请使用Android Studio 3.2及更高版本中的Memory Profiler中的JNI堆视图。

一般提示

尽量减少JNI层的占用空间。

最小化跨JNI层的资源编组。

跨越JNI层进行编组具有非常重要的成本。 尝试设计一个接口,以最大限度地减少编组所需的数据量以及必须对数据进行编组的频率。

避免在托管编程语言编写的代码与可能的C ++编写的代码之间进行异步通信。

这将使您的JNI接口更易于维护。 通常可以通过使用与UI相同的语言保持异步更新来简化异步UI更新。 例如,不是通过JNI从Java代码中的UI线程调用C ++函数,而是最好在Java编程语言中的两个线程之间进行回调,其中一个线程进行阻塞C++调用,然后通知UI线程 当阻塞调用完成时。

最大限度地减少JNI需要触摸或触摸的线程数。

如果确实需要在Java和C ++语言中使用线程池,请尝试在池所有者之间而不是在各个工作线程之间保持JNI通信。

将您的接口代码保存在少量易于识别的C ++和Java源位置,以方便将来的重构。

考虑适当使用JNI自动生成库。

JavaVM and JNIEnv

JNI定义了两个关键数据结构,“JavaVM”和“JNIEnv”。 这两者基本上都是指向函数表指针的指针。 (在C ++版本中,它们是带有指向函数表的指针的类,以及用于指向表中的每个JNI函数的成员函数。)JavaVM提供“调用接口”函数,允许您创建和销毁 JavaVM的。 理论上,每个进程可以有多个JavaVM,但Android只允许一个。

JNIEnv提供了大多数JNI功能。 本地函数都接收JNIEnv作为第一个参数。

JNIEnv用于线程本地存储。 因此,您无法在线程之间共享JNIEnv。 如果一段代码没有其他方法来获取它的JNIEnv,你应该共享JavaVM,并使用GetEnv来发现线程的JNIEnv。 (假设它有一个;请参阅下面的AttachCurrentThread。)

JNIEnv和JavaVM的C声明与C++声明不同。 “jni.h”包含文件提供了不同的typedef,具体取决于它是否包含在C或C ++中。 因此,在两种语言包含的头文件中包含JNIEnv参数是一个坏主意。 (换句话说:如果您的头文件需要#ifdef __cplusplus,如果该头中的任何内容引用JNIEnv,您可能需要做一些额外的工作。)

Threads

所有线程都是Linux线程,由内核调度。 它们通常从托管代码(使用Thread.start)启动,但也可以在其他地方创建,然后附加到JavaVM。 例如,使用pthread_create启动的线程可以使用JNI AttachCurrentThread或AttachCurrentThreadAsDaemon函数附加。 在连接线程之前,它没有JNIEnv,也无法进行JNI调用。

附加本机创建的线程会导致构造java.lang.Thread对象并将其添加到“main”ThreadGroup,使调试器可以看到它。 在已经连接的线程上调用AttachCurrentThread是一个无效操作。

Android不会挂起执行本地代码的线程。 如果正在进行垃圾收集,或者调试器已发出挂起请求,则Android将在下次进行JNI调用时暂停该线程。

通过JNI连接的线程必须在退出之前调用DetachCurrentThread。 如果直接对此进行编码很麻烦,在Android 2.0(Eclair)及更高版本中,您可以使用pthread_key_create来定义将在线程退出之前调用的析构函数,并从那里调用DetachCurrentThread。 (将该键与pthread_setspecific一起使用以将JNIEnv存储在线程局部存储中;这样它将作为参数传递给析构函数。)

jclass, jmethodID, and jfieldID

如果要从本地代码访问对象的字段,请执行以下操作:

  • 使用FindClass获取类的类对象引用
  • 使用GetFieldID获取字段的字段ID
  • 使用适当的内容(例如GetIntField)获取字段的内容

同样,要调用方法,首先要获取类对象引用,然后获取方法ID。 ID通常只是指向内部运行时数据结构的指针。 查找它们可能需要进行多次字符串比较,但是一旦有了它们,实际调用获取字段或调用方法的速度非常快。

如果性能很重要,那么查看值一次并将结果缓存在本地代码中会很有用。 由于每个进程限制一个JavaVM,因此将此数据存储在静态本地结构中是合理的。

在卸载类之前,类引用,字段ID和方法ID保证有效。 只有在与ClassLoader关联的所有类都可以进行垃圾回收时,才会卸载类,这种情况很少见,但在Android中并非不可能。 但请注意,jclass是类引用,必须通过调用NewGlobalRef进行保护(请参阅下一节)。

如果您想在加载类时缓存ID,并在卸载和重新加载类时自动重新缓存它们,初始化ID的正确方法是将一段代码添加到相应的代码中。

/*
 * We use a class initializer to allow the native code to cache some
 * field offsets. This native function looks up and caches interesting
 * class/field/method IDs. Throws on failure.
 */
private static native void nativeInit();

static {
    nativeInit();
}
复制代码

在执行ID查找的C / C ++代码中创建nativeClassInit方法。 在初始化类时,代码将执行一次。 如果该类被卸载然后重新加载,它将再次执行。

Local and global references

每个参数都传递给本机方法,几乎JNI函数返回的每个对象都是“本地引用”。 这意味着它在当前线程中当前本机方法的持续时间内有效。 即使在本机方法返回后对象本身继续存在,引用也无效。

这适用于jobject的所有子类,包括jclass,jstring和jarray。 (当启用扩展JNI检查时,运行时将警告您大多数引用误用。)

获取非本地引用的唯一方法是通过函数NewGlobalRef和NewWeakGlobalRef。

如果要保留较长时间段的引用,则必须使用“全局”引用。 NewGlobalRef函数将本地引用作为参数并返回全局引用。 在调用DeleteGlobalRef之前,保证全局引用有效。

这种模式通常在缓存从FindClass返回的jclass时使用,例如:

jclass localClass = env->FindClass("MyClass"); jclass globalClass = reinterpret_cast<jclass>(env->NewGlobalRef(localClass));
复制代码

所有JNI方法都接受本地和全局引用作为参数。 对同一对象的引用可能具有不同的值。 例如,在同一对象上对NewGlobalRef的连续调用的返回值可能不同。 要查看两个引用是否引用同一对象,必须使用IsSameObject函数。 切勿在本机代码中将引用与==进行比较。

这样做的一个结果是您不能假定对象引用在本机代码中是常量或唯一的。 表示对象的32位值可能与方法的一次调用不同,并且两个不同的对象可能在连续调用上具有相同的32位值。 不要将jobject值用作键。

程序员必须“不要过度分配”本地引用。 实际上,这意味着如果您正在创建大量本地引用,也许在运行对象数组时,您应该使用DeleteLocalRef手动释放它们,而不是让JNI为您执行此操作。 实现仅需要为16个本地引用保留插槽,因此如果您需要更多,则应该随意删除或使用EnsureLocalCapacity / PushLocalFrame来保留更多。

请注意,jfieldIDs和jmethodIDs是不透明的类型,而不是对象引用,不应传递给NewGlobalRef。 GetStringUTFChars和GetByteArrayElements等函数返回的原始数据指针也不是对象。 (它们可以在线程之间传递,并且在匹配的Release调用之前有效。)

一个不寻常的案例值得单独提及。 如果使用AttachCurrentThread附加本机线程,则运行的代码将永远不会自动释放本地引用,直到线程分离。 您创建的任何本地引用都必须手动删除。 通常,在循环中创建本地引用的任何本机代码可能需要进行一些手动删除。

小心使用全局引用。 全局引用可能是不可避免的,但它们很难调试,并且可能导致难以诊断的内存(错误)行为。 在其他条件相同的情况下,具有较少全局引用的解决方案可能更好。

UTF-8 and UTF-16 strings

Java编程语言使用UTF-16。 为方便起见,JNI还提供了使用Modified UTF-8的方法。 修改后的编码对C代码很有用,因为它将\ u0000编码为0xc0 0x80而不是0x00。 关于这一点的好处是你可以依靠C风格的零终止字符串,适合与标准的libc字符串函数一起使用。 缺点是您无法将任意UTF-8数据传递给JNI并期望它能够正常工作。

如果可能,使用UTF-16字符串操作通常会更快。 Android目前不需要GetStringChars中的副本,而GetStringUTFChars需要分配和转换为UTF-8。 请注意,UTF-16字符串不是以零结尾的,并且允许使用\ u0000,因此您需要挂起字符串长度以及jchar指针。

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

传递给NewStringUTF的数据必须采用Modified UTF-8格式。 一个常见的错误是从文件或网络流中读取字符数据并将其交给NewStringUTF而不对其进行过滤。 除非您知道数据是有效的MUTF-8(或7位ASCII,这是兼容的子集),否则您需要删除无效字符或将它们转换为正确的修改的UTF-8格式。 如果不这样做,UTF-16转换可能会提供意外结果。 CheckJNI - 默认情况下为模拟器打开 - 扫描字符串并在VM收到无效输入时中止VM。

Primitive arrays

JNI提供了访问数组对象内容的函数。 虽然一次只能访问一个条目的对象数组,但可以直接读取和写入基元数组,就好像它们是用C语句声明的一样。

为了使接口尽可能高效而不约束VM实现,Get <PrimitiveType> ArrayElements系列调用允许运行时返回指向实际元素的指针,或者分配一些内存并进行复制。 无论哪种方式,返回的原始指针都保证有效,直到发出相应的Release调用(这意味着,如果数据未被复制,则数组对象将被固定,并且不能作为压缩的一部分重新定位 堆)。 您必须释放您获得的每个数组。 此外,如果Get调用失败,则必须确保您的代码稍后不会尝试释放NULL指针。

您可以通过传入isCopy参数的非NULL指针来确定是否复制了数据。 这很少有用。

Release调用采用一个mode参数,该参数可以包含三个值之一。 运行时执行的操作取决于它是否返回指向实际数据的指针或其副本:

  • 0

    实际:数组对象未固定。

    复制:复制数据。释放带有副本的缓冲区。

  • JNI_COMMIT

    实际:什么都不做。

    复制:复制数据。没有释放带有副本的缓冲区。

  • JNI_ABORT

    实际:数组对象未固定。早期的写入不会中止。

    复制:释放带有副本的缓冲区;对它的任何改变都会丢失。

检查isCopy标志的一个原因是知道在更改数组后是否需要使用JNI_COMMIT调用Release - 如果您在进行更改和执行使用数组内容的代码之间交替,则可以跳过 无操作提交。 检查标志的另一个可能原因是有效处理JNI_ABORT。 例如,您可能希望获取一个数组,将其修改到位,将片段传递给其他函数,然后丢弃更改。 如果您知道JNI正在为您制作新副本,则无需创建另一个“可编辑”副本。 如果JNI将原件传给你,那么你需要制作自己的副本。

如果* isCopy为false,则假设您可以跳过Release调用是一个常见的错误(在示例代码中重复)。 不是这种情况。 如果没有分配复制缓冲区,则原始内存必须固定,并且不能被垃圾收集器移动。

另请注意,JNI_COMMIT标志不会释放数组,您最终需要使用不同的标志再次调用Release。

Region calls

除了Get <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);
复制代码

这有几个好处:

  • 需要一个JNI调用而不是2,从而减少开销。
  • 不需要固定或额外的数据副本。
  • 降低程序员错误的风险 - 没有在发生故障后忘记调用Release的风险。

同样,您可以使用Set <Type> ArrayRegion调用将数据复制到数组中,使用GetStringRegion或GetStringUTFRegion将字符复制到String中。

Exceptions

异常处于待处理状态时,您不能调用大多数JNI函数。 您的代码应该注意到异常(通过函数的返回值,ExceptionCheck或ExceptionOccurred)并返回,或者清除异常并处理它。

在异常处于挂起状态时,您可以调用的唯一JNI函数是:

DeleteGlobalRef
DeleteLocalRef
DeleteWeakGlobalRef
ExceptionCheck
ExceptionClear
ExceptionDescribe
ExceptionOccurred
MonitorExit
PopLocalFrame
PushLocalFrame
Release<PrimitiveType>ArrayElements
ReleasePrimitiveArrayCritical
ReleaseStringChars
ReleaseStringCritical
ReleaseStringUTFChars
复制代码

许多JNI调用都可以抛出异常,但通常会提供一种更简单的方法来检查失败。 例如,如果NewString返回非NULL值,则无需检查异常。 但是,如果调用方法(使用类似CallObjectMethod的函数),则必须始终检查异常,因为如果抛出异常,返回值将无效。

请注意,解释代码抛出的异常不会展开本机堆栈帧,Android也不支持C ++异常。 JNI Throw和ThrowNew指令只是在当前线程中设置了一个异常指针。 从本机代码返回托管后,将注意并正确处理该异常。

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

没有用于操作Throwable对象本身的内置函数,所以如果你想(比如)获取异常字符串,你需要找到Throwable类,查找getMessage的方法ID“()Ljava / lang / String ;“,调用它,如果结果是非NULL,则使用GetStringUTFChars获取可以传递给printf(3)或等效的东西。

Extended checking

JNI进行的错误检查很少。 错误通常会导致崩溃。 Android还提供了一种名为CheckJNI的模式,其中JavaVM和JNIEnv函数表指针切换到在调用标准实现之前执行扩展系列检查的函数表。

附加检查包括:

  • 数组:尝试分配负大小的数组。
  • 错误的指针:将错误的jarray / jclass / jobject / jstring传递给JNI调用,或者将NULL指针传递给具有非可空参数的JNI调用。
  • 类名:将类名称的“java / lang / String”样式传递给JNI调用。
  • 关键调用:在“关键”get和相应的release之间进行JNI调用。
  • 直接ByteBuffers:将错误的参数传递给NewDirectByteBuffer。
  • 异常:在异常挂起时进行JNI调用。
  • JNIEnv * s:从错误的线程中使用JNIEnv *。
  • jfieldIDs:使用NULL jfieldID,或使用jfieldID将字段设置为错误类型的值(例如,尝试将StringBuilder分配给String字段),或者使用jfieldID为静态字段设置实例字段或 反之亦然,或者使用来自一个类的jfieldID和另一个类的实例。
  • jmethodIDs:在进行Call *方法JNI调用时使用错误的jmethodID:错误的返回类型,静态/非静态不匹配,'this'(非静态调用)或错误类(静态调用)的错误类型。
  • 引用:对错误的引用使用DeleteGlobalRef / DeleteLocalRef。
  • 释放模式:将错误释放模式传递给释放调用(0,JNI_ABORT或JNI_COMMIT以外的其他模式)。
  • 类型安全:从本机方法返回不兼容的类型(从声明为返回String的方法返回StringBuilder,比如说)。
  • UTF-8:将无效的Modified UTF-8字节序列传递给JNI调用。

(仍未检查方法和字段的可访问性:访问限制不适用于本机代码。)

有几种方法可以启用CheckJNI。

如果您正在使用模拟器,则默认情况下CheckJNI处于启用状态。

如果您有root设备,则可以使用以下命令序列在启用CheckJNI的情况下重新启动运行时:

adb shell stop
adb shell setprop dalvik.vm.checkjni true
adb shell start
复制代码

在其中任何一种情况下,当运行时启动时,您将在logcat输出中看到类似的内容:

D AndroidRuntime: CheckJNI is ON
复制代码

如果您有常规设备,则可以使用以下命令:

adb shell setprop debug.checkjni 1
复制代码

这不会影响已经运行的应用程序,但从那时起启动的任何应用程序都将启用CheckJNI。 (将属性更改为任何其他值或只是重新启动将再次禁用CheckJNI。)在这种情况下,您将在下次应用程序启动时在logcat输出中看到类似的内容:

D Late-enabling CheckJNI
复制代码

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

Native libraries

您可以使用标准System.loadLibrary从共享库加载本机代码。

实际上,旧版本的Android在PackageManager中存在错误,导致本机库的安装和更新不可靠。 ReLinker项目为此和其他本机库加载问题提供了变通方法。

从静态类初始化程序调用System.loadLibrary(或ReLinker.loadLibrary)。 参数是“未修饰”的库名称,因此要加载libfubar.so,您将传入“fubar”。

运行时有两种方法可以找到您的本机方法。 您可以使用RegisterNatives显式注册它们,也可以让运行时使用dlsym动态查找它们。 RegisterNatives的优点是你可以预先检查符号是否存在,而且除了JNI_OnLoad之外,你不能导出任何东西,从而可以拥有更小更快的共享库。 让运行时发现函数的优点是编写的代码略少。

要使用RegisterNatives:

  • 提供JNIEXPORT jint JNI_OnLoad(JavaVM * vm,void * reserved)函数。
  • 在JNI_OnLoad中,使用RegisterNatives注册所有本机方法。
  • 使用-fvisibility = hidden构建,以便只从您的库中导出JNI_OnLoad。 这会产生更快,更小的代码,并避免与加载到应用程序中的其他库发生潜在冲突(但如果应用程序在本机代码中崩溃,则会创建不太有用的堆栈跟踪)。

静态初始化程序应如下所示:

static {
    System.loadLibrary("fubar");
}
复制代码

如果用C ++编写,JNI_OnLoad函数看起来应该是这样的:

JNIEXPORT 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;
}
复制代码

要使用本机方法的“发现”,您需要以特定方式命名它们(有关详细信息,请参阅JNI规范)。 这意味着如果方法签名是错误的,那么在第一次实际调用该方法之前,您将不会知道它。

如果您只有一个具有本机方法的类,则对System.loadLibrary的调用在该类中是有意义的。 否则你应该从应用程序进行调用,这样你就知道它总是被加载,并且总是提前加载。

从JNI_OnLoad进行的任何FindClass调用都将解析用于加载共享库的类加载器的上下文中的类。 通常,FindClass使用与Java堆栈顶部的方法关联的加载器,或者如果没有(因为线程刚刚附加),它使用“系统”类加载器。 这使得JNI_OnLoad成为查找和缓存类对象引用的便利位置。

64-bit considerations

要支持使用64位指针的体系结构,在Java域中存储指向本机结构的指针时,请使用long字段而不是int。

Unsupported features/backwards compatibility

支持所有JNI 1.6功能,但以下情况除外:

  • DefineClass未实现。 Android不使用Java字节码或类文件,因此传入二进制类数据不起作用。

为了向后兼容较旧的Android版本,您可能需要注意:

  • 动态查找本机函数

在Android 2.0(Eclair)之前,在搜索方法名称时,'$'字符未正确转换为“_00024”。 解决此问题需要使用显式注册或将本机方法移出内部类。

  • 分离线程

在Android 2.0(Eclair)之前,不可能使用pthread_key_create析构函数来避免“退出前必须分离线程”检查。 (运行时也使用了一个pthread键析构函数,所以它首先要看哪个被调用。)

  • 弱全局引用

在Android 2.2(Froyo)之前,没有实现弱全局引用。 较旧的版本会强烈拒绝使用它们的尝试。 您可以使用Android平台版本常量来测试支持。 在Android 4.0(Ice Cream Sandwich)之前,弱全局引用只能传递给NewLocalRef,NewGlobalRef和DeleteWeakGlobalRef。 (该规范强烈鼓励程序员在对它们做任何事情之前创建对弱全局变量的硬引用,所以这不应该是任何限制。) 从Android 4.0(Ice Cream Sandwich)开始,弱全局引用可以像任何其他JNI引用一样使用。

  • 本地引用

直到Android 4.0(冰淇淋三明治),本地引用实际上是直接指针。 Ice Cream Sandwich添加了支持更好的垃圾收集器所需的间接,但这意味着在旧版本中无法检测到大量JNI错误。在Android 8.0之前的Android版本中,本地引用的数量限制为特定于版本的限制。 从Android 8.0开始,Android支持无限制的本地引用。

  • 使用GetObjectRefType确定引用类型

直到Android 4.0(冰淇淋三明治),由于使用直接指针(见上文),才能正确实现GetObjectRefType。 相反,我们使用了一种启发式方法,按顺序查看弱全局表,参数,本地表和全局表。 第一次找到你的直接指针时,它会报告你的引用是它正在检查的类型。 这意味着,例如,如果您在全局jclass上调用了GetObjectRefType,该jclass恰好与作为静态本机方法的隐式参数传递的jclass相同,那么您将获得JNILocalRefType而不是JNIGlobalRefType。

常见问题:为什么我会收到UnsatisfiedLinkError?

在处理本机代码时,看到这样的故障并不罕见:

java.lang.UnsatisfiedLinkError: Library foo not found
复制代码

在某些情况下,它意味着它所说的 - 找不到库。 在其他情况下,库存在但无法通过dlopen(3)打开,并且可以在异常的详细消息中找到失败的详细信息。

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

  • 该库不存在或应用程序无法访问。使用adb shell ls -l <path>检查其存在和权限。

  • 该库不是使用NDK构建的。这可能导致对设备上不存在的函数或库的依赖性。

另一类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
复制代码

这意味着运行时试图找到匹配的方法但是不成功。 一些常见的原因是:

  • 该库未加载。 检查logcat输出以获取有关库加载的消息。

  • 由于名称或签名不匹配,找不到该方法。 这通常是由:

    对于惰性方法查找,无法使用extern“C”和适当的可见性(JNIEXPORT)声明C ++函数。 请注意,在冰淇淋三明治之前,JNIEXPORT宏不正确,因此使用带有旧jni.h的新GCC将不起作用。 您可以使用arm-eabi-nm查看库中出现的符号; 如果它们看起来很糟糕(类似于_Z15Java_Foo_myfuncP7_JNIEnvP7_jclass而不是Java_Foo_myfunc),或者如果符号类型是小写的't'而不是大写的'T',那么你需要调整声明。

    对于显式注册,输入方法签名时会出现轻微错误。 确保您传递给注册调用的内容与日志文件中的签名匹配。 请记住,'B'是字节,'Z'是布尔值。 签名中的类名组件以'L'开头,以';'结尾,使用'/'分隔包/类名,并使用''分隔内部类名称(Ljava / util / Map Entry;,比如说 )。

常见问题:为什么FindClass找不到我的class?

(大多数建议同样适用于使用GetMethodID或GetStaticMethodID查找方法的失败,或者使用GetFieldID或GetStaticFieldID的字段。)

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

如果您正在使用ProGuard,请确保ProGuard没有删除您的class。 如果您的类/方法/字段仅用于JNI,则会发生这种情况。

如果类名看起来正确,则可能会遇到类加载器问题。 FindClass希望在与您的代码关联的类加载器中启动类搜索。 它检查调用堆栈,它看起来像:

Foo.myfunc(Native Method) 
Foo.main(Foo.java:10)
复制代码

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

这通常会做你想要的。 如果您自己创建一个线程(可能通过调用pthread_create然后将其与AttachCurrentThread一起附加),您可能会遇到麻烦。 现在您的应用程序中没有堆栈帧。 如果从此线程调用FindClass,JavaVM将从“system”类加载器开始,而不是与应用程序关联的类加载器,因此尝试查找特定于应用程序的类将失败。

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

  • 在JNI_OnLoad中进行一次FindClass查找,并缓存类引用以供以后使用。 作为执行JNI_OnLoad的一部分而进行的任何FindClass调用都将使用与调用System.loadLibrary的函数关联的类加载器(这是一个特殊规则,用于使库初始化更方便)。 如果您的应用程序代码正在加载库,则FindClass将使用正确的类加载器。

  • 通过声明本机方法获取Class参数然后传递Foo.class,将类的实例传递给需要它的函数。

  • 在某个地方缓存对ClassLoader对象的引用,并直接发出loadClass调用。 这需要一些努力。

常见问题:如何与本机代码共享原始数据?

您可能会发现自己需要从托管代码和本机代码访问大型原始数据缓冲区。 常见示例包括操纵位图或声音样本。 有两种基本方法。

您可以将数据存储在byte []中。 这允许从托管代码进行非常快速的访问。 但是,在本机方面,您无法保证无需复制即可访问数据。 在某些实现中,GetByteArrayElements和GetPrimitiveArrayCritical将返回托管堆中原始数据的实际指针,但在其他实现中,它将在本机堆上分配缓冲区并复制数据。

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

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

  • 大多数数据访问是否会发生在用Java或C / C ++编写的代码中?

  • 如果数据最终传递给系统API,那么它必须采用什么形式? (例如,如果数据最终传递给采用byte []的函数,则在直接ByteBuffer中进行处理可能是不明智的。)

如果没有明确的赢家,请使用直接字节缓冲区。 对它们的支持直接构建在JNI中,并且在将来的版本中性能应该得到改善。

JNI数据类型

extern "C"
JNIEXPORT jint
JNICALL
Java_com_dodola_traphooks_MainActivity_intFromJNI(
        JNIEnv *env,
        jobject) {
    int result = add(1, 2);
    ALOG("%d=====", result);
    return result;
}
复制代码
JNIEXPORT

在 Windows 中,定义为__declspec(dllexport)。因为Windows编译 dll 动态库规定,如果动态库中的函数要被外部调用,需要在函数声明中添加此标识,表示将该函数导出在外部可以调用。

在 Linux/Unix/Mac os/Android 这种 Like Unix系统中,定义为__attribute__ ((visibility ("default")))

GCC 有个visibility属性, 该属性是说, 启用这个属性:

  • 当-fvisibility=hidden时

    动态库中的函数默认是被隐藏的即 hidden. 除非显示声明为__attribute__((visibility("default"))).

  • 当-fvisibility=default时

    动态库中的函数默认是可见的.除非显示声明为__attribute__((visibility("hidden"))).

JNICALL

在类Unix中无定义,在Windows中定义为:_stdcall ,一种函数调用约定。

【注意】:类Unix系统中这两个宏可以省略不加。

JNIEnv

其中JNIEnv类型实际上代表了Java环境,通过这个JNIEnv* 指针,就可以对Java端的代码进行操作。例如,创建Java类中的对象,调用Java对象的方法,获取Java对象中的属性等等。JNIEnv的指针会被JNI传入到本地方法的实现函数中来对Java端的代码进行操作。

android NDK开发

CMake

Android Studio 2.2 及更高版本,使用 NDK 和 CMake 将 C 及 C++ 代码编译到原生库中。之后,Android Studio 会使用 IDE 的集成构建系统 Gradle 将您的库封装到 APK。

CMake 是一个跨平台的安装(编译)工具,可以用简单的语句来描述所有平台的安装(编译过程)。他能够输出各种各样的 Makefile 或者 project 文件,CMake 并不直接建构出最终的软件,而是产生标准的建构档(如 Makefile 或 projects)。

构建 CMake

要指示 CMake 从原生源代码创建一个原生库,请将 cmake_minimum_required() 和 add_library() 命令添加到您的构建脚本中:

# Sets the minimum version of CMake required to build your native library.
# This ensures that a certain set of CMake features is available to
# your build.

cmake_minimum_required(VERSION 3.4.1)

# Specifies a library name, specifies whether the library is STATIC or
# SHARED, and provides relative paths to the source code. You can
# define multiple libraries by adding multiple add.library() commands,
# and CMake builds them for you. When you build your app, Gradle
# automatically packages shared libraries with your APK.

add_library( # Specifies the name of the library.
             native-lib

             # Sets the library as a shared library.
             SHARED

             # Provides a relative path to your source file(s).
             src/main/cpp/native-lib.cpp )
复制代码

使用 add_library() 向您的 CMake 构建脚本添加源文件或库时,Android Studio 还会在您同步项目后在 Project 视图下显示关联的标头文件。不过,为了确保 CMake 可以在编译时定位您的标头文件,您需要将 include_directories() 命令添加到 CMake 构建脚本中并指定标头的路径:

add_library(...)

# Specifies a path to native header files.
include_directories(src/main/cpp/include/)
复制代码

CMake 使用以下规范来为库文件命名:

lib库名称.so
复制代码

例如,如果您在构建脚本中指定“native-lib”作为共享库的名称,CMake 将创建一个名称为 libnative-lib.so 的文件。不过,在 Java 代码中加载此库时,请使用您在 CMake 构建脚本中指定的名称:

static {
    System.loadLibrary(“native-lib”);
}
复制代码

注:如果您在 CMake 构建脚本中重命名或移除某个库,您需要先清理项目,Gradle 随后才会应用更改或者从 APK 中移除旧版本的库。要清理项目,请从菜单栏中选择 Build > Clean Project。

Android Studio 会自动将源文件和标头添加到 Project 窗格的 cpp 组中。使用多个 add_library() 命令,您可以为 CMake 定义要从其他源文件构建的更多库。

添加 NDK API

Android NDK 提供了一套实用的原生 API 和库。通过将 NDK 库包含到项目的 CMakeLists.txt 脚本文件中,您可以使用这些 API 中的任意一种。

预构建的 NDK 库已经存在于 Android 平台上,因此,您无需再构建或将其打包到 APK 中。由于 NDK 库已经是 CMake 搜索路径的一部分,您甚至不需要在您的本地 NDK 安装中指定库的位置 - 只需要向 CMake 提供您希望使用的库的名称,并将其关联到您自己的原生库。

将 find_library() 命令添加到您的 CMake 构建脚本中以定位 NDK 库,并将其路径存储为一个变量。您可以使用此变量在构建脚本的其他部分引用 NDK 库。以下示例可以定位 Android 特定的日志支持库并将其路径存储在 log-lib 中:

find_library( # Defines the name of the path variable that stores the
              # location of the NDK library.
              log-lib

              # Specifies the name of the NDK library that
              # CMake needs to locate.
              log )
复制代码

为了确保您的原生库可以在 log 库中调用函数,您需要使用 CMake 构建脚本中的 target_link_libraries() 命令关联库:

find_library(...)

# Links your native library against one or more other native libraries.
target_link_libraries( # Specifies the target library.
                       native-lib

                       # Links the log library to the target library.
                       ${log-lib} )
复制代码

NDK 还以源代码的形式包含一些库,您在构建和关联到您的原生库时需要使用这些代码。您可以使用 CMake 构建脚本中的 add_library() 命令,将源代码编译到原生库中。要提供本地 NDK 库的路径,您可以使用 ANDROID_NDK 路径变量,Android Studio 会自动为您定义此变量。

以下命令可以指示 CMake 构建 android_native_app_glue.c,后者会将 NativeActivity 生命周期事件和触摸输入置于静态库中并将静态库关联到 native-lib:

add_library( app-glue
             STATIC
             ${ANDROID_NDK}/sources/android/native_app_glue/android_native_app_glue.c )

# You need to link static libraries against your shared native library.
target_link_libraries( native-lib app-glue ${log-lib} )
复制代码
添加其他预构建库

添加预构建库与为 CMake 指定要构建的另一个原生库类似。不过,由于库已经预先构建,您需要使用 IMPORTED 标志告知 CMake 您只希望将库导入到项目中:

add_library( imported-lib
             SHARED
             IMPORTED )
复制代码

然后,您需要使用 set_target_properties() 命令指定库的路径,如下所示。

某些库为特定的 CPU 架构(或应用二进制接口 (ABI))提供了单独的软件包,并将其组织到单独的目录中。此方法既有助于库充分利用特定的 CPU 架构,又能让您仅使用所需的库版本。要向 CMake 构建脚本中添加库的多个 ABI 版本,而不必为库的每个版本编写多个命令,您可以使用 ANDROID_ABI 路径变量。此变量使用 NDK 支持的一组默认 ABI,或者您手动配置 Gradle 而让其使用的一组经过筛选的 ABI。例如:

add_library(...)
set_target_properties( # Specifies the target library.
                       imported-lib

                       # Specifies the parameter you want to define.
                       PROPERTIES IMPORTED_LOCATION

                       # Provides the path to the library you want to import.
                       imported-lib/src/${ANDROID_ABI}/libimported-lib.so )
复制代码

为了确保 CMake 可以在编译时定位您的标头文件,您需要使用 include_directories() 命令,并包含标头文件的路径:

include_directories( imported-lib/include/ )
复制代码

要将预构建库关联到您自己的原生库,请将其添加到 CMake 构建脚本的 target_link_libraries() 命令中:

target_link_libraries( native-lib imported-lib app-glue ${log-lib} )
复制代码

在您构建应用时,Gradle 会自动将导入的库打包到 APK 中。您可以使用 APK 分析器验证 Gradle 将哪些库打包到您的 APK 中。

将 Gradle 关联到您的原生库

要将 Gradle 关联到您的原生库,您需要提供一个指向 CMake 或 ndk-build 脚本文件的路径。在您构建应用时,Gradle 会以依赖项的形式运行 CMake 或 ndk-build,并将共享的库打包到您的 APK 中。Gradle 还使用构建脚本来了解要将哪些文件添加到您的 Android Studio 项目中,以便您可以从 Project 窗口访问这些文件。如果您的原生源文件没有构建脚本,则需要先创建 CMake 构建脚本,然后再继续。

将 Gradle 关联到原生项目后,Android Studio 会更新 Project 窗格以在 cpp 组中显示您的源文件和原生库,在 External Build Files 组中显示您的外部构建脚本。

注:更改 Gradle 配置时,请确保通过点击工具栏中的 Sync Project 应用更改。此外,如果在将 CMake 或 ndk-build 脚本文件关联到 Gradle 后再对其进行更改,您应当从菜单栏中选择 Build > Refresh Linked C++ Projects,将 Android Studio 与您的更改同步。

  • Link C++ Project with Gradle
  • 手动配置 Gradle

要手动配置 Gradle 以关联到您的原生库,您需要将 externalNativeBuild {} 块添加到模块级 build.gradle 文件中,并使用 cmake {} 或 ndkBuild {} 对其进行配置:

android {
  ...
  defaultConfig {...}
  buildTypes {...}

  // Encapsulates your external native build configurations.
  externalNativeBuild {

    // Encapsulates your CMake build configurations.
    cmake {

      // Provides a relative path to your CMake build script.
      path "CMakeLists.txt"
    }
  }
}
复制代码
指定可选配置

您可以在模块级 build.gradle 文件的 defaultConfig {} 块中配置另一个 externalNativeBuild {} 块,为 CMake 或 ndk-build 指定可选参数和标志。与 defaultConfig {} 块中的其他属性类似,您也可以在构建配置中为每个产品风味重写这些属性。

例如,如果您的 CMake 或 ndk-build 项目定义多个原生库,您可以使用 targets 属性仅为给定产品风味构建和打包这些库中的一部分。以下代码示例说明了您可以配置的部分属性:

android {
  ...
  defaultConfig {
    ...
    // This block is different from the one you use to link Gradle
    // to your CMake or ndk-build script.
    externalNativeBuild {

      // For ndk-build, instead use ndkBuild {}
      cmake {

        // Passes optional arguments to CMake.
        arguments "-DANDROID_ARM_NEON=TRUE", "-DANDROID_TOOLCHAIN=clang"

        // Sets optional flags for the C compiler.
        cFlags "-D_EXAMPLE_C_FLAG1", "-D_EXAMPLE_C_FLAG2"

        // Sets a flag to enable format macro constants for the C++ compiler.
        cppFlags "-D__STDC_FORMAT_MACROS"
      }
    }
  }

  buildTypes {...}

  productFlavors {
    ...
    demo {
      ...
      externalNativeBuild {
        cmake {
          ...
          // Specifies which native libraries to build and package for this
          // product flavor. If you don't configure this property, Gradle
          // builds and packages all shared object libraries that you define
          // in your CMake or ndk-build project.
          targets "native-lib-demo"
        }
      }
    }

    paid {
      ...
      externalNativeBuild {
        cmake {
          ...
          targets "native-lib-paid"
        }
      }
    }
  }

  // Use this block to link Gradle to your CMake or ndk-build script.
  externalNativeBuild {
    cmake {...}
    // or ndkBuild {...}
  }
}
复制代码
指定 ABI

默认情况下,Gradle 会针对 NDK 支持的 ABI 将您的原生库构建到单独的 .so 文件中,并将其全部打包到您的 APK 中。如果您希望 Gradle 仅构建和打包原生库的特定 ABI 配置,您可以在模块级 build.gradle 文件中使用 ndk.abiFilters 标志指定这些配置,如下所示:

android {
  ...
  defaultConfig {
    ...
    externalNativeBuild {
      cmake {...}
      // or ndkBuild {...}
    }

    ndk {
      // Specifies the ABI configurations of your native
      // libraries Gradle should build and package with your APK.
      abiFilters 'x86', 'x86_64', 'armeabi', 'armeabi-v7a',
                   'arm64-v8a'
    }
  }
  buildTypes {...}
  externalNativeBuild {...}
}
复制代码

在 Gradle 中使用 CMake 变量

将 Gradle 关联到您的 CMake 项目后,您可配置特定 NDK 变量,以改变 CMake 构建您原生库的方式。要将参数从模块级 build.gradle 文件传送到 CMake,请使用以下 DSL:

android {
...
defaultConfig {
    ...
    // This block is different from the one you use to link Gradle
    // to your CMake build script.
    externalNativeBuild {
      cmake {
        ...
        // Use the following syntax when passing arguments to variables:
        // arguments "-DVAR_NAME=ARGUMENT".
        arguments "-DANDROID_ARM_NEON=TRUE",
        // If you're passing multiple arguments to a variable, pass them together:
        // arguments "-DVAR_NAME=ARG_1 ARG_2"
        // The following line passes 'rtti' and 'exceptions' to 'ANDROID_CPP_FEATURES'.
                  "-DANDROID_CPP_FEATURES=rtti exceptions"
      }
    }
  }
  buildTypes {...}

  // Use this block to link Gradle to your CMake build script.
  externalNativeBuild {
    cmake {...}
  }
}
复制代码

下表介绍在将 CMake 与 NDK 搭配使用时,您可以配置的部分变量。

了解 CMake 构建命令

了解在对 Android 进行交叉编译时,Android Studio 所使用的具体构建参数,将有助于调试 CMake 构建问题。

Android Studio 会将其用于执行 CMake 构建的构建参数保存于 cmake_build_command.txt 文件。Android Studio 会针对您应用指向的每个应用二进制界面 (ABI),以及这些 ABI 的每个构建类型(即发行或调试),为每个具体配置生成 cmake_build_command.txt 文件副本。Android Studio 随后会将其生成的文件放置于以下目录:

<project-root>/<module-root>/.externalNativeBuild/cmake/<build-type>/<ABI>/
复制代码

提示:在 Android Studio 中,您可使用键盘快捷键 (shift+shift) 快速浏览这些文件,并在输入字段输入 cmake_build_command.txt。

以下代码片段举例说明用于构建指向 armeabi-v7a 架构的可调式版 hello-jni 示例的 CMake 参数。

Executable : /usr/local/google/home/{$USER}/Android/Sdk/cmake/3.6.3155560/bin/cmake
arguments :
-H/usr/local/google/home/{$USER}/Dev/github-projects/googlesamples/android-ndk/hello-jni/app/src/main/cpp
-B/usr/local/google/home/{$USER}/Dev/github-projects/googlesamples/android-ndk/hello-jni/app/.externalNativeBuild/cmake/arm7Debug/armeabi-v7a
-GAndroid Gradle - Ninja
-DANDROID_ABI=armeabi-v7a
-DANDROID_NDK=/usr/local/google/home/{$USER}/Android/Sdk/ndk-bundle
-DCMAKE_LIBRARY_OUTPUT_DIRECTORY=/usr/local/google/home/{$USER}/Dev/github-projects/googlesamples/android-ndk/hello-jni/app/build/intermediates/cmake/arm7/debug/obj/armeabi-v7a
-DCMAKE_BUILD_TYPE=Debug
-DCMAKE_MAKE_PROGRAM=/usr/local/google/home/{$USER}/Android/Sdk/cmake/3.6.3155560/bin/ninja
-DCMAKE_TOOLCHAIN_FILE=/usr/local/google/home/{$USER}/Android/Sdk/ndk-bundle/build/cmake/android.toolchain.cmake
-DANDROID_NATIVE_API_LEVEL=23
-DANDROID_TOOLCHAIN=clang
jvmArgs :
复制代码
构建参数

下表突出显示用于 Android 的 CMake 关键构建参数。这些构建参数并非由开发者设置。相反,Android Plugin for Gradle 会根据您项目的 build.gradle 配置,设置这些参数。

CMake 中的 YASM 支持

NDK 为构建以 YASM 编写的汇编代码提供 CMake 支持,以便在 x86 和 x86-64 架构上运行。YASM 是基于 NASM 汇编程序且针对 x86 和 x86-64 架构的开源汇编程序。

该程序可用于将汇编语言程序或例程与 C 代码关联,以便从您的汇编代码访问 C 库或函数。您还能在编译完的 C 代码中添加简短的汇编例程,以充分利用汇编代码提供的更出色的机器性能。

要使用 CMake 构建汇编代码,请在您项目的 CMakeLists.txt 中作出以下变更:

  • 调用 enable_language,且值设置为 ASM_NASM。
  • 根据您是构建共享库还是可执行二进制文件来决定调用 add_library 或 add_executable。在参数中,传入源文件列表。源文件包括 YASM 中汇编程序的 .asm 文件,以及关联 C 库或函数的 .c 文件。

以下片段展示如何配置您的 CMakeLists.txt,以将 YASM 程序构建为共享库。

cmake_minimum_required(VERSION 3.6.0) 
enable_language(ASM_NASM) 
add_library(test-yasm SHARED jni/test-yasm.c jni/print_hello.asm)
复制代码

转载于:https://juejin.im/post/5ce3fdaa6fb9a07eb74b1131

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值