您可以捐助,支持我们的公益事业。

1元 10元 50元





认证码:  验证码,看不清楚?请点击刷新验证码 必填



  求知 文章 文库 Lib 视频 iPerson 课程 认证 咨询 工具 讲座 Modeler   Code  
会员   
 
   
 
 
     
   
 订阅
  捐助
Android Robolectric加载运行本地So动态库
 
作者:Rocko 来源:51CTO 发布于:2016-12-12
  2016  次浏览      16
 

前言

Robolectric 是 Android 的单元测试框架,运行无需 Android 真机环境直接运行在 JVM 之上,所以在 test case 运行速度效率上有了很大提升,接近于 Java JUnit test(JUnit test > Robolectric ? androidTest)。不过框架本身并不支持 so 本地库的加载使用,加载时会直接报错,因为实际上运行环境是电脑机器,而我们打出的 so 文件是给手机上用的所以当然会报错。虽然在 GitHub 上很多人问过关于使用 so 的问题但基本都建议说不要在单元测试中去加载本地库,这在原则上是要这么做,但可能有些项目中做起来就有些困难了,比如在代码结构不够好、依赖耦合较大或者本身就对 so 库依赖很大的情况下。所以下面说说在项目中 Robolectric 要怎么解决需要加载运行本地 so 库这个问题。

动态库

动态库又称动态链接库(Dynamic-link library 缩写 DLL),是一个包含可由多个程序同时使用的代码和数据的库,DLL 不是可执行文件。动态链接提供了一种方法,使进程可以调用不属于其可执行代码的函数。函数的可执行代码位于一个 DLL 中,该 DLL 包含一个或多个已被编译、链接并与使用它们的进程分开存储的函数。DLL 还有助于共享数据和资源。多个应用程序可同时访问内存中单个DLL 副本的内容。DLL 是一个包含可由多个程序同时使用的代码和数据的库。Windows下动态库为 .dll 后缀(一般为 PE 格式),在 Linux 在为 .so 后缀(一般为 ELF 格式),macOS下为 .dylib 后缀(一般为 Mach-O 格式)。由于 CPU 架构和动态库文件格式的不同因而在不同平台下不能通用。其它细节的东西就不展开了因为也不会 :-)

而 Android 本身是 Linux 系统,所以用的动态库也是 .so 的文件,因而运行与 JVM 的 Robolectric 是不能直接加载使用的(Linux 某些情况下可用,下面提到)。

Robolectric 中使用动态库

我们知道动态库一般都是打给特定平台、特定 CPU 架构用的,所以要解决在 Robolectric 下加载运行 so 动态库的问题的思路就是在不同 Robolectric 运行平台下去处理加载不同的动态库,所以你要在 Ronbolectriv 中使用的 so 动态库最好要有源码不然在 macOS 和 Windows 下就不就好处理了。

Note: 注意动态库名称已 lib 开头。

Linux 下 Robolectric 中使用动态库

Android 与 Linux 同气连枝,所以底层的东西很多是通用的,动态库也一样。我们 Android 使用 so 时一般也要对不同 CPU 架构的手机下使用不同的 so 文件,譬如:armeabi-v7a、mips、x86。而我们使用的 LInux 发行版一般都是 64 位的,所以原理上我们使用x86-64 的动态库是可以的,不过可能需要处理依赖库问题如果你的本地代码里有 include 其它依赖的话。如果没加进来 Robolectric 运行就会报如下的错误:

java.lang.UnsatisfiedLinkError: xxx/xxx.so xxx 动态库找不到。 

xxx.so 就是你所使用 so 的依赖,比如把新浪微博 SDK 的 x86-64 的 libweibosdkcore.so 加载进来的话就会报 liblog.so 等找不到,因为 libweibosdkcore 中有对 Android liblog 等 so 库的依赖。那这个问题怎么解决呢。我们想想打包 so 库时用的是 ndk,需要使用 ndk-bundle 工具,我们想想,跟编译 apk 差不多,apk 打包需要 sdk 工具,compileSdk 里就是我们编译的依赖,里面有android.jar。所以我们可以到 ndk-bundle 里找找,最后我们发现不同 CPU 架构下的 so 依赖库都是有的,像我们一般的电脑 64 位 CPU 即可使用 arch-x86_64 下的 so 动态库,所以我们只需要在加载我们程序的 so 库之前加载这些必须的依赖即可。处理代码后面贴出。

注意 ndk-bundle 里的 so 也是只能在 Linux 下用的,如果用于其它平台会报错,原因前面已说明。

    java.lang.UnsatisfiedLinkError: xxx.so: unknown file type, first eight bytes: 
0x7F 0x45 0x4C 0x46 0x02 0x01 0x01 0x00 

macOS 下 Robolectric 中使用动态库

前面已提到,不同平台下动态链接库是不通用的,所以必须对源码重新编译打包以移植到不同平台下,如果你的 so 没有源码的话那在 macOS 和 Windows 下就行不通了。重新打包我们可以按如下两步进行:

    # 先生成 .o ,-I 后加进 Java jni 的编译依赖 

cc -c -I/System/Library/Frameworks/JavaVM.framework/Headers *.cpp

# 打包成 .dylib

g++ -dynamiclib -undefined suppress -flat_namespace *.o -o something.dylib

某些依赖库可以到 /usr/lib 下找找,比如 libc 和 libstdc++ 。

Windows 下 Robolectric 中使用动态库

本人没有在 Windows 下开发所以这部分就略过了,思路是一样的。

Sample

下面是简单的处理代码示例。首先新建一个包含 jni 的工程,里面写个基本的本地库,如下:

正常流程

// native-lib.cpp 

#include <jni.h>
#include <string>

extern "C"
jstring
Java_xyz_rocko_rsnl_nativeinterface_NativeSample_stringFromJNI(
JNIEnv *env,
jobject /* this */) {

// 简单返回个字符串
std::string hello = "Hello from Native.";
return env->NewStringUTF(hello.c_str());
}

然后在 Application 启动时会加载这个本地库:

    public class NativeLibsApplication extends Application { 

// Used to load the 'native-lib' library on application startup.
static {
System.loadLibrary("native-lib");
}
}

此时运行 Robolectric 的 test case 就发生如下报错:

java.lang.UnsatisfiedLinkError: no native-lib in java.library.path  

处理后的流程

首先流程应该在我们的代码里避免可以直接加载 so 动态库,然后 Robolectric 在启动时自己去加载需要的动态库。

// NativeLibsApplication.java

    public class NativeLibsApplication extends Application { 

@Override public void onCreate() {
super.onCreate();
loadNativeLibraries();
}

/**
* 简单让子类可自己实现
*/
protected void loadNativeLibraries() {

// 代码里真正加载本地库的地方,当然你自己的可以处理地更解耦一点。
NativeLibrariesManager.loadNativeLibraries();
}
}

然后我们的 Robolectric 里自定义自己的 Application,里面根据需要在不同运行平台下自己加载需要的本地动态库,首先复制我们给 Robolectric 用的本地库到 test 的 libs 文件夹里,按不同平台分类,如下图:

Linux 下的我们从 ndk-bundle 里复制我们需要的 .so,然后我们自己的本地库打一个 x86-64 的即可,注意 compileSdkVersion 选上高一点支持 x86-64 的版本。

然后重新移植打出 macOS 下的动态库,简单写个打包脚本如下:

// make_macOS_dylib.sh

#!/usr/bin/env bash 

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

# .o file
cc -c -I/System/Library/Frameworks/JavaVM.framework/Headers *.cpp -o ${OUTPUT}/libnative-lib.o

# .dylib file
g++ -dynamiclib -undefined suppress -flat_namespace ${OUTPUT}/*.o -o ${OUTPUT}/libnative-lib.dylib

libnative-lib.dylib 就是我们要的。

然后我们自定义 Application 处理加载这些动态库:

// RobolectricApplication.java

    public class RobolectricApplication extends NativeLibsApplication { 

static {
ShadowLog.stream = System.out; //Android logcat output.
}

@Override protected void loadNativeLibraries() {
//Disable super class load so file.
//super.loadNativeLibraries();
Log.d(TAG, "=====>> Robolectric start native libraries.");

String libsBasePath =
new File(new File("").getAbsolutePath() + "/src/test/libs").getAbsolutePath();
String os = System.getProperty("os.name");
os = !TextUtils.isEmpty(os) ? os : "";
List<File> soFileList = new ArrayList<>();
String systemArchPath = libsBasePath + "/framework/";
//!!! 64 位机器下处理
if (os.contains("Mac")) {
//load system library if need
String macSysSoBasePath = systemArchPath + "macOS/";
soFileList.addAll(addLibs(macSysSoBasePath));
// App so...
String macAppSoPath = libsBasePath + "/macOS_x86-64/";
// mac下so要使用macOS专用库
soFileList.addAll(addLibs(macAppSoPath));
} else if (os.contains("Linux")) {
//load system library if need
String linuxSysSoBasePath = systemArchPath + "arch_x86-64/";
soFileList.addAll(addLibs(linuxSysSoBasePath));
// App so...
String linuxAppSoPath = libsBasePath + "/linux_x86-64/";
soFileList.addAll(addLibs(linuxAppSoPath));
} else if (os.contains("Windows")) {
// ignore
}

for (File soFie : soFileList) {
System.load(soFie.getAbsolutePath());
}
}

private List<File> addLibs(@NonNull String path) {
File[] basePathFiles = new File(path).listFiles();
List<File> pathFilesList = new ArrayList<>();
if (basePathFiles != null && basePathFiles.length > 0) {
pathFilesList.addAll(Arrays.asList(basePathFiles));
}
return pathFilesList;
}
}

现在就可以加载了,运行如下 test case,结果如下图,成功了。

    @Test public void testLoadNativeLibrariesSuccess() throws Exception { 
String nativeExcepted = "Hello from Native.";
String result = NativeSample.stringFromJNI();
Log.d(TAG, "result: " + result);
assertEquals(nativeExcepted, result);
}

 

End

Linux 下使用最快速方便,只需要打包程序的 so 时顺便打包出 x86-64 的 so ,然后复制 ndk-bundle 的 so 加上需要的依赖即可。macOS 和 Windows 下就需要自己打包出各自平台下的动态库才可使用,如果代码里有 Android 自带 so 依赖的话那就需要自己去重新移植编译打包 ndk-bundle 里的动态库了。

项目实例源码:RobolectricSupportNativeLibs

   
2016 次浏览       16
 
相关文章

手机软件测试用例设计实践
手机客户端UI测试分析
iPhone消息推送机制实现与探讨
Android手机开发(一)
 
相关文档

Android_UI官方设计教程
手机开发平台介绍
android拍照及上传功能
Android讲义智能手机开发
相关课程

Android高级移动应用程序
Android系统开发
Android应用开发
手机软件测试
最新课程计划
信息架构建模(基于UML+EA)3-21[北京]
软件架构设计师 3-21[北京]
图数据库与知识图谱 3-25[北京]
业务架构设计 4-11[北京]
SysML和EA系统设计与建模 4-22[北京]
DoDAF规范、模型与实例 5-23[北京]

android人机界面指南
Android手机开发(一)
Android手机开发(二)
Android手机开发(三)
Android手机开发(四)
iPhone消息推送机制实现探讨
手机软件测试用例设计实践
手机客户端UI测试分析
手机软件自动化测试研究报告
更多...   


Android高级移动应用程序
Android应用开发
Android系统开发
手机软件测试
嵌入式软件测试
Android软、硬、云整合


领先IT公司 android开发平台最佳实践
北京 Android开发技术进阶
某新能源领域企业 Android开发技术
某航天公司 Android、IOS应用软件开发
阿尔卡特 Linux内核驱动
艾默生 嵌入式软件架构设计
西门子 嵌入式架构设计
更多...