前言
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 啟動時會加載這個本地庫:
// nativelibsapplication.java
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 在啟動時自己去加載需要的動态庫。
@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 {
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")) {
string linuxsyssobasepath = systemarchpath + "arch_x86-64/";
sofilelist.addall(addlibs(linuxsyssobasepath));
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 裡的動态庫了。
作者:rocko
來源:51cto