天天看點

Android Robolectric加載運作本地So動态庫

前言

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 庫之前加載這些必須的依賴即可。處理代碼後面貼出。

Android Robolectric加載運作本地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  

Android Robolectric加載運作本地So動态庫

處理後的流程

首先流程應該在我們的代碼裡避免可以直接加載 so 動态庫,然後 robolectric 在啟動時自己去加載需要的動态庫。

 @override public void oncreate() { 

   super.oncreate(); 

   loadnativelibraries(); 

 /** 

  * 簡單讓子類可自己實作 

  */ 

 protected void loadnativelibraries() { 

    // 代碼裡真正加載本地庫的地方,當然你自己的可以處理地更解耦一點。 

   nativelibrariesmanager.loadnativelibraries(); 

然後我們的 robolectric 裡自定義自己的 application,裡面根據需要在不同運作平台下自己加載需要的本地動态庫,首先複制我們給 robolectric 用的本地庫到 test 的 libs 檔案夾裡,按不同平台分類,如下圖:

Android Robolectric加載運作本地So動态庫

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); 

}   

Android Robolectric加載運作本地So動态庫

end

linux 下使用最快速友善,隻需要打包程式的 so 時順便打包出 x86-64 的 so ,然後複制 ndk-bundle 的 so 加上需要的依賴即可。macos 和 windows 下就需要自己打包出各自平台下的動态庫才可使用,如果代碼裡有 android 自帶 so 依賴的話那就需要自己去重新移植編譯打包 ndk-bundle 裡的動态庫了。

作者:rocko

來源:51cto