NDK

1,607 阅读45分钟

Android NDK 是能将 C 或 C++ 嵌入到 Android 应用中的工具。

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

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

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

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

应用二进制接口 (ABI):ABI 可以非常精确地定义应用的机器代码在运行时应该如何与系统交互。NDK 根据这些定义编译 .so 文件。

JNI

JNI是Java Native Interface。 它定义了Android从托管代码(用Java或Kotlin编程语言编写)编译的字节码的方式,以与本机代码(用C / C ++编写)进行交互。

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参数是一个坏主意。

Thread

所有线程都是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开发

JNI_OnLoad & JNI_UnLoad

JNI在加载时,会调用JNI_OnLoad,而卸载时会调用JNI_UnLoad,所以我们可以通过在JNI_OnLoad里面注册我们的native函数来实现JNI。通过重写JNI_OnLoad(),在JNI_OnLoad()中将函数注册到Android中,以便能通过Java访问。

pthread_once()

某个事件在整个程序中仅执行一次,不确定是那个线程执行。在多线程环境中,有些事仅需要执行一次。通常当初始化应用程序时,可以比较容易地将其放在main函数中。但当你写一个库时,就不能在main里面初始化了,你可以用静态初始化,但使用一次初始化(pthread_once)会比较容易些。

int pthread_once(pthread_once_t *once_control, void (*init_routine) (void));
功能:本函数使用初值为PTHREAD_ONCE_INIT的once_control变量保证init_routine()函数在本进程执行序列中仅执行一次。

在多线程编程环境下,尽管pthread_once()调用会出现在多个线程中,init_routine()函数仅执行一次,究竟在哪个线程中执行是不定的,是由内核调度来决定。 Linux Threads使用互斥锁和条件变量保证由pthread_once()指定的函数执行且仅执行一次,而once_control表示是否执行过。 如果once_control的初值不是PTHREAD_ONCE_INIT(Linux Threads定义为0),pthread_once() 的行为就会不正常。 在LinuxThreads中,实际"一次性函数"的执行状态有三种:NEVER(0)、IN_PROGRESS(1)、DONE (2),如果once初值设为1,则由于所有pthread_once()都必须等待其中一个激发"已执行一次"信号,因此所有pthread_once ()都会陷入永久的等待中;如果设为2,则表示该函数已执行过一次,从而所有pthread_once()都会立即返回0。

#include<iostream>
#include<pthread.h>
using namespace std;
 
pthread_once_t once = PTHREAD_ONCE_INIT;
 
void once_run(void)
{
        cout<<"once_run in thread "<<(unsigned int )pthread_self()<<endl;
}
 
void * child1(void * arg)
{
        pthread_t tid =pthread_self();
        cout<<"thread "<<(unsigned int )tid<<" enter"<<endl;
        pthread_once(&once,once_run);
        cout<<"thread "<<tid<<" return"<<endl;
}
 
 
void * child2(void * arg)
{
        pthread_t tid =pthread_self();
        cout<<"thread "<<(unsigned int )tid<<" enter"<<endl;
        pthread_once(&once,once_run);
        cout<<"thread "<<tid<<" return"<<endl;
}
 
int main(void)
{
        pthread_t tid1,tid2;
        cout<<"hello"<<endl;
        pthread_create(&tid1,NULL,child1,NULL);
        pthread_create(&tid2,NULL,child2,NULL);
        sleep(10);
        cout<<"main thread exit"<<endl;
        return 0;
}

执行结果:

hello
thread 3086535584 enter
once_run in thread 3086535584
thread 3086535584 return
thread 3076045728 enter
thread 3076045728 return
main thread exit

pthread_key_t和pthread_key_create()

在多线程程序中,所有线程共享程序中的变量。现在有一全局变量,所有线程都可以使用它,改变它的值。而如果每个线程希望能单独拥有它,那么就需要使用线程存储了。表面上看起来这是一个全局变量,所有线程都可以使用它,而它的值在每一个线程中又是单独存储的。这就是线程存储的意义。

线程存储的具体用法。

  1. 创建一个类型为 pthread_key_t 类型的变量。

  2. 调用 pthread_key_create() 来创建该变量。该函数有两个参数,第一个参数就是上面声明的 pthread_key_t 变量,第二个参数是一个清理函数,用来在线程释放该线程存储的时候被调用。该函数指针可以设成 NULL ,这样系统将调用默认的清理函数。

  3. 当线程中需要存储特殊值的时候,可以调用 pthread_setspcific() 。该函数有两个参数,第一个为前面声明的 pthread_key_t 变量,第二个为 void* 变量,这样你可以存储任何类型的值。

  4. 如果需要取出所存储的值,调用 pthread_getspecific() 。该函数的参数为前面提到的 pthread_key_t 变量,该函数返回 void * 类型的值。

下面是前面提到的函数的原型:

int pthread_setspecific(pthread_key_t key, const void *value);

void *pthread_getspecific(pthread_key_t key);

int pthread_key_create(pthread_key_t *key, void (*destructor)(void*));

下面是一个如何使用线程存储的例子:

#include <malloc.h>
#include <pthread.h>
#include <stdio.h>

static pthread_key_t thread_log_key;

void write_to_thread_log (const char* message)
{
	FILE* thread_log = (FILE*) pthread_getspecific (thread_log_key);
	fprintf (thread_log, “%s\n”, message);
}

/* Close the log file pointer THREAD_LOG. */
void close_thread_log (void* thread_log)
{
	fclose ((FILE*) thread_log);	
}

void* thread_function (void* args)
{
	char thread_log_filename[20];
	FILE* thread_log;
	/* Generate the filename for this thread’s log file. */
	sprintf (thread_log_filename, “thread%d.log”, (int) pthread_self ());
	/* Open the log file. */
	thread_log = fopen (thread_log_filename, “w”);
	/* Store the file pointer in thread-specific data under thread_log_key. */
	pthread_setspecific (thread_log_key, thread_log);
	write_to_thread_log (“Thread starting.”);
	/* Do work here... */
	return NULL;
}

int main ()
{
	int i;
	pthread_t threads[5];
	pthread_key_create (&thread_log_key, close_thread_log);
	/* Create threads to do the work. */
	for (i = 0; i < 5; ++i)
		pthread_create (&(threads[i]), NULL, thread_function, NULL);
	/* Wait for all threads to finish. */
	for (i = 0; i < 5; ++i)
		pthread_join (threads[i], NULL);
	return 0;
}  

JNI子线程FindClass

在进行jni开发时,Java调用C语言一般都处于主线程中的,但是使用JNI开发,很多情况都是需要开启子线程的(毕竟不能阻塞主线程)

void void *th_fun(void *arg) {}//是子线程的回调函数,我认为就相当于Java里的`Runnable`任务,但是在C语言里是可以传递参数的。
pthread_create(&tid, NULL/*很少用到*/, th_fun/*子线程回调*/, (void *) "no1"/*传递给子线程的参数*/);

有时候在子线程会去调用Java方法,那么如何调用尼?一般我们都会通过env->FIndClass来调用,但是如何在子线程回调函数里拿到env尼?将env设为全局引用,这是一个解决方案,但是env本就是与线程相关的,如果设为全局引用给其他线程调用,这样就搞混乱了,所以不好。那么如何解决尼?其实我们可以通过JavaVM来解决,JavaVM代表的是Java虚拟机,所有工作都是从JavaVM开始的,每个Java程序代表一个JavaVM,Android里每个Android程序都的JavaVM都是一样的。解决方案如下:

static JavaVM *javaVM;

//动态库加载时会执行
//兼容Android SDK 2.2之后,2.2没有这个函数
JNIEXPORT jint JNI_OnLoad(JavaVM *vm, void *reserved) {
    LOGI("%s", "JNI_OnLoad");
    javaVM = vm;
    return JNI_VERSION_1_4;
}
void *th_fun(void *arg) {
    JNIEnv *env = NULL;
    int isAttacked = 0;
    int status = (*javaVM)->GetEnv(javaVM, (void **) &env, JNI_VERSION_1_4);
    if (status < 0) {
        isAttacked = 1;
        (*javaVM)->AttachCurrentThread(javaVM, &env, NULL);
    }
}

有时候会在子线程去调用Java类,但是在我们创建的子线程(通过pthread_create创建)中调用FindClass查找非系统类时会失败(查找系统类不会失败),返回值为NULL,为什么尼?这是因为通过AttachCurrentThread附加到虚拟机的线程在查找类时只会通过系统类加载器进行查找,不会通过应用类加载器进行查找,因此可以加载系统类,但是不能加载非系统类,如自己在java层定义的类会返回NULL。

那么如何解决尼?主要有以下两个方案

获取classLoader,通过调用classLoader的loadClass来加载自定义类。适合自定义类比较多的情况

在主线程创建一个全局的自定义类引用。适合自定义类比较少的情况

#include <jni.h>
#include <pthread.h>
#include <android/log.h>
#include <stdio.h>
#include <unistd.h>

#define LOGI(FORMAT, ...) __android_log_print(ANDROID_LOG_INFO,"dadou",FORMAT,##__VA_ARGS__);
#define LOGE(FORMAT, ...) __android_log_print(ANDROID_LOG_ERROR,"dadou",FORMAT,##__VA_ARGS__);


static JavaVM *javaVM;
static jobject class_loader_obj_ = NULL;
static jmethodID find_class_mid_ = NULL;
static jclass global_ref = NULL;

//动态库加载时会执行
//兼容Android SDK 2.2之后,2.2没有这个函数
JNIEXPORT jint JNI_OnLoad(JavaVM *vm, void *reserved) {
    LOGI("%s", "JNI_OnLoad");
    javaVM = vm;
    LOGI("a=%d,b=%d", vm == NULL, javaVM == NULL);
    //--------------------------------------------方案一--------------------------------------------
// JNIEnv *env = NULL;
// int status = (*javaVM)->GetEnv(javaVM, (void **) &env, JNI_VERSION_1_4);
// if (status == JNI_OK) {//我认为最好在JNI_OnLoad里拿到classLoader的全局引用
// jclass classLoaderClass = (*env)->FindClass(env, "java/lang/ClassLoader");
// jclass adapterClass = (*env)->FindClass(env, "com/example/thread/UUIDUtils");
// if (adapterClass) {
// jmethodID getClassLoader = (*env)->GetStaticMethodID(env, adapterClass,
// "getClassLoader",
// "()Ljava/lang/ClassLoader;");
// jobject obj = (*env)->CallStaticObjectMethod(env, adapterClass, getClassLoader);
// class_loader_obj_ = (*env)->NewGlobalRef(env, obj);
// find_class_mid_ = (*env)->GetMethodID(env, classLoaderClass, "loadClass",
// "(Ljava/lang/String;)Ljava/lang/Class;");
// (*env)->DeleteLocalRef(env, classLoaderClass);
// (*env)->DeleteLocalRef(env, adapterClass);
// (*env)->DeleteLocalRef(env, obj);
// }
// }
    //----------------------------------------------------------------------------------------------
    return JNI_VERSION_1_4;
}

//子线程的回调
/**
 * 在子线程中,不能通过env->FindClass来获取自定义类,(*env)->FindClass(env, "com/example/thread/UUIDUtils");返回NULL,
 * (*env)->FindClass(env,"java/lang/String");能够正确的返回
 * 解决方案一:获取classLoader,通过调用classLoader的loadClass来加载自定义类。适合自定义类比较多的情况
 * 解决方案二:在主线程创建一个全局的自定义类引用。适合自定义类比较少的情况
 * @param arg
 * @return
 */
void *th_fun(void *arg) {
    JNIEnv *env = NULL;
    int isAttacked = 0;
    int status = (*javaVM)->GetEnv(javaVM, (void **) &env, JNI_VERSION_1_4);
    if (status < 0) {
        isAttacked = 1;
        (*javaVM)->AttachCurrentThread(javaVM, &env, NULL);
    }
    jclass clazz = NULL;
    //--------------------------------------------方案一--------------------------------------------
// jstring class_name = (*env)->NewStringUTF(env, "com/example/thread/UUIDUtils");
// clazz = (*env)->CallObjectMethod(env, class_loader_obj_, find_class_mid_,
// class_name);
// (*env)->DeleteLocalRef(env, class_name);
// if (clazz != NULL) {
// jmethodID get_mid = (*env)->GetStaticMethodID(env, clazz, "get",
// "()Ljava/lang/String;");
// jobject uuid = (*env)->CallStaticObjectMethod(env, clazz, get_mid);
// char *uuid_cstr = (char *) (*env)->GetStringUTFChars(env, uuid, NULL);
// LOGI("uuid : %s", uuid_cstr);
// (*env)->ReleaseStringUTFChars(env, uuid, uuid_cstr);
// }
    //----------------------------------------------------------------------------------------------
    //--------------------------------------------方案二--------------------------------------------
    if (global_ref != NULL) {
        jmethodID get_mid = (*env)->GetStaticMethodID(env, global_ref, "get",
                                                      "()Ljava/lang/String;");
        jobject uuid = (*env)->CallStaticObjectMethod(env, global_ref, get_mid);
        char *uuid_cstr = (char *) (*env)->GetStringUTFChars(env, uuid, NULL);
        LOGI("uuid : %s", uuid_cstr);
        (*env)->ReleaseStringUTFChars(env, uuid, uuid_cstr);
    }
    //----------------------------------------------------------------------------------------------

    char *no = (char *) arg;
    int i = 0;
    for (i = 0; i < 5; i++) {
        LOGI("thread %s, i:%d", no, i);
        if (i == 4) {
            if (class_loader_obj_ != NULL) {
                (*env)->DeleteGlobalRef(env, class_loader_obj_);
            }
            //采用方案二时需要释放全局引用
            //---------------释放---------
            if (global_ref != NULL) {
                LOGI("%s", "开始释放全局引用")
                (*env)->DeleteGlobalRef(env, global_ref);
            }
            //----------------------------

            //下面的函数必须最后执行,在后面再使用env会报错
            if (isAttacked == 1) {
                //解除关联
                (*javaVM)->DetachCurrentThread(javaVM);//必须在离开当前线程之前执行
            }
            pthread_exit((void *) 0);
        }
        sleep(1);
    }

}


//JavaVM 代表的是Java虚拟机,所有工作都是从JavaVM开始的,每个Java程序代表一个JavaVM,Android里每个Android程序都的JavaVM都是一样的
//可以通过JavaVM获取到每个线程关联的JNIEnv


//如何获取JavaVM?
//1.在JNI_OnLoad函数中获取
//2.(*env)->GetJavaVM(env,&javaVM);
//每个线程都有独立的JNIEnv
JNIEXPORT jstring JNICALL Java_com_example_thread_MainActivity_stringFromJNI(
        JNIEnv *env, jobject /* this */ object) {
    char str[] = "Hello from C";
    jclass clazz = (*env)->FindClass(env, "com/example/thread/UUIDUtils");
    global_ref = (*env)->NewGlobalRef(env, clazz);
    pthread_t tid;//子线程id
    //创建一个子线程
    pthread_create(&tid, NULL/*很少用到*/, th_fun/*子线程回调*/, (void *) "no1"/*传递给子线程的参数*/);
    return (*env)->NewStringUTF(env, str);
}
public class UUIDUtils {
 public static ClassLoader getClassLoader() {
  return UUIDUtils.class.getClassLoader();
 }
 public static String get(){
  return UUID.randomUUID().toString();
 }
}

如果您自行创建线程(可能通过调用 pthread_create,然后使用 AttachCurrentThread 进行附加),可能会遇到麻烦。现在您的应用中没有堆栈帧。如果从此线程调用 FindClass,JavaVM 会在“系统”类加载器(而不是与应用关联的类加载器)中启动,因此尝试查找特定于应用的类将失败。

您可以通过以下几种方法来解决此问题:

在 JNI_OnLoad 中执行一次 FindClass 查找,然后缓存类引用以供日后使用。在执行 JNI_OnLoad 过程中发出的任何 FindClass 调用都会使用与调用 System.loadLibrary 的函数关联的类加载器(这是一条特殊规则,用于更方便地进行库初始化)。如果您的应用代码要加载库,FindClass 会使用正确的类加载器。

// Java层的本地方法的容器类
#define JNI_CLASS_TEXTURE_CAPTURE     "com/zpw/sdk/sink/player/TextureCapture"

// Java层的方法名,签名,native 方法体
static JNINativeMethod g_methods[] = {
    {"nativeInit",          "()V",          (void *)nativeInit},
    {"nativeStart",         "(IIIZ)I",      (void *)nativeStart},
    {"nativeStop",          "()I",          (void *)nativeStop},
    {"nativeDraw",          "(IIIJ[F)I",    (void *)nativeDraw}
};

jclass cls = (*env)->FindClass(env, JNI_CLASS_TEXTURE_CAPTURE);
(*env)->RegisterNatives(env, cls, g_methods, sizeof(g_methods) / sizeof(g_methods[0]));
(*pEnv)->UnregisterNatives(pEnv, cls);

通过声明原生方法来获取 Class 参数,然后传入 Foo.class,从而将类的实例传递给需要它的函数。

在某个便捷位置缓存对 ClassLoader 对象的引用,然后直接发出 loadClass 调用。

JavaVM.AttachCurrentThread() & JavaVM.DetachCurrentThread()

很多时候,你的native代码建立自己的线程(比如建立线程监听),并在合适的时候回调 Java 代码,在线程中没办法直接获取JNIEnv,此时需要将JavaVM保存在全局,获取JNIEnv的实例需要把你的线程 Attach到JavaVM上去,调用的方法是 JavaVM::AttachCurrentThread

JNIEnv* env;
GetJVM()->AttachCurrentThread(&env, nullptr);

使用完之后你 需要调用 JavaVM::DetachCurrentThread函数解绑线程。

GetJVM()->DetachCurrentThread();

需要注意的是对于一个已经绑定到JavaVM上的线程调用AttachCurrentThread不会有任 何影响。如果你的线程已经绑定到了JavaVM上,你还可以通过调用JavaVM::GetEnv获取 JNIEnv,如果你的线程没有绑定,这个函数返回JNI_EDETACHED。

封装一个 智能指针类自动完成这些操作:

class JNIEnvPtr {
public:
    JNIEnvPtr() : env_{nullptr}, need_detach_{false} {
        if (GetJVM()->GetEnv((void**) &env_, JNI_VERSION_1_6) ==
            JNI_EDETACHED) {
            GetJVM()->AttachCurrentThread(&env_, nullptr);
            need_detach_ = true;
        }
    }

    ~JNIEnvPtr() {
        if (need_detach_) {
            GetJVM()->DetachCurrentThread();
        }
    }

    JNIEnv* operator->() {
        return env_;
    }

private:
    JNIEnvPtr(const JNIEnvPtr&) = delete;
    JNIEnvPtr& operator=(const JNIEnvPtr&) = delete;

private:
    JNIEnv* env_;
    bool need_detach_;
};

这个类在构造函数中调用AttachCurrentThread在析构中调用DetachCurrentThread,然 后重载->操作符。你可以像下面这样使用这个工具类。

NativeClass::NativeMethod() {
    JNIEnvPtr env;
    env->CallVoidMethod(instance, method, args...);
}

JavaVM.ExceptionCheck() & JavaVM.ExceptionClear()

异常检测

处理异常情况从检测开始,并找出是否发生异常

  • 函数返回后检查异常的发生
  • 检查特殊功能返回值

JNI函数(如FindClass())在找不到特定类时返回特殊值。 表面是ClassCircularityError,OutOfmemoryError,ClassFormatError或NoClassDefFoundError中的任何一个异常。 FindClass()所做的是,如果出现上述任何异常,它将返回NULL。 因此,我们可以检查返回的值并采取适当的步骤来处理这种情况。

jclass jcls =
   env->FindClass("org/jnidemo/SomeClass");
{
   /* Handle exception here or free up any resources held
      Exception remains pending until control returns back
      to the Java code.
   */
   return;
}

但是,有些情况下,当本机代码尝试访问超出其数组大小的Java数组的元素并且JVM抛出ArrayIndexOutOfBoundsException时,无法返回标记异常的值,例如,数组超出绑定异常。 在这种情况下,我们可以在异常发生时调用Java对象的函数。 在本机代码中,我们可以做的是在本机函数调用之后调用ExceptionOccurred()或ExceptionCheck()JNI函数。 ExceptionOccurred()返回异常对象的引用,ExceptionCheck()分别返回JNI_TRUE或JNI_FALSE,分别是异常是否发生。

jthrowable flag = env->ExceptionOccurred();
{
   /* Handle exception here or free up any resources held
      Exception remains pending until control returns back
      to the Java code.
   */
   return;
}

jboolean flag = env->ExceptionCheck();
if (flag) {
   /* Handle exception here or free up any resources held
      Exception remains pending until control returns back
      to the Java code.
   */
   return;
}

一旦检测到异常,我们可以:

  • 当控制返回到Java代码时处理它
jboolean flag = env->ExceptionCheck();
if (flag) {
   /* Handle exception here or free up any resources held
      Exception remains pending until control returns back
      to the Java code.
   */
   return;
}
  • 在本机代码中处理它
jboolean flag = env->ExceptionCheck();
if (flag) {
   env->ExceptionClear();

   /* code to handle exception */
}
  • 在本机代码中处理它并传播Java的新异常

    jint Throw(jthrowable obj)

    jint ThrowNew(jclass clazz, const char *message)

这里值得一提的是,在立即遇到throw方法时,控制不会转移到Java代码; 相反,它会一直等到遇到return语句。 throw方法和return语句之间可以有代码行。 throw和JNI函数在成功时返回零,否则返回负值。

if(...){
   jclass jcls =
      env->FindClass("java/lang/Exception");
   jboolean flag = env->ExceptionCheck();
   if (flag) {
      env->ExceptionClear();
      /* code to handle exception */
   }
   env->ThrowNew(jcls, "error message");
   return;
}

char * strdup(const char *str1)

返回指向以null结尾的字节字符串的指针,该字符串是str1指向的字符串的副本。必须将返回的指针传递给free以避免内存泄漏。

如果发生错误,则返回空指针并且可以设置errno。

作为动态内存TR的所有函数,只有在实现定义__STDC_ALLOC_LIB__且用户在包含string.h之前将__STDC_WANT_LIB_EXT2__定义为整数常量1时,才保证strdup可用。

char strstr( const char str, const char* substr )

查找str指向的以null结尾的字节字符串中substr指向的以null结尾的字节字符串的第一个匹配项。不比较终止空字符。

如果str或substr不是指向以null结尾的字节字符串的指针,则行为是未定义的。

指向str中找到的子字符串的第一个字符的指针,如果没有找到这样的子字符串则指向NULL。 如果substr指向空字符串,则返回str。

#include <string.h>
#include <stdio.h>
 
void find_str(char const* str, char const* substr) 
{
    char* pos = strstr(str, substr);
    if(pos) {
        printf("found the string '%s' in '%s' at position: %ld\n", substr, str, pos - str);
    } else {
        printf("the string '%s' was not found in '%s'\n", substr, str);
    }
}
 
int main(void) 
{
    char* str = "one two three";
    find_str(str, "two");
    find_str(str, "");
    find_str(str, "nine");
    find_str(str, "n");
 
    return 0;
}

Output:

found the string 'two' in 'one two three' at position: 4
found the string '' in 'one two three' at position: 0
the string 'nine' was not found in 'one two three'
found the string 'n' in 'one two three' at position: 1

int sscanf( const char *buffer, const char *format, ... )

buffer指向要从中读取的以null结尾的字符串的指针

format指向以null结尾的字符串的指针,指定如何读取输入。

#define __STDC_WANT_LIB_EXT1__ 1
#include <stdio.h>
#include <stddef.h>
#include <locale.h>
 
int main(void)
{
    int i, j;
    float x, y;
    char str1[10], str2[4];
    wchar_t warr[2];
    setlocale(LC_ALL, "en_US.utf8");
 
    char input[] = "25 54.32E-1 Thompson 56789 0123 56ß水";
    /* parse as follows:
       %d: an integer
       %f: a floating-point value
       %9s: a string of at most 9 non-whitespace characters
       %2d: two-digit integer (digits 5 and 6)
       %f:  a floating-point value (digits 7, 8, 9)
       %*d: an integer which isn't stored anywhere
       ' ': all consecutive whitespace
       %3[0-9]: a string of at most 3 decimal digits (digits 5 and 6)
       %2lc: two wide characters, using multibyte to wide conversion  */
    int ret = sscanf(input, "%d%f%9s%2d%f%*d %3[0-9]%2lc",
                     &i, &x, str1, &j, &y, str2, warr);
 
    printf("Converted %d fields:\ni = %d\nx = %f\nstr1 = %s\n"
           "j = %d\ny = %f\nstr2 = %s\n"
           "warr[0] = U+%x warr[1] = U+%x\n",
           ret, i, x, str1, j, y, str2, warr[0], warr[1]);
 
#ifdef __STDC_LIB_EXT1__
    int n = sscanf_s(input, "%d%f%s", &i, &x, str1, (rsize_t)sizeof str1);
    // writes 25 to i, 5.432 to x, the 9 bytes "thompson\0" to str1, and 3 to n.
#endif
}

Output:

Converted 7 fields:
i = 25
x = 5.432000
str1 = Thompson
j = 56
y = 789.000000
str2 = 56
warr[0] = U+df warr[1] = U+6c34

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)

Makefile

Makefile主要就是管理整个工程的编译、链接,比如说一个.c文件你要先用GCC编译成.o,然后多个.o再链接成可执行文件,如果你写好一个Makefile,那么你只需要在控制台输入一个make回车就好了。虽然各个厂商的Makefile语法会有写区别,但大体是类似的,这里就不多做区分了,本文是一个大杂烩。

一般在工程是根目录下会有一个主Makefile文件作为编译入口,在控制台输入make后执行的就是这个文件,在子目录中也有一个Makefile文件,每个Makefile文件管理自己所在的目录(当然,这不是绝对的)。

语法

符号

\: 反斜杠,指下一行还算本行,有时单行太长会进行换行,但Makefile语法上换行就是一条语句的结束了,为了代码阅读方便会在行末使用一个“\”来进行换行
$:取变量的值,如var := 123 那么 $(var)就表示var这个变量的值,也就是123(变量可以不加括号,但一般都加)若要表示$时则用两个$表示(?)
=: 是最基本的赋值,但值是整个文件最后一次赋值的值,如a=1 b=$(a) a=2,此时b的值为2(比较奇葩所以一般不用)
:=:  是覆盖之前的值,依赖与当前所在位置,如a:=1 b=$(a) a:=2,此时b的值为1(比较常见的赋值逻辑,常用的赋值方式)
?=: 是如果没有被赋值过就赋予等号后面的值,如果赋值过就不再赋值(一般用于参数的默认值,Makefile间可以传参)
+=:  是添加等号后面的值,连接之后中间会有一个空格,如 a=abc a+=def,a的值为abc def(一般用于添加依赖文件)

关键字

ifeq  ifneq ifdef ifndef else endif 想必这些不用解释吧
include:包含文件,即在所在位置展开文件,和c文件包含头文件类似,如果找不到且Makefile不会创建这个文件,那么编译报错(No such file or directory)
-include:类似include,但找不到文件时不报错
sinclude:同-include,GNU所支持的书写方式

函数

函数的调用方法:很像变量的使用,也是以“$”来标识的,参数间用“,”隔开
$(<function> <arguments1>,<arguments2>... )
$(subst <from>,<to>,<text>):字符串替换,把字串<text>中的<from>字符串替换成<to>,返回替换后的字符串
$(strip <string>):去掉<string>字串中开头和结尾的空字符
$(findstring <find>,<in>):在字串<in>中查找<find>字串,如果找到,那么返回<find>,否则返回空字符串
$(filter <pattern>,<text>):过滤器,将<text>集中符合<pattern>的过滤出来。如$(filter %.o,a.o a.c a.h)结果为a.o
$(dir <names...>):从文件名序列<names>中取出目录部分。目录部分是指最后一个反斜杠(“/”)之
前的部分。如果没有反斜杠,那么返回“./”。如$(dir src/foo.c hacks)返回值是“src/ ./”
$(notdir <names...>):从文件名序列<names>中取出文件名。即最后一个反斜杠“/”之后的部分
$(suffix <names...>):从文件名序列<names>中取出各个文件名的后缀
$(basename <names...>):从文件名序列<names>中取出各个文件名的前缀。如$(basename src/foo.c)结果为src/foo
$(foreach <var>,<list>,<text>):把参数<list>中的单词逐一取出放到参数<var>变量中,然后再执行<text>表达式。每一次<text>会返回一个字符串,循环结束后,<text>的所返回字符串以空格分隔连接成新的字符串返回。如names:= a b c d $(foreach n,$(names),$(n).o)结果为“a.o b.o c.o d.o”
$(if <condition>,<then-part>,<else-part>):if函数,<condition>为空时返回<then-part>否则返回<else-part>
$(wildcard <string>):基于当前目录使用通配符列出所有文件
$(patsubst <pattern>,<replacement>,<text>):使用通配符替换字符串。如$(patsubst %.c,%.o,a.c b.c)结果为a.o b.o
$(shell <cmd>):执行Linux的shell命令
$(lastword <names...>):返回最后一个字串。如$(lastword foo bar)返回bar
$(call <expression>,<parm1>,<parm2>,<parm3>...):函数调用,<expression>中的变量,如$(1),$(2),$(3)等,会被参数<parm1>,<parm2>,<parm3>取代。<expression>的返回值就是call函数的返回值。如$(call $(1) $(2),a,b)结果为“a b”
$(eval <text>):将text放回Makefile文件当成Makefile脚本再解析一遍。这个有点绕,如$(eval aa:aa.c)相当于直接写aa:aa.c,这个的意义在于aa:aa.c这个字串是可以由Makefile脚本生成的
$(error <text ...>):产生一个致命的错误,<text ...>是错误信息
$(warning <text ...>):产生一个警告,<text ...>是警告信息

命令的变量

Makefile会有一些隐含规则,如.o文件依赖于.c文件,这个我们可以不写,会自动调用编译C程序的隐含规则的命令“(CC) –c(CFLAGS) $(CPPFLAGS)”来生成,这里用到的变量值是可以设置的(一般用来更改默认编译器)

AR   函数库打包程序。默认命令是“ar”。
AS  汇编语言编译程序。默认命令是“as”。
CC  C语言编译程序。默认命令是“cc”。
CXX  C++语言编译程序。默认命令是“g++”。
CO  从 RCS文件中扩展文件程序。默认命令是“co”。
CPP  C程序的预处理器(输出是标准输出设备)。默认命令是“$(CC) –E”。
FC  Fortran 和 Ratfor 的编译器和预处理程序。默认命令是“f77”。
GET  从SCCS文件中扩展文件的程序。默认命令是“get”。
LEX  Lex方法分析器程序(针对于C或Ratfor)。默认命令是“lex”。
PC  Pascal语言编译程序。默认命令是“pc”。
YACC  Yacc文法分析器(针对于C程序)。默认命令是“yacc”。
YACCR  Yacc文法分析器(针对于Ratfor程序)。默认命令是“yacc –r”。
MAKEINFO  转换Texinfo源文件(.texi)到Info文件程序。默认命令是“makeinfo”。
TEX  从TeX源文件创建TeX DVI文件的程序。默认命令是“tex”。
TEXI2DVI  从Texinfo源文件创建军TeX DVI 文件的程序。默认命令是“texi2dvi”。
WEAVE  转换Web到TeX的程序。默认命令是“weave”。
CWEAVE  转换C Web 到 TeX的程序。默认命令是“cweave”。
TANGLE  转换Web到Pascal语言的程序。默认命令是“tangle”。
CTANGLE  转换C Web 到 C。默认命令是“ctangle”。
RM  删除文件命令。默认命令是“rm –f”。
MAKE 即make
ARFLAGS  函数库打包程序AR命令的参数。默认值是“rv”。
ASFLAGS  汇编语言编译器参数。(当明显地调用“.s”或“.S”文件时)。
CFLAGS  C语言编译器参数。
CXXFLAGS  C++语言编译器参数。
COFLAGS  RCS命令参数。
CPPFLAGS  C预处理器参数。( C 和 Fortran 编译器也会用到)。
FFLAGS  Fortran语言编译器参数。
GFLAGS  SCCS “get”程序参数。
LDFLAGS  链接器参数。(如:“ld”)
LFLAGS  Lex文法分析器参数。
PFLAGS  Pascal语言编译器参数。
RFLAGS  Ratfor 程序的Fortran 编译器参数。
YFLAGS  Yacc文法分析器参数。
MAKEFILE_LIST make程序在读取makefile文件的时候将文件名加入此变量,多个文件用空格隔开

显示命令

在命令前面加@符号表示执行时不显示命令只显示输出。如"@echo 这是输出字符"在样在控制台只输出"这是输出字符"如果不加@则会输出"echo 这是输出字符"后换行再输出"这是输出字符"