F212695aa97d361e3abdd4294db38132
Android NDK单元测试

笔者之前已经写了好多关于Android Java单元测试的文章,但NDK单元测一直没写。最近总算决心搞一下。

首先,笔者对单元测试追求编译、运行速度,尽量在jvm上跑。C++单元测试探索:

1.Junit
2.AndroidJUnit
3.Robolectric
4.Googletest

示例:

package com.example.ndk;

public class JNI {
    public native int add(int a, int b);
}

jni.cpp放在app/src/main/cpp/目录下:

#include <jni.h>

extern "C"
JNIEXPORT jint JNICALL
Java_com_example_ndk_JNI_add(JNIEnv *env, jobject instance, jint a, jint b) {
    return a + b;
}

工程部分目录结构

./
├── app
│   └── src
│       ├── androidTest
│       │   └── java
│       │       └── com
│       │           └── example
│       │               └── ndk
│       │                   └── TestJNI.java
│       ├── main
│       │   ├── AndroidManifest.xml
│       │   ├── cpp
│       │   │   ├── CMakeLists.txt
│       │   │   └── jni.cpp
│       │   └── java
│       │       └── com
│       │           └── example
│       │               └── ndk
│       │                   ├── JNI.java
│       │                   └── MainActivity.java
│       └── test
│           └── java
│               └── com
│                   └── example
│                       └── ndk
│                           └── JVMTestJNI.java

Junit

Junit就是java单元测试框架,要测C++要运用到JNI技术,也是本文详细讨论的技术点。

我们先按正常思路写一下测试用例JVMTestJNI(在app/src/test/java/目录下):

public class JVMTestJNI {
    @Test
    public void add() {
        System.loadLibrary("jni");

        JNI jni = new JNI();
        Assert.assertEquals(2, jni.add(1, 1));
    }
}

必然会报错:

java.lang.UnsatisfiedLinkError: no jni in java.library.path

因为System.loadLibrary(...)会从java.library.path环境变量指向目录,加载动态链接库(so、dylib等文件)。我们输出一下java.library.path:

public class JVMTestJNI {
    @Test
    public void printJavaLibraryPath(){
        System.out.println(System.getProperty("java.library.path"));
    }
}

结果:

/Users/kkmike999/Library/Java/Extensions:/Library/Java/Extensions:/Network/Library/Java/Extensions:/System/Library/Java/Extensions:/usr/lib/java:.

而project编译生成的so文件,在app/build/intermediates/cmake/debug/obj/{platform}/libjni.so. ({platform}有4个:arm64-v8aarmeabi-v7ax86x86_64). java.library.path没有引用该目录。

我们常识指定so文件绝对路径(一定要绝对路径,不能是相对路径)加载(轮流试4个platform):

public class JVMTestJNI {
    @Test
    public void add() {
        File file = new File("build/intermediates/cmake/debug/obj/x86_64/libjni.so");
        System.load(file.getAbsolutePath());

        JNI jni = new JNI();
        Assert.assertEquals(2, jni.add(1, 1));
    }
}

Run一下....嗯....也是报错:

java.lang.UnsatisfiedLinkError: .../NdkDemo/app/build/intermediates/cmake/debug/obj/x86_64/libjni.so: dlopen(.../NdkDemo/app/build/intermediates/cmake/debug/obj/x86_64/libjni.so, 1): no suitable image found.  Did find:
    .../NdkDemo/app/build/intermediates/cmake/debug/obj/x86_64/libjni.so: unknown file type, first eight bytes: 0x7F 0x45 0x4C 0x46 0x02 0x01 0x01 0x00
    .../NdkDemo/app/build/intermediates/cmake/debug/obj/x86_64/libjni.so: unknown file type, first eight bytes: 0x7F 0x45 0x4C 0x46 0x02 0x01 0x01 0x00

ndk编译出来的so文件,并不适用于macOS和windows,macOS需要dylib文件,而windows需要dll. 假如你用Linux系统,估计x86_64/libjni.so可以适用。

假如要在macOS或windows使用动态链接库,必须重新编译cpp文件。编译步骤我在下文会详细讲解。

动态链接库文件,是一种不可执行的二进制程序文件,它允许程序共享执行特殊任务所必需的代码和其他资源。 Windows提供的DLL文件中包含了允许基于Windows的程序在Windows环境下操作的许多函数和资源。 一般被存放在C:视窗系统System目录下。

AndroidJunit

AndroidJunit是google本身提供的android单元测试方式。因为代码跑在真机android环境上,所以跟app运行在真机上调用native方法没区别,也是相对简单的一种方式。

Run一下测试用例TestJNI(app/src/androidTest/java/目录下):

public class TestJNI {
    @Test
    public void add() {
        System.loadLibrary("jni");

        JNI jni = new JNI();
        Assert.assertEquals(2, jni.add(1, 1));

        System.out.println("Hello World");
    }
}

结果:

测试跑通了。
同学们留意笔者在最后System.out.println("Hello World"),但在测试结果并没有看到Hello World。我们看看Logcat:

有点小遗憾,AndroidJunit不能直接在Run窗口查看输出流(System.outLog.d等),而需要在Logcat查看。我相信同学们都深有体会Logcat不太稳定,包括笔者也很烦这个问题。

Robolectric

很遗憾Robolectric不支持加载so文件,原因跟上面Junit总结的一样,因为Robolectric也是跑在JVM上。

GoogleTest

GoogleTest是Google官网提供的专门的C++单元测试框架。用起来比较麻烦,要用C++写单元测,而不是java调用库文件,并且要adb push到真机上运行,而不是IDE自带工具完成测试。

有兴趣的可以参考《Android Cmake 配置 Googletest 单元测试》

结论1

根据以上讲解,现成、无成本最适合普通android工程师使用的C++单元测试,就是androidTest.


Junit+JNI做C++单元测试

尽管上面已经有初步结论,用androidTest最方便,但还未分析过Junit+JNI的可能性。接下来我们探讨一下。

在编程领域,JNI (Java Native Interface,Java本地接口)是一种编程框架,使得Java虚拟机中的Java程序可以调用本地应用/或库,也可以被其他程序调用。 本地程序一般是用其它语言(C、C++或汇编语言等)编写的,并且被编译为基于本机硬件和操作系统的程序。

本地库就是在上文Junit一节介绍的动态链接库。我们可以编译cpp文件,生成适合macOS和windows的动态链接库。本文着重介绍macOS编译C.

编译cpp生成dylib

macOS使用的动态链接库格式是dylib,因此在macOS上jvm只能调用dylib。我们把cpp编译成dylib,再加载该库即可。

app目录新建一个make_macOS_dylib.sh(先配置好JAVA_HOME环境变量):
```

!/usr/bin/env bash

指定动态库名称(即cpp文件名)

name=jni

指定cpp目录

INTPUT=./src/main/cpp/

指定dylib输出目录

OUTPUT=./build/dylibs
mkdir -p ${OUTPUT}

top Created with Sketch.