加载本机库的单元测试 Java 类
Posted
技术标签:
【中文标题】加载本机库的单元测试 Java 类【英文标题】:Unit test Java class that loads native library 【发布时间】:2016-04-20 13:13:30 【问题描述】:我正在 android Studio 中运行单元测试。我有一个 Java 类,它使用以下代码加载本机库
static
System.loadLibrary("mylibrary");
但是当我在 src/test
目录中测试这个类时,我得到了
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 个子目录:armeabi
、mips
和 x86
。每一个都包含正确的 .so 文件。我正在使用非实验版本来构建 NDK 库。
我不想使用其他 3rd 方测试库,因为我所有其他“纯”java 类都可以进行单元测试。但如果这不可能,那么我愿意接受替代方案。
这是我抛出错误的测试代码
@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());
【问题讨论】:
我有点困惑——上面的代码怎么会引发这个错误? @PhilLab 如您所见,此代码是加载本机方法的 java 类的一部分。运行我的应用程序时没有问题。但是,在 Android Studio 中测试此类时会出现此错误 但是为什么捕获时会抛出错误?你确定这是加载失败的库还是你的项目中有另一个库? 是的,这是加载本机库的唯一代码块。实际上试图捕捉这个错误是多余的。我故意放置它来回答您提出的完全相同的问题。但是UnsatisfiedLinkError
和NullPointerException
稍后在我的代码中再次出现,因为它找不到来自本机库的特定函数。我所需要的只是一个关于如何在 android studio 中对这样的 java 类进行单元测试的可靠解决方案。我真的很惊讶没有人知道答案
@ThanosFisherman 我提出了另一个 SO 问题的解决方案,看起来很相似 (***.com/a/47325408/992509)。本质上,您设置 Gradle 以构建代码的平台原生版本,以便您可以在计算机的 JVM 上运行它们。
【参考方案1】:
我发现唯一没有黑客攻击的解决方案是通过仪器测试(androidTest 目录)使用 JUnit。 我的课程现在可以在安卓设备或模拟器的帮助下进行测试。
【讨论】:
看来Suresh Joshi 解决了here 的主要障碍。仍然需要解决一些警告。【参考方案2】:如果您的测试需要该库,请使用 AndroidTest(在src/androidTest/...
下)而不是 junit 测试。这将允许您像在代码中的其他地方一样加载和使用本机库。
如果您的测试不需要该库,只需将系统负载包装在 try/catch 中即可。这将允许 JNI 类仍然在 junit 测试中工作(在 src/test/...
下),并且这是一个安全的解决方法,因为它不太可能掩盖错误(如果实际需要本机库,其他东西肯定会失败)。从那里,您可以使用 mockito 之类的东西来存根任何仍然命中 JNI 库的方法调用。
例如在 Kotlin 中:
companion object
init
try
System.loadLibrary("mylibrary")
catch (e: UnsatisfiedLinkError)
// log the error or track it in analytics
【讨论】:
【参考方案3】:我不确定这是否解决了您的问题,但到目前为止,没有人提到在创建过程中处理类预加载库的策略模式。
我们来看例子:
我们要实现斐波那契求解器类。假设我们在本机代码中提供了实现并设法生成了本机库,我们可以实现以下内容:
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;
第三,我们用父类包装求解器,在实例化过程中选择自己的实现:
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 代码的本机代码来解决本机库加载问题。
希望这能有所帮助:)
【讨论】:
这很有趣。我将不得不对此进行一些实验。但是在我的测试类中总是会调用本机方法吗?我不想创建一个mock对象我只想测试一个由native方法计算的值然后传递给ArrayList。所以是的,我猜我需要在测试期间包含本机库。请参阅上面我审查过的代码,了解我希望如何对其进行测试。 "但是本地方法总是在我的测试类中被调用吗?"我假设您的项目中有本机 C 代码,在该代码上生成本机库。如果设备的架构允许加载库,并且您已将本地方法与 Java 接口正确链接,那么可以,将调用本地方法。但我想出的想法是让替代 Java 实现做完全相同的事情,以防无法预加载库。 是的,你所有的假设都是正确的。可悲的是它没有用。它实际上回到了 catch 块,而是测试了FibonnaciJava
:( 我在这里将这个问题转发给了谷歌开发人员 code.google.com/p/android/issues/detail?id=199979 他们告诉我切换到 gradle 实验性插件并使用仪器测试。免得看看会发生什么:P跨度>
希望对您有所帮助。【参考方案4】:
有一种方法可以为本地单元测试配置 Gradle-run VM 的库路径,我将在下面对其进行描述,但剧透:根据我的经验,@ThanosFisherman 是对的:对使用 Android NDK 的东西进行本地单元测试现在似乎是一件愚蠢的事情。
因此,对于正在寻找一种方法来使用 gradle 将共享(即.so
)库加载到单元测试中的其他人来说,这里有一些冗长的摘要:
目标是为运行单元测试的 JVM 设置共享库查找路径。
尽管很多人建议将 lib 路径放入 java.library.path
,但我发现它不起作用,至少在我的 linux 机器上不起作用。 (另外,same results in this CodeRanch thread)
起作用的是设置LD_LIBRARY_PATH
os 环境变量(或者PATH
是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 测试(右键单击测试方法或其他),然后编辑运行配置:
虽然这听起来像是“任务完成”,但我发现当使用 libc.so, libm.so
和我的操作系统中的其他人时,/usr/lib
会给我版本错误(可能是因为我自己的库是由 cmake 用 android ndk 工具包针对它自己的平台库)。并且使用 ndk 包中的平台库会导致 JVM 出现SIGSEGV
错误(由于 ndk 平台库与主机操作系统环境不兼容)
更新正如@AlexCohn 在 cmets 中敏锐地指出的那样,必须针对主机环境库进行构建才能使其正常工作;即使您的机器很可能是 x86_64,针对 NDK 环境构建的 x86_64 二进制文件也不行。
很明显,我可能忽略了一些东西,如果有任何反馈,我将不胜感激,但现在我放弃了整个想法,转而支持仪器化测试。
【讨论】:
您必须为您的主机构建本机库。即使您的 PC 使用兼容 x86_64 的 CPU,您也无法使用来自build/intermediates/cmake/debug/obj/x86_64/
的 Android 版本。
@AlexCohn 是的,当然。那是我停下来的地方,因为这整个体验是有截止日期的任务的一部分,我不想在“弄清楚如何针对主机环境库构建 gradle-cmake 组合”桶上投入更多精力,没有看不到任何直观的好路径。如果您对该主题有什么要分享的,我会很乐意阅读。
看来Suresh Joshi 解决了here 的主要障碍。仍然需要解决一些警告。【参考方案5】:
只需确保包含库的目录包含在java.library.path
系统属性中。
从测试中您可以在加载库之前设置它:
System.setProperty("java.library.path", "... path to the library .../libs/x86");
您可以指定硬编码的路径,但这会降低项目对其他环境的可移植性。所以我建议你以编程方式构建它。
【讨论】:
还是不行。我在测试方法中输入了 x86 库的绝对路径,但仍然出现相同的错误 尝试在loadLibrary
调用中添加.so
扩展。【参考方案6】:
.so 文件将放在
下src/main/jniLibs
不在 src/main/libs 下
(使用 Android Studio 1.2.2 测试)
请查看页面 - http://ph0b.com/android-studio-gradle-and-ndk-integration/ 以供参考,但有些部分可能已过时。
【讨论】:
我只是将我的 .so 文件复制到src/main/jniLibs
中,但没有区别。使用 android studio 1.5.1 出现同样的错误(我正在使用非实验版本来构建 NDK 库)
如果使用1.5.1,请同时发布build.gradle和gradle版本【参考方案7】:
尝试使用 java -XshowSettings:properties 选项运行您的测试代码,并确保系统库的目标路径和此命令的输出中的库路径值相同
【讨论】:
“复制位置”和“库路径值”是什么意思?查看我的堆栈跟踪pastebin.com/sNjnbxgP,让我知道您所说的那些路径在哪里 对不起,我刚刚编辑了我的答案以使其更清楚,我的理解是您的库位于 src/main/libs/xxx 文件夹中,您的测试代码也可能位于 src/test/xxx 中,在我的认为您可以在您的测试目录或堆栈跟踪中显示的系统路径之一下复制库 你的假设是正确的。我将尝试将本机库复制到我的测试目录中,看看会发生什么谢谢【参考方案8】:这非常非常棘手。设置java.library.path
不起作用,但试图理解someone else’s Mac OSX approach 我最终找到了一个可行的解决方案。
合法发布:直接复制到这篇文章中的所有代码示例都可以在CC0 下找到,但我的雇主⮡ tarent,LLCTO 项目 Deutsche Telekom ,作者mirabilos
。
注意事项首先:
有了这个,您正在测试针对您的 system 库编译的本机代码版本(通常是 GNU/Linux 上的 glibc,而在 BSD、Mac OSX 和 Windows 上则更加棘手),因此添加无论如何都应该进行一些仪器测试,仅使用单元测试来更快地测试实际上可以在主机操作系统上测试的东西
我只在 GNU/Linux 主机上对此进行了测试(事实上,我不包括在所有其他主机操作系统上进行的这些本机测试,见下文)
它应该可以在具有 GNU/BSD 样式共享库的 unixoid 操作系统下按原样运行 对上面链接的“别人的”文章进行了小幅改编,它可能适用于 Mac OSX Windows……不,只是不。使用 WSL,它基本上是 Linux,让事情变得更容易,并且更接近 Android,它也基本上是 Linux,而不是 GNUIDE 集成需要在每个开发人员的计算机上执行手动步骤(但这些步骤很容易记录,见下文(很多))
先决条件
您需要确保所有本地代码的构建依赖项也安装在主机系统中。这包括cmake
(因为我们很遗憾不能重用 NDK cmake)和主机 C 编译器。请注意,这些在构建中引入了进一步的差异:您正在测试使用宿主 C 编译器(通常是 GCC,不像 Android 中的 clang)针对宿主 C 库和宿主 clang 的其他库构建的东西。 请在编写测试时考虑这一点。由于无法在 glibc 下进行测试,因此我不得不将其中一项测试移至 Instrumented。
对于文件系统布局,我们假设如下:
~/MYPRJ/build.gradle
是顶层构建文件(由 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
定义来访问一些原型,等等……)
所以我写了一个包含而不是 <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 单元测试,并将相应的测试移至 Instrumented。
让 Gradle 构建主机原生代码
将以下内容添加到应用程序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 版本,如果主机操作系统是“Linux”,则将其挂钩。 (此语法也适用于其他 unixoid 操作系统 — BSD、Mac OSX — 但不适用于 Windows。但无论如何我们可能只能在 Linux 下测试它。WSL 算作 Linux。)它还设置了 JVM 库路径,以便@987654357 @ 将让 JVM 从其路径中获取库。
松散的末端
您可能已经注意到这里有一些松散的结局:
在上一节的最后一段中,我提到了../gradlew test
将拿起图书馆。 IDE 中的测试还不能工作;这涉及手动设置。
我提到如果buildhost OS不是“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 生成的默认值)。
接下来,编辑 *** ~/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()
这样可以确保……
永远不会跳过测试,因为 Gradle 认为它们是最新的 完整显示日志输出和异常 如果您有a~/MYPRJ/app/src/test/resources/logging.properties
,它将使用此设置java.util.logging
(推荐)
现在看看你的测试,比如~/MYPRJ/app/src/test/java/org/example/packagename/JNITest.java
。首先,您应该添加一个可以始终运行的“测试”(我使用的只是测试我的 JNI 类是否可以加载),并确保它首先显示一些信息:
// 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…");
[…]
然后,注释需要在其他操作系统上跳过的实际 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:
logger 调用的输出;实际上,将其复制到剪贴板。您现在需要它来在 IDE 中设置测试。
在项目视图(左侧树)中,右键单击您的 JNITest
类或整个 src/test/java/
目录。点击Run 'JNITest'
(或Run 'Tests in 'java''
),它会像原帖一样显示UnsatisfiedLinkError
。
现在单击菜单栏下方测试下拉菜单中的箭头,然后选择Save JNITest configuration
,然后再次执行此操作并选择Edit configurations…
并选择您的配置。将整个粘贴的内容附加到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
(当然,实际值会有所不同),然后单击确定。重新运行测试,它会成功。
不幸的是,您必须为每个本机测试类以及整个目录执行一次此操作,因此将涵盖所有可能的调用方式。对于每个 IDE 实例,您还必须通过单击来手动执行此操作,并且这些值取决于代码签出到的路径。我还没有找到一种方法来自动化这些(如果你知道的话,请告诉我)。
异常回溯
如果您从代码中抛出自定义异常,您很可能希望包含文件/行号/函数信息。使用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");
实现(假设您缓存类和构造函数方法引用)如下所示(针对自定义参数进行调整):
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, clas-s-reference, 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, clas-s-reference, 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);
行替换为以下sn-p:
#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
项目中实现。我发布了一个永久链接,因为它目前在一个非默认分支上,但预计会被合并(一旦 actual 功能不再是 WIP),所以一定要在某个时候检查master
时间也是如此(尽管这个永久链接可能是一个更好的例子,因为它已经进行了测试,并且没有那么多“实际”代码会妨碍)。请注意,这些链接不属于上述 CC0 授权;虽然这些文件都有一个许可许可证(没有明确的文件(gradle/cmake 文件)与 unittest 类的永久链接相同),但是在本文中重新发布了足够多的文件,所以这应该不是问题为你;这些仅用于显示实际编译和测试的示例。
在这个项目中,它不在app/
中,而是作为一个单独的库模块。
build.gradle
library build.gradle
instrumented tests
unittest class
unittest logging configuration
native CMakeLists.txt
native alog.h
native code 包括缓存引用
Java™ code to which the JNI native code attaches 包括异常类
【讨论】:
【参考方案9】:澄清一下, System.loadlibrary() 调用失败是因为 junit 单元测试使用主机/系统环境,在我的例子中是 windows。因此 loadlibrary() 调用试图在标准共享库文件夹中搜索 .so 文件。但这不是我所期望的。相反,我希望从 .aar 文件(包含 android 资源、jar、jni 库)加载 libxxx.so 文件。
这只能通过两种方式发生:
-
手动将libxxx.so烘焙到APK中:我们自己将libxxx.so文件复制到apk的src/java/根目录下的jnilibs目录(加载共享lib时系统会搜索)文件。
将 .aar 作为外部依赖项添加到模块级 gradle 构建脚本 (implementation/androidTestImplementation),通过这样做,我们使此 aar 可用于 apk 构建过程使用的链接编辑。
但在这两种情况下,应用程序都在 android 环境/vm 中运行,因此 System.loadlibrary() 调用将解析为更正作为 apk 一部分的 libxxx.so。因此没有问题。
但是,在单元测试的情况下,不需要仪器(即,android 设备)并且在运行测试的主机系统(例如:windows/linux/mac)上运行的 JVM 上运行,调用System.loadlibrary() 仅解析主机系统的标准库路径以查找共享库/exe,而不是参考android系统环境。因此问题。
修复:
-
将 libxxx.so 解压到某个临时目录中,然后将此目录添加到系统的库搜索路径(例如:java.library.path、Windows 上的 %PATH% 变量等)。现在运行所需的单元测试,它不需要 android 环境,但涉及使用 JNI 的本机代码测试(如果有)。这应该工作!
(高效的方法) 只需将这些类型的单元测试移动到androidTest(即Instrumentation tests)中,这样上面解释的加载和打包就可以完好无损,并且System.loadlibrary()可以在运行时成功找到libxxx.so仪器内部(安卓设备/操作系统)。这样可以确保在目标设备上调用适当的库类型(x86、x86-64、arm-v7a、arm-v8a(AARCH64)),并在特定目标设备上运行测试。
【讨论】:
以上是关于加载本机库的单元测试 Java 类的主要内容,如果未能解决你的问题,请参考以下文章
PHP 单元测试 - 使用 Mockery 模拟静态自动加载类