加载本机库的单元测试 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 个子目录:armeabi
、mips
和 x86
。其中每一个都包含正确的 .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()
}
这确保了……
- 永远不会跳过测试,因为 Gradle 认为它们 up-to-date
- 完整显示日志输出和异常
- 如果你有 a
~/MYPRJ/app/src/test/resources/logging.properties
它将设置 java.util.logging
这个(推荐)
现在看看你的测试,比如~/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 库)。
这只能通过两种方式发生:
- 手动将libxxx.so烘焙到APK中:我们自己将libxxx.so文件放入src/java/下的jnilibs目录(加载共享库时系统将搜索该目录)通过复制所需文件获取 apk 的根目录。
- 将 .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 系统环境。因此 问题.
修复:
- 将 libxxx.so 解压到某个临时目录中,并将此目录添加到系统的库搜索路径中(例如:java.library.path、windows 上的 %PATH% 变量等)。现在 运行 所需的单元测试不需要 android 环境,但涉及使用 JNI 的本机代码测试(如果有的话)。这应该有用!!
- (高效方法) 只需将这些类型的单元测试移动到 androidTest(即 Instrumentation 测试),以便上面解释的加载和包装完好无损 System.loadlibrary() 可以成功找到libxxx.so when 运行ning inside instrument(android device/os)。通过这种方式,您可以确保在目标设备上调用适当的库类型(x86、x86-64、arm-v7a、arm-v8a(AARCH64)),并且在特定目标设备上进行测试 运行。
我在 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 个子目录:armeabi
、mips
和 x86
。其中每一个都包含正确的 .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()
}
这确保了……
- 永远不会跳过测试,因为 Gradle 认为它们 up-to-date
- 完整显示日志输出和异常
- 如果你有 a
~/MYPRJ/app/src/test/resources/logging.properties
它将设置java.util.logging
这个(推荐)
现在看看你的测试,比如~/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 库)。
这只能通过两种方式发生:
- 手动将libxxx.so烘焙到APK中:我们自己将libxxx.so文件放入src/java/下的jnilibs目录(加载共享库时系统将搜索该目录)通过复制所需文件获取 apk 的根目录。
- 将 .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 系统环境。因此 问题.
修复:
- 将 libxxx.so 解压到某个临时目录中,并将此目录添加到系统的库搜索路径中(例如:java.library.path、windows 上的 %PATH% 变量等)。现在 运行 所需的单元测试不需要 android 环境,但涉及使用 JNI 的本机代码测试(如果有的话)。这应该有用!!
- (高效方法) 只需将这些类型的单元测试移动到 androidTest(即 Instrumentation 测试),以便上面解释的加载和包装完好无损 System.loadlibrary() 可以成功找到libxxx.so when 运行ning inside instrument(android device/os)。通过这种方式,您可以确保在目标设备上调用适当的库类型(x86、x86-64、arm-v7a、arm-v8a(AARCH64)),并且在特定目标设备上进行测试 运行。