加载本机库的单元测试 Java class

Unit test Java class that loads native library

我在 Android Studio 中进行 运行 单元测试。我有一个 Java class 使用以下代码加载本机库

 static
    {
       System.loadLibrary("mylibrary");
    }

但是当我在 src/test 目录中测试这个 class 时,我得到

java.lang.UnsatisfiedLinkError: no mylibrary in java.library.path
    at java.lang.ClassLoader.loadLibrary(ClassLoader.java:1864)
    at java.lang.Runtime.loadLibrary0(Runtime.java:870)
    at java.lang.System.loadLibrary(System.java:1122)

如何让它找到位于 src/main/libs 的本机 .so 库的路径,以便单元测试不出错?

注意:在 src/main/libs 目录中我还有 3 个子目录:armeabimipsx86。其中每一个都包含正确的 .so 文件。我正在使用 非实验版本 构建 NDK 库。

我不想使用其他第 3 方测试库,因为我所有其他 "pure" java classes 都可以进行单元测试。但如果那不可能,那么我愿意接受其他选择。

这是我抛出错误的测试代码

   @Test
    public void testNativeClass() throws Exception
    {
        MyNativeJavaClass test = new MyNativeJavaClass("lalalal")
        List<String> results = test.getResultsFromNativeMethodAndPutThemInArrayList();
        assertEquals("There should be only three result", 3, results.size());
    }

请确保包含库的目录包含在 java.library.path 系统 属性.

根据测试,您可以在加载库之前设置它:

System.setProperty("java.library.path", "... path to the library .../libs/x86");

您可以指定硬编码的路径,但这会降低项目对其他环境的可移植性。所以我建议您以编程方式构建它。

.so 文件将放在

src/main/jniLibs

不在src/main/libs

之下

(使用 Android Studio 1.2.2 测试)

查看页面作为参考 - http://ph0b.com/android-studio-gradle-and-ndk-integration/,尽管某些部分可能已过时。

使用 java -XshowSettings:properties 选项尝试 运行 您的测试代码,并确保系统库的目标路径和此命令的输出中的库路径值相同

我不确定这是否能解决您的问题,但到目前为止还没有人提到在创建过程中处理 classes 预加载库的策略模式。

让我们看例子:

我们想要实现斐波那契求解器 class。假设我们在本机代码中提供了实现并设法生成本机库,我们可以实现以下内容:

public interface Fibonacci {
     long calculate(int steps);
}

首先,我们提供本机实现:

public final class FibonacciNative implements Fibonacci {
    static {
      System.loadLibrary("myfibonacci");
    }

    public native long calculate(int steps);
}

其次,我们提供 Java 斐波那契求解器的实现:

public final class FibonacciJava implements Fibonacci {

   @Override
   public long calculate(int steps) {
       if(steps > 1) {
           return calculate(steps-2) + calculate(steps-1);
       }
       return steps;
   }
}

第三,我们用父 class 包装求解器,在其实例化期间选择自己的实现:

public class FibonnaciSolver implements Fibonacci {

   private static final Fibonacci STRATEGY;

   static {
      Fibonacci implementation;
      try {
         implementation = new FibonnaciNative();
      } catch(Throwable e) {
         implementation = new FibonnaciJava();
      }

      STRATEGY = implementation;
   }

   @Override
   public long calculate(int steps) {
       return STRATEGY.calculate(steps);
   }

}

因此,使用策略查找库路径的问题。但是,如果在测试期间确实需要包含本机库,则这种情况不会解决问题。原生库是第三方库也解决不了问题

基本上,这通过模拟 java 代码的本机代码来解决本机库加载问题。

希望这能有所帮助:)

我发现唯一没有黑客攻击的解决方案是通过仪器测试(android测试目录)使用 JUnit。 我的 class 现在可以正常测试了,但需要 android 设备或​​模拟器的帮助。

配置Gradle-运行虚拟机库路径的方法,用于本地单元测试,我将在下面描述,但剧透:根据我的经验,@ThanosFisherman 是对的:使用 Android NDK 的本地单元测试现在似乎是一件傻事。

因此,对于正在寻找将共享(即 .so)库加载到使用 gradle 进行单元测试的方法的任何其他人,这里有一些冗长的摘要:

目标是为 JVM 运行设置单元测试的共享库查找路径。

尽管很多人建议将 lib 路径放入 java.library.path,但我发现 行不通 ,至少在我的 linux 机器上不行. (另外,same results in this CodeRanch thread

什么 有效 虽然是设置 LD_LIBRARY_PATH os 环境变量(或者 PATH 是 closest Windows)

中的同义词

使用Gradle:

// module-level build.gradle
apply plugin: 'com.android.library' // or application

android {
    ...

    testOptions {
        unitTests {
            all {
                // This is where we have access to the properties of gradle's Test class,
                // look it  up if you want to customize more test parameters

                // next we take our cmake output dir for whatever architecture
                // you can also put some 3rd party libs here, or override
                // the implicitly linked stuff (libc, libm and others)

                def libpath = '' + projectDir + '/build/intermediates/cmake/debug/obj/x86_64/'
                    +':/home/developer/my-project/some-sdk/lib'

                environment 'LD_LIBRARY_PATH', libpath
            }
        }
    }
}

有了它,您可以 运行,例如./gradlew :mymodule:testDebugUnitTest 并且将在您指定的路径中查找本机库。

使用 Android Studio JUnit 插件 对于 Android Studio 的 JUnit 插件,您可以在测试配置的设置中指定 VM 选项和环境变量,因此只需 运行 JUnit 测试(右键单击测试方法或其他)然后编辑 运行 配置:

虽然听起来像 "mission accomplished",但我发现当使用 libc.so, libm.so 和我的 os /usr/lib 中的其他人时,会出现版本错误(可能是因为我自己的库是由 cmake 使用 android ndk 工具包针对它自己的平台库编译)。使用 ndk 包中的平台库导致 JVM 出现 SIGSEGV 错误(由于 ndk 平台库与 host os 环境不兼容)

Update 正如@AlexCohn 在评论中敏锐地指出的那样,必须针对 host 环境库进行构建才能使其工作;即使您的机器 most 可能是 x86_64,但针对 NDK 环境构建的 x86_64 二进制文件不会的。

显然,我可能忽略了一些东西,我会很感激任何反馈,但现在我放弃了整个想法,转而支持仪器测试。

如果您的测试需要该库,请使用 AndroidTest(在 src/androidTest/... 下)而不是 junit 测试。这将允许您像在代码中的其他地方一样加载和使用本机库。

如果您的测试不需要库,只需将系统负载包装在try/catch 中。这将允许 JNI class 仍然在 junit 测试中工作(在 src/test/... 下)并且这是一个安全的解决方法,因为它不太可能掩盖错误(如果本机,其他肯定会失败实际上需要 lib)。从那里,您可以使用 mockito 之类的东西来清除仍然命中 JNI 库的任何方法调用。

例如在 Kotlin 中:

    companion object {
        init {
            try {
                System.loadLibrary("mylibrary")
            } catch (e: UnsatisfiedLinkError) {
                // log the error or track it in analytics
            }
        }
    }

这非常非常棘手。设置 java.library.path 不起作用,但试图理解 someone else’s Mac OSX approach 我最终找到了一个可行的解决方案。

法律声明:所有直接复制到此post中的代码示例在CC0 but it would be appeciated to credit my employer ⮡ tarent, the LLCTO project at Deutsche Telekom下可用,作者mirabilos.

注意事项 首先:

  • 有了这个,您正在测试针对您的 系统 库编译的本机代码版本(通常是 GNU/Linux 上的 glibc,以及BSD,Mac OSX 和 Windows 它甚至更棘手)所以无论如何都应该添加一些仪器测试,仅使用单元测试来更快地测试实际上 可以 在主机上进行测试 OS

  • 我只用 GNU/Linux 主机测试过(事实上,我排除了所有其他主机 OS 上的这些本机测试,见下文)

    • 它应该可以在 unixoid OSes 下使用 GNU/BSD-style 共享库 as-is
    • 根据上面链接的“其他人”文章进行小幅改编,它可能适用于 Mac OSX
    • Windows……不,就是不。使用 WSL,它基本上是 Linux 无论如何让事情变得更容易,并且更接近 Android 这也基本上是 Linux 而不是 GNU
  • IDE 集成需要在每个开发人员的机器上手动执行步骤(但这些很容易记录,请参阅下面的(很多))

先决条件

您需要确保 所有 本机代码的构建依赖项 also 安装在主机系统中。这包括 cmake(因为遗憾的是我们不能重用 NDK cmake)和一个主机 C 编译器。请注意,这些在构建中引入了进一步的差异:您正在针对主机 C 库和主机 clang 的其他库测试使用主机 C 编译器(通常是 GCC,而不是 Android 中的 clang)构建的东西. 在编写测试时考虑这一点。我不得不将其中一项测试移至仪器化,因为无法在 glibc 下进行测试。

对于文件系统布局,我们假设如下:

  • ~/MYPRJ/build.gradle 是 top-level 构建文件(由 IntelliJ / Android Studio 生成)
  • ~/MYPRJ/app/build.gradle 是构建有问题的 Android 代码的地方(由 IntelliJ / Android Studio 生成)
  • ~/MYPRJ/app/src/main/native/CMakeLists.txt 是本机代码所在的位置

这意味着 build.gradle(对于应用程序)已经有了类似的东西,当您开始怀疑您的项目是否可以进行单元测试时:

externalNativeBuild {
    cmake {
        path "src/main/native/CMakeLists.txt"
        return void // WTF‽
    }
}

确保您的代码在主机上构建

乍一看应该很容易:

$ rm -rf /tmp/build
$ mkdir /tmp/build
$ cd /tmp/build
$ cmake ~/MYPRJ/app/src/main/native/
$ make

(确保给 cmake 目录 路径 CMakeLists.txt 主文件位于,但 到该文件本身!)

当然,对于所有重要的事情,这都会失败。大多数人会使用 Android 日志记录。 (它也会失败,因为它找不到 <jni.h>,并且因为 GNU libc 需要额外的 _GNU_SOURCE 定义来访问某些原型,等等……)

所以我写了一个 header 来包含而不是 <android/log.h> 它抽象了日志记录......

#ifndef MYPRJ_ALOG_H
#define MYPRJ_ALOG_H

#ifndef MYPRJ_ALOG_TAG
#define MYPRJ_ALOG_TAG "MYPRJ-JNI"
#endif

#if defined(MYPRJ_ALOG_TYPE) && (MYPRJ_ALOG_TYPE == 1)

#include <android/log.h>

#define ecnlog_err(msg, ...)    __android_log_print(ANDROID_LOG_ERROR, \
            MYPRJ_ALOG_TAG, msg, ##__VA_ARGS__)
#define ecnlog_warn(msg, ...)   __android_log_print(ANDROID_LOG_WARN, \
            MYPRJ_ALOG_TAG, msg, ##__VA_ARGS__)
#define ecnlog_info(msg, ...)   __android_log_print(ANDROID_LOG_INFO, \
            MYPRJ_ALOG_TAG, msg, ##__VA_ARGS__)

#elif defined(MYPRJ_ALOG_TYPE) && (MYPRJ_ALOG_TYPE == 2)

#include <stdio.h>

#define ecnlog_err(msg, ...)    fprintf(stderr, \
            "E: [" MYPRJ_ALOG_TAG "] " msg "\n", ##__VA_ARGS__)
#define ecnlog_warn(msg, ...)   fprintf(stderr, \
            "W: [" MYPRJ_ALOG_TAG "] " msg "\n", ##__VA_ARGS__)
#define ecnlog_info(msg, ...)   fprintf(stderr, \
            "I: [" MYPRJ_ALOG_TAG "] " msg "\n", ##__VA_ARGS__)

#else
# error What logging system to use?
#endif

#endif

… 并更新了我的 CMakeLists.txt 以指示是为 NDK 构建( 必须 是默认的)还是原生的:

cmake_minimum_required(VERSION 3.10)
project(myprj-native)

option(UNDER_NDK        "Build under the Android NDK"   ON)

add_compile_options(-fvisibility=hidden)
add_compile_options(-Wall -Wextra -Wformat)

add_library(myprj-native SHARED
        alog.h
        myprj-jni.c
        )

if (UNDER_NDK)
        add_definitions(-DECNBITS_ALOG_TYPE=1)

        find_library(log-lib log)
        target_link_libraries(myprj-native ${log-lib})
else (UNDER_NDK)
        add_definitions(-DECNBITS_ALOG_TYPE=2)
        include(FindJNI)
        include_directories(${JNI_INCLUDE_DIRS})

        add_definitions(-D_GNU_SOURCE)
endif (UNDER_NDK)

请注意,这也已经包括 <jni.h> (FindJNI) 的修复和额外的定义。

现在让我们再次尝试构建它:

$ rm -rf /tmp/build
$ mkdir /tmp/build
$ cd /tmp/build
$ cmake -DUNDER_NDK=OFF ~/MYPRJ/app/src/main/native/
$ make

对我来说,这就足够了。如果您仍然不在那里,请先解决此问题,然后再继续。如果您无法解决此问题,请放弃对您的 JNI 代码进行 buildhost-local 单元测试,并将相应的测试移动到检测中。

让Gradle构建host-native代码

将以下内容添加到应用程序 build.gradle:

def dirForNativeNoNDK = project.layout.buildDirectory.get().dir("native-noNDK")
def srcForNativeNoNDK = project.layout.projectDirectory.dir("src/main/native").asFile

task createNativeNoNDK() {
    def dstdir = dirForNativeNoNDK.asFile
    if (!dstdir.exists()) dstdir.mkdirs()
}
task buildCMakeNativeNoNDK(type: Exec) {
    dependsOn createNativeNoNDK
    workingDir dirForNativeNoNDK
    commandLine "/usr/bin/env", "cmake", "-DUNDER_NDK=OFF", srcForNativeNoNDK.absolutePath
}
task buildGMakeNativeNoNDK(type: Exec) {
    dependsOn buildCMakeNativeNoNDK
    workingDir dirForNativeNoNDK
    commandLine "/usr/bin/env", "make"
}

project.afterEvaluate {
    if (org.gradle.internal.os.OperatingSystem.current().isLinux()) {
        testDebugUnitTest {
            dependsOn buildGMakeNativeNoNDK
            systemProperty "java.library.path", dirForNativeNoNDK.asFile.absolutePath + ":" + System.getProperty("java.library.path")
        }
        testReleaseUnitTest {
            dependsOn buildGMakeNativeNoNDK
            systemProperty "java.library.path", dirForNativeNoNDK.asFile.absolutePath + ":" + System.getProperty("java.library.path")
        }
    }
}

这定义了一些新任务来编译共享库的 buildhost-native 版本,如果主机 OS 是“Linux”,则将其连接起来。 (此语法也适用于其他 unixoid OSes — BSD,Mac OSX — 但不适用于 Windows。但我们可能可以在 Linux 无论如何。WSL 算作 Linux。)它还设置 JVM 库路径,以便 ../gradlew test 让 JVM 从其路径中获取库。

未完待续

您可能已经注意到这里有一些未解决的问题:

  • 上一节最后一段提到../gradlew test会去取库。来自 IDE 的测试还不能工作;这涉及手动设置。

  • 我提到如果buildhostOS不是“Linux”,相关的单元测试必须跳过;我们还没有这样做。不幸的是,JUnit 4 缺少这样的功能,但是将单元测试切换到 JUnit 5“Jupiter”将使我们能够做到这一点。 (不过,我们 没有 切换插桩测试;那会更具侵入性。)

  • 您可能还没有注意到,由于 Gradle 的默认设置我们需要更改,因此本机代码的日志输出不会显示。

所以,让我们开始吧。首先,再次编辑您的应用 build.gradle 文件。将有一个 dependencies { 块。我们需要为任一 JUnit 填充合适的依赖项:

dependencies {
    testImplementation 'org.junit.jupiter:junit-jupiter-api:5.7.0'
    testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.7.0'

    //noinspection GradleDependency
    androidTestImplementation 'com.android.support.test:runner:1.0.1'
    //noinspection GradleDependency
    androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.1'
    //noinspection GradleDependency
    androidTestImplementation 'junit:junit:4.12'
}

顶部还有一行 apply plugin: 'com.android.application'(或者可能是 apply plugin: 'com.android.library')。在该行的正下方,插入此行:

apply plugin: 'de.mannodermaus.android-junit5'

此外,请确保在 android { defaultConfig {testInstrumentationRunner 仍然是 "android.support.test.runner.AndroidJUnitRunner"(IntelliJ / Android Studio 生成的默认值)。

接下来,编辑 top-level ~/MYPRJ/build.gradle 文件。您已经有了一个 buildscript { dependencies { 并且必须在该部分添加一行以使 JUnit5 插件首先可用:

        //noinspection GradleDependency
        classpath 'de.mannodermaus.gradle.plugins:android-junit5:1.5.2.0'

然后,在 allprojects { 下添加一个新部分:

    tasks.withType(Test) {
        testLogging {
            outputs.upToDateWhen { false }
            showStandardStreams = true
            exceptionFormat = 'full'
        }
        systemProperty 'java.util.logging.config.file', file('src/test/resources/logging.properties').getAbsolutePath()
    }

这确保了……

现在看看你的测试,比如~/MYPRJ/app/src/test/java/org/example/packagename/JNITest.java。首先,你应该添加一个总是可以 运行 的“测试”(我使用的只是测试我的 JNI class 是否可以加载),并确保它首先显示一些信息:

// or Lombok @Log
private static final java.util.logging.Logger LOGGER = java.util.logging.Logger.getLogger(JNITest.class.getName());

@Test
public void testClassBoots() {
    LOGGER.info("running on " + System.getProperty("os.name"));
    if (!LINUX.isCurrentOs()) {
        LOGGER.warning("skipping JNI tests");
    }
    // for copy/paste into IntelliJ run options
    LOGGER.info("VM options: -Djava.library.path=" +
      System.getProperty("java.library.path"));

    LOGGER.info("testing Java™ part of JNI class…");
  […]
}

然后,注释需要在其他 OSes 上跳过的实际 JNI 测试:

@Test
@EnabledOnOs(LINUX)
public void testJNIBoots() {
    LOGGER.info("testing JNI part of JNI class…");
    final long tid;
    try {
        tid = JNI.n_gettid();
    } catch (Throwable t) {
        LOGGER.log(Level.SEVERE, "it failed", t);
        Assertions.fail("JNI does not work");
        return;
    }
    LOGGER.info("it also works: " + tid);
    assertNotEquals(0, tid, "but is 0");
}

为了比较,仪器测试(运行 在 Android 设备或​​模拟器上的单元测试) — 例如~/MYPRJ/app/src/androidTest/java/org/example/packagename/JNIInstrumentedTest.java — 像这样:

@RunWith(AndroidJUnit4.class)
public class JNIInstrumentedTest {
    @Test
    public void testJNIBoots() {
        Log.i("ECN-Bits-JNITest", "testing JNI part of JNI class…");
        final long tid;
        try {
            tid = JNI.n_gettid();
        } catch (Throwable t) {
            Log.e("ECN-Bits-JNITest", "it failed", t);
            fail("JNI does not work");
            return;
        }
        Log.i("ECN-Bits-JNITest", "it also works: " + tid);
        assertNotEquals("but is 0", 0, tid);
    }
}

如果您需要 assertThrows 用于插桩测试(JUnit 5 已经附带一个),请参见 Testable.java,顺便说一句。 (请注意,这不属于上述 CC0 授权,而是属于许可许可。)

现在,您可以 运行 测试、单元测试和(如果 Android 模拟器已启动或设备已启动)仪器测试:

../gradlew test connectedAndroidTest

这样做。注意来自 buildhost-native 单元测试的 VM options: 记录器调用的输出;实际上,将其复制到剪贴板。您现在需要它来在 IDE.

中设置测试

在项目视图(left-side 树)中,right-click 在您的 JNITest class 或整个 src/test/java/ 目录中。单击 Run 'JNITest'(或 Run 'Tests in 'java''),它将失败并显示 UnsatisfiedLinkError,如原始 post。

现在点击菜单栏下方测试drop-down中的箭头,然后selectSave JNITest configuration,然后再做selectEdit configurations…和 select 你的配置。将整个粘贴内容附加到 VM options:,这样该字段现在看起来像 -ea -Djava.library.path=/home/USER/MYPRJ/app/build/native-noNDK:/usr/java/packages/lib:/usr/lib/x86_64-linux-gnu/jni:/lib/x86_64-linux-gnu:/usr/lib/x86_64-linux-gnu:/usr/lib/jni:/lib:/usr/lib(当然,实际值会有所不同),然后单击“确定”。 Re-run测试,一定会成功。

不幸的是,您必须对每个本机测试 class 执行一次并针对整个目录执行此操作,因此将涵盖所有可能的调用方式。您还必须为每个 IDE 实例手动执行此操作,通过四处单击,这些值取决于代码检出的路径。我还没有找到一种方法来自动化这些(如果你知道,一定要告诉)。

异常回溯

如果您从代码中抛出自定义异常,您很可能希望包含 file/lineno/function 信息。使用像 MyprjNativeException(final String file, final int line, final String func, final String msg, … /* custom data */, final Throwable cause) 这样的构造函数,并在调用 super(msg, cause) 之后(可能使用更改的消息),执行以下操作:

        StackTraceElement[] currentStack = getStackTrace();
        StackTraceElement[] newStack = new StackTraceElement[currentStack.length + 1];
        System.arraycopy(currentStack, 0, newStack, 1, currentStack.length);
        newStack[0] = new StackTraceElement("<native>", func, file, line);
        setStackTrace(newStack);

然后,从本机代码中抛出这样的异常:

#define throw(env,...) vthrow(__FILE__, __func__, env, __LINE__, __VA_ARGS__)
static void vthrow(const char *loc_file, const char *loc_func, JNIEnv *env,
    int loc_line, /* custom args */ const char *msg, ...);

使用如下:

if (func() != expected)
        throw(env, /* custom args */ "foo");

实现(假设您缓存 class 和构造方法引用)如下所示(针对自定义参数进行调整):

static void vthrow(const char *loc_file, const char *loc_func, JNIEnv *env,
    int loc_line, const char *fmt, ...)
{
        jthrowable e;
        va_list ap;
        jstring jfile = NULL;
        jint jline = loc_line;
        jstring jfunc = NULL;
        jstring jmsg = NULL;
        jthrowable cause = NULL;
        const char *msg;
        char *msgbuf;

        if ((*env)->PushLocalFrame(env, /* adjust for amount you need */ 5)) {
                cause = (*env)->ExceptionOccurred(env);
                (*env)->ExceptionClear(env);
                (*env)->Throw(env, (*env)->NewObject(env, classreference, constructorreference,
                    jfile, jline, jfunc, jmsg, /* custom */…, cause));
                return;
        }

        if ((cause = (*env)->ExceptionOccurred(env))) {
                /* will be treated as cause */
                (*env)->ExceptionClear(env);
        }

        va_start(ap, fmt);
        if (vasprintf(&msgbuf, fmt, ap) == -1) {
                msgbuf = NULL;
                msg = fmt;
        } else
                msg = msgbuf;
        va_end(ap);

        jmsg = (*env)->NewStringUTF(env, msg);
        free(msgbuf);
        if (!jmsg)
                goto onStringError;

        if (!(jfunc = (*env)->NewStringUTF(env, loc_func)))
                goto onStringError;

        /* allocate NewStringUTF for any custom things you need */
        /* exactly like the one for loc_func above */
        /* increase PushLocalFrame argument for each */

        jfile = (*env)->NewStringUTF(env, loc_file);
        if (!jfile) {
 onStringError:
                (*env)->ExceptionClear(env);
        }

        e = (*env)->PopLocalFrame(env, (*env)->NewObject(env, classreference, constructorreference,
            jfile, jline, jfunc, jmsg, /* custom */…, cause));
        if (e)
                (*env)->Throw(env, e);
}

现在使用 __FILE__ 会将完整的绝对路径放入消息和回溯中。这不是很好。有一个编译器选项可以解决这个问题,但是 NDK r21 的 clang 太旧了,所以我们需要一个解决方法。

CMakeLists.txt:

if (NOT TOPLEV)
    message(FATAL_ERROR "setting the top-level directory is mandatory")
endif (NOT TOPLEV)

[…]

if (UNDER_NDK)
 […]
    execute_process(COMMAND ${CMAKE_CXX_COMPILER} --version OUTPUT_VARIABLE cxx_version_full)
    string(REGEX REPLACE "^Android [^\n]* clang version ([0-9]+)\.[0-9].*$" "\1" cxx_version_major ${cxx_version_full})
    if (${cxx_version_major} VERSION_GREATER_EQUAL 10)
            add_definitions("-ffile-prefix-map=${TOPLEV}=«MyPrj»")
    else (${cxx_version_major} VERSION_GREATER_EQUAL 10)
            add_definitions(-DOLD_CLANG_SRCDIR_HACK="${TOPLEV}/")
    endif (${cxx_version_major} VERSION_GREATER_EQUAL 10)
else (UNDER_NDK)
 […]
    add_definitions("-ffile-prefix-map=${TOPLEV}=«MyPrj»")
endif (UNDER_NDK)

应用build.gradle:

(在应用插件行之后)

def dirToplev = project.layout.projectDirectory.asFile.absolutePath

(在 android { defaultConfig { 内添加一个新块)

    externalNativeBuild {
        cmake {
            //noinspection GroovyAssignabilityCheck because Gradle and the IDE have different world views…
            arguments "-DTOPLEV=" + dirToplev
        }
        return void // WTF‽
    }

(后面调用cmake的地方)

commandLine "/usr/bin/env", "cmake", "-DTOPLEV=" + dirToplev, "-DUNDER_NDK=OFF", srcForNativeNoNDK.absolutePath

然后,将 jfile = (*env)->NewStringUTF(env, loc_file); 行替换为以下代码段:

#ifdef OLD_CLANG_SRCDIR_HACK
        if (!strncmp(loc_file, OLD_CLANG_SRCDIR_HACK, sizeof(OLD_CLANG_SRCDIR_HACK) - 1) &&
            asprintf(&msgbuf, "«ECN-Bits»/%s", loc_file + sizeof(OLD_CLANG_SRCDIR_HACK) - 1) != -1) {
                msg = msgbuf;
        } else {
                msg = loc_file;
                msgbuf = NULL;
        }
#else
#define msg loc_file
#endif
        jfile = (*env)->NewStringUTF(env, msg);
#ifdef OLD_CLANG_SRCDIR_HACK
        free(msgbuf);
#else
#undef msg
#endif

将它们捆绑在一起

这一切都在ECN-Bits项目中实现。我正在 post 建立永久链接,因为它目前在 nōn-default 分支上,但预计会被合并(一旦 actual 功能不再是 WIP),所以一定要在某个时间点检查 master(尽管这个固定链接可能是一个更好的例子,因为它已经过测试并且没有那么多“实际”代码来妨碍)。请注意,这些链接不属于上面的 CC0 授权;尽管这些文件都有一个宽松的许可(没有明确许可的文件(gradle/cmake 文件)与 unittest class 永久链接相同),但足够的是 repost编入这篇文章对你来说应该不是问题;这些仅用于显示 actually-compiling-and-testing 示例。

在这个项目中,它不在 app/ 中,而是作为一个单独的库模块。

澄清一下,System.loadlibrary() 调用失败是因为 junit 单元测试使用 host/system 环境,在我的例子中是 windows。因此 loadlibrary() 调用试图在标准共享库文件夹中搜索 .so 文件。但这不是我所期望的。相反,我希望从 .aar 文件加载 libxxx.so 文件(包含 android 资源、jar、jni 库)。

这只能通过两种方式发生:

  1. 手动将libxxx.so烘焙到APK中:我们自己将libxxx.so文件放入src/java/下的jnilibs目录(加载共享库时系统将搜索该目录)通过复制所需文件获取 apk 的根目录。
  2. 将 .aar 作为外部依赖项添加到模块级别 gradle 构建脚本 (implementation/androidTestImplementation),通过这样做,我们使这个 aar 可用于 apk 使用的 link 编辑构建过程。

但在这两种情况下,android environment/vm 中的应用程序 运行 以及 System.loadlibrary() 调用将解析为更正 libxxx.so将成为 apk 的一部分。因此 没有问题.

但是在单元测试的情况下,不需要仪器(即 android 设备)和 JVM 上的 运行s 运行ning 在主机系统上测试是 运行ning(例如:windows/linux/mac),对 System.loadlibrary() 的调用仅解析用于查找共享 libs/exe 的主机系统的标准库路径,并不引用到 android 系统环境。因此 问题.

修复:

  1. 将 libxxx.so 解压到某个临时目录中,并将此目录添加到系统的库搜索路径中(例如:java.library.path、windows 上的 %PATH% 变量等)。现在 运行 所需的单元测试不需要 android 环境,但涉及使用 JNI 的本机代码测试(如果有的话)。这应该有用!!
  2. (高效方法) 只需将这些类型的单元测试移动到 androidTest(即 Instrumentation 测试),以便上面解释的加载和包装完好无损 System.loadlibrary() 可以成功找到libxxx.so when 运行ning inside instrument(android device/os)。通过这种方式,您可以确保在目标设备上调用适当的库类型(x86、x86-64、arm-v7a、arm-v8a(AARCH64)),并且在特定目标设备上进行测试 运行。